├── .gitignore ├── LICENSE ├── README.md ├── src ├── test │ └── java │ │ └── org │ │ └── javasync │ │ └── streams │ │ └── test │ │ ├── Weather.java │ │ ├── MemoizeTest.java │ │ ├── ReadmeDemos.java │ │ └── ReplayTest.java └── main │ └── java │ └── org │ └── javasync │ └── streams │ └── Replayer.java └── pom.xml /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | pom.xml.tag 3 | pom.xml.releaseBackup 4 | pom.xml.versionsBackup 5 | pom.xml.next 6 | release.properties 7 | dependency-reduced-pom.xml 8 | buildNumber.properties 9 | .mvn/timing.properties 10 | 11 | # Avoid ignoring Maven wrapper jar file (.jar files are usually ignored) 12 | !/.mvn/wrapper/maven-wrapper.jar 13 | 14 | # InteliJ 15 | .idea 16 | *.iml -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 CCISEL 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # streams 2 | 3 | [![Build Status](https://sonarcloud.io/api/project_badges/measure?project=com.github.javasync%3Astreamemo&metric=alert_status)](https://sonarcloud.io/dashboard?id=com.github.javasync%3Astreamemo) 4 | [![Maven Central Version](https://img.shields.io/maven-central/v/com.github.javasync/streamemo.svg)](https://search.maven.org/#search%7Cga%7C1%7Ca%3A%22streamemo%22) 5 | [![Coverage Status](https://sonarcloud.io/api/project_badges/measure?project=com.github.javasync%3Astreamemo&metric=coverage)](https://sonarcloud.io/component_measures?id=com.github.javasync%3Astreamemo&metric=Coverage) 6 | 7 | 8 | Java streams utility methods for memoization 9 | 10 | ## How to replay Java streams? 11 | 12 | Need to use your streams over and over again? Let's cover three different 13 | approaches, their benefits, and their pitfalls when recycling Java streams. 14 | 15 | Read more here https://dzone.com/articles/how-to-replay-java-streams 16 | 17 | ## Usage 18 | 19 | ```java 20 | Random rnd = new Random(); 21 | Stream nrs = Stream.generate(() -> rnd.nextInt(99)); 22 | Supplier> nrsSrc = Replayer.replay(nrs); 23 | 24 | nrsSrc.get().limit(11).map(n -> n + ",").forEach(out::print); // e.g. 88,18,78,75,98,68,15,14,25,54,22, 25 | out.println(); 26 | nrsSrc.get().limit(11).map(n -> n + ",").forEach(out::print); // Print the same previous numbers 27 | ``` 28 | 29 | Note that you cannot achieve this result with an intermediate 30 | collection because `nrs` is an infinite stream. 31 | Thus trying to collect `nrs` incurs in an infinite loop. 32 | Only on-demand memoization like `replay()` achieves this approach. 33 | 34 | ## Installation 35 | 36 | First, in order to include it to your Maven project, 37 | simply add this dependency: 38 | 39 | ```xml 40 | 41 | com.github.javasync 42 | streamemo 43 | 1.0.1 44 | 45 | ``` 46 | 47 | To add a dependency using Gradle: 48 | 49 | ``` 50 | dependencies { 51 | compile 'com.github.javasync:streamemo:1.0.0' 52 | } 53 | ``` 54 | 55 | ## Changelog 56 | 57 | ### 1.0.1 (August, 2019) 58 | 59 | Add the ability to close the original stream. 60 | Now the the `onClose()` method of a stream from the `Supplier.get()` 61 | will trigger a call to the original Stream's onClose() method. 62 | Contribution from shollander issue #2. 63 | 64 | ### 1.0.0 (June, 2018) 65 | 66 | First release according to the article "How to Reuse Java Streams" published on DZone at Jun. 12, 18 -------------------------------------------------------------------------------- /src/test/java/org/javasync/streams/test/Weather.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2018, Miguel Gamboa (gamboa.pt) 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package org.javasync.streams.test; 26 | 27 | import org.asynchttpclient.AsyncHttpClient; 28 | import org.asynchttpclient.Response; 29 | 30 | import java.io.IOException; 31 | import java.io.UncheckedIOException; 32 | import java.time.LocalDate; 33 | import java.util.concurrent.CompletableFuture; 34 | import java.util.regex.Pattern; 35 | import java.util.stream.IntStream; 36 | import java.util.stream.Stream; 37 | 38 | import static org.asynchttpclient.Dsl.asyncHttpClient; 39 | 40 | public class Weather{ 41 | static final String KEY = "e841239881974af5b1480345192207"; 42 | static final String HOST = "http://api.worldweatheronline.com/"; 43 | static final String PATH = HOST + "premium/v1/past-weather.ashx?q=%s,%s&date=%s&enddate=%s&tp=24&format=csv&key=%s"; 44 | static final Pattern NEWLINE = Pattern.compile("\\n"); 45 | static final Pattern COMMA = Pattern.compile(","); 46 | 47 | public static CompletableFuture getTemperaturesAsync( 48 | double lat, 49 | double log, 50 | LocalDate from, 51 | LocalDate to) 52 | { 53 | AsyncHttpClient asyncHttpClient = asyncHttpClient(); 54 | CompletableFuture> csv = asyncHttpClient 55 | .prepareGet(String.format(PATH, lat, log, from, to, KEY)) 56 | .execute() 57 | .toCompletableFuture() 58 | .thenApply(Weather::checkResponseStatus) 59 | .thenApply(Response::getResponseBody) 60 | .thenApply(NEWLINE::splitAsStream); 61 | boolean[] isEven = {true}; 62 | CompletableFuture temps = csv.thenApply(str -> str 63 | .filter(w -> !w.startsWith("#")) // Filter comments 64 | .skip(1) // Skip line: Not Available 65 | .filter(l -> isEven[0] = !isEven[0]) // Filter Even line 66 | .map(line -> COMMA.splitAsStream(line).skip(2).findFirst().get()) // Extract temperature in Celsius 67 | .mapToInt(Integer::parseInt));// Convert to Integer 68 | return temps.thenApply(__ -> { 69 | close(asyncHttpClient); 70 | return __; 71 | }); 72 | } 73 | 74 | private static Response checkResponseStatus(Response resp) { 75 | if(resp.getStatusCode() != 200) 76 | throw new IllegalStateException(String.format( 77 | "%s: %s -- %s", resp.getStatusCode(), resp.getStatusText(), resp.getResponseBody() 78 | )); 79 | return resp; 80 | } 81 | 82 | private static void close(AsyncHttpClient asyncHttpClient) { 83 | try { 84 | asyncHttpClient.close(); 85 | } catch (IOException e) { 86 | new UncheckedIOException(e); 87 | } 88 | } 89 | } -------------------------------------------------------------------------------- /src/test/java/org/javasync/streams/test/MemoizeTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2018, Miguel Gamboa (gamboa.pt) 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package org.javasync.streams.test; 26 | 27 | import org.junit.jupiter.api.Test; 28 | 29 | import java.io.IOException; 30 | import java.util.ArrayList; 31 | import java.util.Comparator; 32 | import java.util.ConcurrentModificationException; 33 | import java.util.Random; 34 | import java.util.Spliterator; 35 | import java.util.Spliterators; 36 | import java.util.function.Consumer; 37 | import java.util.function.Supplier; 38 | import java.util.stream.IntStream; 39 | import java.util.stream.Stream; 40 | 41 | import static java.lang.System.out; 42 | import static java.util.stream.Stream.concat; 43 | import static java.util.stream.StreamSupport.stream; 44 | import static org.junit.jupiter.api.Assertions.assertThrows; 45 | 46 | public class MemoizeTest { 47 | 48 | /** 49 | * Creates a new stream supplier which memoizes items when they are traversed. 50 | * The stream created by the supplier retrieves items from: 1) the mem or 51 | * 2) the data source, depending on whether it has been already requested 52 | * by a previous operation, or not. 53 | * @param src 54 | * @param 55 | * @return 56 | */ 57 | public static Supplier> memoize(Stream src) { 58 | final Spliterator iter = src.spliterator(); 59 | final ArrayList mem = new ArrayList<>(); 60 | class MemoizeIter extends Spliterators.AbstractSpliterator { 61 | MemoizeIter() { super(iter.estimateSize(), iter.characteristics()); } 62 | public boolean tryAdvance(Consumer action) { 63 | return iter.tryAdvance(item -> { 64 | mem.add(item); 65 | action.accept(item); 66 | }); 67 | } 68 | public Comparator getComparator() { 69 | return iter.getComparator(); 70 | } 71 | } 72 | MemoizeIter srcIter = new MemoizeIter(); 73 | return () -> concat(mem.stream(), stream(srcIter, false)); 74 | } 75 | 76 | /** 77 | * This solution does not allow concurrent iterations while the data source has 78 | * not been entirely consumed. 79 | * In that case it throws a ConcurrentModificationException. 80 | */ 81 | @Test 82 | public void testWrongConcurrentIteratorsOnMemoizedStream() { 83 | assertThrows(ConcurrentModificationException.class, () -> { 84 | Random rnd = new Random(); 85 | Supplier> nrs = memoize(IntStream.range(1, 10).boxed()); 86 | Spliterator iter1 = nrs.get().spliterator(); 87 | iter1.tryAdvance(out::println); 88 | iter1.tryAdvance(out::println); 89 | Spliterator iter2 = nrs.get().spliterator(); 90 | iter1.tryAdvance(out::println); 91 | iter2.forEachRemaining(out::print); // throws ConcurrentModificationException 92 | System.out.println(); 93 | }); 94 | } 95 | 96 | @Test 97 | public void fourthExampleOfReadme() throws IOException, InterruptedException { 98 | IntStream nrs = new Random() 99 | .ints(0, 7) 100 | .peek(n -> out.printf("%d, ", n)) 101 | .limit(10); 102 | out.println("Stream nrs created!"); 103 | 104 | Supplier> mem = memoize(nrs.boxed()); 105 | out.println("Nrs wrapped in a memoizable Supplier "); 106 | 107 | Integer max = mem.get().max(Integer::compareTo).get(); 108 | out.println("Nrs traversed to get max = " + max); 109 | long maxOccurrences = mem.get().filter(max::equals).count(); 110 | out.println("Nrs traversed to count max occurrences = " + maxOccurrences); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/test/java/org/javasync/streams/test/ReadmeDemos.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2018, Miguel Gamboa (gamboa.pt) 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package org.javasync.streams.test; 26 | 27 | import org.asynchttpclient.Response; 28 | import org.junit.jupiter.api.Test; 29 | import reactor.core.publisher.Flux; 30 | 31 | import java.io.IOException; 32 | import java.util.List; 33 | import java.util.Random; 34 | import java.util.concurrent.CompletableFuture; 35 | import java.util.function.Supplier; 36 | import java.util.regex.Pattern; 37 | import java.util.stream.IntStream; 38 | import java.util.stream.Stream; 39 | 40 | import static java.lang.System.out; 41 | import static java.time.LocalDate.of; 42 | import static java.util.stream.Collectors.toList; 43 | import static org.asynchttpclient.Dsl.asyncHttpClient; 44 | 45 | 46 | public class ReadmeDemos { 47 | 48 | @Test 49 | public void thirdExample() throws IOException, InterruptedException { 50 | IntStream nrs = new Random() 51 | .ints(0, 7) 52 | .peek(n -> out.printf("%d, ", n)) 53 | .limit(10); 54 | out.println("Stream nrs created!"); 55 | 56 | CompletableFuture> mem = CompletableFuture 57 | .completedFuture(nrs) 58 | .thenApply(strm -> strm.boxed().collect(toList())); 59 | out.println("Nrs wraped in a CF and transformed in CF>!"); 60 | 61 | Supplier> nrsSource = () -> mem 62 | .join() 63 | .stream(); 64 | 65 | Integer max = nrsSource.get().max(Integer::compare).get(); 66 | out.println("Nrs traversed to get max = " + max); 67 | long maxOccurrences = nrsSource.get().filter(max::equals).count(); 68 | out.println("Nrs traversed to count max occurrences = " + maxOccurrences); 69 | } 70 | 71 | @Test 72 | public void secondApproach() throws IOException, InterruptedException { 73 | CompletableFuture> lst = Weather 74 | .getTemperaturesAsync(38.717, -9.133, of(2018, 4, 1), of(2018, 4, 30)) 75 | .thenApply(strm -> strm.boxed().collect(toList())); 76 | 77 | Supplier> lisbonTempsInMarch = () -> lst 78 | .join() 79 | .stream(); 80 | 81 | Integer maxTemp = lisbonTempsInMarch.get().max(Integer::compare).get(); 82 | long nrDaysWithMaxTemp = lisbonTempsInMarch.get().filter(maxTemp::equals).count(); 83 | 84 | out.println(maxTemp); 85 | out.println(nrDaysWithMaxTemp); 86 | } 87 | 88 | @Test 89 | public void firstApproach() throws IOException, InterruptedException { 90 | Supplier> lisbonTempsInMarch = () -> Weather 91 | .getTemperaturesAsync(38.717, -9.133, of(2018, 4, 1), of(2018, 4, 30)); 92 | 93 | long count = lisbonTempsInMarch.get().join().distinct().count(); 94 | int maxTemp = lisbonTempsInMarch.get().join().max().getAsInt(); 95 | 96 | out.println(count); 97 | out.println(maxTemp); 98 | } 99 | 100 | @Test 101 | public void testMemoizeReplayWithReactorFlux() throws InterruptedException { 102 | 103 | CompletableFuture> lisbonTempsInMarch = Weather 104 | .getTemperaturesAsync(38.717, -9.133, of(2018, 4, 1), of(2018, 4, 30)) 105 | .thenApply(IntStream::boxed); 106 | Flux cache = Flux 107 | .fromStream(lisbonTempsInMarch::join) 108 | .cache(); 109 | out.println("HTTP request sent"); 110 | Thread.currentThread().sleep(2000); 111 | out.println("Wake up"); 112 | Integer maxTemp = cache.reduce(Integer::max).block(); 113 | long nrDaysWithMaxTemp = cache.filter(maxTemp::equals).count().block(); 114 | out.println(maxTemp); 115 | out.println(nrDaysWithMaxTemp); 116 | } 117 | 118 | @Test 119 | public void firstExample() throws IOException, InterruptedException { 120 | Pattern pat = Pattern.compile("\\n"); 121 | CompletableFuture> csv = asyncHttpClient() 122 | .prepareGet("http://api.worldweatheronline.com/premium/v1/past-weather.ashx?q=37.017,-7.933&date=2018-04-01&enddate=2018-04-30&tp=24&format=csv&key=54a4f43fc39c435fa2c143536183004") 123 | .execute() 124 | .toCompletableFuture() 125 | .thenApply(Response::getResponseBody) 126 | .thenApply(pat::splitAsStream); 127 | 128 | boolean [] isEven = {true}; 129 | Pattern comma = Pattern.compile(","); 130 | CompletableFuture temps = csv.thenApply(str -> str 131 | .filter(w -> !w.startsWith("#")) // Filter comments 132 | .skip(1) // Skip line: Not Available 133 | .filter(l -> isEven[0] = !isEven[0]) // Filter Even line 134 | .map(line -> comma.splitAsStream(line).skip(2).findFirst().get()) // Extract temperature in Celsius 135 | .mapToInt(Integer::parseInt)); 136 | 137 | temps 138 | .thenAccept(ts -> ts.forEach(t -> System.out.print(t + ","))) 139 | .join(); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | com.github.javasync 5 | streamemo 6 | 1.0.2-SNAPSHOT 7 | jar 8 | 9 | Java streams utility methods for memoization. 10 | 11 | 12 | Memoizing and replaying items on demand into and from an internal buffer. 13 | 14 | https://github.com/javasync/streamemo 15 | 16 | 17 | MIT license 18 | https://opensource.org/licenses/MIT 19 | 20 | 21 | 22 | 23 | Miguel Gamboa 24 | miguelgamboa@outlook.com 25 | CCISEL 26 | http://gamboa.pt 27 | 28 | 29 | 30 | scm:git:git@github.com:javasync/streamemo.git 31 | scm:git:git@github.com:javasync/streamemo.git 32 | https://github.com/javasync/streamemo.git 33 | HEAD 34 | 35 | 36 | 37 | ossrh 38 | https://oss.sonatype.org/content/repositories/snapshots 39 | 40 | 41 | ossrh 42 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 43 | 44 | 45 | 46 | UTF-8 47 | 11 48 | 11 49 | 50 | 51 | 52 | 53 | org.apache.maven.plugins 54 | maven-release-plugin 55 | 2.5 56 | 57 | true 58 | false 59 | release 60 | deploy 61 | false 62 | 63 | 64 | 65 | org.apache.maven.plugins 66 | maven-gpg-plugin 67 | 1.5 68 | 69 | 70 | sign-artifacts 71 | verify 72 | 73 | sign 74 | 75 | 76 | 77 | 78 | 79 | org.apache.maven.plugins 80 | maven-source-plugin 81 | 2.2.1 82 | 83 | 84 | attach-sources 85 | 86 | jar-no-fork 87 | 88 | 89 | 90 | 91 | 92 | org.apache.maven.plugins 93 | maven-javadoc-plugin 94 | 3.0.1 95 | 96 | 97 | attach-javadocs 98 | 99 | jar 100 | 101 | 102 | 103 | 104 | 105 | org.jacoco 106 | jacoco-maven-plugin 107 | 0.8.2 108 | 109 | 110 | default-prepare-agent 111 | 112 | prepare-agent 113 | 114 | 115 | 116 | default-report 117 | prepare-package 118 | 119 | report 120 | 121 | 122 | 123 | default-check 124 | 125 | check 126 | 127 | 128 | 129 | 130 | BUNDLE 131 | 132 | 133 | INSTRUCTION 134 | COVEREDRATIO 135 | 0.80 136 | 137 | 138 | CLASS 139 | MISSEDCOUNT 140 | 0 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | org.slf4j 154 | slf4j-jdk14 155 | 1.7.21 156 | test 157 | 158 | 159 | org.asynchttpclient 160 | async-http-client 161 | 2.4.7 162 | test 163 | 164 | 165 | io.projectreactor 166 | reactor-core 167 | 3.1.7.RELEASE 168 | test 169 | 170 | 171 | org.junit.jupiter 172 | junit-jupiter-engine 173 | 5.1.0 174 | test 175 | 176 | 177 | -------------------------------------------------------------------------------- /src/test/java/org/javasync/streams/test/ReplayTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2018, Miguel Gamboa (gamboa.pt) 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package org.javasync.streams.test; 26 | 27 | import org.javasync.streams.Replayer; 28 | import org.junit.jupiter.api.Assertions; 29 | import org.junit.jupiter.api.Test; 30 | 31 | import java.util.ArrayList; 32 | import java.util.List; 33 | import java.util.Random; 34 | import java.util.Spliterator; 35 | import java.util.concurrent.atomic.AtomicInteger; 36 | import java.util.function.Supplier; 37 | import java.util.stream.IntStream; 38 | import java.util.stream.LongStream; 39 | import java.util.stream.Stream; 40 | 41 | import static java.lang.System.out; 42 | import static java.util.stream.Collectors.joining; 43 | import static java.util.stream.Stream.*; 44 | import static org.junit.jupiter.api.Assertions.assertArrayEquals; 45 | import static org.junit.jupiter.api.Assertions.assertEquals; 46 | import static org.junit.jupiter.api.Assertions.assertThrows; 47 | 48 | public class ReplayTest { 49 | 50 | @Test 51 | @SuppressWarnings("Duplicates") 52 | public void testConcurrentIteratorsOnMemoizedStream() { 53 | Random rnd = new Random(); 54 | Supplier> nrs = Replayer.replay(IntStream.range(1, 10).boxed()); 55 | Spliterator iter1 = nrs.get().spliterator(); 56 | iter1.tryAdvance(out::println); 57 | iter1.tryAdvance(out::println); 58 | Spliterator iter2 = nrs.get().spliterator(); 59 | iter1.tryAdvance(out::println); 60 | iter2.forEachRemaining(out::print); 61 | System.out.println(); // throws ConcurrentModificationException 62 | } 63 | 64 | @Test 65 | public void testReplayInfiniteRandomStream() { 66 | Random rnd = new Random(); 67 | Stream nrs = Stream.generate(() -> rnd.nextInt(99)); 68 | Supplier> nrsSrc = Replayer.replay(nrs); 69 | 70 | nrsSrc.get().limit(11).map(n -> n + ",").forEach(out::print); // e.g. 88,18,78,75,98,68,15,14,25,54,22, 71 | out.println(); 72 | nrsSrc.get().limit(11).map(n -> n + ",").forEach(out::print); // Print the same previous numbers 73 | } 74 | 75 | @Test 76 | public void testPrintInfiniteRandomStream() { 77 | Random rnd = new Random(); 78 | Supplier> nrs = () -> generate(() -> rnd.nextInt(99)).map(Object::toString); 79 | IntStream.range(1, 6).forEach(size -> out.println(nrs.get().limit(size).collect(joining(",")))); 80 | 81 | out.println(); 82 | Supplier> nrsReplay = Replayer.replay(nrs); 83 | IntStream.range(1, 6).forEach(size -> out.println(nrsReplay.get().limit(size).collect(joining(",")))); 84 | } 85 | 86 | @Test 87 | public void testInfiniteRandomStream() { 88 | Random rnd = new Random(); 89 | // Supplier> nrs = () -> generate(() -> rnd.nextInt(99)); 90 | Supplier> nrs = Replayer.replay(() -> generate(() -> rnd.nextInt(99))); 91 | List expected = new ArrayList<>(); 92 | IntStream.range(1, 6).forEach(size -> nrs 93 | .get() 94 | .limit(size) 95 | .reduce((p, n) -> n) // return last 96 | .ifPresent(expected::add)); 97 | Assertions.assertArrayEquals( 98 | expected.toArray(), 99 | nrs.get().limit(5).toArray()); 100 | } 101 | 102 | 103 | @Test 104 | public void testHighlightDifferentAspects() { 105 | boolean[] called = new boolean[10]; 106 | Supplier> s = Replayer.replay(() -> IntStream 107 | .range(0, 10) 108 | .peek(n -> { 109 | if (called[n]) Assertions.fail("Already generated: " + n); 110 | called[n] = true; 111 | }) 112 | .boxed()); 113 | 114 | s.get().findFirst(); 115 | s.get().toArray(); 116 | s.get() 117 | .filter(i -> i < 5) 118 | .forEach(x -> { 119 | }); 120 | s.get().toArray(); 121 | } 122 | 123 | @Test 124 | public void testIntersectionInStreams() { 125 | Random rnd = new Random(); 126 | Stream nrs1 = rnd.ints(1, 20).boxed().limit(10); 127 | Supplier> nrs2 = Replayer.replay(rnd.ints(1, 20).boxed().limit(10)); 128 | nrs1 129 | .filter(n1 -> nrs2.get().anyMatch(n1::equals)) 130 | .distinct() 131 | .forEach(out::println); 132 | } 133 | 134 | @Test 135 | public void testLongStream() { 136 | assertThrows(IllegalStateException.class, () -> { 137 | long size = ((long) Integer.MAX_VALUE) + 10; 138 | Supplier> nrs = Replayer.replay(LongStream.range(0, size).boxed()); 139 | assertEquals(size, nrs.get().count()); 140 | }); 141 | } 142 | @Test 143 | public void testParallel() { 144 | Random rnd = new Random(); 145 | Supplier> nrs = Replayer.replay(rnd.ints(1, 1024).boxed().limit(1024*1024*8)); 146 | assertEquals(1024*1024*8, nrs.get().count()); 147 | assertEquals(1023, (int) nrs 148 | .get() 149 | .parallel() 150 | .max(Integer::compareTo) 151 | .get()); 152 | 153 | } 154 | @Test 155 | public void testOnClose() { 156 | final AtomicInteger closeCounter = new AtomicInteger(); 157 | final Stream originalStream = LongStream.range(1, 5).boxed() 158 | .onClose(() -> closeCounter.getAndIncrement()); 159 | Supplier> replayer = Replayer.replay(originalStream); 160 | try(Stream s = replayer.get()) { 161 | Long[] numbers = s.toArray(Long[]::new); 162 | assertArrayEquals(new Long[] {1L, 2L, 3L, 4L}, numbers); 163 | } 164 | assertEquals(1, closeCounter.get()); 165 | } 166 | @Test 167 | public void testOnCloseWithSupplier() { 168 | final List closeCounter = new ArrayList<>(); 169 | final Supplier> originalStream = () -> { 170 | Stream src = LongStream.range(1, 5).boxed(); 171 | AtomicInteger c = new AtomicInteger(); 172 | closeCounter.add(c); 173 | src.onClose(() -> c.getAndIncrement()); 174 | return src; 175 | }; 176 | 177 | Supplier> replayer = Replayer.replay(originalStream); 178 | // first stream 179 | try(Stream s = replayer.get()) { 180 | Long[] numbers = s.toArray(Long[]::new); 181 | assertArrayEquals(new Long[] {1L, 2L, 3L, 4L}, numbers); 182 | } 183 | assertEquals(1, closeCounter.size()); 184 | closeCounter.forEach(c -> assertEquals(1, c.get())); 185 | 186 | // replay stream 187 | try(Stream s = replayer.get()) { 188 | Long[] numbers = s.toArray(Long[]::new); 189 | assertArrayEquals(new Long[] {1L, 2L, 3L, 4L}, numbers); 190 | } 191 | assertEquals(1, closeCounter.size()); 192 | closeCounter.forEach(c -> assertEquals(1, c.get())); 193 | } 194 | } -------------------------------------------------------------------------------- /src/main/java/org/javasync/streams/Replayer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2018, Miguel Gamboa (gamboa.pt) 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package org.javasync.streams; 26 | 27 | import java.util.ArrayList; 28 | import java.util.Comparator; 29 | import java.util.List; 30 | import java.util.Objects; 31 | import java.util.Spliterator; 32 | import java.util.Spliterators; 33 | import java.util.concurrent.atomic.AtomicBoolean; 34 | import java.util.function.Consumer; 35 | import java.util.function.Supplier; 36 | import java.util.stream.Stream; 37 | 38 | import static java.util.stream.StreamSupport.stream; 39 | 40 | public class Replayer { 41 | 42 | public static Supplier> replay(Stream data) { 43 | return replay(() -> data); 44 | } 45 | 46 | public static Supplier> replay(Supplier> dataSrc) { 47 | final Recorder rec = new Recorder<>(dataSrc); 48 | final AtomicBoolean isClosed = new AtomicBoolean(false); 49 | return () -> { 50 | // MemoizeIter starts on index 0 and reads data from srcIter or 51 | // from an internal mem replay Recorder. 52 | Spliterator iter = rec.memIterator(); 53 | return stream(iter, false) 54 | .onClose(() -> { 55 | if (isClosed.compareAndSet(false, true)) { 56 | rec.close(); 57 | } 58 | }); 59 | }; 60 | } 61 | 62 | static class Recorder implements AutoCloseable { 63 | private final Supplier> dataSrc; 64 | private Stream srcStream; 65 | private Spliterator srcIter; 66 | private long estimateSize; 67 | private boolean hasNext = true; 68 | private ArrayList mem; 69 | 70 | public Recorder(Supplier> dataSrc) { 71 | this.dataSrc= dataSrc; 72 | } 73 | 74 | synchronized Spliterator getSrcIter() { 75 | if(srcIter == null) { 76 | srcStream = dataSrc.get(); 77 | srcIter = srcStream.spliterator(); 78 | estimateSize = srcIter.estimateSize(); 79 | if((srcIter.characteristics() & Spliterator.SIZED) == 0) 80 | mem = new ArrayList<>(); // Unknown size!!! 81 | else { 82 | if(estimateSize > Integer.MAX_VALUE) 83 | throw new IllegalStateException("Replay unsupported for estimated size bigger than Integer.MAX_VALUE!"); 84 | mem = new ArrayList<>((int) estimateSize); 85 | } 86 | } 87 | return srcIter; 88 | } 89 | 90 | public synchronized boolean getOrAdvance( 91 | final int index, 92 | Consumer cons) { 93 | if (index < mem.size()) { 94 | // If it is in mem then just get if from the corresponding index. 95 | cons.accept(mem.get(index)); 96 | return true; 97 | } else if (hasNext) 98 | // If not in mem then advance the srcIter iterator 99 | hasNext = getSrcIter().tryAdvance(item -> { 100 | mem.add(item); 101 | cons.accept(item); 102 | }); 103 | return hasNext; 104 | } 105 | 106 | public Spliterator memIterator() { 107 | return !hasNext 108 | ? new RandomAccessSpliterator() // Fast-path when all items are already saved in mem! 109 | : new MemoizeIter(getSrcIter()); 110 | } 111 | 112 | class MemoizeIter extends Spliterators.AbstractSpliterator { 113 | int index = 0; 114 | public MemoizeIter(Spliterator inner){ 115 | super(estimateSize, inner.characteristics()); 116 | } 117 | public boolean tryAdvance(Consumer cons) { 118 | return getOrAdvance(index++, cons); 119 | } 120 | public Comparator getComparator() { 121 | return getSrcIter().getComparator(); 122 | } 123 | } 124 | 125 | /** 126 | * An index-based split-by-two, lazily initialized Spliterator covering 127 | * a List that access elements via {@link List#get}. 128 | * 129 | * There are no concurrent modifications to the underlying list. 130 | * That list is the mem field of Recorder and this iterator is just used 131 | * when the list is completely filled. 132 | * 133 | * Based on AbstractList.RandomAccessSpliterator 134 | */ 135 | class RandomAccessSpliterator implements Spliterator { 136 | 137 | private int index; // current index, modified on advance/split 138 | private int fence; // -1 until used; then one past last index 139 | 140 | RandomAccessSpliterator() { 141 | this.index = 0; 142 | this.fence = -1; 143 | } 144 | 145 | /** 146 | * Create new spliterator covering the given range 147 | */ 148 | private RandomAccessSpliterator(RandomAccessSpliterator parent, 149 | int origin, int fence) { 150 | this.index = origin; 151 | this.fence = fence; 152 | } 153 | 154 | private int getFence() { // initialize fence to size on first use 155 | int hi; 156 | List lst = mem; 157 | if ((hi = fence) < 0) { 158 | hi = fence = lst.size(); 159 | } 160 | return hi; 161 | } 162 | 163 | public Spliterator trySplit() { 164 | int hi = getFence(), lo = index, mid = (lo + hi) >>> 1; 165 | return (lo >= mid) ? null : // divide range in half unless too small 166 | new RandomAccessSpliterator(this, lo, index = mid); 167 | } 168 | 169 | public boolean tryAdvance(Consumer action) { 170 | if (action == null) 171 | throw new NullPointerException(); 172 | int hi = getFence(), i = index; 173 | if (i < hi) { 174 | index = i + 1; 175 | action.accept(mem.get(i)); 176 | return true; 177 | } 178 | return false; 179 | } 180 | 181 | public void forEachRemaining(Consumer action) { 182 | Objects.requireNonNull(action); 183 | List lst = mem; 184 | int hi = getFence(); 185 | int i = index; 186 | index = hi; 187 | for (; i < hi; i++) { 188 | action.accept(mem.get(i)); 189 | } 190 | } 191 | 192 | public long estimateSize() { 193 | return (long) (getFence() - index); 194 | } 195 | 196 | public int characteristics() { 197 | return Spliterator.ORDERED 198 | | Spliterator.SIZED 199 | | Spliterator.SUBSIZED 200 | | getSrcIter().characteristics(); 201 | } 202 | public Comparator getComparator() { 203 | return getSrcIter().getComparator(); 204 | } 205 | } 206 | 207 | @Override 208 | public void close() { 209 | if(srcStream != null) { 210 | srcStream.close(); 211 | } 212 | } 213 | 214 | } 215 | } 216 | --------------------------------------------------------------------------------