├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── build.gradle ├── gradle.properties ├── gradle ├── buildViaTravis.sh └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src ├── main └── java │ └── rx │ ├── internal │ └── operators │ │ ├── ObservableSplit.java │ │ ├── OnSubscribeInputStream.java │ │ └── OnSubscribeReader.java │ └── observables │ └── StringObservable.java └── test └── java └── rx └── observables ├── AssertObservable.java ├── ObservableSplitTest.java └── StringObservableTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled source # 2 | ################### 3 | *.com 4 | *.class 5 | *.dll 6 | *.exe 7 | *.o 8 | *.so 9 | 10 | # Packages # 11 | ############ 12 | # it's better to unpack these files and commit the raw source 13 | # git has its own built in compression methods 14 | *.7z 15 | *.dmg 16 | *.gz 17 | *.iso 18 | *.jar 19 | *.rar 20 | *.tar 21 | *.zip 22 | 23 | # Logs and databases # 24 | ###################### 25 | *.log 26 | 27 | # OS generated files # 28 | ###################### 29 | .DS_Store* 30 | ehthumbs.db 31 | Icon? 32 | Thumbs.db 33 | 34 | # Editor Files # 35 | ################ 36 | *~ 37 | *.swp 38 | 39 | # Gradle Files # 40 | ################ 41 | .gradle 42 | .gradletasknamecache 43 | .m2 44 | 45 | # Build output directies 46 | target/ 47 | build/ 48 | 49 | # IntelliJ specific files/directories 50 | out 51 | .idea 52 | *.ipr 53 | *.iws 54 | *.iml 55 | atlassian-ide-plugin.xml 56 | 57 | # Eclipse specific files/directories 58 | .classpath 59 | .project 60 | .settings 61 | .metadata 62 | bin/ 63 | 64 | # NetBeans specific files/directories 65 | .nbattrs 66 | /.nb-gradle/profiles/private/ 67 | .nb-gradle-properties 68 | 69 | # Scala build 70 | *.cache 71 | /.nb-gradle/private/ 72 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: 3 | - oraclejdk7 4 | sudo: false 5 | script: gradle/buildViaTravis.sh 6 | cache: 7 | directories: 8 | - "$HOME/.m2" 9 | - "$HOME/.gradle" 10 | env: 11 | global: 12 | - secure: IR3qobWevF1Bj8TVDXCaz9F24vbAHgbQMGmBaxU5NujmiwPO3PsoNgO1HzTc8y1Jr6rjVBFaDPXocTG38YvU8+i0f3r15e/6476oFjG0nKphPGT3FeFqo9b8Dl4t8hIiQO81uQQmSQ+/CIhA8ylBWtl7D6vJgzATZisGrKM7bwY= 13 | - secure: n6Yh5aUc8MK7WiN4v5Q17PlSk7flvaVHQbcCmXC5Ddq6IUm7ywt+Y5rE/S0sXkq3QHZ69tRBZYTmiXnZ+JHeT1jJQrG/t6z1TiGNDuIj5dIC4qT1Qv5/vxSWdejB48yhoemFJfwQFDJuE7TXrooDzHHB2wUmTIalZoNnW6+iVDU= 14 | - secure: AsotpXcQBzlH9y/X3T/TiyPJlMFhq7SyKs6w3QG1gsCJp+OmA7UW658c+YxKGVUzfDTNobSSEcuzvg+mFEf2nqfvheZtHFvQMsTQiA4hPpQ5eG/TmM+m9pLMBio5A7lkDleIPIZ7zMd2Pnyu3VGRqGClJCSP6/9sw8mrOvTPQnE= 15 | - secure: HxdHGvohuzKUeQdY6UD5foEQPzDJ+KIshg6Gv+7Lf09+kmSAZR3ksXeqhG6SzZXf7rBXaxE1RxFpluemEnqSWTisEDQ1uFPle2zrjHL5dkg+0C9hxNyJcopze8ef/zJbEzjPeAcptsfaGZS7iCQCaQzJTBSqJwAFThdYK/WL1Yc= 16 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to RxJava 2 | 3 | If you would like to contribute code you can do so through GitHub by forking the repository and sending a pull request (on a branch other than `master` or `gh-pages`). 4 | 5 | When submitting code, please make every effort to follow existing conventions and style in order to keep the code as readable as possible. 6 | 7 | ## License 8 | 9 | By contributing your code, you agree to license your contribution under the terms of the APLv2: https://github.com/ReactiveX/RxJava/blob/master/LICENSE 10 | 11 | All files are released with the Apache 2.0 license. 12 | 13 | If you are adding a new file it should have a header like this: 14 | 15 | ``` 16 | /** 17 | * Copyright 2014 Netflix, Inc. 18 | * 19 | * Licensed under the Apache License, Version 2.0 (the "License"); 20 | * you may not use this file except in compliance with the License. 21 | * You may obtain a copy of the License at 22 | * 23 | * http://www.apache.org/licenses/LICENSE-2.0 24 | * 25 | * Unless required by applicable law or agreed to in writing, software 26 | * distributed under the License is distributed on an "AS IS" BASIS, 27 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 28 | * See the License for the specific language governing permissions and 29 | * limitations under the License. 30 | */ 31 | ``` 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2012 Netflix, Inc. 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RxJava String 2 | 3 | String and Byte operators for [RxJava](https://github.com/ReactiveX/RxJava): 4 | 5 | * Read bytes from an ```InputStream``` as a stream of byte arrays (```StringObservable.from```) 6 | * Read text from a ```Reader``` as a stream of ```String``` (```StringObservable.from```) 7 | * Convert between ```Observable``` and ```Observable``` (```StringObservable.encode, decode```) 8 | * Split text by regex (rechunks a stream of ```String```) (```StringObservable.split```) 9 | * Join text (```StringObservable.join, stringConcat```) 10 | 11 | ## Master Build Status 12 | 13 | 14 | 15 | ## Communication 16 | 17 | - Google Group: [RxJava](http://groups.google.com/d/forum/rxjava) 18 | - Twitter: [@RxJava](http://twitter.com/RxJava) 19 | - [GitHub Issues](https://github.com/ReactiveX/RxJavaString/issues) 20 | 21 | 22 | ## Binaries 23 | 24 | Binaries and dependency information for Maven, Ivy, Gradle and others can be found at [http://search.maven.org](http://search.maven.org/#search%7Cga%7C1%7Cio.reactivex.rxjava-string). 25 | 26 | Example for Maven: 27 | 28 | ```xml 29 | 30 | io.reactivex 31 | rxjava-string 32 | x.y.z 33 | 34 | ``` 35 | and for Ivy: 36 | 37 | ```xml 38 | 39 | ``` 40 | 41 | ## Build 42 | 43 | To build: 44 | 45 | ``` 46 | $ git clone git@github.com:ReactiveX/RxJavaString.git 47 | $ cd RxJavaString/ 48 | $ ./gradlew build 49 | ``` 50 | 51 | ## Bugs and Feedback 52 | 53 | For bugs, questions and discussions please use the [Github Issues](https://github.com/ReactiveX/RxJavaString/issues). 54 | 55 | 56 | ## LICENSE 57 | 58 | Licensed under the Apache License, Version 2.0 (the "License"); 59 | you may not use this file except in compliance with the License. 60 | You may obtain a copy of the License at 61 | 62 | 63 | 64 | Unless required by applicable law or agreed to in writing, software 65 | distributed under the License is distributed on an "AS IS" BASIS, 66 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 67 | See the License for the specific language governing permissions and 68 | limitations under the License. 69 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { jcenter() } 3 | dependencies { classpath 'com.netflix.nebula:gradle-rxjava-project-plugin:2.+' } 4 | } 5 | 6 | apply plugin: 'rxjava-project' 7 | apply plugin: 'java' 8 | 9 | dependencies { 10 | compile 'io.reactivex:rxjava:1.2.3' 11 | testCompile 'junit:junit-dep:4.10' 12 | testCompile 'org.mockito:mockito-core:1.8.5' 13 | } 14 | 15 | // support for snapshot/final releases with the various branches RxJava uses 16 | nebulaRelease { 17 | addReleaseBranchPattern(/\d+\.\d+\.\d+/) 18 | addReleaseBranchPattern('HEAD') 19 | } 20 | 21 | if (project.hasProperty('release.useLastTag')) { 22 | tasks.prepare.enabled = false 23 | } 24 | 25 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | version=1.0.0-RC1-SNAPSHOT 2 | -------------------------------------------------------------------------------- /gradle/buildViaTravis.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This script will build the project. 3 | 4 | if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then 5 | echo -e "Build Pull Request #$TRAVIS_PULL_REQUEST => Branch [$TRAVIS_BRANCH]" 6 | ./gradlew -Prelease.useLastTag=true build 7 | elif [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_TAG" == "" ]; then 8 | echo -e 'Build Branch with Snapshot => Branch ['$TRAVIS_BRANCH']' 9 | if [ "${bintrayUser}" ] && [ "${bintrayKey}" ] && [ "${sonatypeUsername}" ] && [ "${sonatypePassword}" ]; then 10 | ./gradlew -Prelease.travisci=true -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" -PsonatypeUsername="${sonatypeUsername}" -PsonatypePassword="${sonatypePassword}" build snapshot --stacktrace 11 | else 12 | echo -e 'MISSING USERNAME/PASSWORD ENVIRONMENT VARIABLES' 13 | exit 1 14 | fi 15 | elif [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_TAG" != "" ]; then 16 | echo -e 'Build Branch for Release => Branch ['$TRAVIS_BRANCH'] Tag ['$TRAVIS_TAG']' 17 | if [ "${bintrayUser}" ] && [ "${bintrayKey}" ] && [ "${sonatypeUsername}" ] && [ "${sonatypePassword}" ]; then 18 | ./gradlew -Prelease.travisci=true -Prelease.useLastTag=true -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" -PsonatypeUsername="${sonatypeUsername}" -PsonatypePassword="${sonatypePassword}" final --stacktrace 19 | else 20 | echo -e 'MISSING USERNAME/PASSWORD ENVIRONMENT VARIABLES' 21 | exit 1 22 | fi 23 | else 24 | echo -e 'WARN: Should not be here => Branch ['$TRAVIS_BRANCH'] Tag ['$TRAVIS_TAG'] Pull Request ['$TRAVIS_PULL_REQUEST']' 25 | ./gradlew -Prelease.useLastTag=true build 26 | fi 27 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReactiveX/RxJavaString/3e73b759ff7bffd5d2e1c753630c5408cb5f9e5e/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Dec 13 00:17:06 PST 2014 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-bin.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name='rxjava-string' 2 | -------------------------------------------------------------------------------- /src/main/java/rx/internal/operators/ObservableSplit.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Netflix, Inc. 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 rx.internal.operators; 18 | 19 | import java.util.Queue; 20 | import java.util.concurrent.atomic.*; 21 | import java.util.regex.Pattern; 22 | 23 | import rx.*; 24 | import rx.Observable.OnSubscribe; 25 | import rx.exceptions.Exceptions; 26 | import rx.internal.operators.BackpressureUtils; 27 | import rx.internal.util.atomic.SpscAtomicArrayQueue; 28 | import rx.internal.util.unsafe.*; 29 | import rx.plugins.RxJavaHooks; 30 | 31 | /** 32 | * Split a sequence of strings based on a Rexexp pattern spanning subsequent 33 | * items if necessary. 34 | */ 35 | public final class ObservableSplit implements OnSubscribe { 36 | 37 | final Observable source; 38 | 39 | final Pattern pattern; 40 | 41 | final int bufferSize; 42 | 43 | public ObservableSplit(Observable source, Pattern pattern, int bufferSize) { 44 | this.source = source; 45 | this.pattern = pattern; 46 | this.bufferSize = bufferSize; 47 | } 48 | 49 | @Override 50 | public void call(Subscriber t) { 51 | SplitSubscriber parent = new SplitSubscriber(t, pattern, bufferSize); 52 | t.add(parent.requested); 53 | t.setProducer(parent.requested); 54 | 55 | source.unsafeSubscribe(parent); 56 | } 57 | 58 | static final class SplitSubscriber extends Subscriber { 59 | 60 | final Subscriber actual; 61 | 62 | final Pattern pattern; 63 | 64 | final Requested requested; 65 | 66 | final AtomicInteger wip; 67 | 68 | final int limit; 69 | 70 | final Queue queue; 71 | 72 | String[] current; 73 | 74 | String leftOver; 75 | 76 | int index; 77 | 78 | int produced; 79 | 80 | int empty; 81 | 82 | volatile boolean done; 83 | Throwable error; 84 | 85 | volatile boolean cancelled; 86 | 87 | SplitSubscriber(Subscriber actual, Pattern pattern, int bufferSize) { 88 | this.actual = actual; 89 | this.pattern = pattern; 90 | this.limit = bufferSize - (bufferSize >> 2); 91 | this.requested = new Requested(); 92 | this.wip = new AtomicInteger(); 93 | if (UnsafeAccess.isUnsafeAvailable()) { 94 | this.queue = new SpscArrayQueue(bufferSize); 95 | } else { 96 | this.queue = new SpscAtomicArrayQueue(bufferSize); 97 | } 98 | request(bufferSize); 99 | } 100 | 101 | @Override 102 | public void onNext(String t) { 103 | String lo = leftOver; 104 | String[] a; 105 | try { 106 | if (lo == null || lo.isEmpty()) { 107 | a = pattern.split(t, -1); 108 | } else { 109 | a = pattern.split(lo + t, -1); 110 | } 111 | } catch (Throwable ex) { 112 | Exceptions.throwIfFatal(ex); 113 | unsubscribe(); 114 | onError(ex); 115 | return; 116 | } 117 | 118 | if (a.length == 0) { 119 | leftOver = null; 120 | request(1); 121 | return; 122 | } else 123 | if (a.length == 1) { 124 | leftOver = a[0]; 125 | request(1); 126 | return; 127 | } 128 | leftOver = a[a.length - 1]; 129 | queue.offer(a); 130 | drain(); 131 | } 132 | 133 | @Override 134 | public void onError(Throwable e) { 135 | if (done) { 136 | RxJavaHooks.onError(e); 137 | return; 138 | } 139 | String lo = leftOver; 140 | if (lo != null && !lo.isEmpty()) { 141 | leftOver = null; 142 | queue.offer(new String[] { lo, null }); 143 | } 144 | error = e; 145 | done = true; 146 | drain(); 147 | } 148 | 149 | @Override 150 | public void onCompleted() { 151 | if (!done) { 152 | done = true; 153 | String lo = leftOver; 154 | if (lo != null && !lo.isEmpty()) { 155 | leftOver = null; 156 | queue.offer(new String[] { lo, null }); 157 | } 158 | drain(); 159 | } 160 | } 161 | 162 | void drain() { 163 | if (wip.getAndIncrement() != 0) { 164 | return; 165 | } 166 | 167 | Queue q = queue; 168 | 169 | int missed = 1; 170 | int consumed = produced; 171 | String[] array = current; 172 | int idx = index; 173 | int emptyCount = empty; 174 | 175 | Subscriber a = actual; 176 | 177 | for (;;) { 178 | long r = requested.get(); 179 | long e = 0; 180 | 181 | while (e != r) { 182 | if (cancelled) { 183 | current = null; 184 | q.clear(); 185 | return; 186 | } 187 | 188 | boolean d = done; 189 | 190 | if (array == null) { 191 | array = q.poll(); 192 | if (array != null) { 193 | current = array; 194 | if (++consumed == limit) { 195 | consumed = 0; 196 | request(limit); 197 | } 198 | } 199 | } 200 | 201 | boolean empty = array == null; 202 | 203 | if (d && empty) { 204 | current = null; 205 | Throwable ex = error; 206 | if (ex != null) { 207 | a.onError(ex); 208 | } else { 209 | a.onCompleted(); 210 | } 211 | return; 212 | } 213 | 214 | if (empty) { 215 | break; 216 | } 217 | 218 | if (array.length == idx + 1) { 219 | array = null; 220 | current = null; 221 | idx = 0; 222 | continue; 223 | } 224 | 225 | String v = array[idx]; 226 | 227 | if (v.isEmpty()) { 228 | emptyCount++; 229 | idx++; 230 | } else { 231 | while (emptyCount != 0 && e != r) { 232 | if (cancelled) { 233 | current = null; 234 | q.clear(); 235 | return; 236 | } 237 | a.onNext(""); 238 | e++; 239 | emptyCount--; 240 | } 241 | 242 | if (e != r && emptyCount == 0) { 243 | a.onNext(v); 244 | 245 | e++; 246 | idx++; 247 | } 248 | } 249 | } 250 | 251 | if (e == r) { 252 | if (cancelled) { 253 | current = null; 254 | q.clear(); 255 | return; 256 | } 257 | 258 | boolean d = done; 259 | 260 | if (array == null) { 261 | array = q.poll(); 262 | if (array != null) { 263 | current = array; 264 | if (++consumed == limit) { 265 | consumed = 0; 266 | request(limit); 267 | } 268 | } 269 | } 270 | 271 | boolean empty = array == null; 272 | 273 | if (d && empty) { 274 | current = null; 275 | Throwable ex = error; 276 | if (ex != null) { 277 | a.onError(ex); 278 | } else { 279 | a.onCompleted(); 280 | } 281 | return; 282 | } 283 | } 284 | 285 | if (e != 0L) { 286 | BackpressureUtils.produced(requested, e); 287 | } 288 | 289 | empty = emptyCount; 290 | produced = consumed; 291 | missed = wip.addAndGet(-missed); 292 | if (missed == 0) { 293 | break; 294 | } 295 | } 296 | } 297 | 298 | void cancel() { 299 | cancelled = true; 300 | unsubscribe(); 301 | if (wip.getAndIncrement() == 0) { 302 | current = null; 303 | queue.clear(); 304 | } 305 | } 306 | 307 | boolean isCancelled() { 308 | return isUnsubscribed(); 309 | } 310 | 311 | final class Requested extends AtomicLong implements Producer, Subscription { 312 | 313 | private static final long serialVersionUID = 3399839515828647345L; 314 | 315 | @Override 316 | public void request(long n) { 317 | if (n > 0) { 318 | BackpressureUtils.getAndAddRequest(this, n); 319 | drain(); 320 | } 321 | else if (n < 0) { 322 | throw new IllegalArgumentException("n >= 0 required but it was " + n); 323 | } 324 | } 325 | 326 | @Override 327 | public boolean isUnsubscribed() { 328 | return isCancelled(); 329 | } 330 | 331 | @Override 332 | public void unsubscribe() { 333 | cancel(); 334 | } 335 | } 336 | } 337 | } 338 | -------------------------------------------------------------------------------- /src/main/java/rx/internal/operators/OnSubscribeInputStream.java: -------------------------------------------------------------------------------- 1 | package rx.internal.operators; 2 | 3 | import rx.Observer; 4 | import rx.observables.SyncOnSubscribe; 5 | 6 | import java.io.IOException; 7 | import java.io.InputStream; 8 | import java.util.Arrays; 9 | 10 | public final class OnSubscribeInputStream extends SyncOnSubscribe { 11 | 12 | private final InputStream is; 13 | private final int size; 14 | 15 | public OnSubscribeInputStream(InputStream is, int size) { 16 | this.is = is; 17 | this.size = size; 18 | } 19 | 20 | @Override 21 | protected InputStream generateState() { 22 | return this.is; 23 | } 24 | 25 | @Override 26 | protected InputStream next(InputStream state, Observer observer) { 27 | byte[] buffer = new byte[size]; 28 | try { 29 | int count = state.read(buffer); 30 | if (count == -1) 31 | observer.onCompleted(); 32 | else if (count < size) 33 | observer.onNext(Arrays.copyOf(buffer, count)); 34 | else 35 | observer.onNext(buffer); 36 | } catch (IOException e) { 37 | observer.onError(e); 38 | } 39 | return state; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/rx/internal/operators/OnSubscribeReader.java: -------------------------------------------------------------------------------- 1 | package rx.internal.operators; 2 | 3 | import rx.Observer; 4 | import rx.observables.SyncOnSubscribe; 5 | 6 | import java.io.IOException; 7 | import java.io.Reader; 8 | 9 | public final class OnSubscribeReader extends SyncOnSubscribe { 10 | 11 | private final Reader reader; 12 | private final int size; 13 | 14 | public OnSubscribeReader(Reader reader, int size) { 15 | this.reader = reader; 16 | this.size = size; 17 | } 18 | 19 | @Override 20 | protected Reader generateState() { 21 | return this.reader; 22 | } 23 | 24 | @Override 25 | protected Reader next(Reader state, Observer observer) { 26 | char[] buffer = new char[size]; 27 | try { 28 | int count = state.read(buffer); 29 | if (count == -1) 30 | observer.onCompleted(); 31 | else 32 | observer.onNext(String.valueOf(buffer, 0, count)); 33 | } catch (IOException e) { 34 | observer.onError(e); 35 | } 36 | return state; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/rx/observables/StringObservable.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Netflix, Inc. 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 | package rx.observables; 17 | 18 | import java.io.*; 19 | import java.nio.*; 20 | import java.nio.charset.*; 21 | import java.util.Arrays; 22 | import java.util.concurrent.Callable; 23 | import java.util.regex.Pattern; 24 | 25 | import rx.*; 26 | import rx.Observable.Operator; 27 | import rx.functions.*; 28 | import rx.internal.operators.*; 29 | import rx.internal.util.RxRingBuffer; 30 | 31 | public class StringObservable { 32 | /** 33 | * Reads bytes from a source {@link InputStream} and outputs {@link Observable} of 34 | * {@code byte[]}s. Supports backpressure. 35 | *

