Skip to content

Commit fead705

Browse files
committed
GROOVY-11902: minor groovy-nio improvements
1 parent 5aa4a0e commit fead705

5 files changed

Lines changed: 370 additions & 0 deletions

File tree

build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ dependencies {
109109
}
110110

111111
testImplementation projects.groovyAnt
112+
testImplementation projects.groovyNio
112113
testImplementation projects.groovyXml
113114
testImplementation projects.groovyJson
114115
testImplementation projects.groovyTest

src/spec/doc/_working-with-io.adoc

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,17 @@ makes it very easy actually:
8888
include::../test/gdk/WorkingWithIOSpecTest.groovy[tags=file_bytes,indent=0]
8989
----
9090
91+
Groovy also provides asynchronous variants for reading and writing files via the `groovy-nio` module.
92+
These return a `CompletableFuture`, making them composable and easy to use with async frameworks.
93+
94+
[source,groovy]
95+
----
96+
include::../test/gdk/WorkingWithIOSpecTest.groovy[tags=async_read,indent=0]
97+
----
98+
99+
Similarly, `path.bytesAsync` returns a `CompletableFuture<byte[]>`, and
100+
`path.writeAsync(text)` writes content asynchronously.
101+
91102
Working with I/O is not limited to dealing with files. In fact, a lot of operations rely on input/output streams,
92103
hence why Groovy adds a lot of support methods to those, as you can see in the
93104
http://docs.groovy-lang.org/latest/html/groovy-jdk/java/io/InputStream.html[documentation].

src/spec/test/gdk/WorkingWithIOSpecTest.groovy

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,17 @@ Le bruit de l'eau.''')
7979
// tag::file_bytes[]
8080
byte[] contents = file.bytes
8181
// end::file_bytes[]
82+
83+
// tag::async_read[]
84+
def path = file.toPath()
85+
def future = path.textAsync // returns CompletableFuture<String>
86+
// end::async_read[]
87+
/*
88+
// tag::async_read[]
89+
println future.get() // blocks until content is available
90+
// end::async_read[]
91+
*/
92+
println future.get(5, java.util.concurrent.TimeUnit.SECONDS)
8293
}
8394
}
8495

subprojects/groovy-nio/src/main/java/org/apache/groovy/nio/extensions/NioExtensions.java

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,18 +56,23 @@
5656
import java.io.Reader;
5757
import java.io.Writer;
5858
import java.net.URI;
59+
import java.nio.ByteBuffer;
60+
import java.nio.channels.AsynchronousFileChannel;
61+
import java.nio.channels.CompletionHandler;
5962
import java.nio.charset.Charset;
6063
import java.nio.file.DirectoryStream;
6164
import java.nio.file.Files;
6265
import java.nio.file.LinkOption;
6366
import java.nio.file.Path;
6467
import java.nio.file.Paths;
6568
import java.nio.file.attribute.FileAttribute;
69+
import java.nio.file.StandardOpenOption;
6670
import java.util.HashMap;
6771
import java.util.Iterator;
6872
import java.util.LinkedList;
6973
import java.util.List;
7074
import java.util.Map;
75+
import java.util.concurrent.CompletableFuture;
7176
import java.util.regex.Pattern;
7277

