|
8 | 8 | import static com.softwaremill.jox.structured.Scopes.unsupervised; |
9 | 9 | import static java.lang.Thread.sleep; |
10 | 10 |
|
| 11 | +import java.io.IOException; |
| 12 | +import java.io.InputStream; |
| 13 | +import java.io.OutputStream; |
11 | 14 | import java.nio.ByteBuffer; |
12 | 15 | import java.nio.charset.Charset; |
13 | 16 | import java.nio.charset.StandardCharsets; |
| 17 | +import java.nio.channels.FileChannel; |
| 18 | +import java.nio.channels.SeekableByteChannel; |
| 19 | +import java.nio.file.Files; |
| 20 | +import java.nio.file.Path; |
| 21 | +import java.nio.file.StandardOpenOption; |
14 | 22 | import java.time.Duration; |
15 | 23 | import java.util.ArrayList; |
16 | 24 | import java.util.Arrays; |
@@ -592,7 +600,6 @@ case ChannelError(Throwable cause): |
592 | 600 | if (!buffer.isEmpty()) { |
593 | 601 | sendBufferAndCleanupCost.call(); |
594 | 602 | // cancel existing timeout and start a new one |
595 | | - if (timeoutFork != null) timeoutFork.cancelNow(); |
596 | 603 | timeoutFork = forkTimeout(scope, timerChannel, duration); |
597 | 604 | } |
598 | 605 | yield true; |
@@ -1290,6 +1297,127 @@ public Flow<T> alsoToTap(Sink<T> other) { |
1290 | 1297 | }); |
1291 | 1298 | } |
1292 | 1299 |
|
| 1300 | + /** |
| 1301 | + * Runs the flow into a {@link java.io.InputStream}. |
| 1302 | + * <p> |
| 1303 | + * Must be run within a concurrency scope, as under the hood the flow is run in the background. |
| 1304 | + * <p> |
| 1305 | + * Buffer capacity can be set via scoped value {@link Channel#BUFFER_SIZE}. If not specified in scope, {@link Channel#DEFAULT_BUFFER_SIZE} is used. |
| 1306 | + */ |
| 1307 | + public InputStream runToInputStream(UnsupervisedScope scope) { |
| 1308 | + Source<byte[]> ch = this |
| 1309 | + .map(t -> { |
| 1310 | + if (t instanceof byte[] bytes) { |
| 1311 | + return bytes; |
| 1312 | + } else { |
| 1313 | + throw new IllegalArgumentException("requirement failed: method can be called only on flow containing byte[]"); |
| 1314 | + } |
| 1315 | + }) |
| 1316 | + .runToChannel(scope); |
| 1317 | + |
| 1318 | + return new InputStream() { |
| 1319 | + private ByteArrayIterator currentChunk = ByteArrayIterator.empty(); |
| 1320 | + |
| 1321 | + @Override |
| 1322 | + public int read() { |
| 1323 | + try { |
| 1324 | + if (!currentChunk.hasNext()) { |
| 1325 | + Object result = ch.receiveOrClosed(); |
| 1326 | + if (result instanceof ChannelDone) { |
| 1327 | + return -1; |
| 1328 | + } else if (result instanceof ChannelError error) { |
| 1329 | + throw error.toException(); |
| 1330 | + } else { |
| 1331 | + byte[] chunk = (byte[]) result; |
| 1332 | + currentChunk = new ByteArrayIterator(chunk); |
| 1333 | + } |
| 1334 | + } |
| 1335 | + return currentChunk.next() & 0xff; // Convert to unsigned |
| 1336 | + } catch (InterruptedException e) { |
| 1337 | + throw new RuntimeException(e); |
| 1338 | + } |
| 1339 | + } |
| 1340 | + |
| 1341 | + @Override |
| 1342 | + public int available() { |
| 1343 | + return currentChunk.available(); |
| 1344 | + } |
| 1345 | + }; |
| 1346 | + } |
| 1347 | + |
| 1348 | + /** |
| 1349 | + * Writes content of this flow to an {@link java.io.OutputStream}. |
| 1350 | + * |
| 1351 | + * @param outputStream |
| 1352 | + * Target `OutputStream` to write to. Will be closed after finishing the process or on error. |
| 1353 | + */ |
| 1354 | + public void runToOutputStream(OutputStream outputStream) throws Exception { |
| 1355 | + try { |
| 1356 | + last.run(t -> { |
| 1357 | + if (t instanceof byte[] chunk) { |
| 1358 | + outputStream.write(chunk); |
| 1359 | + } else { |
| 1360 | + throw new IllegalArgumentException("requirement failed: method can be called only on flow containing byte[]"); |
| 1361 | + } |
| 1362 | + }); |
| 1363 | + close(outputStream, null); |
| 1364 | + } catch (Exception e) { |
| 1365 | + close(outputStream, e); |
| 1366 | + throw e; |
| 1367 | + } |
| 1368 | + } |
| 1369 | + |
| 1370 | + /** Writes content of this flow to a file. |
| 1371 | + * |
| 1372 | + * @param path |
| 1373 | + * Path to the target file. If not exists, it will be created.s |
| 1374 | + */ |
| 1375 | + public void runToFile(Path path) throws Exception { |
| 1376 | + if (Files.isDirectory(path)) { |
| 1377 | + throw new IOException("Path %s is a directory".formatted(path)); |
| 1378 | + } |
| 1379 | + final SeekableByteChannel channel = getFileChannel(path); |
| 1380 | + try { |
| 1381 | + map(t -> { |
| 1382 | + if (t instanceof byte[] chunk) { |
| 1383 | + return chunk; |
| 1384 | + } else { |
| 1385 | + throw new IllegalArgumentException("requirement failed: method can be called only on flow containing byte[]"); |
| 1386 | + } |
| 1387 | + }).runForeach(chunk -> { |
| 1388 | + try { |
| 1389 | + channel.write(ByteBuffer.wrap(chunk)); |
| 1390 | + } catch (IOException e) { |
| 1391 | + throw new RuntimeException(e); |
| 1392 | + } |
| 1393 | + }); |
| 1394 | + close(channel, null); |
| 1395 | + } catch (Exception t) { |
| 1396 | + close(channel, t); |
| 1397 | + throw t; |
| 1398 | + } |
| 1399 | + } |
| 1400 | + |
| 1401 | + private SeekableByteChannel getFileChannel(Path path) throws IOException { |
| 1402 | + try { |
| 1403 | + return FileChannel.open(path, StandardOpenOption.WRITE, StandardOpenOption.CREATE); |
| 1404 | + } catch (UnsupportedOperationException e) { |
| 1405 | + // Some file systems don't support file channels |
| 1406 | + return Files.newByteChannel(path, StandardOpenOption.WRITE, StandardOpenOption.CREATE); |
| 1407 | + } |
| 1408 | + } |
| 1409 | + |
| 1410 | + private void close(AutoCloseable closeable, Exception cause) throws Exception { |
| 1411 | + try { |
| 1412 | + closeable.close(); |
| 1413 | + } catch (IOException e) { |
| 1414 | + if (cause != null) { |
| 1415 | + cause.addSuppressed(e); |
| 1416 | + } |
| 1417 | + throw cause != null ? cause : e; |
| 1418 | + } |
| 1419 | + } |
| 1420 | + |
1293 | 1421 | /** Converts this {@link Flow} into a {@link Publisher}. The flow is run every time the publisher is subscribed to. |
1294 | 1422 | * <p> |
1295 | 1423 | * Must be run within a concurrency scope, as upon subscribing, a fork is created to run the publishing process. Hence, the scope should |
|
0 commit comments