36 | * 37 | * 38 | * @param i 39 | * Source {@link InputStream} 40 | * @return the Observable containing read byte arrays from the input 41 | */ 42 | public static Observable from(final InputStream i) { 43 | return from(i, 8 * 1024); 44 | } 45 | 46 | /** 47 | * Func0 that allows throwing an {@link IOException}s commonly thrown during IO operations. 48 | * @see StringObservable#using(UnsafeFunc0, Func1) 49 | * 50 | * @param 51 | */ 52 | public static interface UnsafeFunc0 extends Callable { 53 | public R call() throws Exception; 54 | } 55 | 56 | /** 57 | * Helps in creating an Observable that automatically calls {@link Closeable#close()} on completion, error or unsubscribe. 58 | * 59 | *

 60 |      * StringObservable.using(() -> new FileReader(file), (reader) -> StringObservable.from(reader))
 61 |      * 
62 | * 63 | * @param resourceFactory 64 | * Generates a new {@link Closeable} resource for each new subscription to the returned Observable 65 | * @param observableFactory 66 | * Converts the {@link Closeable} resource into a {@link Observable} with {@link #from(InputStream)} or {@link #from(Reader)} 67 | * @return 68 | * An {@link Observable} that automatically closes the resource when done. 69 | */ 70 | public static Observable using(final UnsafeFunc0 resourceFactory, 71 | final Func1> observableFactory) { 72 | return Observable.using(new Func0() { 73 | @Override 74 | public S call() { 75 | try { 76 | return resourceFactory.call(); 77 | } catch (Throwable e) { 78 | throw new RuntimeException(e); 79 | } 80 | } 81 | }, observableFactory, new Action1() { 82 | @Override 83 | public void call(S resource) { 84 | try { 85 | resource.close(); 86 | } catch (IOException e) { 87 | throw new RuntimeException(e); 88 | } 89 | } 90 | }, true); 91 | } 92 | 93 | /** 94 | * Reads bytes from a source {@link InputStream} and outputs {@link Observable} of 95 | * {@code byte[]}s. Supports backpressure. 96 | *

97 | * 98 | * 99 | * @param is 100 | * Source {@link InputStream} 101 | * @param size 102 | * internal buffer size 103 | * @return the Observable containing read byte arrays from the input 104 | */ 105 | public static Observable from(final InputStream is, final int size) { 106 | return Observable.create(new OnSubscribeInputStream(is, size)); 107 | } 108 | 109 | /** 110 | * Reads characters from a source {@link Reader} and outputs {@link Observable} of 111 | * {@link String}s. Supports backpressure. 112 | *

113 | * 114 | * 115 | * @param i 116 | * Source {@link Reader} 117 | * @return the Observable of Strings read from the source 118 | */ 119 | public static Observable from(final Reader i) { 120 | return from(i, 8 * 1024); 121 | } 122 | 123 | /** 124 | * Reads characters from a source {@link Reader} and outputs {@link Observable} of 125 | * {@link String}s. Supports backpressure. 126 | *

127 | * 128 | * 129 | * @param i 130 | * Source {@link Reader} 131 | * @param size 132 | * internal buffer size 133 | * @return the Observable of Strings read from the source 134 | */ 135 | public static Observable from(final Reader reader, final int size) { 136 | return Observable.create(new OnSubscribeReader(reader, size)); 137 | } 138 | 139 | /** 140 | * Decodes a stream of multibyte chunks into a stream of strings that works on infinite streams 141 | * and where handles when a multibyte character spans two chunks. 142 | *

143 | * 144 | * 145 | * @param src 146 | * @param charsetName 147 | * @return the Observable returning a stream of decoded strings 148 | */ 149 | public static Observable decode(Observable src, String charsetName) { 150 | return decode(src, Charset.forName(charsetName)); 151 | } 152 | 153 | /** 154 | * Decodes a stream of multibyte chunks into a stream of strings that works on infinite streams 155 | * and where handles when a multibyte character spans two chunks. 156 | *

157 | * 158 | * 159 | * @param src 160 | * @param charset 161 | * @return the Observable returning a stream of decoded strings 162 | */ 163 | public static Observable decode(Observable src, Charset charset) { 164 | return decode(src, charset.newDecoder().onMalformedInput(CodingErrorAction.REPLACE).onUnmappableCharacter(CodingErrorAction.REPLACE)); 165 | } 166 | 167 | /** 168 | * Decodes a stream of multibyte chunks into a stream of strings that works on infinite streams 169 | * and handles when a multibyte character spans two chunks. 170 | * This method allows for more control over how malformed and unmappable characters are handled. 171 | *

172 | * 173 | * 174 | * @param src 175 | * @param charsetDecoder 176 | * @return the Observable returning a stream of decoded strings 177 | */ 178 | public static Observable decode(final Observable src, final CharsetDecoder charsetDecoder) { 179 | return src.lift(new Operator() { 180 | @Override 181 | public Subscriber call(final Subscriber o) { 182 | return new Subscriber(o) { 183 | private ByteBuffer leftOver = null; 184 | 185 | @Override 186 | public void onCompleted() { 187 | if (process(null, leftOver, true)) 188 | o.onCompleted(); 189 | } 190 | 191 | @Override 192 | public void onError(Throwable e) { 193 | if (process(null, leftOver, true)) 194 | o.onError(e); 195 | } 196 | 197 | @Override 198 | public void onNext(byte[] bytes) { 199 | process(bytes, leftOver, false); 200 | } 201 | 202 | public boolean process(byte[] next, ByteBuffer last, boolean endOfInput) { 203 | if (o.isUnsubscribed()) 204 | return false; 205 | 206 | ByteBuffer bb; 207 | if (last != null) { 208 | if (next != null) { 209 | // merge leftover in front of the next bytes 210 | bb = ByteBuffer.allocate(last.remaining() + next.length); 211 | bb.put(last); 212 | bb.put(next); 213 | bb.flip(); 214 | } 215 | else { // next == null 216 | bb = last; 217 | } 218 | } 219 | else { // last == null 220 | if (next != null) { 221 | bb = ByteBuffer.wrap(next); 222 | } 223 | else { // next == null 224 | return true; 225 | } 226 | } 227 | 228 | CharBuffer cb = CharBuffer.allocate((int) (bb.limit() * charsetDecoder.averageCharsPerByte())); 229 | CoderResult cr = charsetDecoder.decode(bb, cb, endOfInput); 230 | cb.flip(); 231 | 232 | if (cr.isError()) { 233 | try { 234 | cr.throwException(); 235 | } 236 | catch (CharacterCodingException e) { 237 | o.onError(e); 238 | return false; 239 | } 240 | } 241 | 242 | if (bb.remaining() > 0) { 243 | leftOver = bb; 244 | } 245 | else { 246 | leftOver = null; 247 | } 248 | 249 | String string = cb.toString(); 250 | if (!string.isEmpty()) 251 | o.onNext(string); 252 | 253 | return true; 254 | } 255 | }; 256 | } 257 | }); 258 | } 259 | 260 | /** 261 | * Encodes a possibly infinite stream of strings into an Observable of byte arrays. 262 | *

263 | * 264 | * 265 | * @param src 266 | * @param charsetName 267 | * @return the Observable with a stream of encoded byte arrays 268 | */ 269 | public static Observable encode(Observable src, String charsetName) { 270 | return encode(src, Charset.forName(charsetName)); 271 | } 272 | 273 | /** 274 | * Encodes a possibly infinite stream of strings into an Observable of byte arrays. 275 | *

276 | * 277 | * 278 | * @param src 279 | * @param charset 280 | * @return the Observable with a stream of encoded byte arrays 281 | */ 282 | public static Observable encode(Observable src, Charset charset) { 283 | return encode(src, charset.newEncoder().onMalformedInput(CodingErrorAction.REPLACE).onUnmappableCharacter(CodingErrorAction.REPLACE)); 284 | } 285 | 286 | /** 287 | * Encodes a possibly infinite stream of strings into an Observable of byte arrays. 288 | * This method allows for more control over how malformed and unmappable characters are handled. 289 | *

290 | * 291 | * 292 | * @param src 293 | * @param charsetEncoder 294 | * @return the Observable with a stream of encoded byte arrays 295 | */ 296 | public static Observable encode(Observable src, final CharsetEncoder charsetEncoder) { 297 | return src.map(new Func1() { 298 | @Override 299 | public byte[] call(String str) { 300 | CharBuffer cb = CharBuffer.wrap(str); 301 | ByteBuffer bb; 302 | try { 303 | bb = charsetEncoder.encode(cb); 304 | } catch (CharacterCodingException e) { 305 | throw new RuntimeException(e); 306 | } 307 | return Arrays.copyOfRange(bb.array(), bb.position(), bb.limit()); 308 | } 309 | }); 310 | } 311 | 312 | /** 313 | * Gather up all of the strings in to one string to be able to use it as one message. Don't use 314 | * this on infinite streams. 315 | *

316 | * 317 | * 318 | * @param src 319 | * @return the Observable returing all strings concatenated as a single string 320 | */ 321 | public static Observable stringConcat(Observable src) { 322 | return toString(src.reduce(new StringBuilder(), new Func2() { 323 | @Override 324 | public StringBuilder call(StringBuilder a, String b) { 325 | return a.append(b); 326 | } 327 | })); 328 | } 329 | 330 | /** 331 | * Maps {@link Observable}<{@link Object}> to {@link Observable}<{@link String}> by using {@link String#valueOf(Object)} 332 | * @param src 333 | * @return An {@link Observable} of only {@link String}s. 334 | */ 335 | public static Observable toString(Observable src) { 336 | return src.map(new Func1() { 337 | @Override 338 | public String call(Object obj) { 339 | return String.valueOf(obj); 340 | } 341 | }); 342 | } 343 | 344 | /** 345 | * Rechunks the strings based on a regex pattern and works on infinite stream. 346 | * 347 | *

348 |      * split(["boo:an", "d:foo"], ":") --> ["boo", "and", "foo"]
349 |      * split(["boo:an", "d:foo"], "o") --> ["b", "", ":and:f", "", ""]
350 |      * 
351 | * 352 | * See {@link Pattern} 353 | *

354 | * 355 | * 356 | * @param src 357 | * the source that should be use for the split 358 | * @param regex 359 | * a string that build regular expression modifier 360 | * @return the Observable streaming the split values 361 | */ 362 | 363 | public static Observable split(final Observable src, String regex) { 364 | Pattern pattern = Pattern.compile(regex); 365 | return StringObservable.split(src,pattern); 366 | } 367 | 368 | /** 369 | * Rechunks the strings based on a regex pattern and works on infinite stream. 370 | * 371 | *

372 |      * split(["boo:an", "d:foo"], ":") --> ["boo", "and", "foo"]
373 |      * split(["boo:an", "d:foo"], "o") --> ["b", "", ":and:f", "", ""]
374 |      * 
375 | * 376 | * See {@link Pattern} 377 | *

378 | * 379 | * 380 | * @param src 381 | * the source that should be use for the split 382 | * @param pattern 383 | * pre compiled regular expression pattern for the split functionality 384 | * @return the Observable streaming the split values 385 | */ 386 | public static Observable split(final Observable src, final Pattern pattern) { 387 | return Observable.create(new ObservableSplit(src, pattern, RxRingBuffer.SIZE)); 388 | } 389 | 390 | /** 391 | * Concatenates the sequence of values by adding a separator 392 | * between them and emitting the result once the source completes. 393 | *

394 | * 395 | *

396 | * The conversion from the value type to String is performed via 397 | * {@link java.lang.String#valueOf(java.lang.Object)} calls. 398 | *

399 | * For example: 400 | * 401 | *

402 |      * Observable<Object> source = Observable.from("a", 1, "c");
403 |      * Observable<String> result = join(source, ", ");
404 |      * 
405 | * 406 | * will yield a single element equal to "a, 1, c". 407 | * 408 | * @param source 409 | * the source sequence of CharSequence values 410 | * @param separator 411 | * the separator to a 412 | * @return an Observable which emits a single String value having the concatenated 413 | * values of the source observable with the separator between elements 414 | */ 415 | public static Observable join(final Observable source, final CharSequence separator) { 416 | return source.lift(new Operator() { 417 | @Override 418 | public Subscriber call(final Subscriber child) { 419 | final JoinParentSubscriber parent = new JoinParentSubscriber(child, separator); 420 | child.add(parent); 421 | child.setProducer(new Producer() { 422 | @Override 423 | public void request(long n) { 424 | if (n > 0) { 425 | parent.requestAll(); 426 | } 427 | }}); 428 | return parent; 429 | } 430 | }); 431 | } 432 | 433 | private static final class JoinParentSubscriber extends Subscriber { 434 | 435 | private final Subscriber child; 436 | private final CharSequence separator; 437 | private boolean mayAddSeparator; 438 | private StringBuilder b = new StringBuilder(); 439 | 440 | JoinParentSubscriber(Subscriber child, CharSequence separator) { 441 | this.child = child; 442 | this.separator = separator; 443 | } 444 | 445 | void requestAll() { 446 | request(Long.MAX_VALUE); 447 | } 448 | 449 | @Override 450 | public void onStart() { 451 | request(0); 452 | } 453 | 454 | @Override 455 | public void onCompleted() { 456 | String str = b.toString(); 457 | b = null; 458 | if (!child.isUnsubscribed()) 459 | child.onNext(str); 460 | if (!child.isUnsubscribed()) 461 | child.onCompleted(); 462 | } 463 | 464 | @Override 465 | public void onError(Throwable e) { 466 | b = null; 467 | if (!child.isUnsubscribed()) 468 | child.onError(e); 469 | } 470 | 471 | @Override 472 | public void onNext(String t) { 473 | if (mayAddSeparator) { 474 | b.append(separator); 475 | } 476 | mayAddSeparator = true; 477 | b.append(t); 478 | } 479 | 480 | } 481 | 482 | /** 483 | * Splits the {@link Observable} of Strings by line ending characters in a platform independent way. It is equivalent to 484 | *
split(src, "(\\r\\n)|\\n|\\r|\\u0085|\\u2028|\\u2029")
485 | *

486 | * 487 | * 488 | * @param source 489 | * @return the Observable conaining the split lines of the source 490 | * @see StringObservable#split(Observable, Pattern) 491 | */ 492 | public static Observable byLine(Observable source) { 493 | return split(source, ByLinePatternHolder.BY_LINE); 494 | } 495 | 496 | /** 497 | * Lazy initialization of the pattern. 498 | * 499 | * https://docs.oracle.com/javase/7/docs/api/java/util/regex/Pattern.html#lt 500 | */ 501 | private static final class ByLinePatternHolder { 502 | private static final Pattern BY_LINE = Pattern.compile("(\\r\\n)|\\n|\\r|\\u0085|\\u2028|\\u2029"); 503 | } 504 | 505 | /** 506 | * Converts a String into an Observable that emits the chars in the String. 507 | *

508 | * 509 | * 510 | * @param str 511 | * the source String 512 | * @return an Observable that emits each char in the source String 513 | * @see RxJava wiki: from 514 | */ 515 | public static Observable byCharacter(Observable source) { 516 | return source.lift(new Operator() { 517 | @Override 518 | public Subscriber call(final Subscriber subscriber) { 519 | return new Subscriber(subscriber) { 520 | @Override 521 | public void onCompleted() { 522 | subscriber.onCompleted(); 523 | } 524 | 525 | @Override 526 | public void onError(Throwable e) { 527 | subscriber.onError(e); 528 | } 529 | 530 | @Override 531 | public void onNext(String str) { 532 | for (char c : str.toCharArray()) { 533 | subscriber.onNext(Character.toString(c)); 534 | } 535 | } 536 | }; 537 | } 538 | }); 539 | } 540 | } 541 | -------------------------------------------------------------------------------- /src/test/java/rx/observables/AssertObservable.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Netflix, Inc. 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 | package rx.observables; 17 | 18 | import rx.Notification; 19 | import rx.Observable; 20 | import rx.functions.Func1; 21 | import rx.functions.Func2; 22 | 23 | public class AssertObservable { 24 | /** 25 | * Asserts that two Observables are equal. If they are not, an {@link AssertionError} is thrown 26 | * with the given message. If expecteds and actuals are 27 | * null, they are considered equal. 28 | * 29 | * @param expected 30 | * Observable with expected values. 31 | * @param actual 32 | * Observable with actual values 33 | */ 34 | public static void assertObservableEqualsBlocking(Observable expected, Observable actual) { 35 | assertObservableEqualsBlocking(null, expected, actual); 36 | } 37 | 38 | /** 39 | * Asserts that two Observables are equal. If they are not, an {@link AssertionError} is thrown 40 | * with the given message. If expected and actual are 41 | * null, they are considered equal. 42 | * 43 | * @param message 44 | * the identifying message for the {@link AssertionError} (null okay) 45 | * @param expected 46 | * Observable with expected values. 47 | * @param actual 48 | * Observable with actual values 49 | */ 50 | public static void assertObservableEqualsBlocking(String message, Observable expected, Observable actual) { 51 | assertObservableEquals(expected, actual).toBlocking().lastOrDefault(null); 52 | } 53 | 54 | /** 55 | * Asserts that two {@link Observable}s are equal and returns an empty {@link Observable}. If 56 | * they are not, an {@link Observable} is returned that calls onError with an {@link AssertionError} when subscribed to. If expected and actual 57 | * are null, they are considered equal. 58 | * 59 | * @param message 60 | * the identifying message for the {@link AssertionError} (null okay) 61 | * @param expected 62 | * Observable with expected values. 63 | * @param actual 64 | * Observable with actual values 65 | */ 66 | public static Observable assertObservableEquals(Observable expected, Observable actual) { 67 | return assertObservableEquals(null, expected, actual); 68 | } 69 | 70 | /** 71 | * Asserts that two {@link Observable}s are equal and returns an empty {@link Observable}. If 72 | * they are not, an {@link Observable} is returned that calls onError with an {@link AssertionError} when subscribed to with the given message. If expected 73 | * and actual are null, they are considered equal. 74 | * 75 | * @param message 76 | * the identifying message for the {@link AssertionError} (null okay) 77 | * @param expected 78 | * Observable with expected values. 79 | * @param actual 80 | * Observable with actual values 81 | */ 82 | public static Observable assertObservableEquals(final String message, Observable expected, Observable actual) { 83 | if (actual == null && expected != null) { 84 | return Observable.error(new AssertionError((message != null ? message + ": " : "") + "Actual was null and expected was not")); 85 | } 86 | if (actual != null && expected == null) { 87 | return Observable.error(new AssertionError((message != null ? message + ": " : "") + "Expected was null and actual was not")); 88 | } 89 | if (actual == null && expected == null) { 90 | return Observable.empty(); 91 | } 92 | 93 | Func2, ? super Notification, Notification> zipFunction = new Func2, Notification, Notification>() { 94 | @Override 95 | public Notification call(Notification expectedNotfication, Notification actualNotification) { 96 | if (expectedNotfication.equals(actualNotification)) { 97 | StringBuilder message = new StringBuilder(); 98 | message.append(expectedNotfication.getKind()); 99 | if (expectedNotfication.hasValue()) 100 | message.append(" ").append(expectedNotfication.getValue()); 101 | if (expectedNotfication.hasThrowable()) 102 | message.append(" ").append(expectedNotfication.getThrowable()); 103 | return Notification.createOnNext("equals " + message.toString()); 104 | } 105 | else { 106 | StringBuilder error = new StringBuilder(); 107 | error.append("expected:<").append(expectedNotfication.getKind()); 108 | if (expectedNotfication.hasValue()) 109 | error.append(" ").append(expectedNotfication.getValue()); 110 | if (expectedNotfication.hasThrowable()) 111 | error.append(" ").append(expectedNotfication.getThrowable()); 112 | error.append("> but was:<").append(actualNotification.getKind()); 113 | if (actualNotification.hasValue()) 114 | error.append(" ").append(actualNotification.getValue()); 115 | if (actualNotification.hasThrowable()) 116 | error.append(" ").append(actualNotification.getThrowable()); 117 | error.append(">"); 118 | 119 | return Notification.createOnError(new AssertionError(error.toString())); 120 | } 121 | } 122 | }; 123 | 124 | Func2, Notification, Notification> accumulator = new Func2, Notification, Notification>() { 125 | @Override 126 | public Notification call(Notification a, Notification b) { 127 | String message = a.isOnError() ? a.getThrowable().getMessage() : a.getValue(); 128 | boolean fail = a.isOnError(); 129 | 130 | message += "\n\t" + (b.isOnError() ? b.getThrowable().getMessage() : b.getValue()); 131 | fail |= b.isOnError(); 132 | 133 | if (fail) 134 | return Notification.createOnError(new AssertionError(message)); 135 | else 136 | return Notification.createOnNext(message); 137 | } 138 | }; 139 | 140 | Observable outcomeObservable = Observable.zip(expected.materialize(), actual.materialize(), zipFunction).reduce(accumulator).map(new Func1, Notification>() { 141 | @Override 142 | public Notification call(Notification outcome) { 143 | if (outcome.isOnError()) { 144 | String fullMessage = (message != null ? message + ": " : "") + "Observables are different\n\t" + outcome.getThrowable().getMessage(); 145 | return Notification.createOnError(new AssertionError(fullMessage)); 146 | } 147 | return Notification.createOnCompleted(); 148 | } 149 | }).dematerialize(); 150 | return outcomeObservable; 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/test/java/rx/observables/ObservableSplitTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Netflix, Inc. 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 | package rx.observables; 17 | 18 | import java.io.IOException; 19 | 20 | import org.junit.Test; 21 | 22 | import rx.Observable; 23 | import rx.observers.TestSubscriber; 24 | 25 | public class ObservableSplitTest { 26 | 27 | void test(String[] input, String pattern, String[] output, boolean rebatch) { 28 | TestSubscriber ts = new TestSubscriber(); 29 | Observable o = StringObservable.split(Observable.from(input), pattern); 30 | if (rebatch) { 31 | o = o.rebatchRequests(1); 32 | } 33 | o.subscribe(ts); 34 | 35 | ts.assertValues(output); 36 | ts.assertNoErrors(); 37 | ts.assertCompleted(); 38 | } 39 | 40 | @Test 41 | public void split1() { 42 | test(new String[] {"ab", ":cd", "e:fgh" }, ":", new String[] { "ab", "cde", "fgh" }, false); 43 | } 44 | 45 | @Test 46 | public void split1Request1() { 47 | test(new String[] {"ab", ":cd", "e:fgh" }, ":", new String[] { "ab", "cde", "fgh" }, true); 48 | } 49 | 50 | @Test 51 | public void split2() { 52 | test(new String[] { "abcdefgh" }, ":", new String[] { "abcdefgh" }, false); 53 | } 54 | 55 | @Test 56 | public void split2Request1() { 57 | test(new String[] { "abcdefgh" }, ":", new String[] { "abcdefgh" }, true); 58 | } 59 | 60 | @Test 61 | public void splitEmpty() { 62 | test(new String[] { }, ":", new String[] { }, false); 63 | } 64 | 65 | @Test 66 | public void splitError() { 67 | TestSubscriber ts = new TestSubscriber(); 68 | Observable o = StringObservable.split(Observable.empty().concatWith(Observable.error(new IOException())), ":"); 69 | o.subscribe(ts); 70 | 71 | ts.assertNoValues(); 72 | ts.assertNotCompleted(); 73 | ts.assertError(IOException.class); 74 | } 75 | 76 | @Test 77 | public void splitExample1() { 78 | test(new String[] { "boo:and:foo" }, ":", new String[] { "boo", "and", "foo" }, false); 79 | } 80 | 81 | @Test 82 | public void splitExample1Request1() { 83 | test(new String[] { "boo:and:foo" }, ":", new String[] { "boo", "and", "foo" }, true); 84 | } 85 | 86 | @Test 87 | public void splitExample2() { 88 | test(new String[] { "boo:and:foo" }, "o", new String[] { "b", "", ":and:f" }, false); 89 | } 90 | 91 | @Test 92 | public void splitExample2Request1() { 93 | test(new String[] { "boo:and:foo" }, "o", new String[] { "b", "", ":and:f" }, true); 94 | } 95 | 96 | @Test 97 | public void split3() { 98 | test(new String[] { "abqw", "ercdqw", "eref" }, "qwer", new String[] { "ab", "cd", "ef" }, false); 99 | } 100 | 101 | @Test 102 | public void split3Buffer1Request1() { 103 | test(new String[] { "abqw", "ercdqw", "eref" }, "qwer", new String[] { "ab", "cd", "ef" }, true); 104 | } 105 | 106 | @Test 107 | public void split4() { 108 | test(new String[] { "ab", ":", "", "", "c:d", "", "e:" }, ":", new String[] { "ab", "c", "de" }, false); 109 | } 110 | 111 | @Test 112 | public void split4Request1() { 113 | test(new String[] { "ab", ":", "", "", "c:d", "", "e:" }, ":", new String[] { "ab", "c", "de" }, true); 114 | } 115 | 116 | } 117 | -------------------------------------------------------------------------------- /src/test/java/rx/observables/StringObservableTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Netflix, Inc. 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 | package rx.observables; 17 | 18 | import static org.junit.Assert.assertArrayEquals; 19 | import static org.junit.Assert.assertEquals; 20 | import static org.junit.Assert.assertFalse; 21 | import static org.junit.Assert.assertNotSame; 22 | import static org.junit.Assert.assertTrue; 23 | import static org.junit.Assert.fail; 24 | import static org.mockito.Matchers.any; 25 | import static org.mockito.Mockito.mock; 26 | import static org.mockito.Mockito.never; 27 | import static org.mockito.Mockito.spy; 28 | import static org.mockito.Mockito.times; 29 | import static org.mockito.Mockito.verify; 30 | import static rx.Observable.just; 31 | import static rx.observables.StringObservable.byCharacter; 32 | import static rx.observables.StringObservable.byLine; 33 | import static rx.observables.StringObservable.decode; 34 | import static rx.observables.StringObservable.encode; 35 | import static rx.observables.StringObservable.from; 36 | import static rx.observables.StringObservable.join; 37 | import static rx.observables.StringObservable.split; 38 | import static rx.observables.StringObservable.using; 39 | 40 | import java.io.ByteArrayInputStream; 41 | import java.io.FilterReader; 42 | import java.io.IOException; 43 | import java.io.InputStreamReader; 44 | import java.io.Reader; 45 | import java.io.StringReader; 46 | import java.nio.charset.Charset; 47 | import java.nio.charset.CharsetDecoder; 48 | import java.nio.charset.MalformedInputException; 49 | import java.util.Arrays; 50 | import java.util.Iterator; 51 | import java.util.List; 52 | import java.util.concurrent.atomic.AtomicBoolean; 53 | import java.util.concurrent.atomic.AtomicInteger; 54 | 55 | import org.junit.Test; 56 | 57 | import rx.Observable; 58 | import rx.Observer; 59 | import rx.functions.Func1; 60 | import rx.observables.StringObservable.UnsafeFunc0; 61 | import rx.observers.TestObserver; 62 | import rx.observers.TestSubscriber; 63 | 64 | public class StringObservableTest { 65 | 66 | @Test 67 | public void testMultibyteSpanningTwoBuffers() { 68 | Observable src = Observable.just(new byte[] { (byte) 0xc2 }, 69 | new byte[] { (byte) 0xa1 }); 70 | String out = StringObservable.decode(src, "UTF-8").toBlocking().single(); 71 | 72 | assertEquals("\u00A1", out); 73 | } 74 | 75 | @Test 76 | public void testMalformedAtTheEndReplace() { 77 | Observable src = Observable.just(new byte[] { (byte) 0xc2 }); 78 | String out = decode(src, "UTF-8").toBlocking().single(); 79 | 80 | // REPLACEMENT CHARACTER 81 | assertEquals("\uFFFD", out); 82 | } 83 | 84 | @Test 85 | public void testMalformedInTheMiddleReplace() { 86 | Observable src = Observable.just(new byte[] { (byte) 0xc2, 65 }); 87 | String out = decode(src, "UTF-8").toBlocking().single(); 88 | 89 | // REPLACEMENT CHARACTER 90 | assertEquals("\uFFFDA", out); 91 | } 92 | 93 | @Test(expected = RuntimeException.class) 94 | public void testMalformedAtTheEndReport() { 95 | Observable src = Observable.just(new byte[] { (byte) 0xc2 }); 96 | CharsetDecoder charsetDecoder = Charset.forName("UTF-8").newDecoder(); 97 | decode(src, charsetDecoder).toBlocking().single(); 98 | } 99 | 100 | @Test(expected = RuntimeException.class) 101 | public void testMalformedInTheMiddleReport() { 102 | Observable src = Observable.just(new byte[] { (byte) 0xc2, 65 }); 103 | CharsetDecoder charsetDecoder = Charset.forName("UTF-8").newDecoder(); 104 | decode(src, charsetDecoder).toBlocking().single(); 105 | } 106 | 107 | @Test 108 | public void testPropagateError() { 109 | Observable src = Observable.just(new byte[] { 65 }); 110 | Observable err = Observable.error(new IOException()); 111 | CharsetDecoder charsetDecoder = Charset.forName("UTF-8").newDecoder(); 112 | try { 113 | decode(Observable.concat(src, err), charsetDecoder).toList().toBlocking().single(); 114 | fail(); 115 | } catch (RuntimeException e) { 116 | assertEquals(IOException.class, e.getCause().getClass()); 117 | } 118 | } 119 | 120 | @Test 121 | public void testPropagateErrorInTheMiddleOfMultibyte() { 122 | Observable src = Observable.just(new byte[] { (byte) 0xc2 }); 123 | Observable err = Observable.error(new IOException()); 124 | CharsetDecoder charsetDecoder = Charset.forName("UTF-8").newDecoder(); 125 | try { 126 | decode(Observable.concat(src, err), charsetDecoder).toList().toBlocking().single(); 127 | fail(); 128 | } catch (RuntimeException e) { 129 | assertEquals(MalformedInputException.class, e.getCause().getClass()); 130 | } 131 | } 132 | 133 | @Test 134 | public void testEncode() { 135 | assertArrayEquals(new byte[] { (byte) 0xc2, (byte) 0xa1 }, 136 | encode(Observable.just("\u00A1"), "UTF-8").toBlocking().single()); 137 | } 138 | 139 | @Test 140 | public void testSplitOnColon() { 141 | testSplit("boo:and:foo", ":", 0, "boo", "and", "foo"); 142 | } 143 | 144 | @Test 145 | public void testSplitOnOh() { 146 | testSplit("boo:and:foo", "o", 0, "b", "", ":and:f"); 147 | } 148 | 149 | @Test 150 | public void testSplitOnEmptyStream() { 151 | assertEquals(0, (int) StringObservable.split(Observable. empty(), "\n").count() 152 | .toBlocking().single()); 153 | } 154 | 155 | @Test 156 | public void testSplitOnStreamThatThrowsExceptionImmediately() { 157 | RuntimeException ex = new RuntimeException("boo"); 158 | try { 159 | StringObservable.split(Observable. error(ex), "\n").count().toBlocking() 160 | .single(); 161 | fail(); 162 | } catch (RuntimeException e) { 163 | assertEquals(ex, e); 164 | } 165 | } 166 | 167 | public void testSplit(String str, String regex, int limit, String... parts) { 168 | testSplit(str, regex, 0, Observable.just(str), parts); 169 | for (int i = 0; i < str.length(); i++) { 170 | String a = str.substring(0, i); 171 | String b = str.substring(i, str.length()); 172 | testSplit(a + "|" + b, regex, limit, Observable.just(a, b), parts); 173 | } 174 | } 175 | 176 | public void testSplit(String message, String regex, int limit, Observable src, 177 | String... parts) { 178 | Observable act = split(src, regex); 179 | Observable exp = Observable.from(parts); 180 | AssertObservable.assertObservableEqualsBlocking("when input is " + message 181 | + " and limit = " + limit, exp, act); 182 | } 183 | 184 | @Test 185 | public void testSplitLongPattern() { 186 | Iterator iter = StringObservable.split(Observable.just("asdfqw","erasdf"), "qwer").toBlocking().getIterator(); 187 | assertTrue(iter.hasNext()); 188 | assertEquals("asdf", iter.next()); 189 | assertTrue(iter.hasNext()); 190 | assertEquals("asdf", iter.next()); 191 | assertFalse(iter.hasNext()); 192 | } 193 | 194 | @Test 195 | public void testJoinMixed() { 196 | Observable source = Observable.from(Arrays.asList("a", "1", "c")); 197 | 198 | Observable result = join(source, ", "); 199 | 200 | @SuppressWarnings("unchecked") 201 | Observer observer = mock(Observer.class); 202 | 203 | result.subscribe(new TestObserver(observer)); 204 | 205 | verify(observer, times(1)).onNext("a, 1, c"); 206 | verify(observer, times(1)).onCompleted(); 207 | verify(observer, never()).onError(any(Throwable.class)); 208 | } 209 | 210 | @Test 211 | public void testJoinWithEmptyString() { 212 | Observable source = Observable.just("", "b", "c"); 213 | 214 | Observable result = join(source, ", "); 215 | 216 | @SuppressWarnings("unchecked") 217 | Observer observer = mock(Observer.class); 218 | 219 | result.subscribe(new TestObserver(observer)); 220 | 221 | verify(observer, times(1)).onNext(", b, c"); 222 | verify(observer, times(1)).onCompleted(); 223 | verify(observer, never()).onError(any(Throwable.class)); 224 | } 225 | 226 | @Test 227 | public void testJoinWithNull() { 228 | Observable source = Observable.just("a", null, "c"); 229 | 230 | Observable result = join(source, ", "); 231 | 232 | @SuppressWarnings("unchecked") 233 | Observer observer = mock(Observer.class); 234 | 235 | result.subscribe(new TestObserver(observer)); 236 | 237 | verify(observer, times(1)).onNext("a, null, c"); 238 | verify(observer, times(1)).onCompleted(); 239 | verify(observer, never()).onError(any(Throwable.class)); 240 | } 241 | 242 | @Test 243 | public void testJoinSingle() { 244 | Observable source = Observable.just("a"); 245 | 246 | Observable result = join(source, ", "); 247 | 248 | @SuppressWarnings("unchecked") 249 | Observer observer = mock(Observer.class); 250 | 251 | result.subscribe(new TestObserver(observer)); 252 | 253 | verify(observer, times(1)).onNext("a"); 254 | verify(observer, times(1)).onCompleted(); 255 | verify(observer, never()).onError(any(Throwable.class)); 256 | } 257 | 258 | @Test 259 | public void testJoinEmpty() { 260 | Observable source = Observable.empty(); 261 | 262 | Observable result = join(source, ", "); 263 | 264 | @SuppressWarnings("unchecked") 265 | Observer observer = mock(Observer.class); 266 | 267 | result.subscribe(new TestObserver(observer)); 268 | 269 | verify(observer, times(1)).onNext(""); 270 | verify(observer, times(1)).onCompleted(); 271 | verify(observer, never()).onError(any(Throwable.class)); 272 | } 273 | 274 | @Test 275 | public void testJoinThrows() { 276 | Observable source = Observable.concat(Observable.just("a"), 277 | Observable. error(new RuntimeException("Forced failure"))); 278 | 279 | Observable result = join(source, ", "); 280 | 281 | @SuppressWarnings("unchecked") 282 | Observer observer = mock(Observer.class); 283 | 284 | result.subscribe(new TestObserver(observer)); 285 | 286 | verify(observer, never()).onNext("a"); 287 | verify(observer, never()).onCompleted(); 288 | verify(observer, times(1)).onError(any(Throwable.class)); 289 | } 290 | 291 | @Test 292 | public void testFromInputStream() { 293 | final byte[] inBytes = "test".getBytes(); 294 | final byte[] outBytes = from(new ByteArrayInputStream(inBytes)).toBlocking().single(); 295 | assertNotSame(inBytes, outBytes); 296 | assertArrayEquals(inBytes, outBytes); 297 | } 298 | 299 | @Test 300 | public void testFromEmptyInputStream() { 301 | final byte[] inBytes = new byte[0]; 302 | int count = from(new ByteArrayInputStream(inBytes)).count().toBlocking().single(); 303 | assertEquals(0, count); 304 | } 305 | 306 | @Test 307 | public void testFromInputStreamUsesBufferSize() { 308 | final byte[] inBytes = "test".getBytes(); 309 | List list = from(new ByteArrayInputStream(inBytes), 2).toList().toBlocking() 310 | .single(); 311 | assertEquals(2, list.size()); 312 | assertArrayEquals("te".getBytes(), list.get(0)); 313 | assertArrayEquals("st".getBytes(), list.get(1)); 314 | } 315 | 316 | @Test 317 | public void testFromInputStreamWillUnsubscribeBeforeCallingNextRead() { 318 | final byte[] inBytes = "test".getBytes(); 319 | final AtomicInteger numReads = new AtomicInteger(0); 320 | ByteArrayInputStream is = new ByteArrayInputStream(inBytes) { 321 | 322 | @Override 323 | public synchronized int read(byte[] b, int off, int len) { 324 | numReads.incrementAndGet(); 325 | return super.read(b, off, len); 326 | } 327 | }; 328 | StringObservable.from(is).first().toBlocking().single(); 329 | assertEquals(1, numReads.get()); 330 | } 331 | 332 | @Test 333 | public void testFromReader() { 334 | final String inStr = "test"; 335 | String outStr = from(new StringReader(inStr)).toBlocking().single(); 336 | assertNotSame(inStr, outStr); 337 | assertEquals(inStr, outStr); 338 | } 339 | 340 | @Test 341 | public void testFromEmptyReader() { 342 | int count = from(new StringReader("")).count().toBlocking().single(); 343 | assertEquals(0, count); 344 | } 345 | 346 | @Test 347 | public void testFromReaderUsesBufferSize() { 348 | List list = from(new StringReader("test"), 2).toList().toBlocking().single(); 349 | assertEquals(2, list.size()); 350 | assertEquals("te", list.get(0)); 351 | assertEquals("st", list.get(1)); 352 | } 353 | 354 | @Test 355 | public void testFromReaderWillUnsubscribeBeforeCallingNextRead() { 356 | final byte[] inBytes = "test".getBytes(); 357 | final AtomicInteger numReads = new AtomicInteger(0); 358 | ByteArrayInputStream is = new ByteArrayInputStream(inBytes) { 359 | 360 | @Override 361 | public synchronized int read(byte[] b, int off, int len) { 362 | numReads.incrementAndGet(); 363 | return super.read(b, off, len); 364 | } 365 | }; 366 | StringObservable.from(new InputStreamReader(is)).first().toBlocking().single(); 367 | assertEquals(1, numReads.get()); 368 | } 369 | 370 | @Test 371 | public void testByLine_Newline() { 372 | String newLine = String.valueOf(new char[] { 0x0A }); 373 | testByLineGeneric(newLine); 374 | } 375 | 376 | @Test 377 | public void testByLine_CarriageNewline() { 378 | String newLine = String.valueOf(new char[] { 0x0D, 0x0A }); 379 | testByLineGeneric(newLine); 380 | } 381 | 382 | @Test 383 | public void testByLine_Carriage() { 384 | String newLine = String.valueOf(new char[] { 0x0D }); 385 | testByLineGeneric(newLine); 386 | } 387 | 388 | @Test 389 | public void testByLine_NextLine() { 390 | String newLine = String.valueOf(new char[] { 0x0085 }); 391 | testByLineGeneric(newLine); 392 | } 393 | 394 | @Test 395 | public void testByLine_LineSeparator() { 396 | String newLine = String.valueOf(new char[] { 0x2028 }); 397 | testByLineGeneric(newLine); 398 | } 399 | 400 | @Test 401 | public void testByLine_ParagraphSeparator() { 402 | String newLine = String.valueOf(new char[] { 0x2029 }); 403 | testByLineGeneric(newLine); 404 | } 405 | 406 | private void testByLineGeneric(String newLine) { 407 | List lines = byLine( 408 | Observable.from(Arrays.asList("qwer", newLine + "asdf" + newLine, "zx", "cv"))) 409 | .toList().toBlocking().single(); 410 | 411 | assertEquals(Arrays.asList("qwer", "asdf", "zxcv"), lines); 412 | } 413 | 414 | @Test 415 | public void testByCharacter() { 416 | List chars = byCharacter(Observable.from(Arrays.asList("foo", "bar"))).toList() 417 | .toBlocking().single(); 418 | 419 | assertEquals(Arrays.asList("f", "o", "o", "b", "a", "r"), chars); 420 | } 421 | 422 | @Test 423 | public void testUsingCloseOnComplete() throws IOException { 424 | final TestSubscriber subscriber = new TestSubscriber(); 425 | final Reader reader = spy(new StringReader("hello")); 426 | 427 | using(new UnsafeFunc0() { 428 | @Override 429 | public Reader call() throws Exception { 430 | return reader; 431 | } 432 | }, new Func1>() { 433 | @Override 434 | public Observable call(Reader reader) { 435 | return from(reader, 2); 436 | } 437 | }).subscribe(subscriber); 438 | 439 | assertArrayEquals(new String[] { "he", "ll", "o" }, subscriber.getOnNextEvents().toArray()); 440 | assertEquals(1, subscriber.getOnCompletedEvents().size()); 441 | assertEquals(0, subscriber.getOnErrorEvents().size()); 442 | 443 | verify(reader, times(1)).close(); 444 | } 445 | 446 | @Test 447 | public void testUsingCloseOnError() throws IOException { 448 | final TestSubscriber subscriber = new TestSubscriber(); 449 | final AtomicBoolean closed = new AtomicBoolean(); 450 | final Reader reader = new FilterReader(new StringReader("hello")) { 451 | @Override 452 | public int read(char[] cbuf) throws IOException { 453 | throw new IOException("boo"); 454 | } 455 | 456 | @Override 457 | public void close() throws IOException { 458 | closed.set(true); 459 | } 460 | }; 461 | 462 | using(new UnsafeFunc0() { 463 | @Override 464 | public Reader call() throws Exception { 465 | return reader; 466 | } 467 | }, new Func1>() { 468 | @Override 469 | public Observable call(Reader reader) { 470 | return from(reader, 2); 471 | } 472 | }).subscribe(subscriber); 473 | 474 | assertEquals(0, subscriber.getOnNextEvents().size()); 475 | assertEquals(0, subscriber.getOnCompletedEvents().size()); 476 | assertEquals(1, subscriber.getOnErrorEvents().size()); 477 | 478 | assertTrue(closed.get()); 479 | } 480 | 481 | @Test 482 | public void testUsingCloseOnUnsubscribe() throws IOException { 483 | final TestSubscriber subscriber = new TestSubscriber(); 484 | final Reader reader = spy(new StringReader("hello")); 485 | 486 | using(new UnsafeFunc0() { 487 | @Override 488 | public Reader call() throws Exception { 489 | return reader; 490 | } 491 | }, new Func1>() { 492 | @Override 493 | public Observable call(Reader reader) { 494 | return from(reader, 2); 495 | } 496 | }).take(1).subscribe(subscriber); 497 | 498 | assertArrayEquals(new String[] { "he" }, subscriber.getOnNextEvents().toArray()); 499 | assertEquals(1, subscriber.getOnNextEvents().size()); 500 | assertEquals(1, subscriber.getOnCompletedEvents().size()); 501 | assertEquals(0, subscriber.getOnErrorEvents().size()); 502 | 503 | verify(reader, times(1)).close(); 504 | } 505 | 506 | @Test(timeout=5000) 507 | public void testJoinDoesNotStallIssue23() { 508 | String s = StringObservable 509 | .join(just("a","b","c"),",") 510 | .toBlocking().single(); 511 | assertEquals("a,b,c", s); 512 | } 513 | 514 | @Test 515 | public void testJoinBackpressure() { 516 | TestSubscriber ts = new TestSubscriber(0); 517 | StringObservable 518 | .join(just("a","b","c"),",") 519 | .subscribe(ts); 520 | ts.assertNoValues(); 521 | ts.assertNoTerminalEvent(); 522 | ts.requestMore(1); 523 | ts.assertValues("a,b,c"); 524 | ts.assertCompleted(); 525 | } 526 | } 527 | --------------------------------------------------------------------------------