7378
import static java.lang.Boolean.FALSE;
@@ -2036,4 +2041,251 @@ public static <T> T withCloseable(Closeable self, @ClosureParams(value = SimpleT
20362041
return IOGroovyMethods.withCloseable(self, action);
20372042
}
20382043

2044+
// -------------------------------------------------------------------------
2045+
// Asynchronous file I/O extensions
2046+
// -------------------------------------------------------------------------
2047+
2048+
/**
2049+
* Asynchronously read the content of the Path and return it as a String
2050+
* using the platform's default charset.
2051+
* <p>
2052+
* Uses {@link AsynchronousFileChannel} internally, making the I/O
2053+
* non-blocking. The returned {@link CompletableFuture} completes
2054+
* when the entire file has been read.
2055+
*
2056+
* <pre class="groovyTestCase">
2057+
* import java.nio.file.Files
2058+
* def path = Files.createTempFile('test', '.txt')
2059+
* path.text = 'Hello async'
2060+
* assert path.textAsync.get() == 'Hello async'
2061+
* Files.delete(path)
2062+
* </pre>
2063+
*
2064+
* @param self the file whose content we want to read
2065+
* @return a CompletableFuture that completes with the file content as a String
2066+
* @since 6.0.0
2067+
*/
2068+
public static CompletableFuture<String> getTextAsync(Path self) {
2069+
return getTextAsync(self, Charset.defaultCharset().name());
2070+
}
2071+
2072+
/**
2073+
* Asynchronously read the content of the Path and return it as a String
2074+
* using the specified charset.
2075+
*
2076+
* <pre class="groovyTestCase">
2077+
* import java.nio.file.Files
2078+
* def path = Files.createTempFile('test', '.txt')
2079+
* path.text = 'Hello async'
2080+
* assert path.getTextAsync('UTF-8').get() == 'Hello async'
2081+
* Files.delete(path)
2082+
* </pre>
2083+
*
2084+
* @param self the file whose content we want to read
2085+
* @param charset the charset used to decode the file content
2086+
* @return a CompletableFuture that completes with the file content as a String
2087+
* @since 6.0.0
2088+
*/
2089+
public static CompletableFuture<String> getTextAsync(Path self, String charset) {
2090+
return getBytesAsync(self).thenApply(bytes -> new String(bytes, Charset.forName(charset)));
2091+
}
2092+
2093+
/**
2094+
* Asynchronously read the content of the Path and return it as a byte array.
2095+
* <p>
2096+
* Uses {@link AsynchronousFileChannel} internally. The entire file content
2097+
* is read into a byte array.
2098+
*
2099+
* <pre class="groovyTestCase">
2100+
* import java.nio.file.Files
2101+
* def path = Files.createTempFile('test', '.txt')
2102+
* path.bytes = [72, 105] as byte[]
2103+
* assert path.bytesAsync.get() == [72, 105] as byte[]
2104+
* Files.delete(path)
2105+
* </pre>
2106+
*
2107+
* @param self the file whose content we want to read
2108+
* @return a CompletableFuture that completes with the file content as a byte array
2109+
* @since 6.0.0
2110+
*/
2111+
public static CompletableFuture<byte[]> getBytesAsync(Path self) {
2112+
CompletableFuture<byte[]> result = new CompletableFuture<>();
2113+
AsynchronousFileChannel channel = null;
2114+
try {
2115+
channel = AsynchronousFileChannel.open(self, StandardOpenOption.READ);
2116+
long size = channel.size();
2117+
if (size > Integer.MAX_VALUE) {
2118+
channel.close();
2119+
result.completeExceptionally(new IOException("File too large to read into a single byte array: " + size + " bytes"));
2120+
return result;
2121+
}
2122+
ByteBuffer buf = ByteBuffer.allocate((int) size);
2123+
readFully(channel, buf, 0, result);
2124+
} catch (IOException | RuntimeException e) {
2125+
if (channel != null) {
2126+
try {
2127+
channel.close();
2128+
} catch (IOException ignored) {
2129+
}
2130+
}
2131+
result.completeExceptionally(e);
2132+
}
2133+
return result;
2134+
}
2135+
2136+
private static void readFully(AsynchronousFileChannel channel, ByteBuffer buf,
2137+
long position, CompletableFuture<byte[]> result) {
2138+
channel.read(buf, position, null, new CompletionHandler<Integer, Void>() {
2139+
@Override
2140+
public void completed(Integer bytesRead, Void attachment) {
2141+
if (bytesRead == -1 || !buf.hasRemaining()) {
2142+
try {
2143+
channel.close();
2144+
} catch (IOException e) {
2145+
result.completeExceptionally(e);
2146+
return;
2147+
}
2148+
buf.flip();
2149+
byte[] bytes = new byte[buf.remaining()];
2150+
buf.get(bytes);
2151+
result.complete(bytes);
2152+
} else {
2153+
readFully(channel, buf, position + bytesRead, result);
2154+
}
2155+
}
2156+
2157+
@Override
2158+
public void failed(Throwable exc, Void attachment) {
2159+
try {
2160+
channel.close();
2161+
} catch (IOException ignored) {
2162+
}
2163+
result.completeExceptionally(exc);
2164+
}
2165+
});
2166+
}
2167+
2168+
/**
2169+
* Asynchronously write the String content to the Path using the
2170+
* platform's default charset.
2171+
*
2172+
* <pre class="groovyTestCase">
2173+
* import java.nio.file.Files
2174+
* def path = Files.createTempFile('test', '.txt')
2175+
* path.writeAsync('Hello async').get()
2176+
* assert path.text == 'Hello async'
2177+
* Files.delete(path)
2178+
* </pre>
2179+
*
2180+
* @param self the file to write to
2181+
* @param text the text to write
2182+
* @return a CompletableFuture that completes when the write is finished
2183+
* @since 6.0.0
2184+
*/
2185+
public static CompletableFuture<Void> writeAsync(Path self, String text) {
2186+
return writeAsync(self, text, Charset.defaultCharset().name());
2187+
}
2188+
2189+
/**
2190+
* Asynchronously write the String content to the Path using the
2191+
* specified charset.
2192+
*
2193+
* <pre class="groovyTestCase">
2194+
* import java.nio.file.Files
2195+
* def path = Files.createTempFile('test', '.txt')
2196+
* path.writeAsync('Hello async', 'UTF-8').get()
2197+
* assert path.text == 'Hello async'
2198+
* Files.delete(path)
2199+
* </pre>
2200+
*
2201+
* @param self the file to write to
2202+
* @param text the text to write
2203+
* @param charset the charset used to encode the text
2204+
* @return a CompletableFuture that completes when the write is finished
2205+
* @since 6.0.0
2206+
*/
2207+
public static CompletableFuture<Void> writeAsync(Path self, String text, String charset) {
2208+
try {
2209+
return writeBytesAsync(self, text.getBytes(Charset.forName(charset)));
2210+
} catch (RuntimeException e) {
2211+
CompletableFuture<Void> result = new CompletableFuture<>();
2212+
result.completeExceptionally(e);
2213+
return result;
2214+
}
2215+
}
2216+
2217+
/**
2218+
* Asynchronously write the byte array to the Path, creating the file
2219+
* if it doesn't exist and truncating it if it does.
2220+
*
2221+
* <pre class="groovyTestCase">
2222+
* import java.nio.file.Files
2223+
* def path = Files.createTempFile('test', '.txt')
2224+
* path.writeBytesAsync([72, 105] as byte[]).get()
2225+
* assert path.bytes == [72, 105] as byte[]
2226+
* Files.delete(path)
2227+
* </pre>
2228+
*
2229+
* @param self the file to write to
2230+
* @param bytes the byte array to write
2231+
* @return a CompletableFuture that completes when the write is finished
2232+
* @since 6.0.0
2233+
*/
2234+
public static CompletableFuture<Void> writeBytesAsync(Path self, byte[] bytes) {
2235+
CompletableFuture<Void> result = new CompletableFuture<>();
2236+
AsynchronousFileChannel channel = null;
2237+
try {
2238+
channel = AsynchronousFileChannel.open(self,
2239+
StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
2240+
ByteBuffer buf = ByteBuffer.wrap(bytes);
2241+
writeFully(channel, buf, 0, result);
2242+
} catch (IOException | RuntimeException e) {
2243+
if (channel != null) {
2244+
try {
2245+
channel.close();
2246+
} catch (IOException ignored) {
2247+
}
2248+
}
2249+
result.completeExceptionally(e);
2250+
}
2251+
return result;
2252+
}
2253+
2254+
private static void writeFully(AsynchronousFileChannel channel, ByteBuffer buf,
2255+
long position, CompletableFuture<Void> result) {
2256+
channel.write(buf, position, null, new CompletionHandler<Integer, Void>() {
2257+
@Override
2258+
public void completed(Integer bytesWritten, Void attachment) {
2259+
if (buf.hasRemaining()) {
2260+
if (bytesWritten <= 0) {
2261+
try {
2262+
channel.close();
2263+
} catch (IOException ignored) {
2264+
}
2265+
result.completeExceptionally(new IOException("Async write made no progress"));
2266+
return;
2267+
}
2268+
writeFully(channel, buf, position + bytesWritten, result);
2269+
} else {
2270+
try {
2271+
channel.close();
2272+
} catch (IOException e) {
2273+
result.completeExceptionally(e);
2274+
return;
2275+
}
2276+
result.complete(null);
2277+
}
2278+
}
2279+
2280+
@Override
2281+
public void failed(Throwable exc, Void attachment) {
2282+
try {
2283+
channel.close();
2284+
} catch (IOException ignored) {
2285+
}
2286+
result.completeExceptionally(exc);
2287+
}
2288+
});
2289+
}
2290+
20392291
}

0 commit comments

Comments
 (0)