├── settings.gradle ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties └── javadocStyleSheet.css ├── .travis.yml ├── .gitignore ├── .gitattributes ├── HEADER ├── gradle.properties ├── push.sh ├── src ├── main │ └── java │ │ └── hu │ │ └── akarnokd │ │ └── rxjava3 │ │ └── jdk8interop │ │ ├── CompletableInterop.java │ │ ├── SingleInterop.java │ │ ├── ObservableMapOptional.java │ │ ├── MaybeMapOptional.java │ │ ├── ZeroOneIterator.java │ │ ├── MaybeInterop.java │ │ ├── FlowableCollector.java │ │ ├── ObservableCollector.java │ │ ├── ObservableFromStream.java │ │ ├── FlowableFromStream.java │ │ ├── FlowableMapOptional.java │ │ ├── FlowableInterop.java │ │ └── ObservableInterop.java └── test │ └── java │ └── hu │ └── akarnokd │ └── rxjava3 │ └── jdk8interop │ ├── SingleInteropTest.java │ ├── CompletableInteropTest.java │ ├── ObservableFromStreamTest.java │ ├── MaybeInteropTest.java │ ├── FlowableFromStreamTest.java │ ├── TestObserverEx.java │ ├── BaseTestConsumerEx.java │ ├── TestSubscriberEx.java │ ├── ObservableInteropTest.java │ └── FlowableInteropTest.java ├── gradlew.bat ├── README.md ├── gradlew ├── LICENSE └── pmd.xml /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'rxjava3-jdk8-interop' 2 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akarnokd/RxJavaJdk8Interop/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.0.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | 3 | jdk: 4 | - openjdk8 5 | 6 | before_install: 7 | - chmod +x gradlew 8 | - chmod +x push.sh 9 | 10 | after_success: 11 | - bash <(curl -s https://codecov.io/bash) 12 | - ./push.sh 13 | 14 | # cache between builds 15 | cache: 16 | directories: 17 | - $HOME/.m2 18 | - $HOME/.gradle 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | 3 | # Package Files # 4 | *.jar 5 | *.war 6 | *.ear 7 | /build/ 8 | 9 | .gradle 10 | .m2 11 | /.nb-gradle/ 12 | /bin/ 13 | .settings/ 14 | .nb-gradle-properties 15 | .classpath 16 | .project 17 | .settings 18 | .metadata 19 | .checkstyle 20 | bin/ 21 | !/gradle/wrapper/gradle-wrapper.jar 22 | .pmd 23 | .ruleset 24 | local.properties -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set default behaviour, in case users don't have core.autocrlf set. 2 | * text=auto 3 | 4 | # Explicitly declare text files we want to always be normalized and converted 5 | # to native line endings on checkout. 6 | *.java text 7 | *.groovy text 8 | *.scala text 9 | *.clj text 10 | *.txt text 11 | *.md text 12 | 13 | # Denote all files that are truly binary and should not be modified. 14 | *.png binary 15 | *.jpg binary 16 | -------------------------------------------------------------------------------- /HEADER: -------------------------------------------------------------------------------- 1 | Copyright ${year} David Karnok 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | GROUP=com.github.akarnokd 2 | VERSION_NAME=3.0.0-RC6 3 | version=3.0.0-RC6 4 | 5 | POM_ARTIFACT_ID=rxjava3-jdk8-interop 6 | POM_NAME=RxJava 3 interop library for Java 8 7 | POM_PACKAGING=jar 8 | 9 | POM_DESCRIPTION=RxJava 3 interop library for supporting Java 8 features such as Optional, Stream and CompletableFuture. 10 | POM_INCEPTION_YEAR=2019 11 | 12 | POM_URL=https://github.com/akarnokd/RxJavaJdk8Interop/ 13 | POM_SCM_URL=https://github.com/akarnokd/RxJavaJdk8Interop/ 14 | POM_SCM_CONNECTION=scm:git:git://github.com/akarnokd/RxJavaJdk8Interop.git 15 | POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/akarnokd/RxJavaJdk8Interop.git 16 | 17 | POM_LICENCE_NAME=The Apache Software License, Version 2.0 18 | POM_LICENCE_URL=https://www.apache.org/licenses/LICENSE-2.0.txt 19 | POM_LICENCE_DIST=repo 20 | 21 | POM_DEVELOPER_ID=akarnokd 22 | POM_DEVELOPER_NAME=David Karnok 23 | 24 | -------------------------------------------------------------------------------- /push.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # ---------------------------------------------------------- 3 | # Automatically push back the generated JavaDocs to gh-pages 4 | # ---------------------------------------------------------- 5 | # based on https://gist.github.com/willprice/e07efd73fb7f13f917ea 6 | 7 | # specify the common address for the repository 8 | targetRepo=github.com/akarnokd/RxJavaJdk8Interop.git 9 | # ======================================================================= 10 | 11 | # only for main pushes, for now 12 | # if [ "$TRAVIS_PULL_REQUEST" == "true" ] || [ "$TRAVIS_TAG" == "" ]; then 13 | if [ "$TRAVIS_PULL_REQUEST" == "true" ]; then 14 | echo -e "Pull request detected, skipping JavaDocs pushback." 15 | exit 0 16 | fi 17 | 18 | # check if the token is actually there 19 | if [ "$GITHUB_TOKEN" == "" ]; then 20 | echo -e "No access to GitHub, skipping JavaDocs pushback." 21 | exit 0 22 | fi 23 | 24 | # prepare the git information 25 | git config --global user.email "travis@travis-ci.org" 26 | git config --global user.name "Travis CI" 27 | 28 | # setup the remote 29 | git remote add origin-pages https://${GITHUB_TOKEN}@${targetRepo} > /dev/null 2>&1 30 | 31 | # stash changes due to chmod 32 | git stash 33 | 34 | # get the gh-pages 35 | git fetch --all 36 | git branch -a 37 | git checkout -b gh-pages origin-pages/gh-pages 38 | 39 | # remove old dir 40 | rm -rf javadoc 41 | 42 | # copy and overwrite new doc 43 | yes | cp -rfv ./build/docs/javadoc/ javadoc/ 44 | 45 | # stage all changed and new files 46 | git add *.html 47 | git add *.css 48 | git add *.js 49 | git add javadoc/package-list 50 | 51 | # commit all 52 | git commit --message "Travis build: $TRAVIS_BUILD_NUMBER" 53 | 54 | 55 | # push it 56 | git push --quiet --set-upstream origin-pages gh-pages 57 | 58 | # we are done 59 | -------------------------------------------------------------------------------- /gradle/javadocStyleSheet.css: -------------------------------------------------------------------------------- 1 | # originally from http://sensemaya.org/files/stylesheet.css and then modified 2 | # http://sensemaya.org/maya/2009/07/10/making-javadoc-more-legible 3 | 4 | /* Javadoc style sheet */ 5 | 6 | /* Define colors, fonts and other style attributes here to override the defaults */ 7 | 8 | /* Page background color */ 9 | body { background-color: #FFFFFF; color:#333; font-size: 100%; } 10 | 11 | body { font-size: 0.875em; line-height: 1.286em; font-family: "Helvetica", "Arial", sans-serif; } 12 | 13 | code { color: #777; line-height: 1.286em; font-family: "Consolas", "Lucida Console", "Droid Sans Mono", "Andale Mono", "Monaco", "Lucida Sans Typewriter"; } 14 | 15 | a { text-decoration: none; color: #16569A; /* also try #2E85ED, #0033FF, #6C93C6, #1D7BBE, #1D8DD2 */ } 16 | a:hover { text-decoration: underline; } 17 | 18 | 19 | table[border="1"] { border: 1px solid #ddd; } 20 | table[border="1"] td, table[border="1"] th { border: 1px solid #ddd; } 21 | table[cellpadding="3"] td { padding: 0.5em; } 22 | 23 | font[size="-1"] { font-size: 0.85em; line-height: 1.5em; } 24 | font[size="-2"] { font-size: 0.8em; } 25 | font[size="+2"] { font-size: 1.4em; line-height: 1.3em; padding: 0.4em 0; } 26 | 27 | /* Headings */ 28 | h1 { font-size: 1.5em; line-height: 1.286em;} 29 | h2.title { color: #c81f08; } 30 | 31 | /* Table colors */ 32 | .TableHeadingColor { background: #ccc; color:#444; } /* Dark mauve */ 33 | .TableSubHeadingColor { background: #ddd; color:#444; } /* Light mauve */ 34 | .TableRowColor { background: #FFFFFF; color:#666; font-size: 0.95em; } /* White */ 35 | .TableRowColor code { color:#000; } /* White */ 36 | 37 | /* Font used in left-hand frame lists */ 38 | .FrameTitleFont { font-size: 100%; } 39 | .FrameHeadingFont { font-size: 90%; } 40 | .FrameItemFont { font-size: 0.9em; line-height: 1.3em; 41 | } 42 | /* Java Interfaces */ 43 | .FrameItemFont a i { 44 | font-style: normal; color: #16569A; 45 | } 46 | .FrameItemFont a:hover i { 47 | text-decoration: underline; 48 | } 49 | 50 | 51 | /* Navigation bar fonts and colors */ 52 | .NavBarCell1 { background-color:#E0E6DF; } /* Light mauve */ 53 | .NavBarCell1Rev { background-color:#16569A; color:#FFFFFF} /* Dark Blue */ 54 | .NavBarFont1 { } 55 | .NavBarFont1Rev { color:#FFFFFF; } 56 | 57 | .NavBarCell2 { background-color:#FFFFFF; color:#000000} 58 | .NavBarCell3 { background-color:#FFFFFF; color:#000000} 59 | 60 | -------------------------------------------------------------------------------- /src/main/java/hu/akarnokd/rxjava3/jdk8interop/CompletableInterop.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 David Karnok 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package hu.akarnokd.rxjava3.jdk8interop; 18 | 19 | import java.util.concurrent.*; 20 | import java.util.stream.Stream; 21 | 22 | import io.reactivex.rxjava3.core.*; 23 | import io.reactivex.rxjava3.subjects.CompletableSubject; 24 | 25 | /** 26 | * Utility methods, sources and operators supporting RxJava 2 and the Jdk 8 API 27 | * interoperation. 28 | * 29 | * @since 0.1.0 30 | */ 31 | public final class CompletableInterop { 32 | 33 | /** Utility class. */ 34 | private CompletableInterop() { 35 | throw new IllegalStateException("No instances!"); 36 | } 37 | 38 | /** 39 | * Returns a CompletionStage that signals a null value or error if the Completable terminates. 40 | * @param the target value type (unused) 41 | * @return the converter function to be used with {@code Completable.to()} 42 | */ 43 | public static CompletableConverter> await() { 44 | return c -> { 45 | CompletableFuture cf = new CompletableFuture<>(); 46 | c.subscribe(() -> cf.complete(null), cf::completeExceptionally); 47 | return cf; 48 | }; 49 | } 50 | 51 | /** 52 | * Returns a blocking Stream that waits for the Completable's terminal event. 53 | * @param the value type 54 | * @return the converter function to be used with {@code Completable.to()} 55 | */ 56 | public static CompletableConverter> toStream() { 57 | return c -> { 58 | ZeroOneIterator zoi = new ZeroOneIterator<>(); 59 | c.subscribe(zoi); 60 | return ZeroOneIterator.toStream(zoi); 61 | }; 62 | } 63 | 64 | /** 65 | * Returns a Completable that terminates when the given CompletionStage terminates. 66 | * @param future the source CompletionStage instance 67 | * @return the new Completable instance 68 | */ 69 | public static Completable fromFuture(CompletionStage future) { 70 | CompletableSubject cs = CompletableSubject.create(); 71 | 72 | future.whenComplete((v, e) -> { 73 | if (e != null) { 74 | cs.onError(e); 75 | } else { 76 | cs.onComplete(); 77 | } 78 | }); 79 | 80 | return cs; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/hu/akarnokd/rxjava3/jdk8interop/SingleInterop.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 David Karnok 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package hu.akarnokd.rxjava3.jdk8interop; 18 | 19 | import java.util.NoSuchElementException; 20 | import java.util.concurrent.*; 21 | import java.util.stream.Stream; 22 | 23 | import io.reactivex.rxjava3.core.*; 24 | import io.reactivex.rxjava3.subjects.SingleSubject; 25 | 26 | /** 27 | * Utility methods, sources and operators supporting RxJava 2 and the Jdk 8 API 28 | * interoperation. 29 | * 30 | * @since 0.1.0 31 | */ 32 | public final class SingleInterop { 33 | 34 | /** Utility class. */ 35 | private SingleInterop() { 36 | throw new IllegalStateException("No instances!"); 37 | } 38 | 39 | /** 40 | * Returns a CompletionStage that signals the success value or error of the 41 | * source Single. 42 | * @param the value type 43 | * @return the converter function to be used with {@code Single.to()} 44 | */ 45 | public static SingleConverter> get() { 46 | return c -> { 47 | CompletableFuture cf = new CompletableFuture<>(); 48 | c.subscribe(cf::complete, cf::completeExceptionally); 49 | return cf; 50 | }; 51 | } 52 | 53 | /** 54 | * Returns a blocking Stream of the single success value of the source Single. 55 | * @param the value type 56 | * @return the converter function to be used with {@code Single.to()} 57 | */ 58 | public static SingleConverter> toStream() { 59 | return s -> { 60 | ZeroOneIterator zoi = new ZeroOneIterator<>(); 61 | s.subscribe(zoi); 62 | return ZeroOneIterator.toStream(zoi); 63 | }; 64 | } 65 | 66 | /** 67 | * Returns a Single that emits the value of the CompletionStage, its error or 68 | * NoSuchElementException if it signals null. 69 | * @param the value type 70 | * @param future the source CompletionStage instance 71 | * @return the new Single instance 72 | */ 73 | public static Single fromFuture(CompletionStage future) { 74 | SingleSubject cs = SingleSubject.create(); 75 | 76 | future.whenComplete((v, e) -> { 77 | if (e != null) { 78 | cs.onError(e); 79 | } else 80 | if (v != null) { 81 | cs.onSuccess(v); 82 | } else { 83 | cs.onError(new NoSuchElementException()); 84 | } 85 | }); 86 | 87 | return cs; 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 33 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 34 | 35 | @rem Find java.exe 36 | if defined JAVA_HOME goto findJavaFromJavaHome 37 | 38 | set JAVA_EXE=java.exe 39 | %JAVA_EXE% -version >NUL 2>&1 40 | if "%ERRORLEVEL%" == "0" goto init 41 | 42 | echo. 43 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 44 | echo. 45 | echo Please set the JAVA_HOME variable in your environment to match the 46 | echo location of your Java installation. 47 | 48 | goto fail 49 | 50 | :findJavaFromJavaHome 51 | set JAVA_HOME=%JAVA_HOME:"=% 52 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 53 | 54 | if exist "%JAVA_EXE%" goto init 55 | 56 | echo. 57 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 58 | echo. 59 | echo Please set the JAVA_HOME variable in your environment to match the 60 | echo location of your Java installation. 61 | 62 | goto fail 63 | 64 | :init 65 | @rem Get command-line arguments, handling Windows variants 66 | 67 | if not "%OS%" == "Windows_NT" goto win9xME_args 68 | 69 | :win9xME_args 70 | @rem Slurp the command line arguments. 71 | set CMD_LINE_ARGS= 72 | set _SKIP=2 73 | 74 | :win9xME_args_slurp 75 | if "x%~1" == "x" goto execute 76 | 77 | set CMD_LINE_ARGS=%* 78 | 79 | :execute 80 | @rem Setup the command line 81 | 82 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 83 | 84 | @rem Execute Gradle 85 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 86 | 87 | :end 88 | @rem End local scope for the variables with windows NT shell 89 | if "%ERRORLEVEL%"=="0" goto mainEnd 90 | 91 | :fail 92 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 93 | rem the _cmd.exe /c_ return code! 94 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 95 | exit /b 1 96 | 97 | :mainEnd 98 | if "%OS%"=="Windows_NT" endlocal 99 | 100 | :omega 101 | -------------------------------------------------------------------------------- /src/test/java/hu/akarnokd/rxjava3/jdk8interop/SingleInteropTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 David Karnok 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package hu.akarnokd.rxjava3.jdk8interop; 18 | 19 | import java.util.*; 20 | import java.util.concurrent.*; 21 | import java.util.stream.Collectors; 22 | 23 | import org.junit.*; 24 | 25 | import hu.akarnokd.rxjava3.jdk8interop.SingleInterop; 26 | import io.reactivex.rxjava3.core.Single; 27 | 28 | public class SingleInteropTest { 29 | 30 | @Test 31 | public void utilityClass() { 32 | TestHelper.checkUtilityClass(SingleInterop.class); 33 | } 34 | 35 | 36 | @Test 37 | public void fromFuture() { 38 | SingleInterop.fromFuture(CompletableFuture.supplyAsync(() -> 1)) 39 | .test() 40 | .awaitDone(5, TimeUnit.SECONDS) 41 | .assertResult(1); 42 | } 43 | 44 | @Test 45 | public void fromFutureEmpty() { 46 | SingleInterop.fromFuture(CompletableFuture.supplyAsync(() -> null)) 47 | .test() 48 | .awaitDone(5, TimeUnit.SECONDS) 49 | .assertFailure(NoSuchElementException.class); 50 | 51 | SingleInterop.fromFuture(CompletableFuture.runAsync(() -> { })) 52 | .test() 53 | .awaitDone(5, TimeUnit.SECONDS) 54 | .assertFailure(NoSuchElementException.class); 55 | } 56 | 57 | @Test 58 | public void fromFutureError() { 59 | TestObserverEx ts = new TestObserverEx<>(); 60 | 61 | SingleInterop.fromFuture( 62 | CompletableFuture.supplyAsync(() -> { throw new IllegalArgumentException(); })) 63 | .subscribeWith(ts) 64 | .awaitDone(5, TimeUnit.SECONDS) 65 | .assertFailure(CompletionException.class); 66 | 67 | Throwable c = ts.errors().get(0).getCause(); 68 | Assert.assertTrue(c.toString(), c instanceof IllegalArgumentException); 69 | } 70 | 71 | @Test 72 | public void get() { 73 | TestHelper.assertFuture(1, Single.just(1) 74 | .to(SingleInterop.get())); 75 | } 76 | 77 | @Test(expected = IllegalArgumentException.class) 78 | public void getError() { 79 | TestHelper.assertFuture(null, Single.error(new IllegalArgumentException()) 80 | .to(SingleInterop.get())); 81 | } 82 | 83 | @Test 84 | public void toStream() { 85 | List list = Single.just(1) 86 | .to(SingleInterop.toStream()) 87 | .collect(Collectors.toList()); 88 | 89 | Assert.assertEquals(Arrays.asList(1), list); 90 | } 91 | 92 | @Test(expected = IllegalArgumentException.class) 93 | public void toStreamError() { 94 | Single.error(new IllegalArgumentException()) 95 | .to(SingleInterop.toStream()) 96 | .collect(Collectors.toList()); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/test/java/hu/akarnokd/rxjava3/jdk8interop/CompletableInteropTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 David Karnok 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package hu.akarnokd.rxjava3.jdk8interop; 18 | 19 | import java.util.List; 20 | import java.util.concurrent.*; 21 | import java.util.stream.Collectors; 22 | 23 | import org.junit.*; 24 | 25 | import hu.akarnokd.rxjava3.jdk8interop.CompletableInterop; 26 | import io.reactivex.rxjava3.core.Completable; 27 | 28 | public class CompletableInteropTest { 29 | 30 | @Test 31 | public void utilityClass() { 32 | TestHelper.checkUtilityClass(CompletableInterop.class); 33 | } 34 | 35 | @Test 36 | public void await() { 37 | TestHelper.assertFuture(null, Completable.complete() 38 | .to(CompletableInterop.await())); 39 | } 40 | 41 | @Test(expected = IllegalArgumentException.class) 42 | public void awaitError() { 43 | TestHelper.assertFuture(null, Completable.error(new IllegalArgumentException()) 44 | .to(CompletableInterop.await())); 45 | } 46 | 47 | 48 | @Test 49 | public void toStreamEmpty() { 50 | List list = Completable.complete() 51 | .to(CompletableInterop.toStream()) 52 | .collect(Collectors.toList()); 53 | 54 | Assert.assertTrue(list.isEmpty()); 55 | } 56 | 57 | @Test(expected = IllegalArgumentException.class) 58 | public void toStreamError() { 59 | Completable.error(new IllegalArgumentException()) 60 | .to(CompletableInterop.toStream()) 61 | .collect(Collectors.toList()); 62 | } 63 | 64 | @Test 65 | public void fromFuture() { 66 | CompletableInterop.fromFuture(CompletableFuture.supplyAsync(() -> 1)) 67 | .test() 68 | .awaitDone(5, TimeUnit.SECONDS) 69 | .assertResult(); 70 | } 71 | 72 | @Test 73 | public void fromFutureEmpty() { 74 | CompletableInterop.fromFuture(CompletableFuture.supplyAsync(() -> null)) 75 | .test() 76 | .awaitDone(5, TimeUnit.SECONDS) 77 | .assertResult(); 78 | 79 | CompletableInterop.fromFuture(CompletableFuture.runAsync(() -> { })) 80 | .test() 81 | .awaitDone(5, TimeUnit.SECONDS) 82 | .assertResult(); 83 | } 84 | 85 | @Test 86 | public void fromFutureError() { 87 | TestObserverEx ts = new TestObserverEx<>(); 88 | 89 | CompletableInterop.fromFuture( 90 | CompletableFuture.supplyAsync(() -> { throw new IllegalArgumentException(); })) 91 | .subscribeWith(ts) 92 | .awaitDone(5, TimeUnit.SECONDS) 93 | .assertFailure(CompletionException.class); 94 | 95 | Throwable c = ts.errors().get(0).getCause(); 96 | Assert.assertTrue(c.toString(), c instanceof IllegalArgumentException); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/main/java/hu/akarnokd/rxjava3/jdk8interop/ObservableMapOptional.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 David Karnok 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package hu.akarnokd.rxjava3.jdk8interop; 18 | 19 | import java.util.Optional; 20 | 21 | import io.reactivex.rxjava3.core.*; 22 | import io.reactivex.rxjava3.exceptions.Exceptions; 23 | import io.reactivex.rxjava3.functions.Function; 24 | import io.reactivex.rxjava3.internal.functions.ObjectHelper; 25 | import io.reactivex.rxjava3.internal.observers.BasicFuseableObserver; 26 | 27 | /** 28 | * Maps an upstream value into an Optional and emits its value if not empty. 29 | * 30 | * @param the upstream value type 31 | * @param the result value type 32 | */ 33 | final class ObservableMapOptional extends Observable { 34 | 35 | final ObservableSource source; 36 | 37 | final Function> mapper; 38 | 39 | ObservableMapOptional(ObservableSource source, Function> mapper) { 40 | this.source = source; 41 | this.mapper = mapper; 42 | } 43 | 44 | @Override 45 | protected void subscribeActual(Observer s) { 46 | source.subscribe(new MapOptionalObserver<>(s, mapper)); 47 | } 48 | 49 | static final class MapOptionalObserver extends BasicFuseableObserver { 50 | 51 | final Function> mapper; 52 | 53 | public MapOptionalObserver(Observer actual, Function> mapper) { 54 | super(actual); 55 | this.mapper = mapper; 56 | } 57 | 58 | @Override 59 | public void onNext(T t) { 60 | if (done) { 61 | return; 62 | } 63 | 64 | if (sourceMode == ASYNC) { 65 | downstream.onNext(null); 66 | return; 67 | } 68 | 69 | Optional o; 70 | 71 | try { 72 | o = ObjectHelper.requireNonNull(mapper.apply(t), "The mapper returned a null Optional"); 73 | } catch (Throwable ex) { 74 | Exceptions.throwIfFatal(ex); 75 | fail(ex); 76 | return; 77 | } 78 | 79 | if (o.isPresent()) { 80 | downstream.onNext(o.get()); 81 | } 82 | } 83 | 84 | @Override 85 | public int requestFusion(int mode) { 86 | return transitiveBoundaryFusion(mode); 87 | } 88 | 89 | @Override 90 | public R poll() throws Throwable { 91 | for (;;) { 92 | T t = qd.poll(); 93 | 94 | if (t == null) { 95 | return null; 96 | } 97 | 98 | Optional o = mapper.apply(t); 99 | 100 | if (o.isPresent()) { 101 | return o.get(); 102 | } 103 | } 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/main/java/hu/akarnokd/rxjava3/jdk8interop/MaybeMapOptional.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 David Karnok 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package hu.akarnokd.rxjava3.jdk8interop; 18 | 19 | import java.util.Optional; 20 | 21 | import io.reactivex.rxjava3.core.*; 22 | import io.reactivex.rxjava3.disposables.Disposable; 23 | import io.reactivex.rxjava3.exceptions.Exceptions; 24 | import io.reactivex.rxjava3.functions.Function; 25 | import io.reactivex.rxjava3.internal.disposables.DisposableHelper; 26 | import io.reactivex.rxjava3.internal.functions.ObjectHelper; 27 | 28 | /** 29 | * Maps the success value of the source Maybe into an Optional and emits this Optional's value if present, 30 | * otherwise terminates. 31 | * 32 | * @param the upstream value type 33 | * @param the result value type 34 | */ 35 | final class MaybeMapOptional extends Maybe { 36 | 37 | final MaybeSource source; 38 | 39 | final Function> mapper; 40 | 41 | public MaybeMapOptional(MaybeSource source, Function> mapper) { 42 | this.source = source; 43 | this.mapper = mapper; 44 | } 45 | 46 | @Override 47 | protected void subscribeActual(MaybeObserver observer) { 48 | source.subscribe(new MapOptionalObserver<>(observer, mapper)); 49 | } 50 | 51 | static final class MapOptionalObserver implements MaybeObserver, Disposable { 52 | 53 | final MaybeObserver actual; 54 | 55 | final Function> mapper; 56 | 57 | Disposable d; 58 | 59 | public MapOptionalObserver(MaybeObserver actual, Function> mapper) { 60 | super(); 61 | this.actual = actual; 62 | this.mapper = mapper; 63 | } 64 | 65 | @Override 66 | public void dispose() { 67 | d.dispose(); 68 | } 69 | 70 | @Override 71 | public boolean isDisposed() { 72 | return d.isDisposed(); 73 | } 74 | 75 | @Override 76 | public void onSubscribe(Disposable d) { 77 | if (DisposableHelper.validate(this.d, d)) { 78 | this.d = d; 79 | 80 | actual.onSubscribe(this); 81 | } 82 | } 83 | 84 | @Override 85 | public void onSuccess(T value) { 86 | Optional v; 87 | 88 | try { 89 | v = ObjectHelper.requireNonNull(mapper.apply(value), "The mapper returned a null Optional"); 90 | } catch (Throwable ex) { 91 | Exceptions.throwIfFatal(ex); 92 | actual.onError(ex); 93 | return; 94 | } 95 | 96 | if (v.isPresent()) { 97 | actual.onSuccess(v.get()); 98 | } else { 99 | actual.onComplete(); 100 | } 101 | } 102 | 103 | @Override 104 | public void onError(Throwable e) { 105 | actual.onError(e); 106 | } 107 | 108 | @Override 109 | public void onComplete() { 110 | actual.onComplete(); 111 | } 112 | 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/main/java/hu/akarnokd/rxjava3/jdk8interop/ZeroOneIterator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 David Karnok 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package hu.akarnokd.rxjava3.jdk8interop; 18 | 19 | import java.util.*; 20 | import java.util.concurrent.CountDownLatch; 21 | import java.util.stream.*; 22 | 23 | import io.reactivex.rxjava3.core.*; 24 | import io.reactivex.rxjava3.disposables.Disposable; 25 | import io.reactivex.rxjava3.internal.disposables.DisposableHelper; 26 | import io.reactivex.rxjava3.internal.util.ExceptionHelper; 27 | 28 | /** 29 | * Iterator that emits 0 or 1 values from a reactive source of Single, Maybe or Completable. 30 | * 31 | * @param the value type 32 | */ 33 | final class ZeroOneIterator extends CountDownLatch implements Iterator, Disposable, MaybeObserver, 34 | SingleObserver, CompletableObserver { 35 | 36 | T value; 37 | 38 | Throwable error; 39 | 40 | Disposable d; 41 | 42 | volatile boolean disposed; 43 | 44 | ZeroOneIterator() { 45 | super(1); 46 | } 47 | 48 | @Override 49 | public boolean hasNext() { 50 | if (getCount() != 0) { 51 | try { 52 | await(); 53 | } catch (InterruptedException ex) { 54 | dispose(); 55 | throw ExceptionHelper.wrapOrThrow(ex); 56 | } 57 | } 58 | Throwable ex = error; 59 | if (ex != null) { 60 | throw ExceptionHelper.wrapOrThrow(ex); 61 | } 62 | return value != null; 63 | } 64 | 65 | @Override 66 | public T next() { 67 | if (hasNext()) { 68 | T v = value; 69 | value = null; 70 | return v; 71 | } 72 | throw new NoSuchElementException(); 73 | } 74 | 75 | @Override 76 | public void onSubscribe(Disposable d) { 77 | if (DisposableHelper.validate(this.d, d)) { 78 | this.d = d; 79 | if (disposed) { 80 | d.dispose(); 81 | } 82 | } 83 | } 84 | 85 | @Override 86 | public void onSuccess(T value) { 87 | this.value = value; 88 | countDown(); 89 | } 90 | 91 | @Override 92 | public void onError(Throwable e) { 93 | this.error = e; 94 | countDown(); 95 | } 96 | 97 | @Override 98 | public void onComplete() { 99 | countDown(); 100 | } 101 | 102 | @Override 103 | public void dispose() { 104 | disposed = true; 105 | Disposable d = this.d; 106 | if (d != null) { 107 | d.dispose(); 108 | } 109 | } 110 | 111 | @Override 112 | public boolean isDisposed() { 113 | return disposed; 114 | } 115 | 116 | /** 117 | * Creates a Stream from an Iterator which also calls dispose() when closed. 118 | * @param the value type 119 | * @param it the source iterator 120 | * @return the new Stream instance 121 | */ 122 | public static Stream toStream(Iterator it) { 123 | Stream s = StreamSupport.stream(Spliterators.spliterator(it, 0, 0), false); 124 | 125 | return s.onClose(() -> ((Disposable)it).dispose()); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/main/java/hu/akarnokd/rxjava3/jdk8interop/MaybeInterop.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 David Karnok 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package hu.akarnokd.rxjava3.jdk8interop; 18 | 19 | import java.util.Optional; 20 | import java.util.concurrent.*; 21 | import java.util.stream.Stream; 22 | 23 | import io.reactivex.rxjava3.core.*; 24 | import io.reactivex.rxjava3.functions.Function; 25 | import io.reactivex.rxjava3.plugins.RxJavaPlugins; 26 | import io.reactivex.rxjava3.subjects.MaybeSubject; 27 | 28 | /** 29 | * Utility methods, sources and operators supporting RxJava 2 and the Jdk 8 API 30 | * interoperation. 31 | * 32 | * @since 0.1.0 33 | */ 34 | public final class MaybeInterop { 35 | 36 | /** Utility class. */ 37 | private MaybeInterop() { 38 | throw new IllegalStateException("No instances!"); 39 | } 40 | 41 | /** 42 | * Returns a Maybe that emits the value of the Optional or is 43 | * empty if the Optional is also empty. 44 | * @param the value type 45 | * @param opt the Optional value 46 | * @return the new Maybe instance 47 | */ 48 | public static Maybe fromOptional(Optional opt) { 49 | return opt.map(Maybe::just).orElseGet(Maybe::empty); 50 | } 51 | 52 | /** 53 | * Returns a Maybe that emits the resulting value of the CompletionStage or 54 | * its error, treating null as empty source. 55 | * @param the value type 56 | * @param cs the source CompletionStage instance 57 | * @return the new Maybe instance 58 | */ 59 | public static Maybe fromFuture(CompletionStage cs) { 60 | MaybeSubject ms = MaybeSubject.create(); 61 | cs.whenComplete((v, e) -> { 62 | if (e != null) { 63 | ms.onError(e); 64 | } else 65 | if (v != null) { 66 | ms.onSuccess(v); 67 | } else { 68 | ms.onComplete(); 69 | } 70 | }); 71 | return ms; 72 | } 73 | 74 | /** 75 | * Returns a CompletionStage that signals the single value or terminal event 76 | * of the given Maybe source. 77 | *

An empty Maybe will complete with a null value 78 | * @param the value type 79 | * @return the converter function to be used with {@code Maybe.to()} 80 | */ 81 | public static MaybeConverter> get() { 82 | return m -> { 83 | CompletableFuture cf = new CompletableFuture<>(); 84 | m.subscribe(cf::complete, cf::completeExceptionally, () -> cf.complete(null)); 85 | return cf; 86 | }; 87 | } 88 | 89 | /** 90 | * Returns a blocking Stream of a potentially zero or one value (or error) of 91 | * the Maybe. 92 | * @param the value type 93 | * @return the converter function to be used with {@code Maybe.to()} 94 | */ 95 | public static MaybeConverter> toStream() { 96 | return m -> { 97 | ZeroOneIterator zoi = new ZeroOneIterator<>(); 98 | m.subscribe(zoi); 99 | return ZeroOneIterator.toStream(zoi); 100 | }; 101 | } 102 | 103 | /** 104 | * Block until the source Maybe completes and return its possible value as Optional. 105 | * @param the value type 106 | * @return the converter Function to be used with {@code Maybe.to()}. 107 | */ 108 | public static MaybeConverter> element() { 109 | return m -> Optional.ofNullable(m.blockingGet()); 110 | } 111 | 112 | /** 113 | * Maps the upstream value into an optional and extracts its optional value to be emitted towards 114 | * the downstream if present. 115 | * @param the upstream value type 116 | * @param the result value type 117 | * @param mapper the function receiving the upstream value and should return an Optional 118 | * @return the Transformer instance to be used with {@code Flowable.compose()} 119 | */ 120 | public static MaybeTransformer mapOptional(Function> mapper) { 121 | return m -> RxJavaPlugins.onAssembly(new MaybeMapOptional<>(m, mapper)); 122 | } 123 | 124 | } 125 | -------------------------------------------------------------------------------- /src/main/java/hu/akarnokd/rxjava3/jdk8interop/FlowableCollector.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 David Karnok 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package hu.akarnokd.rxjava3.jdk8interop; 18 | 19 | import java.util.function.*; 20 | import java.util.stream.Collector; 21 | 22 | import org.reactivestreams.*; 23 | 24 | import io.reactivex.rxjava3.core.*; 25 | import io.reactivex.rxjava3.exceptions.Exceptions; 26 | import io.reactivex.rxjava3.internal.subscriptions.*; 27 | import io.reactivex.rxjava3.plugins.RxJavaPlugins; 28 | 29 | /** 30 | * Collect elements of the upstream with the help of the Collector's callback functions. 31 | * 32 | * @param the upstream value type 33 | * @param the accumulated type 34 | * @param the result type 35 | */ 36 | final class FlowableCollector extends Flowable { 37 | 38 | final Publisher source; 39 | 40 | final Collector collector; 41 | 42 | FlowableCollector(Publisher source, Collector collector) { 43 | this.source = source; 44 | this.collector = collector; 45 | } 46 | 47 | @Override 48 | protected void subscribeActual(Subscriber s) { 49 | A initialValue; 50 | BiConsumer accumulator; 51 | Function finisher; 52 | 53 | try { 54 | initialValue = collector.supplier().get(); 55 | 56 | accumulator = collector.accumulator(); 57 | 58 | finisher = collector.finisher(); 59 | } catch (Throwable ex) { 60 | Exceptions.throwIfFatal(ex); 61 | EmptySubscription.error(ex, s); 62 | return; 63 | } 64 | 65 | source.subscribe(new CollectorSubscriber<>(s, initialValue, accumulator, finisher)); 66 | } 67 | 68 | static final class CollectorSubscriber extends DeferredScalarSubscription 69 | implements FlowableSubscriber { 70 | 71 | private static final long serialVersionUID = 2129956429647866524L; 72 | 73 | final BiConsumer accumulator; 74 | 75 | final Function finisher; 76 | 77 | A intermediate; 78 | 79 | Subscription upstream; 80 | 81 | boolean done; 82 | 83 | public CollectorSubscriber(Subscriber actual, 84 | A initialValue, BiConsumer accumulator, Function finisher) { 85 | super(actual); 86 | this.intermediate = initialValue; 87 | this.accumulator = accumulator; 88 | this.finisher = finisher; 89 | } 90 | 91 | @Override 92 | public void onSubscribe(Subscription s) { 93 | if (SubscriptionHelper.validate(this.upstream, s)) { 94 | this.upstream = s; 95 | 96 | downstream.onSubscribe(this); 97 | 98 | s.request(Long.MAX_VALUE); 99 | } 100 | } 101 | 102 | @Override 103 | public void onNext(T t) { 104 | if (!done) { 105 | try { 106 | accumulator.accept(intermediate, t); 107 | } catch (Throwable ex) { 108 | Exceptions.throwIfFatal(ex); 109 | upstream.cancel(); 110 | onError(ex); 111 | } 112 | } 113 | } 114 | 115 | @Override 116 | public void onError(Throwable t) { 117 | if (done) { 118 | RxJavaPlugins.onError(t); 119 | } else { 120 | done = true; 121 | intermediate = null; 122 | downstream.onError(t); 123 | } 124 | } 125 | 126 | @Override 127 | public void onComplete() { 128 | if (!done) { 129 | R r; 130 | 131 | try { 132 | r = finisher.apply(intermediate); 133 | } catch (Throwable ex) { 134 | Exceptions.throwIfFatal(ex); 135 | onError(ex); 136 | return; 137 | } 138 | 139 | intermediate = null; 140 | complete(r); 141 | } 142 | } 143 | 144 | @Override 145 | public void cancel() { 146 | super.cancel(); 147 | upstream.cancel(); 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/main/java/hu/akarnokd/rxjava3/jdk8interop/ObservableCollector.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 David Karnok 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package hu.akarnokd.rxjava3.jdk8interop; 18 | 19 | import java.util.function.*; 20 | import java.util.stream.Collector; 21 | 22 | import io.reactivex.rxjava3.core.*; 23 | import io.reactivex.rxjava3.disposables.Disposable; 24 | import io.reactivex.rxjava3.exceptions.Exceptions; 25 | import io.reactivex.rxjava3.internal.disposables.*; 26 | import io.reactivex.rxjava3.internal.observers.DeferredScalarDisposable; 27 | import io.reactivex.rxjava3.plugins.RxJavaPlugins; 28 | 29 | /** 30 | * Collect elements of the upstream with the help of the Collector's callback functions. 31 | * 32 | * @param the upstream value type 33 | * @param the accumulated type 34 | * @param the result type 35 | */ 36 | final class ObservableCollector extends Observable { 37 | 38 | final ObservableSource source; 39 | 40 | final Collector collector; 41 | 42 | ObservableCollector(ObservableSource source, Collector collector) { 43 | this.source = source; 44 | this.collector = collector; 45 | } 46 | 47 | @Override 48 | protected void subscribeActual(Observer s) { 49 | A initialValue; 50 | BiConsumer accumulator; 51 | Function finisher; 52 | 53 | try { 54 | initialValue = collector.supplier().get(); 55 | 56 | accumulator = collector.accumulator(); 57 | 58 | finisher = collector.finisher(); 59 | } catch (Throwable ex) { 60 | Exceptions.throwIfFatal(ex); 61 | EmptyDisposable.error(ex, s); 62 | return; 63 | } 64 | 65 | source.subscribe(new CollectorObserver<>(s, initialValue, accumulator, finisher)); 66 | } 67 | 68 | static final class CollectorObserver extends DeferredScalarDisposable 69 | implements Observer { 70 | 71 | private static final long serialVersionUID = 2129956429647866524L; 72 | 73 | final BiConsumer accumulator; 74 | 75 | final Function finisher; 76 | 77 | A intermediate; 78 | 79 | Disposable upstream; 80 | 81 | boolean done; 82 | 83 | public CollectorObserver(Observer actual, 84 | A initialValue, BiConsumer accumulator, Function finisher) { 85 | super(actual); 86 | this.intermediate = initialValue; 87 | this.accumulator = accumulator; 88 | this.finisher = finisher; 89 | } 90 | 91 | @Override 92 | public void onSubscribe(Disposable d) { 93 | if (DisposableHelper.validate(this.upstream, d)) { 94 | this.upstream = d; 95 | 96 | downstream.onSubscribe(this); 97 | } 98 | } 99 | 100 | @Override 101 | public void onNext(T t) { 102 | if (!done) { 103 | try { 104 | accumulator.accept(intermediate, t); 105 | } catch (Throwable ex) { 106 | Exceptions.throwIfFatal(ex); 107 | upstream.dispose(); 108 | onError(ex); 109 | } 110 | } 111 | } 112 | 113 | @Override 114 | public void onError(Throwable t) { 115 | if (done) { 116 | RxJavaPlugins.onError(t); 117 | } else { 118 | done = true; 119 | intermediate = null; 120 | downstream.onError(t); 121 | } 122 | } 123 | 124 | @Override 125 | public void onComplete() { 126 | if (!done) { 127 | R r; 128 | 129 | try { 130 | r = finisher.apply(intermediate); 131 | } catch (Throwable ex) { 132 | Exceptions.throwIfFatal(ex); 133 | onError(ex); 134 | return; 135 | } 136 | 137 | intermediate = null; 138 | complete(r); 139 | } 140 | } 141 | 142 | @Override 143 | public void dispose() { 144 | super.dispose(); 145 | upstream.dispose(); 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/main/java/hu/akarnokd/rxjava3/jdk8interop/ObservableFromStream.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 David Karnok 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package hu.akarnokd.rxjava3.jdk8interop; 18 | 19 | import java.util.Iterator; 20 | import java.util.concurrent.atomic.AtomicInteger; 21 | import java.util.stream.Stream; 22 | 23 | import io.reactivex.rxjava3.core.*; 24 | import io.reactivex.rxjava3.disposables.Disposable; 25 | import io.reactivex.rxjava3.exceptions.Exceptions; 26 | import io.reactivex.rxjava3.internal.disposables.EmptyDisposable; 27 | import io.reactivex.rxjava3.internal.functions.ObjectHelper; 28 | import io.reactivex.rxjava3.plugins.RxJavaPlugins; 29 | 30 | /** 31 | * Consume a {@link Stream} and close it when the sequence is done 32 | * or gets disposed. 33 | * @since 0.3.4 34 | */ 35 | final class ObservableFromStream extends Observable { 36 | 37 | final Stream stream; 38 | 39 | ObservableFromStream(Stream stream) { 40 | this.stream = stream; 41 | } 42 | 43 | @Override 44 | protected void subscribeActual(Observer observer) { 45 | Iterator iterator; 46 | try { 47 | iterator = stream.iterator(); 48 | } catch (Throwable ex) { 49 | Exceptions.throwIfFatal(ex); 50 | EmptyDisposable.error(ex, observer); 51 | return; 52 | } 53 | StreamDisposable d = new StreamDisposable<>(observer, stream, iterator); 54 | observer.onSubscribe(d); 55 | d.run(); 56 | } 57 | 58 | static final class StreamDisposable 59 | extends AtomicInteger 60 | implements Disposable { 61 | 62 | private static final long serialVersionUID = -7262727127695950226L; 63 | 64 | final Observer downstream; 65 | 66 | AutoCloseable stream; 67 | 68 | volatile Iterator iterator; 69 | 70 | StreamDisposable(Observer downstream, 71 | AutoCloseable stream, Iterator iterator) { 72 | this.downstream = downstream; 73 | this.stream = stream; 74 | this.iterator = iterator; 75 | } 76 | 77 | void run() { 78 | Iterator iterator = this.iterator; 79 | 80 | for (;;) { 81 | 82 | if (getAndIncrement() == 0) { 83 | boolean hasNext; 84 | 85 | try { 86 | hasNext = iterator.hasNext(); 87 | } catch (Throwable ex) { 88 | Exceptions.throwIfFatal(ex); 89 | close(); 90 | downstream.onError(ex); 91 | break; 92 | } 93 | 94 | if (!hasNext) { 95 | close(); 96 | downstream.onComplete(); 97 | break; 98 | } 99 | 100 | if (get() != 1) { 101 | close(); 102 | break; 103 | } 104 | } else { 105 | break; 106 | } 107 | 108 | 109 | T next; 110 | 111 | try { 112 | next = ObjectHelper.requireNonNull(iterator.next(), "The Iterator.next returned a null value"); 113 | } catch (Throwable ex) { 114 | Exceptions.throwIfFatal(ex); 115 | close(); 116 | downstream.onError(ex); 117 | break; 118 | } 119 | if (decrementAndGet() != 0) { 120 | close(); 121 | break; 122 | } 123 | 124 | downstream.onNext(next); 125 | } 126 | } 127 | 128 | void close() { 129 | AutoCloseable ac = stream; 130 | stream = null; 131 | iterator = null; 132 | try { 133 | ac.close(); 134 | } catch (Throwable ex) { 135 | Exceptions.throwIfFatal(ex); 136 | RxJavaPlugins.onError(ex); 137 | } 138 | } 139 | 140 | @Override 141 | public void dispose() { 142 | if (getAndIncrement() == 0) { 143 | close(); 144 | } 145 | } 146 | 147 | @Override 148 | public boolean isDisposed() { 149 | return iterator == null; 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RxJavaJdk8Interop 2 | 3 | ## :warning: Discontinued 4 | 5 | The features of this library (and more) have been integrated into *RxJava 3* proper starting with version **3.0.0-RC7**. 6 | 7 | ---------- 8 | 9 | 10 | [![codecov.io](http://codecov.io/github/akarnokd/RxJavaJdk8Interop/coverage.svg?branch=3.x)](http://codecov.io/github/akarnokd/RxJavaJdk8Interop?branch=3.x) 11 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.github.akarnokd/rxjava3-jdk8-interop/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.github.akarnokd/rxjava3-jdk8-interop) 12 | 13 | RxJava 3.x: [![RxJava 3.x](https://maven-badges.herokuapp.com/maven-central/io.reactivex.rxjava3/rxjava/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.reactivex.rxjava3/rxjava) 14 | 15 | RxJava 3 interop library for supporting Java 8 features such as Optional, Stream and CompletableFuture. 16 | 17 | # Release 18 | 19 | ### RxJava 3 20 | 21 | ```groovy 22 | compile 'com.github.akarnokd:rxjava3-jdk8-interop:3.0.0-RC6' 23 | ``` 24 | 25 | ### [RxJava 2](https://github.com/akarnokd/RxJavaJdk8Interop/tree/master) 26 | 27 | ```groovy 28 | compile 'com.github.akarnokd:rxjava2-jdk8-interop:0.3.7' 29 | ``` 30 | 31 | # Examples 32 | 33 | Javadocs: [https://akarnokd.github.com/RxJavaJdk8Interop/javadoc/index.html](https://akarnokd.github.com/RxJavaJdk8Interop/javadoc/index.html) 34 | 35 | The main entry points are: 36 | 37 | - `FlowableInterop` 38 | - `ObservableInterop` 39 | - `SingleInterop` 40 | - `MaybeInterop` 41 | - `CompletableInterop` 42 | 43 | ## Stream to RxJava 44 | 45 | Note that `java.util.stream.Stream` can be consumed at most once and only 46 | synchronously. 47 | 48 | ```java 49 | Stream stream = ... 50 | 51 | Flowable flow = FlowableInterop.fromStream(stream); 52 | 53 | Observable obs = ObservableInterop.fromStream(stream); 54 | ``` 55 | 56 | ## Optional to RxJava 57 | 58 | ```java 59 | Optional opt = ... 60 | 61 | Flowable flow = FlowableInterop.fromOptional(opt); 62 | 63 | Observable obs = ObservableInterop.fromOptional(opt); 64 | ``` 65 | 66 | ## CompletionStage to RxJava 67 | 68 | Note that cancelling the Subscription won't cancel the `CompletionStage`. 69 | 70 | ```java 71 | CompletionStage cs = ... 72 | 73 | Flowable flow = FlowableInterop.fromFuture(cs); 74 | 75 | Observable flow = ObservableInterop.fromFuture(cs); 76 | ``` 77 | 78 | ## Using Stream Collectors 79 | 80 | ```java 81 | Flowable.range(1, 10) 82 | .compose(FlowableInterop.collect(Collectors.toList())) 83 | .test() 84 | .assertResult(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)); 85 | ``` 86 | 87 | ## Return the first/single/last element as a CompletionStage 88 | 89 | ```java 90 | CompletionStage cs = Flowable.just(1) 91 | .delay(1, TimeUnit.SECONDS) 92 | // return first 93 | .to(FlowableInterop.first()); 94 | 95 | // return single 96 | // .to(FlowableInterop.single()); 97 | 98 | // return last 99 | // .to(FlowableInterop.last()); 100 | 101 | cs.whenComplete((v, e) -> { 102 | System.out.println(v); 103 | System.out.println(e); 104 | }); 105 | ``` 106 | 107 | ## Return the only element as a CompletionStage 108 | 109 | ### Single 110 | 111 | ```java 112 | CompletionStage cs = Single.just(1) 113 | .delay(1, TimeUnit.SECONDS) 114 | .to(SingleInterop.get()); 115 | 116 | cs.whenComplete((v, e) -> { 117 | System.out.println(v); 118 | System.out.println(e); 119 | }); 120 | ``` 121 | 122 | ### Maybe 123 | 124 | ```java 125 | CompletionStage cs = Maybe.just(1) 126 | .delay(1, TimeUnit.SECONDS) 127 | .to(MaybeInterop.get()); 128 | 129 | cs.whenComplete((v, e) -> { 130 | System.out.println(v); 131 | System.out.println(e); 132 | }); 133 | ``` 134 | 135 | ## Await completion as CompletionStage 136 | 137 | ### Completable 138 | 139 | ```java 140 | CompletionStage cs = Completable.complete() 141 | .delay(1, TimeUnit.SECONDS) 142 | .to(CompletableInterop.await()); 143 | 144 | cs.whenComplete((v, e) -> { 145 | System.out.println(v); 146 | System.out.println(e); 147 | }); 148 | ``` 149 | 150 | ## Return the first/last element optionally 151 | 152 | This is a blocking operation 153 | 154 | ```java 155 | Optional opt = Flowable.just(1) 156 | .to(FlowableInterop.firstElement()); 157 | 158 | System.out.println(opt.map(v -> v + 1).orElse(-1)); 159 | ``` 160 | 161 | ## Convert to Java Stream 162 | 163 | This is a blocking operation. Closing the stream will cancel the RxJava sequence. 164 | 165 | ```java 166 | Flowable.range(1, 10) 167 | .to(FlowableInterop.toStream()) 168 | .parallel() 169 | .map(v -> v + 1) 170 | .forEach(System.out::println); 171 | ``` 172 | 173 | ## FlatMap Java Streams 174 | 175 | Note that since consuming a stream is practically blocking, there is no need 176 | for a `maxConcurrency` parameter. 177 | 178 | ```java 179 | 180 | Flowable.range(1, 5) 181 | .compose(FlowableInterop.flatMapStream(v -> Arrays.asList(v, v + 1).stream())) 182 | .test() 183 | .assertResult(1, 2, 2, 3, 3, 4, 4, 5, 5, 6); 184 | ``` 185 | 186 | ## Map based on Java Optional 187 | 188 | ```java 189 | Flowable.range(1, 5) 190 | .compose(FlowableInterop.mapOptional(v -> v % 2 == 0 ? Optional.of(v) : Optional.empty())) 191 | .test() 192 | .assertResult(2, 4); 193 | ``` 194 | -------------------------------------------------------------------------------- /src/main/java/hu/akarnokd/rxjava3/jdk8interop/FlowableFromStream.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 David Karnok 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package hu.akarnokd.rxjava3.jdk8interop; 18 | 19 | import java.util.Iterator; 20 | import java.util.concurrent.atomic.*; 21 | import java.util.stream.Stream; 22 | 23 | import org.reactivestreams.*; 24 | 25 | import io.reactivex.rxjava3.core.Flowable; 26 | import io.reactivex.rxjava3.exceptions.Exceptions; 27 | import io.reactivex.rxjava3.internal.functions.ObjectHelper; 28 | import io.reactivex.rxjava3.internal.subscriptions.*; 29 | import io.reactivex.rxjava3.internal.util.BackpressureHelper; 30 | import io.reactivex.rxjava3.plugins.RxJavaPlugins; 31 | 32 | /** 33 | * Consume a {@link Stream} and close it when the sequence is done 34 | * or gets disposed. 35 | * @since 0.3.4 36 | */ 37 | final class FlowableFromStream extends Flowable { 38 | 39 | final Stream stream; 40 | 41 | FlowableFromStream(Stream stream) { 42 | this.stream = stream; 43 | } 44 | 45 | @Override 46 | protected void subscribeActual(Subscriber s) { 47 | Iterator iterator; 48 | boolean hasNext; 49 | try { 50 | iterator = stream.iterator(); 51 | hasNext = iterator.hasNext(); 52 | if (!hasNext) { 53 | stream.close(); 54 | } 55 | } catch (Throwable ex) { 56 | Exceptions.throwIfFatal(ex); 57 | EmptySubscription.error(ex, s); 58 | return; 59 | } 60 | if (!hasNext) { 61 | EmptySubscription.complete(s); 62 | return; 63 | } 64 | s.onSubscribe(new StreamSubscription<>(s, stream, iterator)); 65 | } 66 | 67 | static final class StreamSubscription extends AtomicInteger implements Subscription { 68 | 69 | private static final long serialVersionUID = 497982641532135424L; 70 | 71 | final Subscriber downstream; 72 | 73 | AutoCloseable stream; 74 | 75 | Iterator iterator; 76 | 77 | final AtomicLong requested; 78 | 79 | StreamSubscription(Subscriber downstream, AutoCloseable stream, Iterator iterator) { 80 | this.downstream = downstream; 81 | this.stream = stream; 82 | this.iterator = iterator; 83 | this.requested = new AtomicLong(); 84 | } 85 | 86 | @Override 87 | public void request(long n) { 88 | if (SubscriptionHelper.validate(n)) { 89 | if (BackpressureHelper.add(requested, n) == 0) { 90 | run(n); 91 | } 92 | } 93 | } 94 | 95 | void run(long requested) { 96 | Iterator iterator = this.iterator; 97 | 98 | long emitted = 0L; 99 | for (;;) { 100 | 101 | if (getAndIncrement() == 0) { 102 | T next; 103 | 104 | try { 105 | next = ObjectHelper.requireNonNull(iterator.next(), "The Iterator.next returned a null value"); 106 | } catch (Throwable ex) { 107 | Exceptions.throwIfFatal(ex); 108 | close(); 109 | downstream.onError(ex); 110 | return; 111 | } 112 | 113 | downstream.onNext(next); 114 | emitted++; 115 | 116 | if (get() != 1) { 117 | close(); 118 | return; 119 | } 120 | } else { 121 | return; 122 | } 123 | 124 | boolean hasNext; 125 | 126 | try { 127 | hasNext = iterator.hasNext(); 128 | } catch (Throwable ex) { 129 | Exceptions.throwIfFatal(ex); 130 | close(); 131 | downstream.onError(ex); 132 | return; 133 | } 134 | 135 | if (decrementAndGet() != 0) { 136 | close(); 137 | return; 138 | } 139 | 140 | if (!hasNext) { 141 | close(); 142 | downstream.onComplete(); 143 | return; 144 | } 145 | 146 | if (emitted == requested) { 147 | 148 | requested = this.requested.get(); 149 | 150 | if (emitted == requested) { 151 | if (this.requested.compareAndSet(requested, 0)) { 152 | return; 153 | } 154 | emitted = 0L; 155 | requested = this.requested.get(); 156 | } 157 | } 158 | } 159 | } 160 | 161 | void close() { 162 | AutoCloseable ac = stream; 163 | stream = null; 164 | iterator = null; 165 | try { 166 | ac.close(); 167 | } catch (Throwable ex) { 168 | Exceptions.throwIfFatal(ex); 169 | RxJavaPlugins.onError(ex); 170 | } 171 | } 172 | 173 | @Override 174 | public void cancel() { 175 | if (getAndIncrement() == 0) { 176 | close(); 177 | } 178 | } 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/main/java/hu/akarnokd/rxjava3/jdk8interop/FlowableMapOptional.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 David Karnok 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package hu.akarnokd.rxjava3.jdk8interop; 18 | 19 | import java.util.Optional; 20 | 21 | import org.reactivestreams.*; 22 | 23 | import io.reactivex.rxjava3.core.Flowable; 24 | import io.reactivex.rxjava3.exceptions.Exceptions; 25 | import io.reactivex.rxjava3.functions.Function; 26 | import io.reactivex.rxjava3.internal.functions.ObjectHelper; 27 | import io.reactivex.rxjava3.internal.fuseable.ConditionalSubscriber; 28 | import io.reactivex.rxjava3.internal.subscribers.*; 29 | 30 | /** 31 | * Maps an upstream value into an Optional and emits its value if not empty. 32 | * 33 | * @param the upstream value type 34 | * @param the result value type 35 | */ 36 | final class FlowableMapOptional extends Flowable { 37 | 38 | final Publisher source; 39 | 40 | final Function> mapper; 41 | 42 | FlowableMapOptional(Publisher source, Function> mapper) { 43 | this.source = source; 44 | this.mapper = mapper; 45 | } 46 | 47 | @Override 48 | protected void subscribeActual(Subscriber s) { 49 | if (s instanceof ConditionalSubscriber) { 50 | source.subscribe(new MapOptionalConditionalSubscriber<>((ConditionalSubscriber)s, mapper)); 51 | } else { 52 | source.subscribe(new MapOptionalSubscriber<>(s, mapper)); 53 | } 54 | } 55 | 56 | static final class MapOptionalSubscriber extends BasicFuseableSubscriber 57 | implements ConditionalSubscriber { 58 | 59 | final Function> mapper; 60 | 61 | public MapOptionalSubscriber(Subscriber actual, Function> mapper) { 62 | super(actual); 63 | this.mapper = mapper; 64 | } 65 | 66 | @Override 67 | public void onNext(T t) { 68 | if (!tryOnNext(t)) { 69 | upstream.request(1); 70 | } 71 | } 72 | 73 | @Override 74 | public boolean tryOnNext(T t) { 75 | if (done) { 76 | return false; 77 | } 78 | 79 | if (sourceMode == ASYNC) { 80 | downstream.onNext(null); 81 | return true; 82 | } 83 | 84 | Optional o; 85 | 86 | try { 87 | o = ObjectHelper.requireNonNull(mapper.apply(t), "The mapper returned a null Optional"); 88 | } catch (Throwable ex) { 89 | Exceptions.throwIfFatal(ex); 90 | fail(ex); 91 | return false; 92 | } 93 | 94 | if (o.isPresent()) { 95 | downstream.onNext(o.get()); 96 | return true; 97 | } 98 | return false; 99 | } 100 | 101 | @Override 102 | public int requestFusion(int mode) { 103 | return transitiveBoundaryFusion(mode); 104 | } 105 | 106 | @Override 107 | public R poll() throws Throwable { 108 | for (;;) { 109 | T t = qs.poll(); 110 | 111 | if (t == null) { 112 | return null; 113 | } 114 | 115 | Optional o = mapper.apply(t); 116 | 117 | if (o.isPresent()) { 118 | return o.get(); 119 | } 120 | 121 | if (sourceMode != SYNC) { 122 | upstream.request(1); 123 | } 124 | } 125 | } 126 | } 127 | 128 | static final class MapOptionalConditionalSubscriber extends BasicFuseableConditionalSubscriber 129 | implements ConditionalSubscriber { 130 | 131 | final Function> mapper; 132 | 133 | public MapOptionalConditionalSubscriber(ConditionalSubscriber actual, Function> mapper) { 134 | super(actual); 135 | this.mapper = mapper; 136 | } 137 | 138 | @Override 139 | public void onNext(T t) { 140 | if (!tryOnNext(t)) { 141 | upstream.request(1); 142 | } 143 | } 144 | 145 | @Override 146 | public boolean tryOnNext(T t) { 147 | if (done) { 148 | return false; 149 | } 150 | 151 | if (sourceMode == ASYNC) { 152 | return downstream.tryOnNext(null); 153 | } 154 | 155 | Optional o; 156 | 157 | try { 158 | o = ObjectHelper.requireNonNull(mapper.apply(t), "The mapper returned a null Optional"); 159 | } catch (Throwable ex) { 160 | Exceptions.throwIfFatal(ex); 161 | fail(ex); 162 | return false; 163 | } 164 | 165 | if (o.isPresent()) { 166 | return downstream.tryOnNext(o.get()); 167 | } 168 | return false; 169 | } 170 | 171 | @Override 172 | public int requestFusion(int mode) { 173 | return transitiveBoundaryFusion(mode); 174 | } 175 | 176 | @Override 177 | public R poll() throws Throwable { 178 | for (;;) { 179 | T t = qs.poll(); 180 | 181 | if (t == null) { 182 | return null; 183 | } 184 | 185 | Optional o = mapper.apply(t); 186 | 187 | if (o.isPresent()) { 188 | return o.get(); 189 | } 190 | 191 | if (sourceMode != SYNC) { 192 | upstream.request(1); 193 | } 194 | } 195 | } 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | # Determine the Java command to use to start the JVM. 86 | if [ -n "$JAVA_HOME" ] ; then 87 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 88 | # IBM's JDK on AIX uses strange locations for the executables 89 | JAVACMD="$JAVA_HOME/jre/sh/java" 90 | else 91 | JAVACMD="$JAVA_HOME/bin/java" 92 | fi 93 | if [ ! -x "$JAVACMD" ] ; then 94 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 95 | 96 | Please set the JAVA_HOME variable in your environment to match the 97 | location of your Java installation." 98 | fi 99 | else 100 | JAVACMD="java" 101 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 102 | 103 | Please set the JAVA_HOME variable in your environment to match the 104 | location of your Java installation." 105 | fi 106 | 107 | # Increase the maximum file descriptors if we can. 108 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 109 | MAX_FD_LIMIT=`ulimit -H -n` 110 | if [ $? -eq 0 ] ; then 111 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 112 | MAX_FD="$MAX_FD_LIMIT" 113 | fi 114 | ulimit -n $MAX_FD 115 | if [ $? -ne 0 ] ; then 116 | warn "Could not set maximum file descriptor limit: $MAX_FD" 117 | fi 118 | else 119 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 120 | fi 121 | fi 122 | 123 | # For Darwin, add options to specify how the application appears in the dock 124 | if $darwin; then 125 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 126 | fi 127 | 128 | # For Cygwin or MSYS, switch paths to Windows format before running java 129 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 130 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 131 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 132 | JAVACMD=`cygpath --unix "$JAVACMD"` 133 | 134 | # We build the pattern for arguments to be converted via cygpath 135 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 136 | SEP="" 137 | for dir in $ROOTDIRSRAW ; do 138 | ROOTDIRS="$ROOTDIRS$SEP$dir" 139 | SEP="|" 140 | done 141 | OURCYGPATTERN="(^($ROOTDIRS))" 142 | # Add a user-defined pattern to the cygpath arguments 143 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 144 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 145 | fi 146 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 147 | i=0 148 | for arg in "$@" ; do 149 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 150 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 151 | 152 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 153 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 154 | else 155 | eval `echo args$i`="\"$arg\"" 156 | fi 157 | i=$((i+1)) 158 | done 159 | case $i in 160 | (0) set -- ;; 161 | (1) set -- "$args0" ;; 162 | (2) set -- "$args0" "$args1" ;; 163 | (3) set -- "$args0" "$args1" "$args2" ;; 164 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 165 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 166 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 167 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 168 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 169 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 170 | esac 171 | fi 172 | 173 | # Escape application args 174 | save () { 175 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 176 | echo " " 177 | } 178 | APP_ARGS=$(save "$@") 179 | 180 | # Collect all arguments for the java command, following the shell quoting and substitution rules 181 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 182 | 183 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 184 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 185 | cd "$(dirname "$0")" 186 | fi 187 | 188 | exec "$JAVACMD" "$@" 189 | -------------------------------------------------------------------------------- /src/test/java/hu/akarnokd/rxjava3/jdk8interop/ObservableFromStreamTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 David Karnok 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package hu.akarnokd.rxjava3.jdk8interop; 18 | 19 | import static org.junit.Assert.assertEquals; 20 | 21 | import java.util.*; 22 | import java.util.concurrent.atomic.AtomicInteger; 23 | import java.util.stream.IntStream; 24 | 25 | import org.junit.Test; 26 | 27 | import hu.akarnokd.rxjava3.jdk8interop.ObservableInterop; 28 | import hu.akarnokd.rxjava3.jdk8interop.ObservableFromStream.StreamDisposable; 29 | import io.reactivex.rxjava3.core.Observable; 30 | import io.reactivex.rxjava3.observers.TestObserver; 31 | import io.reactivex.rxjava3.plugins.RxJavaPlugins; 32 | 33 | public class ObservableFromStreamTest { 34 | 35 | @Test 36 | public void closedAtTheEnd() { 37 | AtomicInteger closed = new AtomicInteger(); 38 | ObservableInterop.fromStream(IntStream.range(1, 6) 39 | .onClose(() -> closed.getAndIncrement()) 40 | .boxed()) 41 | .test() 42 | .assertResult(1, 2, 3, 4, 5); 43 | 44 | assertEquals(1, closed.get()); 45 | } 46 | 47 | @Test 48 | public void closedInTheMiddle() { 49 | AtomicInteger closed = new AtomicInteger(); 50 | 51 | ObservableInterop.fromStream(IntStream.range(1, 6) 52 | .onClose(() -> closed.getAndIncrement()) 53 | .boxed()) 54 | .take(3) 55 | .test() 56 | .assertResult(1, 2, 3); 57 | 58 | assertEquals(1, closed.get()); 59 | } 60 | 61 | @Test 62 | public void noReuse() { 63 | Observable source = ObservableInterop.fromStream(Collections.singleton(1).stream()); 64 | 65 | source.test().assertResult(1); 66 | 67 | source.test().assertFailure(IllegalStateException.class); 68 | } 69 | 70 | @Test 71 | public void disposed() { 72 | TestHelper.checkDisposed(ObservableInterop.fromStream(Collections.singleton(1).stream())); 73 | } 74 | 75 | @Test 76 | public void iteratorHasNextCrash() { 77 | TestObserver to = new TestObserver<>(); 78 | 79 | AtomicInteger calls = new AtomicInteger(); 80 | 81 | StreamDisposable sd = new StreamDisposable<>(to, 82 | () -> { }, 83 | new Iterator() { 84 | 85 | @Override 86 | public boolean hasNext() { 87 | throw new IllegalArgumentException(); 88 | } 89 | 90 | @Override 91 | public Integer next() { 92 | calls.getAndIncrement(); 93 | return 1; 94 | } 95 | } 96 | ); 97 | 98 | to.onSubscribe(sd); 99 | 100 | sd.run(); 101 | 102 | to.assertFailure(IllegalArgumentException.class); 103 | 104 | assertEquals(0, calls.get()); 105 | } 106 | 107 | @Test 108 | public void iteratorHasNextDisposeAfter() { 109 | TestObserver to = new TestObserver<>(); 110 | 111 | AtomicInteger calls = new AtomicInteger(); 112 | 113 | StreamDisposable sd = new StreamDisposable<>(to, 114 | () -> { }, 115 | new Iterator() { 116 | 117 | @Override 118 | public boolean hasNext() { 119 | to.dispose(); 120 | return true; 121 | } 122 | 123 | @Override 124 | public Integer next() { 125 | calls.getAndIncrement(); 126 | return 1; 127 | } 128 | } 129 | ); 130 | 131 | to.onSubscribe(sd); 132 | 133 | sd.run(); 134 | 135 | to.assertEmpty(); 136 | 137 | assertEquals(0, calls.get()); 138 | } 139 | 140 | @Test 141 | public void iteratorNextCrash() { 142 | TestObserver to = new TestObserver<>(); 143 | 144 | AtomicInteger calls = new AtomicInteger(); 145 | 146 | StreamDisposable sd = new StreamDisposable<>(to, 147 | () -> { }, 148 | new Iterator() { 149 | 150 | @Override 151 | public boolean hasNext() { 152 | return true; 153 | } 154 | 155 | @Override 156 | public Integer next() { 157 | calls.getAndIncrement(); 158 | throw new IllegalArgumentException(); 159 | } 160 | } 161 | ); 162 | 163 | to.onSubscribe(sd); 164 | 165 | sd.run(); 166 | 167 | to.assertFailure(IllegalArgumentException.class); 168 | 169 | assertEquals(1, calls.get()); 170 | } 171 | 172 | @Test 173 | public void iteratorNextDisposeAfter() { 174 | TestObserver to = new TestObserver<>(); 175 | 176 | AtomicInteger calls = new AtomicInteger(); 177 | 178 | StreamDisposable sd = new StreamDisposable<>(to, 179 | () -> { }, 180 | new Iterator() { 181 | 182 | @Override 183 | public boolean hasNext() { 184 | return true; 185 | } 186 | 187 | @Override 188 | public Integer next() { 189 | calls.getAndIncrement(); 190 | to.dispose(); 191 | return 1; 192 | } 193 | } 194 | ); 195 | 196 | to.onSubscribe(sd); 197 | 198 | sd.run(); 199 | 200 | to.assertEmpty(); 201 | 202 | assertEquals(1, calls.get()); 203 | } 204 | 205 | 206 | @Test 207 | public void closeCrash() { 208 | List errors = TestHelper.trackPluginErrors(); 209 | try { 210 | TestObserver to = new TestObserver<>(); 211 | 212 | StreamDisposable sd = new StreamDisposable<>(to, 213 | () -> { throw new IllegalArgumentException(); }, 214 | Collections.emptyIterator()); 215 | 216 | to.onSubscribe(sd); 217 | 218 | sd.run(); 219 | 220 | to.assertResult(); 221 | 222 | TestHelper.assertError(errors, 0, IllegalArgumentException.class); 223 | assertEquals(1, errors.size()); 224 | } finally { 225 | RxJavaPlugins.reset(); 226 | } 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /src/main/java/hu/akarnokd/rxjava3/jdk8interop/FlowableInterop.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 David Karnok 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package hu.akarnokd.rxjava3.jdk8interop; 18 | 19 | import java.util.Optional; 20 | import java.util.concurrent.*; 21 | import java.util.stream.*; 22 | 23 | import io.reactivex.rxjava3.core.*; 24 | import io.reactivex.rxjava3.functions.Function; 25 | import io.reactivex.rxjava3.plugins.RxJavaPlugins; 26 | import io.reactivex.rxjava3.processors.AsyncProcessor; 27 | 28 | /** 29 | * Utility methods, sources and operators supporting RxJava 2 and the Jdk 8 API 30 | * interoperation. 31 | * 32 | * @since 0.1.0 33 | */ 34 | public final class FlowableInterop { 35 | 36 | /** Utility class. */ 37 | private FlowableInterop() { 38 | throw new IllegalStateException("No instances!"); 39 | } 40 | 41 | /** 42 | * Wrap a Stream into a Flowable. 43 | *

Note that Streams can only be consumed once and non-concurrently. 44 | * @param the value type 45 | * @param stream the source Stream 46 | * @return the new Flowable instance 47 | */ 48 | public static Flowable fromStream(Stream stream) { 49 | return RxJavaPlugins.onAssembly(new FlowableFromStream<>(stream)); 50 | } 51 | 52 | /** 53 | * Returns a Flowable for the value (or lack of) in the given Optional. 54 | * @param the value type 55 | * @param opt the optional to wrap 56 | * @return the new Flowable instance 57 | */ 58 | public static Flowable fromOptional(Optional opt) { 59 | return opt.map(Flowable::just).orElseGet(Flowable::empty); 60 | } 61 | 62 | /** 63 | * Create a Flowable that signals the terminal value or error of the given 64 | * CompletionStage. 65 | *

Cancelling the Flowable subscription doesn't cancel the CompletionStage. 66 | * @param the value type 67 | * @param cs the CompletionStage instance 68 | * @return the new Flowable instance 69 | */ 70 | public static Flowable fromFuture(CompletionStage cs) { 71 | AsyncProcessor ap = AsyncProcessor.create(); 72 | cs.whenComplete((v, e) -> { 73 | if (e != null) { 74 | ap.onError(e); 75 | } else { 76 | ap.onNext(v); 77 | ap.onComplete(); 78 | } 79 | }); 80 | return ap; 81 | } 82 | 83 | /** 84 | * Collect the elements of the Flowable via the help of Collector and its callback 85 | * functions. 86 | * @param the upstream value type 87 | * @param the accumulated type 88 | * @param the result type 89 | * @param collector the Collector object providing the callbacks 90 | * @return the Transformer instance to be used with {@code Flowable.compose()} 91 | */ 92 | public static FlowableTransformer collect(Collector collector) { 93 | return f -> RxJavaPlugins.onAssembly(new FlowableCollector<>(f, collector)); 94 | } 95 | 96 | /** 97 | * Returns a CompletionStage that signals the first element of the Flowable 98 | * or a NoSuchElementException if the Flowable is empty. 99 | * @param the value type 100 | * @return the converter function to be used via {@code Flowable.to}. 101 | */ 102 | public static FlowableConverter> first() { 103 | return f -> { 104 | CompletableFuture cf = new CompletableFuture<>(); 105 | f.firstOrError().subscribe(cf::complete, cf::completeExceptionally); 106 | return cf; 107 | }; 108 | } 109 | 110 | /** 111 | * Returns a CompletionStage that signals the single element of the Flowable, 112 | * IllegalArgumentException if the Flowable is longer than 1 element 113 | * or a NoSuchElementException if the Flowable is empty. 114 | * @param the value type 115 | * @return the converter function to be used with {@code Flowable.to}. 116 | */ 117 | public static FlowableConverter> single() { 118 | return f -> { 119 | CompletableFuture cf = new CompletableFuture<>(); 120 | f.singleOrError().subscribe(cf::complete, cf::completeExceptionally); 121 | return cf; 122 | }; 123 | } 124 | 125 | /** 126 | * Returns a CompletionStage that emits the last element of the Flowable or 127 | * NoSuchElementException if the Flowable is empty. 128 | * @param the value type 129 | * @return the converter function to be used with {@code Flowable.to}. 130 | */ 131 | public static FlowableConverter> last() { 132 | return f -> { 133 | CompletableFuture cf = new CompletableFuture<>(); 134 | f.lastOrError().subscribe(cf::complete, cf::completeExceptionally); 135 | return cf; 136 | }; 137 | } 138 | 139 | /** 140 | * Returns a blocking Stream of the elements of the Flowable. 141 | *

142 | * Closing the Stream will cancel the flow. 143 | * @param the value type 144 | * @return the converter function to be used with {@code Flowable.to}. 145 | */ 146 | public static FlowableConverter> toStream() { 147 | return f -> ZeroOneIterator.toStream(f.blockingIterable().iterator()); 148 | } 149 | 150 | /** 151 | * Block until the source Flowable emits its first item and return that as Optional. 152 | * @param the value type 153 | * @return the converter Function to be used with {@code Flowable.to()}. 154 | */ 155 | public static FlowableConverter> firstElement() { 156 | return f -> Optional.ofNullable(f.blockingFirst(null)); 157 | } 158 | 159 | /** 160 | * Block until the source Flowable completes and return its last value as Optional. 161 | * @param the value type 162 | * @return the converter Function to be used with {@code Flowable.to()}. 163 | */ 164 | public static FlowableConverter> lastElement() { 165 | return f -> Optional.ofNullable(f.blockingLast(null)); 166 | } 167 | 168 | /** 169 | * Map each value of the upstream into a Stream and flatten them into a single sequence. 170 | * @param the input value type 171 | * @param the Stream type 172 | * @param mapper the function that returns a Stream for each upstream value 173 | * @return the Transformer instance to be used with {@code Flowable.compose()} 174 | */ 175 | public static FlowableTransformer flatMapStream(Function> mapper) { 176 | return f -> f.concatMap(v -> fromStream(mapper.apply(v))); 177 | } 178 | 179 | /** 180 | * Maps the upstream value into an optional and extracts its optional value to be emitted towards 181 | * the downstream if present. 182 | * @param the upstream value type 183 | * @param the result value type 184 | * @param mapper the function receiving the upstream value and should return an Optional 185 | * @return the Transformer instance to be used with {@code Flowable.compose()} 186 | */ 187 | public static FlowableTransformer mapOptional(Function> mapper) { 188 | return f -> RxJavaPlugins.onAssembly(new FlowableMapOptional<>(f, mapper)); 189 | } 190 | 191 | } 192 | -------------------------------------------------------------------------------- /src/main/java/hu/akarnokd/rxjava3/jdk8interop/ObservableInterop.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 David Karnok 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package hu.akarnokd.rxjava3.jdk8interop; 18 | 19 | import java.util.Optional; 20 | import java.util.concurrent.*; 21 | import java.util.stream.*; 22 | 23 | import io.reactivex.rxjava3.core.*; 24 | import io.reactivex.rxjava3.functions.Function; 25 | import io.reactivex.rxjava3.internal.functions.ObjectHelper; 26 | import io.reactivex.rxjava3.plugins.RxJavaPlugins; 27 | import io.reactivex.rxjava3.subjects.AsyncSubject; 28 | 29 | /** 30 | * Utility methods, sources and operators supporting RxJava 2 and the Jdk 8 API 31 | * interoperation. 32 | * 33 | * @since 0.1.0 34 | */ 35 | public final class ObservableInterop { 36 | 37 | /** Utility class. */ 38 | private ObservableInterop() { 39 | throw new IllegalStateException("No instances!"); 40 | } 41 | 42 | /** 43 | * Wrap a Stream into a Observable. 44 | *

45 | * Note that Streams can only be consumed once and non-concurrently. 46 | *

47 | * The operator closes the stream. Exceptions thrown by Stream.close() 48 | * are routed to the global RxJavaPlugins.onError handler. 49 | * @param the value type 50 | * @param stream the source Stream 51 | * @return the new Observable instance 52 | */ 53 | public static Observable fromStream(Stream stream) { 54 | ObjectHelper.requireNonNull(stream, "stream is null"); 55 | return RxJavaPlugins.onAssembly(new ObservableFromStream<>(stream)); 56 | } 57 | 58 | /** 59 | * Returns a Observable for the value (or lack of) in the given Optional. 60 | * @param the value type 61 | * @param opt the optional to wrap 62 | * @return the new Observable instance 63 | */ 64 | public static Observable fromOptional(Optional opt) { 65 | return opt.map(Observable::just).orElseGet(Observable::empty); 66 | } 67 | 68 | /** 69 | * Create a Observable that signals the terminal value or error of the given 70 | * CompletionStage. 71 | *

Cancelling the Observable subscription doesn't cancel the CompletionStage. 72 | * @param the value type 73 | * @param cs the CompletionStage instance 74 | * @return the new Observable instance 75 | */ 76 | public static Observable fromFuture(CompletionStage cs) { 77 | AsyncSubject ap = AsyncSubject.create(); 78 | cs.whenComplete((v, e) -> { 79 | if (e != null) { 80 | ap.onError(e); 81 | } else { 82 | ap.onNext(v); 83 | ap.onComplete(); 84 | } 85 | }); 86 | return ap; 87 | } 88 | 89 | /** 90 | * Collect the elements of the Observable via the help of Collector and its callback 91 | * functions. 92 | * @param the upstream value type 93 | * @param the accumulated type 94 | * @param the result type 95 | * @param collector the Collector object providing the callbacks 96 | * @return the Transformer instance to be used with {@code Observable.compose()} 97 | */ 98 | public static ObservableTransformer collect(Collector collector) { 99 | return f -> RxJavaPlugins.onAssembly(new ObservableCollector<>(f, collector)); 100 | } 101 | 102 | /** 103 | * Returns a CompletionStage that signals the first element of the Observable 104 | * or a NoSuchElementException if the Observable is empty. 105 | * @param the value type 106 | * @return the converter function to be used via {@code Observable.to}. 107 | */ 108 | public static ObservableConverter> first() { 109 | return f -> { 110 | CompletableFuture cf = new CompletableFuture<>(); 111 | f.firstOrError().subscribe(cf::complete, cf::completeExceptionally); 112 | return cf; 113 | }; 114 | } 115 | 116 | /** 117 | * Returns a CompletionStage that signals the single element of the Observable, 118 | * IllegalArgumentException if the Observable is longer than 1 element 119 | * or a NoSuchElementException if the Observable is empty. 120 | * @param the value type 121 | * @return the converter function to be used with {@code Observable.to}. 122 | */ 123 | public static ObservableConverter> single() { 124 | return f -> { 125 | CompletableFuture cf = new CompletableFuture<>(); 126 | f.singleOrError().subscribe(cf::complete, cf::completeExceptionally); 127 | return cf; 128 | }; 129 | } 130 | 131 | /** 132 | * Returns a CompletionStage that emits the last element of the Observable or 133 | * NoSuchElementException if the Observable is empty. 134 | * @param the value type 135 | * @return the converter function to be used with {@code Observable.to}. 136 | */ 137 | public static ObservableConverter> last() { 138 | return f -> { 139 | CompletableFuture cf = new CompletableFuture<>(); 140 | f.lastOrError().subscribe(cf::complete, cf::completeExceptionally); 141 | return cf; 142 | }; 143 | } 144 | 145 | /** 146 | * Returns a blocking Stream of the elements of the Observable. 147 | *

148 | * Closing the Stream will cancel the flow. 149 | * @param the value type 150 | * @return the converter function to be used with {@code Observable.to}. 151 | */ 152 | public static ObservableConverter> toStream() { 153 | return f -> ZeroOneIterator.toStream(f.blockingIterable().iterator()); 154 | } 155 | 156 | /** 157 | * Block until the source Observable emits its first item and return that as Optional. 158 | * @param the value type 159 | * @return the converter Function to be used with {@code Observable.to()}. 160 | */ 161 | public static ObservableConverter> firstElement() { 162 | return o -> Optional.ofNullable(o.blockingFirst(null)); 163 | } 164 | 165 | /** 166 | * Block until the source Observable completes and return its last value as Optional. 167 | * @param the value type 168 | * @return the converter Function to be used with {@code Observable.to()}. 169 | */ 170 | public static ObservableConverter> lastElement() { 171 | return o -> Optional.ofNullable(o.blockingLast(null)); 172 | } 173 | 174 | /** 175 | * Maps the upstream value into an optional and extracts its optional value to be emitted towards 176 | * the downstream if present. 177 | * @param the upstream value type 178 | * @param the result value type 179 | * @param mapper the function receiving the upstream value and should return an Optional 180 | * @return the Transformer instance to be used with {@code Observable.compose()} 181 | */ 182 | public static ObservableTransformer mapOptional(Function> mapper) { 183 | return f -> RxJavaPlugins.onAssembly(new ObservableMapOptional<>(f, mapper)); 184 | } 185 | 186 | /** 187 | * Map each value of the upstream into a Stream and flatten them into a single sequence. 188 | * @param the input value type 189 | * @param the Stream type 190 | * @param mapper the function that returns a Stream for each upstream value 191 | * @return the new Transformer instance 192 | */ 193 | public static ObservableTransformer flatMapStream(Function> mapper) { 194 | return o -> o.concatMap(v -> fromStream(mapper.apply(v))); 195 | } 196 | 197 | } 198 | -------------------------------------------------------------------------------- /src/test/java/hu/akarnokd/rxjava3/jdk8interop/MaybeInteropTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 David Karnok 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package hu.akarnokd.rxjava3.jdk8interop; 18 | 19 | import java.util.*; 20 | import java.util.concurrent.*; 21 | import java.util.concurrent.atomic.AtomicReference; 22 | import java.util.stream.*; 23 | 24 | import org.junit.*; 25 | 26 | import io.reactivex.rxjava3.core.*; 27 | import io.reactivex.rxjava3.disposables.*; 28 | 29 | public class MaybeInteropTest { 30 | 31 | @Test 32 | public void utilityClass() { 33 | TestHelper.checkUtilityClass(MaybeInterop.class); 34 | } 35 | 36 | @Test 37 | public void fromOptional() { 38 | MaybeInterop.fromOptional(Optional.of(1)) 39 | .test() 40 | .assertResult(1); 41 | 42 | MaybeInterop.fromOptional(Optional.empty()) 43 | .test() 44 | .assertResult(); 45 | } 46 | 47 | @Test 48 | public void fromFuture() { 49 | MaybeInterop.fromFuture(CompletableFuture.supplyAsync(() -> 1)) 50 | .test() 51 | .awaitDone(5, TimeUnit.SECONDS) 52 | .assertResult(1); 53 | } 54 | 55 | @Test 56 | public void fromFutureEmpty() { 57 | MaybeInterop.fromFuture(CompletableFuture.supplyAsync(() -> null)) 58 | .test() 59 | .awaitDone(5, TimeUnit.SECONDS) 60 | .assertResult(); 61 | 62 | MaybeInterop.fromFuture(CompletableFuture.runAsync(() -> { })) 63 | .test() 64 | .awaitDone(5, TimeUnit.SECONDS) 65 | .assertResult(); 66 | } 67 | 68 | @Test 69 | public void fromFutureError() { 70 | TestObserverEx ts = MaybeInterop.fromFuture( 71 | CompletableFuture.supplyAsync(() -> { throw new IllegalArgumentException(); })) 72 | .subscribeWith(new TestObserverEx<>()) 73 | .awaitDone(5, TimeUnit.SECONDS) 74 | .assertFailure(CompletionException.class); 75 | 76 | Throwable c = ts.errors().get(0).getCause(); 77 | Assert.assertTrue(c.toString(), c instanceof IllegalArgumentException); 78 | } 79 | 80 | @Test 81 | public void get() { 82 | TestHelper.assertFuture(1, Maybe.just(1) 83 | .to(MaybeInterop.get())); 84 | } 85 | 86 | @Test(expected = IllegalArgumentException.class) 87 | public void getError() { 88 | TestHelper.assertFuture(null, Maybe.error(new IllegalArgumentException()) 89 | .to(MaybeInterop.get())); 90 | } 91 | 92 | @Test 93 | public void getEmpty() { 94 | TestHelper.assertFuture(null, Maybe.empty() 95 | .to(MaybeInterop.get())); 96 | } 97 | 98 | @Test 99 | public void toStream() { 100 | List list = Maybe.just(1) 101 | .to(MaybeInterop.toStream()) 102 | .collect(Collectors.toList()); 103 | 104 | Assert.assertEquals(Arrays.asList(1), list); 105 | } 106 | 107 | @Test 108 | public void toStreamEmpty() { 109 | List list = Maybe.empty() 110 | .to(MaybeInterop.toStream()) 111 | .collect(Collectors.toList()); 112 | 113 | Assert.assertTrue(list.isEmpty()); 114 | } 115 | 116 | @Test(expected = IllegalArgumentException.class) 117 | public void toStreamError() { 118 | Maybe.error(new IllegalArgumentException()) 119 | .to(MaybeInterop.toStream()) 120 | .collect(Collectors.toList()); 121 | } 122 | 123 | @Test 124 | public void element() { 125 | Assert.assertEquals((Integer)1, Maybe.just(1) 126 | .to(MaybeInterop.element()).get()); 127 | } 128 | 129 | @Test 130 | public void elementEmpty() { 131 | Assert.assertFalse(Maybe.empty().to(MaybeInterop.element()).isPresent()); 132 | } 133 | 134 | @Test(expected = IllegalArgumentException.class) 135 | public void elementError() { 136 | Maybe.error(new IllegalArgumentException()) 137 | .to(MaybeInterop.element()) 138 | .get(); 139 | } 140 | 141 | @Test 142 | public void mapOptional() { 143 | Maybe.just(1) 144 | .compose(MaybeInterop.mapOptional(v -> Optional.of(-v))) 145 | .test() 146 | .assertResult(-1); 147 | } 148 | 149 | @Test 150 | public void mapOptionalEmpty() { 151 | Maybe.just(1) 152 | .compose(MaybeInterop.mapOptional(v -> Optional.empty())) 153 | .test() 154 | .assertResult(); 155 | } 156 | 157 | @Test 158 | public void mapOptionalEmpty2() { 159 | Maybe.empty() 160 | .compose(MaybeInterop.mapOptional(v -> Optional.of(-v))) 161 | .test() 162 | .assertResult(); 163 | } 164 | 165 | @Test 166 | public void mapOptionalError() { 167 | Maybe.error(new IllegalArgumentException()) 168 | .compose(MaybeInterop.mapOptional(v -> Optional.of(-v))) 169 | .test() 170 | .assertFailure(IllegalArgumentException.class); 171 | } 172 | 173 | @Test 174 | public void mapOptionalMapperCrash() { 175 | Maybe.just(1) 176 | .compose(MaybeInterop.mapOptional(v -> null)) 177 | .test() 178 | .assertFailure(NullPointerException.class); 179 | } 180 | 181 | @Test 182 | public void badSource() { 183 | TestHelper.checkDoubleOnSubscribeMaybe(m -> 184 | m.compose(MaybeInterop.mapOptional(v -> Optional.of(v))) 185 | ); 186 | } 187 | 188 | @Test 189 | public void disposed() { 190 | TestHelper.checkDisposed( 191 | Maybe.fromCallable(() -> 1) 192 | .compose(MaybeInterop.mapOptional(v -> Optional.of(v))) 193 | ); 194 | } 195 | 196 | @Test 197 | public void toStreamDelayed() { 198 | Iterator it = Maybe.timer(100, TimeUnit.MILLISECONDS) 199 | .to(MaybeInterop.toStream()) 200 | .iterator(); 201 | 202 | Assert.assertEquals(0L, it.next().longValue()); 203 | 204 | try { 205 | it.next(); 206 | Assert.fail("Should have thrown"); 207 | } catch (NoSuchElementException expected) { 208 | // expected 209 | } 210 | } 211 | 212 | @Test 213 | public void toStreamInterrupted() { 214 | boolean[] disposed = { false }; 215 | Iterator it = Maybe.never() 216 | .doOnDispose(() -> disposed[0] = true) 217 | .to(MaybeInterop.toStream()) 218 | .iterator(); 219 | 220 | Thread.currentThread().interrupt(); 221 | try { 222 | try { 223 | it.hasNext(); 224 | Assert.fail("Should have thrown"); 225 | } catch (RuntimeException ex) { 226 | Assert.assertTrue(ex.getCause() + "", ex.getCause() instanceof InterruptedException); 227 | } 228 | Assert.assertTrue(disposed[0]); 229 | } finally { 230 | Thread.interrupted(); 231 | } 232 | } 233 | 234 | @Test 235 | public void lateSubscriberAlreadyCancelled() { 236 | AtomicReference> sub = new AtomicReference<>(); 237 | Stream it = new Maybe() { 238 | @Override 239 | protected void subscribeActual(MaybeObserver observer) { 240 | sub.set(observer); 241 | } 242 | } 243 | .to(MaybeInterop.toStream()); 244 | 245 | it.close(); 246 | 247 | Disposable d1 = Disposables.empty(); 248 | sub.get().onSubscribe(d1); 249 | Assert.assertTrue(d1.isDisposed()); 250 | 251 | Disposable d2 = Disposables.empty(); 252 | sub.get().onSubscribe(d2); 253 | Assert.assertTrue(d2.isDisposed()); 254 | } 255 | 256 | @Test 257 | public void zeroOneDirect() { 258 | ZeroOneIterator z = Maybe.just(1).subscribeWith(new ZeroOneIterator<>()); 259 | 260 | Assert.assertEquals(1, z.next().intValue()); 261 | 262 | try { 263 | z.next(); 264 | Assert.fail("Should have thrown"); 265 | } catch (NoSuchElementException expected) { 266 | // expected 267 | } 268 | } 269 | 270 | 271 | @Test 272 | public void zeroOneDirectNever() { 273 | ZeroOneIterator z = Maybe.never().subscribeWith(new ZeroOneIterator<>()); 274 | 275 | Assert.assertFalse(z.isDisposed()); 276 | 277 | z.dispose(); 278 | 279 | Assert.assertTrue(z.isDisposed()); 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /src/test/java/hu/akarnokd/rxjava3/jdk8interop/FlowableFromStreamTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 David Karnok 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package hu.akarnokd.rxjava3.jdk8interop; 18 | 19 | import static org.junit.Assert.assertEquals; 20 | 21 | import java.util.*; 22 | import java.util.concurrent.atomic.AtomicInteger; 23 | import java.util.stream.IntStream; 24 | 25 | import org.junit.Test; 26 | 27 | import hu.akarnokd.rxjava3.jdk8interop.FlowableInterop; 28 | import hu.akarnokd.rxjava3.jdk8interop.FlowableFromStream.StreamSubscription; 29 | import io.reactivex.rxjava3.core.Flowable; 30 | import io.reactivex.rxjava3.plugins.RxJavaPlugins; 31 | import io.reactivex.rxjava3.schedulers.Schedulers; 32 | import io.reactivex.rxjava3.subscribers.TestSubscriber; 33 | 34 | public class FlowableFromStreamTest { 35 | 36 | @Test 37 | public void closedAtTheEnd() { 38 | AtomicInteger closed = new AtomicInteger(); 39 | FlowableInterop.fromStream(IntStream.range(1, 6) 40 | .onClose(() -> closed.getAndIncrement()) 41 | .boxed()) 42 | .test() 43 | .assertResult(1, 2, 3, 4, 5); 44 | 45 | assertEquals(1, closed.get()); 46 | } 47 | 48 | @Test 49 | public void empty() { 50 | AtomicInteger closed = new AtomicInteger(); 51 | FlowableInterop.fromStream(IntStream.range(1, 1) 52 | .onClose(() -> closed.getAndIncrement()) 53 | .boxed()) 54 | .test() 55 | .assertResult(); 56 | 57 | assertEquals(1, closed.get()); 58 | } 59 | 60 | @Test 61 | public void closedInTheMiddle() { 62 | AtomicInteger closed = new AtomicInteger(); 63 | 64 | FlowableInterop.fromStream(IntStream.range(1, 6) 65 | .onClose(() -> closed.getAndIncrement()) 66 | .boxed()) 67 | .take(3) 68 | .test() 69 | .assertResult(1, 2, 3); 70 | 71 | assertEquals(1, closed.get()); 72 | } 73 | 74 | @Test 75 | public void noReuse() { 76 | Flowable source = FlowableInterop.fromStream(Collections.singleton(1).stream()); 77 | 78 | source.test().assertResult(1); 79 | 80 | source.test().assertFailure(IllegalStateException.class); 81 | } 82 | 83 | @Test 84 | public void iteratorHasNextCrash() { 85 | TestSubscriber ts = new TestSubscriber<>(); 86 | 87 | AtomicInteger calls = new AtomicInteger(); 88 | 89 | StreamSubscription sd = new StreamSubscription<>(ts, 90 | () -> { }, 91 | new Iterator() { 92 | 93 | @Override 94 | public boolean hasNext() { 95 | throw new IllegalArgumentException(); 96 | } 97 | 98 | @Override 99 | public Integer next() { 100 | calls.getAndIncrement(); 101 | return 1; 102 | } 103 | } 104 | ); 105 | 106 | ts.onSubscribe(sd); 107 | 108 | ts.assertFailure(IllegalArgumentException.class, 1); 109 | 110 | assertEquals(1, calls.get()); 111 | } 112 | 113 | @Test 114 | public void iteratorHasNextDisposeAfter() { 115 | TestSubscriber ts = new TestSubscriber<>(); 116 | 117 | AtomicInteger calls = new AtomicInteger(); 118 | 119 | StreamSubscription sd = new StreamSubscription<>(ts, 120 | () -> { }, 121 | new Iterator() { 122 | 123 | @Override 124 | public boolean hasNext() { 125 | ts.cancel(); 126 | return true; 127 | } 128 | 129 | @Override 130 | public Integer next() { 131 | calls.getAndIncrement(); 132 | return 1; 133 | } 134 | } 135 | ); 136 | 137 | ts.onSubscribe(sd); 138 | 139 | ts.assertValuesOnly(1); 140 | 141 | assertEquals(1, calls.get()); 142 | } 143 | 144 | @Test 145 | public void iteratorNextCrash() { 146 | TestSubscriber ts = new TestSubscriber<>(); 147 | 148 | AtomicInteger calls = new AtomicInteger(); 149 | 150 | StreamSubscription sd = new StreamSubscription<>(ts, 151 | () -> { }, 152 | new Iterator() { 153 | 154 | @Override 155 | public boolean hasNext() { 156 | return true; 157 | } 158 | 159 | @Override 160 | public Integer next() { 161 | calls.getAndIncrement(); 162 | throw new IllegalArgumentException(); 163 | } 164 | } 165 | ); 166 | 167 | ts.onSubscribe(sd); 168 | 169 | ts.assertFailure(IllegalArgumentException.class); 170 | 171 | assertEquals(1, calls.get()); 172 | } 173 | 174 | @Test 175 | public void iteratorNextDisposeAfter() { 176 | TestSubscriber ts = new TestSubscriber<>(); 177 | 178 | AtomicInteger calls = new AtomicInteger(); 179 | 180 | StreamSubscription sd = new StreamSubscription<>(ts, 181 | () -> { }, 182 | new Iterator() { 183 | 184 | @Override 185 | public boolean hasNext() { 186 | return true; 187 | } 188 | 189 | @Override 190 | public Integer next() { 191 | calls.getAndIncrement(); 192 | ts.cancel(); 193 | return 1; 194 | } 195 | } 196 | ); 197 | 198 | ts.onSubscribe(sd); 199 | 200 | ts.assertValuesOnly(1); 201 | 202 | assertEquals(1, calls.get()); 203 | } 204 | 205 | 206 | @Test 207 | public void closeCrash() { 208 | List errors = TestHelper.trackPluginErrors(); 209 | try { 210 | TestSubscriber ts = new TestSubscriber<>(); 211 | 212 | StreamSubscription sd = new StreamSubscription<>(ts, 213 | () -> { throw new IllegalArgumentException(); }, 214 | Collections.singleton(1).iterator()); 215 | 216 | ts.onSubscribe(sd); 217 | 218 | ts.assertResult(1); 219 | 220 | TestHelper.assertError(errors, 0, IllegalArgumentException.class); 221 | assertEquals(1, errors.size()); 222 | } finally { 223 | RxJavaPlugins.reset(); 224 | } 225 | } 226 | 227 | @Test 228 | public void backpressured() { 229 | TestSubscriber ts = FlowableInterop.fromStream(IntStream.range(1, 6) 230 | .boxed()) 231 | .test(0L); 232 | 233 | ts.assertEmpty(); 234 | 235 | ts.request(1); 236 | 237 | ts.assertValuesOnly(1); 238 | 239 | ts.request(2); 240 | 241 | ts.assertValuesOnly(1, 2, 3); 242 | 243 | ts.request(2); 244 | 245 | ts.assertResult(1, 2, 3, 4, 5); 246 | } 247 | 248 | @Test 249 | public void sngleStep() { 250 | TestSubscriber ts = new TestSubscriber(1L) { 251 | @Override 252 | public void onNext(Integer t) { 253 | super.onNext(t); 254 | request(1); 255 | } 256 | }; 257 | FlowableInterop.fromStream(IntStream.range(1, 6) 258 | .boxed()) 259 | .subscribe(ts); 260 | 261 | ts.assertResult(1, 2, 3, 4, 5); 262 | } 263 | 264 | @Test 265 | public void badRequest() { 266 | TestHelper.assertBadRequestReported(FlowableInterop.fromStream(IntStream.range(1, 6) 267 | .boxed())); 268 | } 269 | 270 | @Test 271 | public void asyncRequests() { 272 | for (int i = 0; i < 1000; i++) { 273 | TestSubscriber ts = new TestSubscriber<>(0L); 274 | 275 | FlowableInterop.fromStream(IntStream.range(1, 10001).boxed()) 276 | .subscribe(ts); 277 | 278 | Runnable r = () -> { 279 | for (int j = 0; j < 10000; j++) { 280 | ts.request(1); 281 | } 282 | }; 283 | 284 | TestHelper.race(r, r, Schedulers.single()); 285 | 286 | ts.assertValueCount(10000) 287 | .assertNoErrors() 288 | .assertComplete(); 289 | } 290 | } 291 | 292 | @Test 293 | public void cancelAndRequest() { 294 | TestSubscriber ts = new TestSubscriber<>(0L); 295 | 296 | AtomicInteger calls = new AtomicInteger(); 297 | 298 | StreamSubscription sd = new StreamSubscription<>(ts, 299 | () -> { }, 300 | new Iterator() { 301 | 302 | @Override 303 | public boolean hasNext() { 304 | return true; 305 | } 306 | 307 | @Override 308 | public Integer next() { 309 | calls.getAndIncrement(); 310 | return 1; 311 | } 312 | } 313 | ); 314 | 315 | ts.onSubscribe(sd); 316 | 317 | sd.cancel(); 318 | sd.request(1); 319 | } 320 | } 321 | -------------------------------------------------------------------------------- /src/test/java/hu/akarnokd/rxjava3/jdk8interop/TestObserverEx.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 David Karnok 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package hu.akarnokd.rxjava3.jdk8interop; 18 | 19 | import java.util.concurrent.atomic.AtomicReference; 20 | 21 | import io.reactivex.rxjava3.core.*; 22 | import io.reactivex.rxjava3.disposables.Disposable; 23 | import io.reactivex.rxjava3.internal.disposables.DisposableHelper; 24 | import io.reactivex.rxjava3.internal.fuseable.*; 25 | 26 | /** 27 | * An extended test Observer that records events and allows making assertions about them. 28 | * 29 | *

You can override the onSubscribe, onNext, onError, onComplete, onSuccess and 30 | * cancel methods but not the others (this is by design). 31 | * 32 | *

The TestObserver implements Disposable for convenience where dispose calls cancel. 33 | * 34 | * @param the value type 35 | */ 36 | public class TestObserverEx 37 | extends BaseTestConsumerEx> 38 | implements Observer, Disposable, MaybeObserver, SingleObserver, CompletableObserver { 39 | /** The actual observer to forward events to. */ 40 | private final Observer downstream; 41 | 42 | /** Holds the current subscription if any. */ 43 | private final AtomicReference upstream = new AtomicReference<>(); 44 | 45 | private QueueDisposable qd; 46 | 47 | /** 48 | * Constructs a non-forwarding TestObserver. 49 | */ 50 | public TestObserverEx() { 51 | this(EmptyObserver.INSTANCE); 52 | } 53 | 54 | /** 55 | * Constructs a forwarding TestObserver. 56 | * @param downstream the actual Observer to forward events to 57 | */ 58 | public TestObserverEx(Observer downstream) { 59 | this.downstream = downstream; 60 | } 61 | 62 | /** 63 | * Constructs a TestObserverEx with the given initial fusion mode. 64 | * @param fusionMode the fusion mode, see {@link QueueFuseable} 65 | */ 66 | public TestObserverEx(int fusionMode) { 67 | this(); 68 | setInitialFusionMode(fusionMode); 69 | } 70 | 71 | @SuppressWarnings("unchecked") 72 | @Override 73 | public void onSubscribe(Disposable d) { 74 | lastThread = Thread.currentThread(); 75 | 76 | if (d == null) { 77 | errors.add(new NullPointerException("onSubscribe received a null Subscription")); 78 | return; 79 | } 80 | if (!upstream.compareAndSet(null, d)) { 81 | d.dispose(); 82 | if (upstream.get() != DisposableHelper.DISPOSED) { 83 | errors.add(new IllegalStateException("onSubscribe received multiple subscriptions: " + d)); 84 | } 85 | return; 86 | } 87 | 88 | if (initialFusionMode != 0) { 89 | if (d instanceof QueueDisposable) { 90 | qd = (QueueDisposable)d; 91 | 92 | int m = qd.requestFusion(initialFusionMode); 93 | establishedFusionMode = m; 94 | 95 | if (m == QueueFuseable.SYNC) { 96 | checkSubscriptionOnce = true; 97 | lastThread = Thread.currentThread(); 98 | try { 99 | T t; 100 | while ((t = qd.poll()) != null) { 101 | values.add(t); 102 | } 103 | completions++; 104 | 105 | upstream.lazySet(DisposableHelper.DISPOSED); 106 | } catch (Throwable ex) { 107 | errors.add(ex); 108 | } 109 | return; 110 | } 111 | } 112 | } 113 | 114 | downstream.onSubscribe(d); 115 | } 116 | 117 | @Override 118 | public void onNext(T t) { 119 | if (!checkSubscriptionOnce) { 120 | checkSubscriptionOnce = true; 121 | if (upstream.get() == null) { 122 | errors.add(new IllegalStateException("onSubscribe not called in proper order")); 123 | } 124 | } 125 | 126 | lastThread = Thread.currentThread(); 127 | 128 | if (establishedFusionMode == QueueFuseable.ASYNC) { 129 | try { 130 | while ((t = qd.poll()) != null) { 131 | values.add(t); 132 | } 133 | } catch (Throwable ex) { 134 | errors.add(ex); 135 | qd.dispose(); 136 | } 137 | return; 138 | } 139 | 140 | values.add(t); 141 | 142 | if (t == null) { 143 | errors.add(new NullPointerException("onNext received a null value")); 144 | } 145 | 146 | downstream.onNext(t); 147 | } 148 | 149 | @Override 150 | public void onError(Throwable t) { 151 | if (!checkSubscriptionOnce) { 152 | checkSubscriptionOnce = true; 153 | if (upstream.get() == null) { 154 | errors.add(new IllegalStateException("onSubscribe not called in proper order")); 155 | } 156 | } 157 | 158 | try { 159 | lastThread = Thread.currentThread(); 160 | if (t == null) { 161 | errors.add(new NullPointerException("onError received a null Throwable")); 162 | } else { 163 | errors.add(t); 164 | } 165 | 166 | downstream.onError(t); 167 | } finally { 168 | done.countDown(); 169 | } 170 | } 171 | 172 | @Override 173 | public void onComplete() { 174 | if (!checkSubscriptionOnce) { 175 | checkSubscriptionOnce = true; 176 | if (upstream.get() == null) { 177 | errors.add(new IllegalStateException("onSubscribe not called in proper order")); 178 | } 179 | } 180 | 181 | try { 182 | lastThread = Thread.currentThread(); 183 | completions++; 184 | 185 | downstream.onComplete(); 186 | } finally { 187 | done.countDown(); 188 | } 189 | } 190 | 191 | @Override 192 | public final void dispose() { 193 | DisposableHelper.dispose(upstream); 194 | } 195 | 196 | @Override 197 | public final boolean isDisposed() { 198 | return DisposableHelper.isDisposed(upstream.get()); 199 | } 200 | 201 | // state retrieval methods 202 | /** 203 | * Returns true if this TestObserver received a subscription. 204 | * @return true if this TestObserver received a subscription 205 | */ 206 | public final boolean hasSubscription() { 207 | return upstream.get() != null; 208 | } 209 | 210 | /** 211 | * Assert that the onSubscribe method was called exactly once. 212 | * @return this; 213 | */ 214 | @Override 215 | public final TestObserverEx assertSubscribed() { 216 | if (upstream.get() == null) { 217 | throw fail("Not subscribed!"); 218 | } 219 | return this; 220 | } 221 | 222 | /** 223 | * Assert that the onSubscribe method hasn't been called at all. 224 | * @return this; 225 | */ 226 | public final TestObserverEx assertNotSubscribed() { 227 | if (upstream.get() != null) { 228 | throw fail("Subscribed!"); 229 | } else 230 | if (!errors.isEmpty()) { 231 | throw fail("Not subscribed but errors found"); 232 | } 233 | return this; 234 | } 235 | 236 | /** 237 | * Sets the initial fusion mode if the upstream supports fusion. 238 | *

Package-private: avoid leaking the now internal fusion properties into the public API. 239 | * Use ObserverFusion to work with such tests. 240 | * @param mode the mode to establish, see the {@link QueueDisposable} constants 241 | * @return this 242 | */ 243 | public final TestObserverEx setInitialFusionMode(int mode) { 244 | this.initialFusionMode = mode; 245 | return this; 246 | } 247 | 248 | /** 249 | * Asserts that the given fusion mode has been established 250 | *

Package-private: avoid leaking the now internal fusion properties into the public API. 251 | * Use ObserverFusion to work with such tests. 252 | * @param mode the expected mode 253 | * @return this 254 | */ 255 | public final TestObserverEx assertFusionMode(int mode) { 256 | int m = establishedFusionMode; 257 | if (m != mode) { 258 | if (qd != null) { 259 | throw new AssertionError("Fusion mode different. Expected: " + fusionModeToString(mode) 260 | + ", actual: " + fusionModeToString(m)); 261 | } else { 262 | throw fail("Upstream is not fuseable"); 263 | } 264 | } 265 | return this; 266 | } 267 | 268 | /** 269 | * Assert that the upstream is a fuseable source. 270 | *

Package-private: avoid leaking the now internal fusion properties into the public API. 271 | * Use ObserverFusion to work with such tests. 272 | * @return this 273 | */ 274 | public final TestObserverEx assertFuseable() { 275 | if (qd == null) { 276 | throw new AssertionError("Upstream is not fuseable."); 277 | } 278 | return this; 279 | } 280 | 281 | /** 282 | * Assert that the upstream is not a fuseable source. 283 | *

Package-private: avoid leaking the now internal fusion properties into the public API. 284 | * Use ObserverFusion to work with such tests. 285 | * @return this 286 | */ 287 | public final TestObserverEx assertNotFuseable() { 288 | if (qd != null) { 289 | throw new AssertionError("Upstream is fuseable."); 290 | } 291 | return this; 292 | } 293 | 294 | @Override 295 | public void onSuccess(T value) { 296 | onNext(value); 297 | onComplete(); 298 | } 299 | 300 | /** 301 | * An observer that ignores all events and does not report errors. 302 | */ 303 | enum EmptyObserver implements Observer { 304 | INSTANCE; 305 | 306 | @Override 307 | public void onSubscribe(Disposable d) { 308 | } 309 | 310 | @Override 311 | public void onNext(Object t) { 312 | } 313 | 314 | @Override 315 | public void onError(Throwable t) { 316 | } 317 | 318 | @Override 319 | public void onComplete() { 320 | } 321 | } 322 | } -------------------------------------------------------------------------------- /src/test/java/hu/akarnokd/rxjava3/jdk8interop/BaseTestConsumerEx.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 David Karnok 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package hu.akarnokd.rxjava3.jdk8interop; 18 | 19 | import java.util.List; 20 | 21 | import io.reactivex.rxjava3.functions.Predicate; 22 | import io.reactivex.rxjava3.internal.functions.ObjectHelper; 23 | import io.reactivex.rxjava3.internal.fuseable.QueueFuseable; 24 | import io.reactivex.rxjava3.internal.util.ExceptionHelper; 25 | import io.reactivex.rxjava3.observers.BaseTestConsumer; 26 | 27 | /** 28 | * Base class with shared infrastructure to support TestSubscriber and TestObserver. 29 | * @param the value type consumed 30 | * @param the subclass of this BaseTestConsumer 31 | */ 32 | public abstract class BaseTestConsumerEx> 33 | extends BaseTestConsumer { 34 | 35 | protected int initialFusionMode; 36 | 37 | protected int establishedFusionMode; 38 | 39 | /** 40 | * The optional tag associated with this test consumer. 41 | * @since 2.0.7 42 | */ 43 | protected CharSequence tag; 44 | 45 | /** 46 | * Indicates that one of the awaitX method has timed out. 47 | * @since 2.0.7 48 | */ 49 | protected boolean timeout; 50 | 51 | public BaseTestConsumerEx() { 52 | super(); 53 | } 54 | 55 | /** 56 | * Returns the last thread which called the onXXX methods of this TestObserver/TestSubscriber. 57 | * @return the last thread which called the onXXX methods 58 | */ 59 | public final Thread lastThread() { 60 | return lastThread; 61 | } 62 | 63 | // assertion methods 64 | 65 | /** 66 | * Assert that this TestObserver/TestSubscriber did not receive an onNext value which is equal to 67 | * the given value with respect to null-safe Object.equals. 68 | * 69 | *

History: 2.0.5 - experimental 70 | * @param value the value to expect not being received 71 | * @return this 72 | * @since 2.1 73 | */ 74 | @SuppressWarnings("unchecked") 75 | public final U assertNever(T value) { 76 | int s = values.size(); 77 | 78 | for (int i = 0; i < s; i++) { 79 | T v = this.values.get(i); 80 | if (ObjectHelper.equals(v, value)) { 81 | throw fail("Value at position " + i + " is equal to " + valueAndClass(value) + "; Expected them to be different"); 82 | } 83 | } 84 | return (U) this; 85 | } 86 | 87 | /** 88 | * Asserts that this TestObserver/TestSubscriber did not receive any onNext value for which 89 | * the provided predicate returns true. 90 | * 91 | *

History: 2.0.5 - experimental 92 | * @param valuePredicate the predicate that receives the onNext value 93 | * and should return true for the expected value. 94 | * @return this 95 | * @since 2.1 96 | */ 97 | @SuppressWarnings("unchecked") 98 | public final U assertNever(Predicate valuePredicate) { 99 | int s = values.size(); 100 | 101 | for (int i = 0; i < s; i++) { 102 | T v = this.values.get(i); 103 | try { 104 | if (valuePredicate.test(v)) { 105 | throw fail("Value at position " + i + " matches predicate " + valuePredicate.toString() + ", which was not expected."); 106 | } 107 | } catch (Throwable ex) { 108 | throw ExceptionHelper.wrapOrThrow(ex); 109 | } 110 | } 111 | return (U)this; 112 | } 113 | 114 | /** 115 | * Assert that the TestObserver/TestSubscriber terminated (i.e., the terminal latch reached zero). 116 | * @return this 117 | */ 118 | @SuppressWarnings("unchecked") 119 | public final U assertTerminated() { 120 | if (done.getCount() != 0) { 121 | throw fail("Subscriber still running!"); 122 | } 123 | long c = completions; 124 | if (c > 1) { 125 | throw fail("Terminated with multiple completions: " + c); 126 | } 127 | int s = errors.size(); 128 | if (s > 1) { 129 | throw fail("Terminated with multiple errors: " + s); 130 | } 131 | 132 | if (c != 0 && s != 0) { 133 | throw fail("Terminated with multiple completions and errors: " + c); 134 | } 135 | return (U)this; 136 | } 137 | 138 | /** 139 | * Assert that the TestObserver/TestSubscriber has not terminated (i.e., the terminal latch is still non-zero). 140 | * @return this 141 | */ 142 | @SuppressWarnings("unchecked") 143 | public final U assertNotTerminated() { 144 | if (done.getCount() == 0) { 145 | throw fail("Subscriber terminated!"); 146 | } 147 | return (U)this; 148 | } 149 | 150 | /** 151 | * Assert that there is a single error and it has the given message. 152 | * @param message the message expected 153 | * @return this 154 | */ 155 | @SuppressWarnings("unchecked") 156 | public final U assertErrorMessage(String message) { 157 | int s = errors.size(); 158 | if (s == 0) { 159 | throw fail("No errors"); 160 | } else 161 | if (s == 1) { 162 | Throwable e = errors.get(0); 163 | String errorMessage = e.getMessage(); 164 | if (!ObjectHelper.equals(message, errorMessage)) { 165 | throw fail("Error message differs; exptected: " + message + " but was: " + errorMessage); 166 | } 167 | } else { 168 | throw fail("Multiple errors"); 169 | } 170 | return (U)this; 171 | } 172 | 173 | /** 174 | * Assert that the upstream signalled the specified values in order and then failed 175 | * with a Throwable for which the provided predicate returns true. 176 | * @param errorPredicate 177 | * the predicate that receives the error Throwable 178 | * and should return true for expected errors. 179 | * @param values the expected values, asserted in order 180 | * @return this 181 | */ 182 | @SafeVarargs 183 | public final U assertFailure(Predicate errorPredicate, T... values) { 184 | return assertSubscribed() 185 | .assertValues(values) 186 | .assertError(errorPredicate) 187 | .assertNotComplete(); 188 | } 189 | 190 | /** 191 | * Assert that the upstream signalled the specified values in order, 192 | * then failed with a specific class or subclass of Throwable 193 | * and with the given exact error message. 194 | * @param error the expected exception (parent) class 195 | * @param message the expected failure message 196 | * @param values the expected values, asserted in order 197 | * @return this 198 | */ 199 | @SafeVarargs 200 | public final U assertFailureAndMessage(Class error, 201 | String message, T... values) { 202 | return assertSubscribed() 203 | .assertValues(values) 204 | .assertError(error) 205 | .assertErrorMessage(message) 206 | .assertNotComplete(); 207 | } 208 | 209 | /** 210 | * Returns true if an await timed out. 211 | * @return true if one of the timeout-based await methods has timed out. 212 | *

History: 2.0.7 - experimental 213 | * @see #clearTimeout() 214 | * @see #assertTimeout() 215 | * @see #assertNoTimeout() 216 | * @since 2.1 217 | */ 218 | public final boolean isTimeout() { 219 | return timeout; 220 | } 221 | 222 | /** 223 | * Clears the timeout flag set by the await methods when they timed out. 224 | *

History: 2.0.7 - experimental 225 | * @return this 226 | * @since 2.1 227 | * @see #isTimeout() 228 | */ 229 | @SuppressWarnings("unchecked") 230 | public final U clearTimeout() { 231 | timeout = false; 232 | return (U)this; 233 | } 234 | 235 | /** 236 | * Asserts that some awaitX method has timed out. 237 | *

History: 2.0.7 - experimental 238 | * @return this 239 | * @since 2.1 240 | */ 241 | @SuppressWarnings("unchecked") 242 | public final U assertTimeout() { 243 | if (!timeout) { 244 | throw fail("No timeout?!"); 245 | } 246 | return (U)this; 247 | } 248 | 249 | /** 250 | * Asserts that some awaitX method has not timed out. 251 | *

History: 2.0.7 - experimental 252 | * @return this 253 | * @since 2.1 254 | */ 255 | @SuppressWarnings("unchecked") 256 | public final U assertNoTimeout() { 257 | if (timeout) { 258 | throw fail("Timeout?!"); 259 | } 260 | return (U)this; 261 | } 262 | 263 | /** 264 | * Returns the internal shared list of errors. 265 | * @return Returns the internal shared list of errors. 266 | */ 267 | public final List errors() { 268 | return errors; 269 | } 270 | 271 | /** 272 | * Returns true if this test consumer has terminated in any fashion. 273 | * @return true if this test consumer has terminated in any fashion 274 | */ 275 | public final boolean isTerminated() { 276 | return done.getCount() == 0; 277 | } 278 | 279 | /** 280 | * Returns the number of times onComplete() was called. 281 | * @return the number of times onComplete() was called 282 | */ 283 | public final long completions() { 284 | return completions; 285 | } 286 | 287 | /** 288 | * Fail with the given message and add the sequence of errors as suppressed ones. 289 | *

Note this is deliberately the only fail method. Most of the times an assertion 290 | * would fail but it is possible it was due to an exception somewhere. This construct 291 | * will capture those potential errors and report it along with the original failure. 292 | * 293 | * @param message the message to use 294 | * @return AssertionError the prepared AssertionError instance 295 | */ 296 | public final AssertionError failWith(String message) { 297 | return fail(message); 298 | } 299 | 300 | static String fusionModeToString(int mode) { 301 | switch (mode) { 302 | case QueueFuseable.NONE : return "NONE"; 303 | case QueueFuseable.SYNC : return "SYNC"; 304 | case QueueFuseable.ASYNC : return "ASYNC"; 305 | default: return "Unknown(" + mode + ")"; 306 | } 307 | } 308 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/test/java/hu/akarnokd/rxjava3/jdk8interop/TestSubscriberEx.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 David Karnok 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package hu.akarnokd.rxjava3.jdk8interop; 18 | 19 | import java.util.concurrent.atomic.*; 20 | 21 | import org.reactivestreams.*; 22 | 23 | import io.reactivex.rxjava3.core.FlowableSubscriber; 24 | import io.reactivex.rxjava3.internal.fuseable.*; 25 | import io.reactivex.rxjava3.internal.subscriptions.SubscriptionHelper; 26 | 27 | /** 28 | * An extended test subscriber that records events and allows making assertions about them. 29 | * 30 | *

You can override the onSubscribe, onNext, onError, onComplete, request and 31 | * cancel methods but not the others (this is by design). 32 | * 33 | *

The TestSubscriber implements Disposable for convenience where dispose calls cancel. 34 | * 35 | *

When calling the default request method, you are requesting on behalf of the 36 | * wrapped actual subscriber. 37 | * 38 | * @param the value type 39 | */ 40 | public class TestSubscriberEx 41 | extends BaseTestConsumerEx> 42 | implements FlowableSubscriber, Subscription { 43 | /** The actual subscriber to forward events to. */ 44 | private final Subscriber downstream; 45 | 46 | /** Makes sure the incoming Subscriptions get cancelled immediately. */ 47 | private volatile boolean cancelled; 48 | 49 | /** Holds the current subscription if any. */ 50 | private final AtomicReference upstream; 51 | 52 | /** Holds the requested amount until a subscription arrives. */ 53 | private final AtomicLong missedRequested; 54 | 55 | private QueueSubscription qs; 56 | 57 | /** 58 | * Constructs a non-forwarding TestSubscriber with an initial request value of Long.MAX_VALUE. 59 | */ 60 | public TestSubscriberEx() { 61 | this(EmptySubscriber.INSTANCE, Long.MAX_VALUE); 62 | } 63 | 64 | /** 65 | * Constructs a non-forwarding TestSubscriber with the specified initial request value. 66 | *

The TestSubscriber doesn't validate the initialRequest value so one can 67 | * test sources with invalid values as well. 68 | * @param initialRequest the initial request value 69 | */ 70 | public TestSubscriberEx(long initialRequest) { 71 | this(EmptySubscriber.INSTANCE, initialRequest); 72 | } 73 | 74 | /** 75 | * Constructs a forwarding TestSubscriber but leaves the requesting to the wrapped subscriber. 76 | * @param downstream the actual Subscriber to forward events to 77 | */ 78 | public TestSubscriberEx(Subscriber downstream) { 79 | this(downstream, Long.MAX_VALUE); 80 | } 81 | 82 | /** 83 | * Constructs a forwarding TestSubscriber with the specified initial request value. 84 | *

The TestSubscriber doesn't validate the initialRequest value so one can 85 | * test sources with invalid values as well. 86 | * @param downstream the actual Subscriber to forward events to 87 | * @param initialRequest the initial request value 88 | */ 89 | public TestSubscriberEx(Subscriber downstream, long initialRequest) { 90 | super(); 91 | if (initialRequest < 0) { 92 | throw new IllegalArgumentException("Negative initial request not allowed"); 93 | } 94 | this.downstream = downstream; 95 | this.upstream = new AtomicReference<>(); 96 | this.missedRequested = new AtomicLong(initialRequest); 97 | } 98 | 99 | @SuppressWarnings("unchecked") 100 | @Override 101 | public void onSubscribe(Subscription s) { 102 | lastThread = Thread.currentThread(); 103 | 104 | if (s == null) { 105 | errors.add(new NullPointerException("onSubscribe received a null Subscription")); 106 | return; 107 | } 108 | if (!upstream.compareAndSet(null, s)) { 109 | s.cancel(); 110 | if (upstream.get() != SubscriptionHelper.CANCELLED) { 111 | errors.add(new IllegalStateException("onSubscribe received multiple subscriptions: " + s)); 112 | } 113 | return; 114 | } 115 | 116 | if (initialFusionMode != 0) { 117 | if (s instanceof QueueSubscription) { 118 | qs = (QueueSubscription)s; 119 | 120 | int m = qs.requestFusion(initialFusionMode); 121 | establishedFusionMode = m; 122 | 123 | if (m == QueueFuseable.SYNC) { 124 | checkSubscriptionOnce = true; 125 | lastThread = Thread.currentThread(); 126 | try { 127 | T t; 128 | while ((t = qs.poll()) != null) { 129 | values.add(t); 130 | } 131 | completions++; 132 | } catch (Throwable ex) { 133 | // Exceptions.throwIfFatal(e); TODO add fatal exceptions? 134 | errors.add(ex); 135 | } 136 | return; 137 | } 138 | } 139 | } 140 | 141 | downstream.onSubscribe(s); 142 | 143 | long mr = missedRequested.getAndSet(0L); 144 | if (mr != 0L) { 145 | s.request(mr); 146 | } 147 | 148 | onStart(); 149 | } 150 | 151 | /** 152 | * Called after the onSubscribe is called and handled. 153 | */ 154 | protected void onStart() { 155 | 156 | } 157 | 158 | @Override 159 | public void onNext(T t) { 160 | if (!checkSubscriptionOnce) { 161 | checkSubscriptionOnce = true; 162 | if (upstream.get() == null) { 163 | errors.add(new IllegalStateException("onSubscribe not called in proper order")); 164 | } 165 | } 166 | lastThread = Thread.currentThread(); 167 | 168 | if (establishedFusionMode == QueueFuseable.ASYNC) { 169 | try { 170 | while ((t = qs.poll()) != null) { 171 | values.add(t); 172 | } 173 | } catch (Throwable ex) { 174 | // Exceptions.throwIfFatal(e); TODO add fatal exceptions? 175 | errors.add(ex); 176 | qs.cancel(); 177 | } 178 | return; 179 | } 180 | 181 | values.add(t); 182 | 183 | if (t == null) { 184 | errors.add(new NullPointerException("onNext received a null value")); 185 | } 186 | 187 | downstream.onNext(t); 188 | } 189 | 190 | @Override 191 | public void onError(Throwable t) { 192 | if (!checkSubscriptionOnce) { 193 | checkSubscriptionOnce = true; 194 | if (upstream.get() == null) { 195 | errors.add(new NullPointerException("onSubscribe not called in proper order")); 196 | } 197 | } 198 | try { 199 | lastThread = Thread.currentThread(); 200 | errors.add(t); 201 | 202 | if (t == null) { 203 | errors.add(new IllegalStateException("onError received a null Throwable")); 204 | } 205 | 206 | downstream.onError(t); 207 | } finally { 208 | done.countDown(); 209 | } 210 | } 211 | 212 | @Override 213 | public void onComplete() { 214 | if (!checkSubscriptionOnce) { 215 | checkSubscriptionOnce = true; 216 | if (upstream.get() == null) { 217 | errors.add(new IllegalStateException("onSubscribe not called in proper order")); 218 | } 219 | } 220 | try { 221 | lastThread = Thread.currentThread(); 222 | completions++; 223 | 224 | downstream.onComplete(); 225 | } finally { 226 | done.countDown(); 227 | } 228 | } 229 | 230 | @Override 231 | public final void request(long n) { 232 | SubscriptionHelper.deferredRequest(upstream, missedRequested, n); 233 | } 234 | 235 | @Override 236 | public final void cancel() { 237 | if (!cancelled) { 238 | cancelled = true; 239 | SubscriptionHelper.cancel(upstream); 240 | } 241 | } 242 | 243 | /** 244 | * Returns true if this TestSubscriber has been cancelled. 245 | * @return true if this TestSubscriber has been cancelled 246 | */ 247 | public final boolean isCancelled() { 248 | return cancelled; 249 | } 250 | 251 | @Override 252 | protected final void dispose() { 253 | cancel(); 254 | } 255 | 256 | @Override 257 | protected final boolean isDisposed() { 258 | return cancelled; 259 | } 260 | 261 | // state retrieval methods 262 | 263 | /** 264 | * Returns true if this TestSubscriber received a subscription. 265 | * @return true if this TestSubscriber received a subscription 266 | */ 267 | public final boolean hasSubscription() { 268 | return upstream.get() != null; 269 | } 270 | 271 | // assertion methods 272 | 273 | /** 274 | * Assert that the onSubscribe method was called exactly once. 275 | * @return this 276 | */ 277 | @Override 278 | public final TestSubscriberEx assertSubscribed() { 279 | if (upstream.get() == null) { 280 | throw fail("Not subscribed!"); 281 | } 282 | return this; 283 | } 284 | 285 | /** 286 | * Assert that the onSubscribe method hasn't been called at all. 287 | * @return this 288 | */ 289 | public final TestSubscriberEx assertNotSubscribed() { 290 | if (upstream.get() != null) { 291 | throw fail("Subscribed!"); 292 | } else 293 | if (!errors.isEmpty()) { 294 | throw fail("Not subscribed but errors found"); 295 | } 296 | return this; 297 | } 298 | 299 | /** 300 | * Sets the initial fusion mode if the upstream supports fusion. 301 | *

Package-private: avoid leaking the now internal fusion properties into the public API. 302 | * Use SubscriberFusion to work with such tests. 303 | * @param mode the mode to establish, see the {@link QueueSubscription} constants 304 | * @return this 305 | */ 306 | public final TestSubscriberEx setInitialFusionMode(int mode) { 307 | this.initialFusionMode = mode; 308 | return this; 309 | } 310 | 311 | /** 312 | * Asserts that the given fusion mode has been established 313 | *

Package-private: avoid leaking the now internal fusion properties into the public API. 314 | * Use SubscriberFusion to work with such tests. 315 | * @param mode the expected mode 316 | * @return this 317 | */ 318 | public final TestSubscriberEx assertFusionMode(int mode) { 319 | int m = establishedFusionMode; 320 | if (m != mode) { 321 | if (qs != null) { 322 | throw new AssertionError("Fusion mode different. Expected: " + fusionModeToString(mode) 323 | + ", actual: " + fusionModeToString(m)); 324 | } else { 325 | throw fail("Upstream is not fuseable"); 326 | } 327 | } 328 | return this; 329 | } 330 | 331 | /** 332 | * Assert that the upstream is a fuseable source. 333 | *

Package-private: avoid leaking the now internal fusion properties into the public API. 334 | * Use SubscriberFusion to work with such tests. 335 | * @return this 336 | */ 337 | public final TestSubscriberEx assertFuseable() { 338 | if (qs == null) { 339 | throw new AssertionError("Upstream is not fuseable."); 340 | } 341 | return this; 342 | } 343 | 344 | /** 345 | * Assert that the upstream is not a fuseable source. 346 | *

Package-private: avoid leaking the now internal fusion properties into the public API. 347 | * Use SubscriberFusion to work with such tests. 348 | * @return this 349 | */ 350 | public final TestSubscriberEx assertNotFuseable() { 351 | if (qs != null) { 352 | throw new AssertionError("Upstream is fuseable."); 353 | } 354 | return this; 355 | } 356 | 357 | /** 358 | * Calls {@link #request(long)} and returns this. 359 | *

History: 2.0.1 - experimental 360 | * @param n the request amount 361 | * @return this 362 | * @since 2.1 363 | */ 364 | public final TestSubscriberEx requestMore(long n) { 365 | request(n); 366 | return this; 367 | } 368 | 369 | /** 370 | * A subscriber that ignores all events and does not report errors. 371 | */ 372 | enum EmptySubscriber implements FlowableSubscriber { 373 | INSTANCE; 374 | 375 | @Override 376 | public void onSubscribe(Subscription s) { 377 | } 378 | 379 | @Override 380 | public void onNext(Object t) { 381 | } 382 | 383 | @Override 384 | public void onError(Throwable t) { 385 | } 386 | 387 | @Override 388 | public void onComplete() { 389 | } 390 | } 391 | } -------------------------------------------------------------------------------- /pmd.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | RxJava PMD ruleset 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | -------------------------------------------------------------------------------- /src/test/java/hu/akarnokd/rxjava3/jdk8interop/ObservableInteropTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 David Karnok 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package hu.akarnokd.rxjava3.jdk8interop; 18 | 19 | import java.io.IOException; 20 | import java.util.*; 21 | import java.util.concurrent.*; 22 | import java.util.function.*; 23 | import java.util.stream.*; 24 | 25 | import org.junit.*; 26 | 27 | import hu.akarnokd.rxjava3.jdk8interop.ObservableInterop; 28 | import io.reactivex.rxjava3.core.Observable; 29 | import io.reactivex.rxjava3.core.Observer; 30 | import io.reactivex.rxjava3.disposables.Disposables; 31 | import io.reactivex.rxjava3.internal.functions.Functions; 32 | import io.reactivex.rxjava3.internal.fuseable.QueueSubscription; 33 | import io.reactivex.rxjava3.plugins.RxJavaPlugins; 34 | import io.reactivex.rxjava3.subjects.UnicastSubject; 35 | 36 | public class ObservableInteropTest { 37 | 38 | @Test 39 | public void utilityClass() { 40 | TestHelper.checkUtilityClass(ObservableInterop.class); 41 | } 42 | 43 | @Test 44 | public void fromStream() { 45 | ObservableInterop.fromStream(Arrays.asList(1, 2, 3, 4, 5).stream()) 46 | .test() 47 | .assertResult(1, 2, 3, 4, 5); 48 | } 49 | 50 | @Test 51 | public void fromStream2() { 52 | Stream s = Arrays.asList(1, 2, 3, 4, 5).stream(); 53 | ObservableInterop.fromStream(s) 54 | .test() 55 | .assertResult(1, 2, 3, 4, 5); 56 | 57 | ObservableInterop.fromStream(s) 58 | .test() 59 | .assertFailure(IllegalStateException.class); 60 | } 61 | 62 | @Test 63 | public void fromOptional() { 64 | ObservableInterop.fromOptional(Optional.of(1)) 65 | .test() 66 | .assertResult(1); 67 | 68 | ObservableInterop.fromOptional(Optional.empty()) 69 | .test() 70 | .assertResult(); 71 | } 72 | 73 | @Test 74 | public void fromFuture() { 75 | ObservableInterop.fromFuture(CompletableFuture.supplyAsync(() -> 1)) 76 | .test() 77 | .awaitDone(5, TimeUnit.SECONDS) 78 | .assertResult(1); 79 | } 80 | 81 | @Test 82 | public void fromFutureError() { 83 | TestObserverEx ts = new TestObserverEx<>(); 84 | 85 | ObservableInterop.fromFuture(CompletableFuture.supplyAsync(() -> { throw new IllegalArgumentException(); })) 86 | .subscribeWith(ts) 87 | .awaitDone(5, TimeUnit.SECONDS) 88 | .assertFailure(CompletionException.class); 89 | 90 | Throwable c = ts.errors().get(0).getCause(); 91 | Assert.assertTrue(c.toString(), c instanceof IllegalArgumentException); 92 | } 93 | 94 | @SuppressWarnings("unchecked") 95 | @Test 96 | public void collector() { 97 | Observable.range(1, 5) 98 | .compose(ObservableInterop.collect(Collectors.toList())) 99 | .test() 100 | .assertResult(Arrays.asList(1, 2, 3, 4, 5)); 101 | } 102 | 103 | @Test 104 | public void first() { 105 | TestHelper.assertFuture(1, Observable.range(1, 5) 106 | .to(ObservableInterop.first()) 107 | ); 108 | } 109 | 110 | @Test(expected = NoSuchElementException.class) 111 | public void firstEmpty() { 112 | TestHelper.assertFuture(null, Observable.empty() 113 | .to(ObservableInterop.first()) 114 | ); 115 | } 116 | 117 | @Test 118 | public void last() { 119 | TestHelper.assertFuture(5, Observable.range(1, 5) 120 | .to(ObservableInterop.last()) 121 | ); 122 | } 123 | 124 | @Test(expected = NoSuchElementException.class) 125 | public void lastEmpty() { 126 | TestHelper.assertFuture(null, Observable.empty() 127 | .to(ObservableInterop.last()) 128 | ); 129 | } 130 | 131 | @Test 132 | public void single() { 133 | TestHelper.assertFuture(1, Observable.just(1) 134 | .to(ObservableInterop.single()) 135 | ); 136 | } 137 | 138 | @Test(expected = NoSuchElementException.class) 139 | public void singleEmpty() { 140 | TestHelper.assertFuture(null, Observable.empty() 141 | .to(ObservableInterop.single()) 142 | ); 143 | } 144 | 145 | @Test(expected = IllegalArgumentException.class) 146 | public void singleLonger() { 147 | TestHelper.assertFuture(null, Observable.range(1, 5) 148 | .to(ObservableInterop.single()) 149 | ); 150 | } 151 | 152 | @Test 153 | public void toStream() { 154 | List list = Observable.just(1, 2, 3, 4, 5) 155 | .to(ObservableInterop.toStream()) 156 | .collect(Collectors.toList()); 157 | 158 | Assert.assertEquals(Arrays.asList(1, 2, 3, 4, 5), list); 159 | } 160 | 161 | @Test 162 | public void toStreamCancel() { 163 | UnicastSubject up = UnicastSubject.create(); 164 | 165 | up.onNext(1); 166 | up.onNext(2); 167 | up.onNext(3); 168 | up.onNext(4); 169 | up.onNext(5); 170 | 171 | try (Stream s = up 172 | .to(ObservableInterop.toStream()).limit(3)) { 173 | Assert.assertTrue(up.hasObservers()); 174 | 175 | List list = s.collect(Collectors.toList()); 176 | Assert.assertEquals(Arrays.asList(1, 2, 3), list); 177 | } 178 | 179 | Assert.assertFalse(up.hasObservers()); 180 | } 181 | 182 | @Test 183 | public void firstElement() { 184 | Assert.assertEquals((Integer)1, Observable.range(1, 5) 185 | .to(ObservableInterop.firstElement()).get()); 186 | } 187 | 188 | @Test 189 | public void firstElementEmpty() { 190 | Assert.assertFalse(Observable.empty() 191 | .to(ObservableInterop.firstElement()).isPresent()); 192 | } 193 | 194 | @Test 195 | public void lastElement() { 196 | Assert.assertEquals((Integer)5, Observable.range(1, 5) 197 | .to(ObservableInterop.lastElement()).get()); 198 | } 199 | 200 | @Test 201 | public void lastElementEmpty() { 202 | Assert.assertFalse(Observable.empty() 203 | .to(ObservableInterop.lastElement()).isPresent()); 204 | } 205 | 206 | @Test 207 | public void flatMapStream() { 208 | Observable.range(1, 5) 209 | .compose(ObservableInterop.flatMapStream(v -> Arrays.asList(v, v + 1).stream())) 210 | .test() 211 | .assertResult(1, 2, 2, 3, 3, 4, 4, 5, 5, 6); 212 | } 213 | 214 | @Test 215 | public void mapOptional() { 216 | Observable.range(1, 5).hide() 217 | .compose(ObservableInterop.mapOptional(v -> { 218 | if (v % 2 == 0) { 219 | return Optional.of(-v); 220 | } 221 | return Optional.empty(); 222 | })) 223 | .test() 224 | .assertResult(-2, -4); 225 | } 226 | 227 | @Test 228 | public void mapOptionalError() { 229 | Observable.error(new IOException()) 230 | .compose(ObservableInterop.mapOptional(v -> { 231 | if (v % 2 == 0) { 232 | return Optional.of(-v); 233 | } 234 | return Optional.empty(); 235 | })) 236 | .test() 237 | .assertFailure(IOException.class); 238 | } 239 | 240 | @Test 241 | public void mapOptionalSyncFused() { 242 | TestObserverEx ts = TestHelper.fusedObserver(QueueSubscription.ANY); 243 | 244 | Observable.range(1, 5) 245 | .compose(ObservableInterop.mapOptional(v -> { 246 | if (v % 2 == 0) { 247 | return Optional.of(-v); 248 | } 249 | return Optional.empty(); 250 | })) 251 | .subscribeWith(ts) 252 | .assertFusionMode(QueueSubscription.SYNC) 253 | .assertResult(-2, -4); 254 | } 255 | 256 | @Test 257 | public void mapOptionalAsyncFused() { 258 | TestObserverEx ts = TestHelper.fusedObserver(QueueSubscription.ANY); 259 | 260 | UnicastSubject up = UnicastSubject.create(); 261 | TestHelper.emit(up, 1, 2, 3, 4, 5); 262 | 263 | up 264 | .compose(ObservableInterop.mapOptional(v -> { 265 | if (v % 2 == 0) { 266 | return Optional.of(-v); 267 | } 268 | return Optional.empty(); 269 | })) 270 | .subscribeWith(ts) 271 | .assertFusionMode(QueueSubscription.ASYNC) 272 | .assertResult(-2, -4); 273 | } 274 | 275 | @Test 276 | public void mapOptionalConditional() { 277 | Observable.range(1, 5).hide() 278 | .compose(ObservableInterop.mapOptional(v -> { 279 | if (v % 2 == 0) { 280 | return Optional.of(-v); 281 | } 282 | return Optional.empty(); 283 | })) 284 | .filter(Functions.alwaysTrue()) 285 | .test() 286 | .assertResult(-2, -4); 287 | } 288 | 289 | @Test 290 | public void mapOptionalErrorConditional() { 291 | Observable.error(new IOException()) 292 | .compose(ObservableInterop.mapOptional(v -> { 293 | if (v % 2 == 0) { 294 | return Optional.of(-v); 295 | } 296 | return Optional.empty(); 297 | })) 298 | .filter(Functions.alwaysTrue()) 299 | .test() 300 | .assertFailure(IOException.class); 301 | } 302 | 303 | @Test 304 | public void mapOptionalSyncFusedConditional() { 305 | TestObserverEx ts = TestHelper.fusedObserver(QueueSubscription.ANY); 306 | 307 | Observable.range(1, 5) 308 | .compose(ObservableInterop.mapOptional(v -> { 309 | if (v % 2 == 0) { 310 | return Optional.of(-v); 311 | } 312 | return Optional.empty(); 313 | })) 314 | .filter(Functions.alwaysTrue()) 315 | .subscribeWith(ts) 316 | .assertFusionMode(QueueSubscription.SYNC) 317 | .assertResult(-2, -4); 318 | } 319 | 320 | @Test 321 | public void mapOptionalAsyncFusedConditional() { 322 | TestObserverEx ts = TestHelper.fusedObserver(QueueSubscription.ANY); 323 | 324 | UnicastSubject up = UnicastSubject.create(); 325 | TestHelper.emit(up, 1, 2, 3, 4, 5); 326 | 327 | up 328 | .compose(ObservableInterop.mapOptional(v -> { 329 | if (v % 2 == 0) { 330 | return Optional.of(-v); 331 | } 332 | return Optional.empty(); 333 | })) 334 | .filter(Functions.alwaysTrue()) 335 | .subscribeWith(ts) 336 | .assertFusionMode(QueueSubscription.ASYNC) 337 | .assertResult(-2, -4); 338 | } 339 | 340 | @Test 341 | public void mapOptionalMapperCrash() { 342 | Observable.just(1) 343 | .compose(ObservableInterop.mapOptional(v -> null)) 344 | .test() 345 | .assertFailure(NullPointerException.class); 346 | } 347 | 348 | @Test 349 | public void mapOptionalCancelIgnored() { 350 | new Observable() { 351 | @Override 352 | protected void subscribeActual(Observer observer) { 353 | observer.onSubscribe(Disposables.empty()); 354 | observer.onNext(1); 355 | observer.onNext(2); 356 | } 357 | } 358 | .compose(ObservableInterop.mapOptional(v -> { throw new IOException(); })) 359 | .test() 360 | .assertFailure(IOException.class); 361 | } 362 | 363 | @Test 364 | @SuppressWarnings({ "unchecked", "rawtypes" }) 365 | public void collectorInitCrash() { 366 | Observable.range(1, 5) 367 | .compose(ObservableInterop.collect( 368 | new Collector>() { 369 | 370 | @Override 371 | public Supplier supplier() { 372 | throw new IllegalArgumentException(); 373 | } 374 | 375 | @Override 376 | public BiConsumer accumulator() { 377 | return (BiConsumer)Collectors.toList().accumulator(); 378 | } 379 | 380 | @Override 381 | public BinaryOperator combiner() { 382 | return (BinaryOperator)Collectors.toList().combiner(); 383 | } 384 | 385 | @Override 386 | public Function> finisher() { 387 | return (Function)Collectors.toList().finisher(); 388 | } 389 | 390 | @Override 391 | public Set characteristics() { 392 | return Collectors.toList().characteristics(); 393 | } 394 | })) 395 | .test() 396 | .assertFailure(IllegalArgumentException.class); 397 | } 398 | 399 | @Test 400 | public void collectorDoubleOnSubscribe() { 401 | TestHelper.checkDoubleOnSubscribeObservable(o -> 402 | o.compose(ObservableInterop.collect(Collectors.toList())) 403 | ); 404 | } 405 | 406 | @Test 407 | public void dispose() { 408 | TestHelper.checkDisposed( 409 | Observable.never() 410 | .compose(ObservableInterop.collect(Collectors.toList())) 411 | ); 412 | } 413 | 414 | @Test 415 | @SuppressWarnings({ "unchecked", "rawtypes" }) 416 | public void collectorAccumulatorCrash() { 417 | List errors = TestHelper.trackPluginErrors(); 418 | try { 419 | new Observable() { 420 | @Override 421 | protected void subscribeActual(Observer observer) { 422 | observer.onSubscribe(Disposables.empty()); 423 | observer.onNext(1); 424 | observer.onNext(2); 425 | observer.onComplete(); 426 | observer.onError(new IOException()); 427 | } 428 | } 429 | .compose(ObservableInterop.collect( 430 | new Collector>() { 431 | 432 | @Override 433 | public Supplier supplier() { 434 | return (Supplier)Collectors.toList().supplier(); 435 | } 436 | 437 | @Override 438 | public BiConsumer accumulator() { 439 | return (o, i) -> { throw new IllegalArgumentException(); }; 440 | } 441 | 442 | @Override 443 | public BinaryOperator combiner() { 444 | return (BinaryOperator)Collectors.toList().combiner(); 445 | } 446 | 447 | @Override 448 | public Function> finisher() { 449 | return (Function)Collectors.toList().finisher(); 450 | } 451 | 452 | @Override 453 | public Set characteristics() { 454 | return Collectors.toList().characteristics(); 455 | } 456 | })) 457 | .test() 458 | .assertFailure(IllegalArgumentException.class); 459 | 460 | TestHelper.assertUndeliverable(errors, 0, IOException.class); 461 | } finally { 462 | RxJavaPlugins.reset(); 463 | } 464 | } 465 | 466 | @Test 467 | @SuppressWarnings({ "unchecked", "rawtypes" }) 468 | public void collectorFinisherCrash() { 469 | new Observable() { 470 | @Override 471 | protected void subscribeActual(Observer observer) { 472 | observer.onSubscribe(Disposables.empty()); 473 | observer.onNext(1); 474 | observer.onNext(2); 475 | observer.onComplete(); 476 | } 477 | } 478 | .compose(ObservableInterop.collect( 479 | new Collector>() { 480 | 481 | @Override 482 | public Supplier supplier() { 483 | return (Supplier)Collectors.toList().supplier(); 484 | } 485 | 486 | @Override 487 | public BiConsumer accumulator() { 488 | return (BiConsumer)Collectors.toList().accumulator(); 489 | } 490 | 491 | @Override 492 | public BinaryOperator combiner() { 493 | return (BinaryOperator)Collectors.toList().combiner(); 494 | } 495 | 496 | @Override 497 | public Function> finisher() { 498 | return o -> { throw new IllegalArgumentException(); }; 499 | } 500 | 501 | @Override 502 | public Set characteristics() { 503 | return Collectors.toList().characteristics(); 504 | } 505 | })) 506 | .test() 507 | .assertFailure(IllegalArgumentException.class); 508 | } 509 | } 510 | -------------------------------------------------------------------------------- /src/test/java/hu/akarnokd/rxjava3/jdk8interop/FlowableInteropTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 David Karnok 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package hu.akarnokd.rxjava3.jdk8interop; 18 | 19 | import java.io.IOException; 20 | import java.util.*; 21 | import java.util.concurrent.*; 22 | import java.util.function.*; 23 | import java.util.stream.*; 24 | 25 | import org.junit.*; 26 | import org.reactivestreams.Subscriber; 27 | 28 | import hu.akarnokd.rxjava3.jdk8interop.FlowableInterop; 29 | import io.reactivex.rxjava3.core.Flowable; 30 | import io.reactivex.rxjava3.internal.functions.Functions; 31 | import io.reactivex.rxjava3.internal.fuseable.QueueSubscription; 32 | import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription; 33 | import io.reactivex.rxjava3.plugins.RxJavaPlugins; 34 | import io.reactivex.rxjava3.processors.UnicastProcessor; 35 | 36 | public class FlowableInteropTest { 37 | 38 | @Test 39 | public void utilityClass() { 40 | TestHelper.checkUtilityClass(FlowableInterop.class); 41 | } 42 | 43 | @Test 44 | public void fromStream() { 45 | FlowableInterop.fromStream(Arrays.asList(1, 2, 3, 4, 5).stream()) 46 | .test() 47 | .assertResult(1, 2, 3, 4, 5); 48 | } 49 | 50 | @Test 51 | public void fromStream2() { 52 | Stream s = Arrays.asList(1, 2, 3, 4, 5).stream(); 53 | FlowableInterop.fromStream(s) 54 | .test() 55 | .assertResult(1, 2, 3, 4, 5); 56 | 57 | FlowableInterop.fromStream(s) 58 | .test() 59 | .assertFailure(IllegalStateException.class); 60 | } 61 | 62 | @Test 63 | public void fromOptional() { 64 | FlowableInterop.fromOptional(Optional.of(1)) 65 | .test() 66 | .assertResult(1); 67 | 68 | FlowableInterop.fromOptional(Optional.empty()) 69 | .test() 70 | .assertResult(); 71 | } 72 | 73 | @Test 74 | public void fromFuture() { 75 | FlowableInterop.fromFuture(CompletableFuture.supplyAsync(() -> 1)) 76 | .test() 77 | .awaitDone(5, TimeUnit.SECONDS) 78 | .assertResult(1); 79 | } 80 | 81 | @Test 82 | public void fromFutureError() { 83 | TestSubscriberEx ts = FlowableInterop.fromFuture( 84 | CompletableFuture.supplyAsync(() -> { throw new IllegalArgumentException(); })) 85 | .subscribeWith(new TestSubscriberEx<>()) 86 | .awaitDone(5, TimeUnit.SECONDS) 87 | .assertFailure(CompletionException.class); 88 | 89 | Throwable c = ts.errors().get(0).getCause(); 90 | Assert.assertTrue(c.toString(), c instanceof IllegalArgumentException); 91 | } 92 | 93 | @SuppressWarnings("unchecked") 94 | @Test 95 | public void collector() { 96 | Flowable.range(1, 5) 97 | .compose(FlowableInterop.collect(Collectors.toList())) 98 | .test() 99 | .assertResult(Arrays.asList(1, 2, 3, 4, 5)); 100 | } 101 | 102 | @Test 103 | public void first() { 104 | TestHelper.assertFuture(1, Flowable.range(1, 5) 105 | .to(FlowableInterop.first()) 106 | ); 107 | } 108 | 109 | @Test(expected = NoSuchElementException.class) 110 | public void firstEmpty() { 111 | TestHelper.assertFuture(null, Flowable.empty() 112 | .to(FlowableInterop.first()) 113 | ); 114 | } 115 | 116 | @Test 117 | public void last() { 118 | TestHelper.assertFuture(5, Flowable.range(1, 5) 119 | .to(FlowableInterop.last()) 120 | ); 121 | } 122 | 123 | @Test(expected = NoSuchElementException.class) 124 | public void lastEmpty() { 125 | TestHelper.assertFuture(null, Flowable.empty() 126 | .to(FlowableInterop.last()) 127 | ); 128 | } 129 | 130 | @Test 131 | public void single() { 132 | TestHelper.assertFuture(1, Flowable.just(1) 133 | .to(FlowableInterop.single()) 134 | ); 135 | } 136 | 137 | @Test(expected = NoSuchElementException.class) 138 | public void singleEmpty() { 139 | TestHelper.assertFuture(null, Flowable.empty() 140 | .to(FlowableInterop.single()) 141 | ); 142 | } 143 | 144 | @Test(expected = IllegalArgumentException.class) 145 | public void singleLonger() { 146 | TestHelper.assertFuture(null, Flowable.range(1, 5) 147 | .to(FlowableInterop.single()) 148 | ); 149 | } 150 | 151 | @Test 152 | public void toStream() { 153 | List list = Flowable.just(1, 2, 3, 4, 5) 154 | .to(FlowableInterop.toStream()) 155 | .collect(Collectors.toList()); 156 | 157 | Assert.assertEquals(Arrays.asList(1, 2, 3, 4, 5), list); 158 | } 159 | 160 | @Test 161 | public void toStreamCancel() { 162 | UnicastProcessor up = UnicastProcessor.create(); 163 | 164 | up.onNext(1); 165 | up.onNext(2); 166 | up.onNext(3); 167 | up.onNext(4); 168 | up.onNext(5); 169 | 170 | try (Stream s = up 171 | .to(FlowableInterop.toStream()).limit(3)) { 172 | Assert.assertTrue(up.hasSubscribers()); 173 | 174 | List list = s.collect(Collectors.toList()); 175 | Assert.assertEquals(Arrays.asList(1, 2, 3), list); 176 | } 177 | 178 | Assert.assertFalse(up.hasSubscribers()); 179 | } 180 | 181 | @Test 182 | public void firstElement() { 183 | Assert.assertEquals((Integer)1, Flowable.range(1, 5) 184 | .to(FlowableInterop.firstElement()).get()); 185 | } 186 | 187 | @Test 188 | public void firstElementEmpty() { 189 | Assert.assertFalse(Flowable.empty() 190 | .to(FlowableInterop.firstElement()).isPresent()); 191 | } 192 | 193 | @Test 194 | public void lastElement() { 195 | Assert.assertEquals((Integer)5, Flowable.range(1, 5) 196 | .to(FlowableInterop.lastElement()).get()); 197 | } 198 | 199 | @Test 200 | public void lastElementEmpty() { 201 | Assert.assertFalse(Flowable.empty() 202 | .to(FlowableInterop.lastElement()).isPresent()); 203 | } 204 | 205 | @Test 206 | public void flatMapStream() { 207 | Flowable.range(1, 5) 208 | .compose(FlowableInterop.flatMapStream(v -> Arrays.asList(v, v + 1).stream())) 209 | .test() 210 | .assertResult(1, 2, 2, 3, 3, 4, 4, 5, 5, 6); 211 | } 212 | 213 | @Test 214 | public void mapOptional() { 215 | Flowable.range(1, 5).hide() 216 | .compose(FlowableInterop.mapOptional(v -> { 217 | if (v % 2 == 0) { 218 | return Optional.of(-v); 219 | } 220 | return Optional.empty(); 221 | })) 222 | .test() 223 | .assertResult(-2, -4); 224 | } 225 | 226 | @Test 227 | public void mapOptionalError() { 228 | Flowable.error(new IOException()) 229 | .compose(FlowableInterop.mapOptional(v -> { 230 | if (v % 2 == 0) { 231 | return Optional.of(-v); 232 | } 233 | return Optional.empty(); 234 | })) 235 | .test() 236 | .assertFailure(IOException.class); 237 | } 238 | 239 | @Test 240 | public void mapOptionalSyncFused() { 241 | TestSubscriberEx ts = TestHelper.fusedSubscriber(QueueSubscription.ANY); 242 | 243 | Flowable.range(1, 5) 244 | .compose(FlowableInterop.mapOptional(v -> { 245 | if (v % 2 == 0) { 246 | return Optional.of(-v); 247 | } 248 | return Optional.empty(); 249 | })) 250 | .subscribeWith(ts) 251 | .assertFusionMode(QueueSubscription.SYNC) 252 | .assertResult(-2, -4); 253 | } 254 | 255 | @Test 256 | public void mapOptionalAsyncFused() { 257 | TestSubscriberEx ts = TestHelper.fusedSubscriber(QueueSubscription.ANY); 258 | 259 | UnicastProcessor up = UnicastProcessor.create(); 260 | TestHelper.emit(up, 1, 2, 3, 4, 5); 261 | 262 | up 263 | .compose(FlowableInterop.mapOptional(v -> { 264 | if (v % 2 == 0) { 265 | return Optional.of(-v); 266 | } 267 | return Optional.empty(); 268 | })) 269 | .subscribeWith(ts) 270 | .assertFusionMode(QueueSubscription.ASYNC) 271 | .assertResult(-2, -4); 272 | } 273 | 274 | @Test 275 | public void mapOptionalConditional() { 276 | Flowable.range(1, 5).hide() 277 | .compose(FlowableInterop.mapOptional(v -> { 278 | if (v % 2 == 0) { 279 | return Optional.of(-v); 280 | } 281 | return Optional.empty(); 282 | })) 283 | .filter(Functions.alwaysTrue()) 284 | .test() 285 | .assertResult(-2, -4); 286 | } 287 | 288 | @Test 289 | public void mapOptionalErrorConditional() { 290 | Flowable.error(new IOException()) 291 | .compose(FlowableInterop.mapOptional(v -> { 292 | if (v % 2 == 0) { 293 | return Optional.of(-v); 294 | } 295 | return Optional.empty(); 296 | })) 297 | .filter(Functions.alwaysTrue()) 298 | .test() 299 | .assertFailure(IOException.class); 300 | } 301 | 302 | @Test 303 | public void mapOptionalSyncFusedConditional() { 304 | TestSubscriberEx ts = TestHelper.fusedSubscriber(QueueSubscription.ANY); 305 | 306 | Flowable.range(1, 5) 307 | .compose(FlowableInterop.mapOptional(v -> { 308 | if (v % 2 == 0) { 309 | return Optional.of(-v); 310 | } 311 | return Optional.empty(); 312 | })) 313 | .filter(Functions.alwaysTrue()) 314 | .subscribeWith(ts) 315 | .assertFusionMode(QueueSubscription.SYNC) 316 | .assertResult(-2, -4); 317 | } 318 | 319 | @Test 320 | public void mapOptionalAsyncFusedConditional() { 321 | TestSubscriberEx ts = TestHelper.fusedSubscriber(QueueSubscription.ANY); 322 | 323 | UnicastProcessor up = UnicastProcessor.create(); 324 | TestHelper.emit(up, 1, 2, 3, 4, 5); 325 | 326 | up 327 | .compose(FlowableInterop.mapOptional(v -> { 328 | if (v % 2 == 0) { 329 | return Optional.of(-v); 330 | } 331 | return Optional.empty(); 332 | })) 333 | .filter(Functions.alwaysTrue()) 334 | .subscribeWith(ts) 335 | .assertFusionMode(QueueSubscription.ASYNC) 336 | .assertResult(-2, -4); 337 | } 338 | 339 | @Test 340 | public void mapOptionalMapperCrash() { 341 | Flowable.just(1) 342 | .compose(FlowableInterop.mapOptional(v -> null)) 343 | .test() 344 | .assertFailure(NullPointerException.class); 345 | } 346 | 347 | @Test 348 | public void mapOptionalMapperCrashConditional() { 349 | Flowable.just(1) 350 | .compose(FlowableInterop.mapOptional(v -> null)) 351 | .filter(v -> true) 352 | .test() 353 | .assertFailure(NullPointerException.class); 354 | } 355 | 356 | @Test 357 | public void mapOptionalCancelIgnoredConditional() { 358 | new Flowable() { 359 | @Override 360 | protected void subscribeActual(Subscriber observer) { 361 | observer.onSubscribe(new BooleanSubscription()); 362 | observer.onNext(1); 363 | observer.onNext(2); 364 | } 365 | } 366 | .compose(FlowableInterop.mapOptional(v -> { throw new IOException(); })) 367 | .filter(v -> true) 368 | .test() 369 | .assertFailure(IOException.class); 370 | } 371 | 372 | @Test 373 | public void mapOptionalCancelIgnored() { 374 | new Flowable() { 375 | @Override 376 | protected void subscribeActual(Subscriber observer) { 377 | observer.onSubscribe(new BooleanSubscription()); 378 | observer.onNext(1); 379 | observer.onNext(2); 380 | } 381 | } 382 | .compose(FlowableInterop.mapOptional(v -> { throw new IOException(); })) 383 | .test() 384 | .assertFailure(IOException.class); 385 | } 386 | 387 | @Test 388 | @SuppressWarnings({ "unchecked", "rawtypes" }) 389 | public void collectorInitCrash() { 390 | Flowable.range(1, 5) 391 | .compose(FlowableInterop.collect( 392 | new Collector>() { 393 | 394 | @Override 395 | public Supplier supplier() { 396 | throw new IllegalArgumentException(); 397 | } 398 | 399 | @Override 400 | public BiConsumer accumulator() { 401 | return (BiConsumer)Collectors.toList().accumulator(); 402 | } 403 | 404 | @Override 405 | public BinaryOperator combiner() { 406 | return (BinaryOperator)Collectors.toList().combiner(); 407 | } 408 | 409 | @Override 410 | public Function> finisher() { 411 | return (Function)Collectors.toList().finisher(); 412 | } 413 | 414 | @Override 415 | public Set characteristics() { 416 | return Collectors.toList().characteristics(); 417 | } 418 | })) 419 | .test() 420 | .assertFailure(IllegalArgumentException.class); 421 | } 422 | 423 | @Test 424 | public void collectorDoubleOnSubscribe() { 425 | TestHelper.checkDoubleOnSubscribeFlowable(o -> 426 | o.compose(FlowableInterop.collect(Collectors.toList())) 427 | ); 428 | } 429 | 430 | @Test 431 | @SuppressWarnings({ "unchecked", "rawtypes" }) 432 | public void collectorAccumulatorCrash() { 433 | List errors = TestHelper.trackPluginErrors(); 434 | try { 435 | new Flowable() { 436 | @Override 437 | protected void subscribeActual(Subscriber observer) { 438 | observer.onSubscribe(new BooleanSubscription()); 439 | observer.onNext(1); 440 | observer.onNext(2); 441 | observer.onComplete(); 442 | observer.onError(new IOException()); 443 | } 444 | } 445 | .compose(FlowableInterop.collect( 446 | new Collector>() { 447 | 448 | @Override 449 | public Supplier supplier() { 450 | return (Supplier)Collectors.toList().supplier(); 451 | } 452 | 453 | @Override 454 | public BiConsumer accumulator() { 455 | return (o, i) -> { throw new IllegalArgumentException(); }; 456 | } 457 | 458 | @Override 459 | public BinaryOperator combiner() { 460 | return (BinaryOperator)Collectors.toList().combiner(); 461 | } 462 | 463 | @Override 464 | public Function> finisher() { 465 | return (Function)Collectors.toList().finisher(); 466 | } 467 | 468 | @Override 469 | public Set characteristics() { 470 | return Collectors.toList().characteristics(); 471 | } 472 | })) 473 | .test() 474 | .assertFailure(IllegalArgumentException.class); 475 | 476 | TestHelper.assertUndeliverable(errors, 0, IOException.class); 477 | } finally { 478 | RxJavaPlugins.reset(); 479 | } 480 | } 481 | 482 | @Test 483 | @SuppressWarnings({ "unchecked", "rawtypes" }) 484 | public void collectorFinisherCrash() { 485 | new Flowable() { 486 | @Override 487 | protected void subscribeActual(Subscriber observer) { 488 | observer.onSubscribe(new BooleanSubscription()); 489 | observer.onNext(1); 490 | observer.onNext(2); 491 | observer.onComplete(); 492 | } 493 | } 494 | .compose(FlowableInterop.collect( 495 | new Collector>() { 496 | 497 | @Override 498 | public Supplier supplier() { 499 | return (Supplier)Collectors.toList().supplier(); 500 | } 501 | 502 | @Override 503 | public BiConsumer accumulator() { 504 | return (BiConsumer)Collectors.toList().accumulator(); 505 | } 506 | 507 | @Override 508 | public BinaryOperator combiner() { 509 | return (BinaryOperator)Collectors.toList().combiner(); 510 | } 511 | 512 | @Override 513 | public Function> finisher() { 514 | return o -> { throw new IllegalArgumentException(); }; 515 | } 516 | 517 | @Override 518 | public Set characteristics() { 519 | return Collectors.toList().characteristics(); 520 | } 521 | })) 522 | .test() 523 | .assertFailure(IllegalArgumentException.class); 524 | } 525 | 526 | @Test 527 | public void collectCancel() { 528 | boolean[] cancelled = { false }; 529 | Flowable.never() 530 | .doOnCancel(() -> cancelled[0] = true) 531 | .compose(FlowableInterop.collect(Collectors.toList())) 532 | .test() 533 | .cancel(); 534 | 535 | Assert.assertTrue(cancelled[0]); 536 | } 537 | } 538 | --------------------------------------------------------------------------------