|
56 | 56 | import java.io.Reader; |
57 | 57 | import java.io.Writer; |
58 | 58 | import java.net.URI; |
| 59 | +import java.nio.ByteBuffer; |
| 60 | +import java.nio.channels.AsynchronousFileChannel; |
| 61 | +import java.nio.channels.CompletionHandler; |
59 | 62 | import java.nio.charset.Charset; |
60 | 63 | import java.nio.file.DirectoryStream; |
61 | 64 | import java.nio.file.Files; |
62 | 65 | import java.nio.file.LinkOption; |
63 | 66 | import java.nio.file.Path; |
64 | 67 | import java.nio.file.Paths; |
65 | 68 | import java.nio.file.attribute.FileAttribute; |
| 69 | +import java.nio.file.StandardOpenOption; |
66 | 70 | import java.util.HashMap; |
67 | 71 | import java.util.Iterator; |
68 | 72 | import java.util.LinkedList; |
69 | 73 | import java.util.List; |
70 | 74 | import java.util.Map; |
| 75 | +import java.util.concurrent.CompletableFuture; |
71 | 76 | import java.util.regex.Pattern; |
72 | 77 |
|
73 | 78 | import static java.lang.Boolean.FALSE; |
@@ -2036,4 +2041,251 @@ public static <T> T withCloseable(Closeable self, @ClosureParams(value = SimpleT |
2036 | 2041 | return IOGroovyMethods.withCloseable(self, action); |
2037 | 2042 | } |
2038 | 2043 |
|
| 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 | + |
2039 | 2291 | } |
0 commit comments