[ 
https://issues.apache.org/jira/browse/GROOVY-11902?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18071018#comment-18071018
 ] 

ASF GitHub Bot commented on GROOVY-11902:
-----------------------------------------

Copilot commented on code in PR #2433:
URL: https://github.com/apache/groovy/pull/2433#discussion_r3035498166


##########
subprojects/groovy-nio/src/main/java/org/apache/groovy/nio/extensions/NioExtensions.java:
##########
@@ -2036,4 +2041,230 @@ public static <T> T withCloseable(Closeable self, 
@ClosureParams(value = SimpleT
         return IOGroovyMethods.withCloseable(self, action);
     }
 
+    // 
-------------------------------------------------------------------------
+    // Asynchronous file I/O extensions
+    // 
-------------------------------------------------------------------------
+
+    /**
+     * Asynchronously read the content of the Path and return it as a String
+     * using the platform's default charset.
+     * <p>
+     * Uses {@link AsynchronousFileChannel} internally, making the I/O
+     * non-blocking. The returned {@link CompletableFuture} completes
+     * when the entire file has been read.
+     *
+     * <pre class="groovyTestCase">
+     * import java.nio.file.Files
+     * def path = Files.createTempFile('test', '.txt')
+     * path.text = 'Hello async'
+     * assert path.textAsync.get() == 'Hello async'
+     * Files.delete(path)
+     * </pre>
+     *
+     * @param self the file whose content we want to read
+     * @return a CompletableFuture that completes with the file content as a 
String
+     * @since 6.0.0
+     */
+    public static CompletableFuture<String> getTextAsync(Path self) {
+        return getTextAsync(self, Charset.defaultCharset().name());
+    }
+
+    /**
+     * Asynchronously read the content of the Path and return it as a String
+     * using the specified charset.
+     *
+     * <pre class="groovyTestCase">
+     * import java.nio.file.Files
+     * def path = Files.createTempFile('test', '.txt')
+     * path.text = 'Hello async'
+     * assert path.getTextAsync('UTF-8').get() == 'Hello async'
+     * Files.delete(path)
+     * </pre>
+     *
+     * @param self    the file whose content we want to read
+     * @param charset the charset used to decode the file content
+     * @return a CompletableFuture that completes with the file content as a 
String
+     * @since 6.0.0
+     */
+    public static CompletableFuture<String> getTextAsync(Path self, String 
charset) {
+        return getBytesAsync(self).thenApply(bytes -> new String(bytes, 
Charset.forName(charset)));
+    }
+
+    /**
+     * Asynchronously read the content of the Path and return it as a byte 
array.
+     * <p>
+     * Uses {@link AsynchronousFileChannel} internally. The entire file content
+     * is read into a byte array.
+     *
+     * <pre class="groovyTestCase">
+     * import java.nio.file.Files
+     * def path = Files.createTempFile('test', '.txt')
+     * path.bytes = [72, 105] as byte[]
+     * assert path.bytesAsync.get() == [72, 105] as byte[]
+     * Files.delete(path)
+     * </pre>
+     *
+     * @param self the file whose content we want to read
+     * @return a CompletableFuture that completes with the file content as a 
byte array
+     * @since 6.0.0
+     */
+    public static CompletableFuture<byte[]> getBytesAsync(Path self) {
+        CompletableFuture<byte[]> result = new CompletableFuture<>();
+        AsynchronousFileChannel channel = null;
+        try {
+            channel = AsynchronousFileChannel.open(self, 
StandardOpenOption.READ);
+            long size = channel.size();
+            if (size > Integer.MAX_VALUE) {
+                channel.close();
+                result.completeExceptionally(new IOException("File too large 
to read into a single byte array: " + size + " bytes"));
+                return result;
+            }
+            ByteBuffer buf = ByteBuffer.allocate((int) size);
+            readFully(channel, buf, 0, result);
+        } catch (IOException | RuntimeException e) {
+            if (channel != null) {
+                try {
+                    channel.close();
+                } catch (IOException ignored) {
+                }
+            }
+            result.completeExceptionally(e);
+        }
+        return result;
+    }
+
+    private static void readFully(AsynchronousFileChannel channel, ByteBuffer 
buf,
+                                  long position, CompletableFuture<byte[]> 
result) {
+        channel.read(buf, position, null, new CompletionHandler<Integer, 
Void>() {
+            @Override
+            public void completed(Integer bytesRead, Void attachment) {
+                if (bytesRead == -1 || !buf.hasRemaining()) {
+                    try {
+                        channel.close();
+                    } catch (IOException e) {
+                        result.completeExceptionally(e);
+                        return;
+                    }
+                    buf.flip();
+                    byte[] bytes = new byte[buf.remaining()];
+                    buf.get(bytes);
+                    result.complete(bytes);
+                } else {
+                    readFully(channel, buf, position + bytesRead, result);
+                }
+            }
+
+            @Override
+            public void failed(Throwable exc, Void attachment) {
+                try {
+                    channel.close();
+                } catch (IOException ignored) {
+                }
+                result.completeExceptionally(exc);
+            }
+        });
+    }
+
+    /**
+     * Asynchronously write the String content to the Path using the
+     * platform's default charset.
+     *
+     * <pre class="groovyTestCase">
+     * import java.nio.file.Files
+     * def path = Files.createTempFile('test', '.txt')
+     * path.writeAsync('Hello async').get()
+     * assert path.text == 'Hello async'
+     * Files.delete(path)
+     * </pre>
+     *
+     * @param self the file to write to
+     * @param text the text to write
+     * @return a CompletableFuture that completes when the write is finished
+     * @since 6.0.0
+     */
+    public static CompletableFuture<Void> writeAsync(Path self, String text) {
+        return writeAsync(self, text, Charset.defaultCharset().name());
+    }
+
+    /**
+     * Asynchronously write the String content to the Path using the
+     * specified charset.
+     *
+     * <pre class="groovyTestCase">
+     * import java.nio.file.Files
+     * def path = Files.createTempFile('test', '.txt')
+     * path.writeAsync('Hello async', 'UTF-8').get()
+     * assert path.text == 'Hello async'
+     * Files.delete(path)
+     * </pre>
+     *
+     * @param self    the file to write to
+     * @param text    the text to write
+     * @param charset the charset used to encode the text
+     * @return a CompletableFuture that completes when the write is finished
+     * @since 6.0.0
+     */
+    public static CompletableFuture<Void> writeAsync(Path self, String text, 
String charset) {
+        return writeBytesAsync(self, text.getBytes(Charset.forName(charset)));
+    }
+
+    /**
+     * Asynchronously write the byte array to the Path, creating the file
+     * if it doesn't exist and truncating it if it does.
+     *
+     * <pre class="groovyTestCase">
+     * import java.nio.file.Files
+     * def path = Files.createTempFile('test', '.txt')
+     * path.writeBytesAsync([72, 105] as byte[]).get()
+     * assert path.bytes == [72, 105] as byte[]
+     * Files.delete(path)
+     * </pre>
+     *
+     * @param self  the file to write to
+     * @param bytes the byte array to write
+     * @return a CompletableFuture that completes when the write is finished
+     * @since 6.0.0
+     */
+    public static CompletableFuture<Void> writeBytesAsync(Path self, byte[] 
bytes) {
+        CompletableFuture<Void> result = new CompletableFuture<>();
+        try {
+            AsynchronousFileChannel channel = 
AsynchronousFileChannel.open(self,
+                    StandardOpenOption.WRITE, StandardOpenOption.CREATE, 
StandardOpenOption.TRUNCATE_EXISTING);
+            ByteBuffer buf = ByteBuffer.wrap(bytes);
+            writeFully(channel, buf, 0, result);
+        } catch (IOException e) {
+            result.completeExceptionally(e);
+        }
+        return result;
+    }
+
+    private static void writeFully(AsynchronousFileChannel channel, ByteBuffer 
buf,
+                                   long position, CompletableFuture<Void> 
result) {
+        channel.write(buf, position, null, new CompletionHandler<Integer, 
Void>() {
+            @Override
+            public void completed(Integer bytesWritten, Void attachment) {
+                if (buf.hasRemaining()) {
+                    writeFully(channel, buf, position + bytesWritten, result);
+                } else {
+                    try {
+                        channel.close();
+                    } catch (IOException e) {

Review Comment:
   `writeFully` recurses with `position + bytesWritten`. If `bytesWritten` is 
`0` while the buffer still has remaining data (possible for async writes), this 
can recurse forever without progress. Handle the `bytesWritten == 0` case 
explicitly to avoid infinite recursion / stack overflow.



##########
subprojects/groovy-nio/src/test/groovy/org/apache/groovy/nio/extensions/NioExtensionsTest.groovy:
##########
@@ -1296,4 +1296,99 @@ class NioExtensionsTest extends Specification {
         Files.exists(destPath)
         !Files.exists(path)
     }
+
+    // --

> minor groovy-nio improvements
> -----------------------------
>
>                 Key: GROOVY-11902
>                 URL: https://issues.apache.org/jira/browse/GROOVY-11902
>             Project: Groovy
>          Issue Type: Improvement
>            Reporter: Paul King
>            Assignee: Paul King
>            Priority: Major
>
> ⏺ Asynchronous file I/O extension methods for java.nio.file.Path              
>                                                                               
>                                     
>   Adds six new extension methods to Path via groovy-nio that use 
> AsynchronousFileChannel for non-blocking file I/O, returning 
> CompletableFuture results:
>   Reading:                                                                    
>                                                                               
>                                     
>   - path.getTextAsync() → CompletableFuture<String> (default charset)
>   - path.getTextAsync(charset) → CompletableFuture<String>                    
>                                                                               
>                                     
>   - path.getBytesAsync() → CompletableFuture<byte[]>        
>   Writing:                                                                    
>                                                                               
>                                     
>   - path.writeAsync(text) → CompletableFuture<Void> (default charset)
>   - path.writeAsync(text, charset) → CompletableFuture<Void>                  
>                                                                               
>                                     
>   - path.writeBytesAsync(bytes) → CompletableFuture<Void>   
>   Groovy property syntax works naturally: path.textAsync, path.bytesAsync.
> Usage: 
> {code:groovy}                                                   
> // blocking                                                                   
>                                                                               
>                                   
> def text = path.textAsync.get()                           
> // composition                                                                
>                                                                               
>                                   
> path.textAsync.thenApply { it.toUpperCase() }
> // with async/await (potential future Groovy 6 feature)
> def text = await path.textAsync
> {code}



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to