├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── CONTRIBUTING.md ├── Jenkinsfile ├── LICENSE ├── README.md ├── copyright-header.txt ├── pom.xml └── src ├── main └── java │ ├── module-info.java │ └── org │ └── eclipse │ └── jetty │ └── reactive │ └── client │ ├── ReactiveRequest.java │ ├── ReactiveResponse.java │ └── internal │ ├── AbstractBufferingProcessor.java │ ├── AbstractEventPublisher.java │ ├── AbstractSingleProcessor.java │ ├── AbstractSinglePublisher.java │ ├── AdapterRequestContent.java │ ├── ByteArrayBufferingProcessor.java │ ├── ByteBufferBufferingProcessor.java │ ├── DiscardingProcessor.java │ ├── PublisherContent.java │ ├── QueuedSinglePublisher.java │ ├── RequestEventPublisher.java │ ├── ResponseEventPublisher.java │ ├── ResponseListenerProcessor.java │ ├── ResultProcessor.java │ ├── StringBufferingProcessor.java │ ├── StringContent.java │ └── Transformer.java └── test ├── java └── org │ └── eclipse │ └── jetty │ └── reactive │ └── client │ ├── AbstractTest.java │ ├── MetricsTest.java │ ├── ReactiveTest.java │ ├── ReactorTest.java │ ├── RxJava2Test.java │ └── internal │ ├── QueuedSinglePublisherTCKTest.java │ ├── QueuedSinglePublisherTest.java │ ├── SingleProcessorTCKTest.java │ ├── SingleProcessorTest.java │ └── StringContentTCKTest.java └── resources └── jetty-logging.properties /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "maven" 4 | directory: "/" 5 | open-pull-requests-limit: 50 6 | target-branch: "1.1.x" 7 | commit-message: 8 | prefix: "[1.1.x] " 9 | schedule: 10 | interval: "daily" 11 | ignore: 12 | - dependency-name: "org.eclipse.jetty:*" 13 | versions: [ ">=10.0.0" ] 14 | - dependency-name: "org.springframework:*" 15 | versions: [ ">=6.0.0" ] 16 | - dependency-name: "org.testng:testng" 17 | versions: [ ">=7.6.0" ] 18 | - dependency-name: "org.apache.felix:maven-bundle-plugin" 19 | versions: [ ">=6.0.0" ] 20 | - dependency-name: "com.mycila:license-maven-plugin" 21 | versions: [ ">=5.0.0" ] 22 | 23 | - package-ecosystem: "maven" 24 | directory: "/" 25 | open-pull-requests-limit: 50 26 | target-branch: "2.0.x" 27 | commit-message: 28 | prefix: "[2.0.x] " 29 | schedule: 30 | interval: "daily" 31 | ignore: 32 | - dependency-name: "org.eclipse.jetty:*" 33 | versions: [ ">=11.0.0" ] 34 | - dependency-name: "org.springframework:*" 35 | versions: [ ">=6.0.0" ] 36 | - dependency-name: "org.apache.felix:maven-bundle-plugin" 37 | versions: [ ">=6.0.0" ] 38 | 39 | - package-ecosystem: "maven" 40 | directory: "/" 41 | open-pull-requests-limit: 50 42 | target-branch: "4.0.x" 43 | commit-message: 44 | prefix: "[4.0.x] " 45 | schedule: 46 | interval: "daily" 47 | ignore: 48 | - dependency-name: "org.eclipse.jetty:*" 49 | versions: [ ">=12.1.0" ] 50 | 51 | - package-ecosystem: "github-actions" 52 | directory: "/" 53 | schedule: 54 | interval: "daily" 55 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: GitHub CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | 8 | strategy: 9 | matrix: 10 | os: [ubuntu-latest] 11 | java: [17] 12 | fail-fast: false 13 | 14 | runs-on: ${{ matrix.os }} 15 | 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | - name: Set Java 20 | uses: actions/setup-java@v4 21 | with: 22 | distribution: 'temurin' 23 | java-version: ${{ matrix.java }} 24 | check-latest: true 25 | cache: 'maven' 26 | - name: Build with Maven 27 | run: mvn clean install -e -B -V 28 | 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing to the Jetty Reactive HttpClient Project ## 2 | 3 | Welcome ! 4 | 5 | To contribute code to the Jetty Reactive HttpClient Project, you need to comply with 6 | the following two requirements: 7 | 8 | * You must certify the [Developer Certificate of Origin](http://developercertificate.org/) 9 | and sign-off your `git` commits. 10 | By signing off your commits, you agree that you can certify 11 | what stated by the Developer Certificate of Origin. 12 | * You must license your contributed code under the 13 | [CometD project license](LICENSE.txt), i.e. the Apache 14 | License version 2.0. 15 | 16 | Complying with these two requirements is enough for the Jetty Reactive HttpClient Project 17 | to accept your contribution. 18 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | #!groovy 2 | 3 | pipeline { 4 | agent none 5 | // save some io during the build 6 | options { 7 | skipDefaultCheckout() 8 | durabilityHint('PERFORMANCE_OPTIMIZED') 9 | buildDiscarder logRotator( numToKeepStr: '30' ) 10 | disableRestartFromStage() 11 | } 12 | 13 | stages { 14 | stage("Parallel Stage") { 15 | parallel { 16 | stage("Build / Test / Javadoc - JDK17") { 17 | agent { node { label 'linux-light' } } 18 | steps { 19 | timeout( time: 180, unit: 'MINUTES' ) { 20 | checkout scm 21 | mavenBuild( "jdk17", "clean install -Dmaven.test.failure.ignore=true javadoc:javadoc -Djacoco.skip=true", "maven3", false) 22 | } 23 | } 24 | } 25 | stage("Build / Test / Javadoc - JDK21") { 26 | agent { node { label 'linux-light' } } 27 | steps { 28 | timeout( time: 180, unit: 'MINUTES' ) { 29 | checkout scm 30 | mavenBuild( "jdk21", "clean install -Dmaven.test.failure.ignore=true javadoc:javadoc", "maven3", true) 31 | } 32 | } 33 | } 34 | stage("Build / Test / Javadoc - JDK24") { 35 | agent { node { label 'linux-light' } } 36 | steps { 37 | timeout( time: 180, unit: 'MINUTES' ) { 38 | checkout scm 39 | mavenBuild( "jdk24", "clean install -Dmaven.test.failure.ignore=true javadoc:javadoc -Djacoco.skip=true", "maven3", false) 40 | } 41 | } 42 | } 43 | } 44 | } 45 | } 46 | } 47 | 48 | def mavenBuild(String jdk, String cmdline, String mvnName, boolean recordJacoco) { 49 | script { 50 | try { 51 | withEnv(["JAVA_HOME=${ tool "$jdk" }", 52 | "PATH+MAVEN=${ tool "$jdk" }/bin:${tool "$mvnName"}/bin", 53 | "MAVEN_OPTS=-Xms3g -Xmx3g -Djava.awt.headless=true -client -XX:+UnlockDiagnosticVMOptions -XX:GCLockerRetryAllocationCount=100"]) { 54 | configFileProvider( 55 | [configFile(fileId: 'oss-settings.xml', variable: 'GLOBAL_MVN_SETTINGS')]) { 56 | sh "mvn $cmdline -ntp -s $GLOBAL_MVN_SETTINGS -V -B -e -U" 57 | } 58 | } 59 | } 60 | finally 61 | { 62 | junit testResults: '**/target/surefire-reports/*.xml', allowEmptyResults: true 63 | if(recordJacoco) { 64 | // Collect the JaCoCo execution results. 65 | recordCoverage name: "Coverage ${jdk}", id: "coverage-${jdk}", tools: [[parser: 'JACOCO']], sourceCodeRetention: 'MODIFIED', 66 | sourceDirectories: [[path: 'src/main/java']] 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![GitHub CI](https://github.com/jetty-project/jetty-reactive-httpclient/workflows/GitHub%20CI/badge.svg) 2 | 3 | # Jetty ReactiveStream HttpClient 4 | 5 | A [ReactiveStreams](http://www.reactive-streams.org/) wrapper around [Jetty](https://eclipse.dev/jetty)'s [HttpClient](https://www.eclipse.dev/jetty/documentation/jetty-12/programming-guide/index.html#pg-client-http). 6 | 7 | ## Versions 8 | 9 | | Jetty ReactiveStream HttpClient Versions | Min Java Version | Jetty Version | Status | 10 | |------------------------------------------|------------------|---------------|--------------------------------------------| 11 | | `4.0.x` | Java 17 | Jetty 12.0.x | Stable | 12 | | `3.0.x` | Java 11 | Jetty 11.0.x | End of Community Support (see [#461](https://github.com/jetty-project/jetty-reactive-httpclient/issues/461)) | 13 | | `2.0.x` | Java 11 | Jetty 10.0.x | End of Community Support (see [#461](https://github.com/jetty-project/jetty-reactive-httpclient/issues/461)) | 14 | | `1.1.x` | Java 8 | Jetty 9.4.x | End of Community Support (see [#153](https://github.com/jetty-project/jetty-reactive-httpclient/issues/153)) | 15 | 16 | ## Usage 17 | 18 | ### Plain ReactiveStreams Usage 19 | 20 | ```java 21 | // Create and start Jetty's HttpClient. 22 | HttpClient httpClient = new HttpClient(); 23 | httpClient.start(); 24 | 25 | // Create a request using the HttpClient APIs. 26 | Request request = httpClient.newRequest("http://localhost:8080/path"); 27 | 28 | // Wrap the request using the API provided by this project. 29 | ReactiveRequest reactiveRequest = ReactiveRequest.newBuilder(request).build(); 30 | 31 | // Obtain a ReactiveStream Publisher for the response, discarding the response content. 32 | Publisher publisher = reactiveRequest.response(ReactiveResponse.Content.discard()); 33 | 34 | // Subscribe to the Publisher to send the request. 35 | publisher.subscribe(new Subscriber() { 36 | @Override 37 | public void onSubscribe(Subscription subscription) { 38 | // This is where the request is actually sent. 39 | subscription.request(1); 40 | } 41 | 42 | @Override 43 | public void onNext(ReactiveResponse response) { 44 | // Use the response 45 | } 46 | 47 | @Override 48 | public void onError(Throwable failure) { 49 | } 50 | 51 | @Override 52 | public void onComplete() { 53 | } 54 | }); 55 | ``` 56 | 57 | ### RxJava 3 Usage 58 | 59 | ```java 60 | // Create and start Jetty's HttpClient. 61 | HttpClient httpClient = new HttpClient(); 62 | httpClient.start(); 63 | 64 | // Create a request using the HttpClient APIs. 65 | Request request = httpClient.newRequest("http://localhost:8080/path"); 66 | 67 | // Wrap the request using the API provided by this project. 68 | ReactiveRequest reactiveRequest = ReactiveRequest.newBuilder(request).build(); 69 | 70 | // Obtain a ReactiveStreams Publisher for the response, discarding the response content. 71 | Publisher publisher = reactiveRequest.response(ReactiveResponse.Content.discard()); 72 | 73 | // Wrap the ReactiveStreams Publisher with RxJava. 74 | int status = Single.fromPublisher(publisher) 75 | .map(ReactiveResponse::getStatus) 76 | .blockingGet(); 77 | ``` 78 | 79 | ### Response Content Processing 80 | 81 | The response content is processed by passing a `BiFunction` to `ReactiveRequest.response()`. 82 | 83 | The `BiFunction` takes as parameters the `ReactiveResponse` and a `Publisher` for the response content, and must return a `Publisher` of items of type `T` that is the result of the response content processing. 84 | 85 | Built-in utility functions can be found in `ReactiveResponse.Content`. 86 | 87 | #### Example: discarding the response content 88 | 89 | ```java 90 | Publisher response = request.response(ReactiveResponse.Content.discard()); 91 | ``` 92 | 93 | #### Example: converting the response content to a String 94 | 95 | ```java 96 | Publisher string = request.response(ReactiveResponse.Content.asString()); 97 | ``` 98 | 99 | #### Example: discarding non 200 OK response content 100 | 101 | ```java 102 | Publisher> publisher = request.response((response, content) -> { 103 | if (response.getStatus() == HttpStatus.OK_200) { 104 | return ReactiveResponse.Content.asStringResult().apply(response, content); 105 | } else { 106 | return ReactiveResponse.Content.asDiscardResult().apply(response, content); 107 | } 108 | }); 109 | ``` 110 | 111 | Class `ReactiveResponse.Result` is a Java `record` that holds the response and the response content to allow application code to implement logic that uses both response information such as response status code and response headers, and response content information. 112 | 113 | Alternatively, you can write your own processing `BiFunction` using any ReactiveStreams library, such as RxJava 3 (which provides class `Flowable`): 114 | 115 | #### Example: discarding non 200 OK response content 116 | 117 | ```java 118 | Publisher publisher = reactiveRequest.response((reactiveResponse, contentPublisher) -> { 119 | if (reactiveResponse.getStatus() == HttpStatus.OK_200) { 120 | // Return the response content itself. 121 | return contentPublisher; 122 | } else { 123 | // Discard the response content. 124 | return Flowable.fromPublisher(contentPublisher) 125 | .filter(chunk -> { 126 | // Tell HttpClient that you are done with this chunk. 127 | chunk.release(); 128 | // Discard this chunk. 129 | return false; 130 | }); 131 | } 132 | }); 133 | ``` 134 | 135 | The response content (if any) can be further processed: 136 | 137 | ```java 138 | Single contentLength = Flowable.fromPublisher(publisher) 139 | .map(chunk -> { 140 | // Tell HttpClient that you are done with this chunk. 141 | chunk.release(); 142 | // Return the number of bytes of this chunk. 143 | return chunk.remaining(); 144 | }) 145 | // Sum the bytes of the chunks. 146 | .reduce(0L, Long::sum); 147 | ``` 148 | 149 | ### Providing Request Content 150 | 151 | Request content can be provided in a ReactiveStreams way, through the `ReactiveRequest.Content` class, which _is-a_ `Publisher` with the additional specification of the content length and the content type. 152 | 153 | Below you can find an example using the utility methods in `ReactiveRequest.Content` to create request content from a String: 154 | 155 | ```java 156 | HttpClient httpClient = ...; 157 | 158 | String text = "content"; 159 | ReactiveRequest request = ReactiveRequest.newBuilder(httpClient, "http://localhost:8080/path") 160 | .content(ReactiveRequest.Content.fromString(text, "text/plain", StandardCharsets.UTF_8)) 161 | .build(); 162 | ``` 163 | 164 | Below another example of creating request content from another `Publisher`: 165 | 166 | ```java 167 | HttpClient httpClient = ...; 168 | 169 | // The Publisher of request content. 170 | Publisher publisher = ...; 171 | 172 | // Transform items of type T into ByteBuffer chunks. 173 | Charset charset = StandardCharsets.UTF_8; 174 | Flowable chunks = Flowable.fromPublisher(publisher) 175 | .map((T t) -> toJSON(t)) 176 | .map((String json) -> json.getBytes(charset)) 177 | .map((byte[] bytes) -> ByteBuffer.wrap(bytes)) 178 | .map(byteBuffer -> Content.Chunk.from(byteBuffer, false)); 179 | 180 | ReactiveRequest request = ReactiveRequest.newBuilder(httpClient, "http://localhost:8080/path") 181 | .content(ReactiveRequest.Content.fromPublisher(chunks, "application/json", charset)) 182 | .build(); 183 | ``` 184 | 185 | ### Events 186 | 187 | If you are interested in the request and/or response events that are emitted by the Jetty HttpClient APIs, you can obtain a `Publisher` for request and/or response events, and subscribe a listener to be notified of the events. 188 | 189 | The event `Publisher`s are "hot" producers and do no buffer events. 190 | 191 | If you subscribe to an event `Publisher` after the events have started, the `Subscriber` will not be notified of events that already happened, and will be notified of any event that will happen. 192 | 193 | ```java 194 | HttpClient httpClient = ...; 195 | 196 | ReactiveRequest request = ReactiveRequest.newBuilder(httpClient, "http://localhost:8080/path").build(); 197 | Publisher requestEvents = request.requestEvents(); 198 | 199 | // Subscribe to the request events before sending the request. 200 | requestEvents.subscribe(new Subscriber() { 201 | ... 202 | }); 203 | 204 | // Similarly for response events. 205 | Publisher responseEvents = request.responseEvents(); 206 | 207 | // Subscribe to the response events before sending the request. 208 | responseEvents.subscribe(new Subscriber() { 209 | ... 210 | }); 211 | 212 | // Send the request. 213 | ReactiveResponse response = Single.fromPublisher(request.response(ReactiveResponse.Content.discard())) 214 | .blockingGet(); 215 | ``` 216 | -------------------------------------------------------------------------------- /copyright-header.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) ${project.inceptionYear} the original author or authors. 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 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 4.0.0 5 | org.eclipse.jetty 6 | jetty-reactive-httpclient 7 | 4.0.11-SNAPSHOT 8 | bundle 9 | Jetty ReactiveStreams HttpClient 10 | Jetty ReactiveStreams HttpClient 11 | 12 | https://github.com/jetty-project/jetty-reactive-httpclient 13 | 14 | 2017 15 | 16 | 17 | The Jetty Project 18 | https://eclipse.org/jetty 19 | 20 | 21 | 22 | 23 | Apache License Version 2.0 24 | http://www.apache.org/licenses/LICENSE-2.0 25 | 26 | 27 | 28 | 29 | 30 | sbordet 31 | Simone Bordet 32 | sbordet@webtide.com 33 | Webtide 34 | https://webtide.com 35 | 36 | 37 | 38 | 39 | scm:git:git://github.com/jetty-project/jetty-reactive-httpclient.git 40 | scm:git:ssh://git@github.com/jetty-project/jetty-reactive-httpclient.git 41 | HEAD 42 | https://github.com/jetty-project/jetty-reactive-httpclient 43 | 44 | 45 | 46 | 47 | sonatype-cp 48 | Central Portal 49 | https://repo.maven.apache.org/maven2 50 | 51 | 52 | sonatype-cp 53 | Central Portal 54 | https://central.sonatype.com/repository/maven-snapshots 55 | 56 | 57 | 58 | 59 | 12.0.21 60 | true 61 | 0.6.1 62 | UTF-8 63 | 1.0.4 64 | 3.1.10 65 | 2.0.17 66 | 6.2.7 67 | 68 | 69 | 70 | 71 | org.eclipse.jetty 72 | jetty-client 73 | ${jetty-version} 74 | 75 | 76 | org.reactivestreams 77 | reactive-streams 78 | ${reactivestreams-version} 79 | 80 | 81 | org.slf4j 82 | slf4j-api 83 | ${slf4j-version} 84 | 85 | 86 | 87 | io.reactivex.rxjava3 88 | rxjava 89 | ${rxjava-version} 90 | test 91 | 92 | 93 | org.awaitility 94 | awaitility 95 | 4.3.0 96 | test 97 | 98 | 99 | org.eclipse.jetty 100 | jetty-server 101 | ${jetty-version} 102 | test 103 | 104 | 105 | org.eclipse.jetty 106 | jetty-slf4j-impl 107 | ${jetty-version} 108 | test 109 | 110 | 111 | org.eclipse.jetty.http2 112 | jetty-http2-client-transport 113 | ${jetty-version} 114 | test 115 | 116 | 117 | org.eclipse.jetty.http2 118 | jetty-http2-server 119 | ${jetty-version} 120 | test 121 | 122 | 123 | org.eclipse.jetty.toolchain 124 | jetty-perf-helper 125 | 1.0.7 126 | test 127 | 128 | 129 | org.hdrhistogram 130 | HdrHistogram 131 | 2.2.2 132 | test 133 | 134 | 135 | org.junit.jupiter 136 | junit-jupiter 137 | 5.12.2 138 | test 139 | 140 | 141 | org.reactivestreams 142 | reactive-streams-tck 143 | ${reactivestreams-version} 144 | test 145 | 146 | 147 | org.springframework 148 | spring-webflux 149 | ${spring-version} 150 | test 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | com.diffplug.spotless 160 | spotless-maven-plugin 161 | 2.44.4 162 | 163 | 164 | 165 | pom.xml 166 | 167 | 168 | false 169 | 2 170 | recommended_2008_06 171 | scope,groupId,artifactId 172 | groupId,artifactId 173 | true 174 | false 175 | groupId,artifactId 176 | true 177 | true 178 | 179 | 180 | 181 | true 182 | 183 | 184 | 185 | 186 | com.mycila 187 | license-maven-plugin 188 | 5.0.0 189 | 190 | 191 | eu.maveniverse.maven.plugins 192 | njord 193 | ${njord.version} 194 | 195 | 196 | org.apache.felix 197 | maven-bundle-plugin 198 | 6.0.0 199 | 200 | 201 | maven-antrun-plugin 202 | 3.1.0 203 | 204 | 205 | maven-assembly-plugin 206 | 3.7.1 207 | 208 | 209 | maven-clean-plugin 210 | 3.4.1 211 | 212 | 213 | maven-compiler-plugin 214 | 3.14.0 215 | 216 | 17 217 | 17 218 | 17 219 | 220 | 221 | 222 | maven-dependency-plugin 223 | 3.8.1 224 | 225 | 226 | maven-deploy-plugin 227 | 3.1.4 228 | 229 | 230 | maven-enforcer-plugin 231 | 3.5.0 232 | 233 | 234 | maven-gpg-plugin 235 | 3.2.7 236 | 237 | 238 | maven-install-plugin 239 | 3.1.4 240 | 241 | 242 | maven-jar-plugin 243 | 3.4.2 244 | 245 | 246 | maven-javadoc-plugin 247 | 3.11.2 248 | 249 | 250 | maven-release-plugin 251 | 3.1.1 252 | 253 | deploy 254 | clean install 255 | forked-path 256 | true 257 | release 258 | @{project.version} 259 | 260 | 261 | 262 | maven-resources-plugin 263 | 3.3.1 264 | 265 | 266 | maven-site-plugin 267 | 3.21.0 268 | 269 | 270 | maven-source-plugin 271 | 3.3.1 272 | 273 | 274 | maven-surefire-plugin 275 | 3.5.3 276 | 277 | external 278 | 279 | 280 | 281 | org.jacoco 282 | jacoco-maven-plugin 283 | 0.8.13 284 | 285 | 286 | 287 | 288 | 289 | 290 | com.mycila 291 | license-maven-plugin 292 | 293 | 294 | 295 |
copyright-header.txt
296 | 297 | **/*.java 298 | 299 |
300 |
301 | true 302 | true 303 | 304 | ${project.inceptionYear}-2022 305 | 306 | 307 | SLASHSTAR_STYLE 308 | 309 |
310 | 311 | 312 | check-headers 313 | 314 | check 315 | 316 | validate 317 | 318 | 319 |
320 | 321 | org.apache.felix 322 | maven-bundle-plugin 323 | true 324 | 325 | 326 | ${project.groupId}.reactive.client 327 | 328 | 329 | 330 | 331 | maven-enforcer-plugin 332 | 333 | 334 | require-jdk17 335 | 336 | enforce 337 | 338 | 339 | 340 | 341 | [17,) 342 | 343 | 344 | 3.9.0 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | org.jacoco 353 | jacoco-maven-plugin 354 | 355 | 356 | jacoco-initialize 357 | 358 | prepare-agent 359 | 360 | initialize 361 | 362 | 363 | jacoco-report 364 | 365 | report 366 | 367 | package 368 | 369 | 370 | 371 |
372 | 373 | 374 | eu.maveniverse.maven.njord 375 | extension 376 | ${njord.version} 377 | 378 | 379 |
380 | 381 | 382 | 383 | release 384 | 385 | 386 | 387 | maven-gpg-plugin 388 | 389 | 390 | sign-artifacts 391 | 392 | sign 393 | 394 | verify 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 |
404 | -------------------------------------------------------------------------------- /src/main/java/module-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017 the original author or authors. 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 | module org.eclipse.jetty.reactive.client { 17 | exports org.eclipse.jetty.reactive.client; 18 | 19 | requires transitive org.eclipse.jetty.client; 20 | requires transitive org.reactivestreams; 21 | requires org.slf4j; 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/org/eclipse/jetty/reactive/client/ReactiveRequest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017 the original author or authors. 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 org.eclipse.jetty.reactive.client; 17 | 18 | import java.nio.ByteBuffer; 19 | import java.nio.charset.Charset; 20 | import java.util.function.BiFunction; 21 | 22 | import org.eclipse.jetty.client.HttpClient; 23 | import org.eclipse.jetty.client.Request; 24 | import org.eclipse.jetty.io.Content.Chunk; 25 | import org.eclipse.jetty.reactive.client.internal.AdapterRequestContent; 26 | import org.eclipse.jetty.reactive.client.internal.PublisherContent; 27 | import org.eclipse.jetty.reactive.client.internal.RequestEventPublisher; 28 | import org.eclipse.jetty.reactive.client.internal.ResponseEventPublisher; 29 | import org.eclipse.jetty.reactive.client.internal.ResponseListenerProcessor; 30 | import org.eclipse.jetty.reactive.client.internal.StringContent; 31 | import org.reactivestreams.Publisher; 32 | import org.reactivestreams.Subscription; 33 | 34 | /** 35 | *

A reactive wrapper over Jetty's {@code HttpClient} {@link Request}.

36 | *

A ReactiveRequest can be obtained via a builder:

37 | *
 38 |  * // Built with HttpClient and a string URI.
 39 |  * ReactiveRequest request = ReactiveRequest.newBuilder(httpClient, uri()).build();
 40 |  *
 41 |  * // Built by wrapping a Request.
 42 |  * Request req = httpClient.newRequest(...);
 43 |  * ...
 44 |  * ReactiveRequest request = ReactiveRequest.newBuilder(req).build();
 45 |  * 
46 | *

Once created, a ReactiveRequest can be sent to obtain a {@link Publisher} 47 | * for a {@link ReactiveResponse} passing a function that handles the response 48 | * content:

49 | *
 50 |  * Publisher<T> response = request.response((response, content) -> { ... });
 51 |  * 
52 | */ 53 | public class ReactiveRequest { 54 | /** 55 | * @param httpClient the HttpClient instance 56 | * @param uri the target URI for the request - must be properly encoded already 57 | * @return a builder for a GET request for the given URI 58 | */ 59 | public static ReactiveRequest.Builder newBuilder(HttpClient httpClient, String uri) { 60 | return new Builder(httpClient, uri); 61 | } 62 | 63 | /** 64 | * @param request the request instance 65 | * @return a builder for the given Request 66 | */ 67 | public static ReactiveRequest.Builder newBuilder(Request request) { 68 | return new Builder(request); 69 | } 70 | 71 | private final RequestEventPublisher requestEvents = new RequestEventPublisher(this); 72 | private final ResponseEventPublisher responseEvents = new ResponseEventPublisher(this); 73 | private final Request request; 74 | private final boolean abortOnCancel; 75 | private volatile ReactiveResponse response; 76 | 77 | protected ReactiveRequest(Request request) { 78 | this(request, false); 79 | } 80 | 81 | private ReactiveRequest(Request request, boolean abortOnCancel) { 82 | this.request = request.listener(requestEvents) 83 | .onResponseBegin(r -> { 84 | this.response = new ReactiveResponse(this, r); 85 | }) 86 | .onResponseBegin(responseEvents) 87 | .onResponseHeaders(responseEvents) 88 | .onResponseContentSource(responseEvents) 89 | .onResponseSuccess(responseEvents) 90 | .onResponseFailure(responseEvents) 91 | .onComplete(responseEvents); 92 | this.abortOnCancel = abortOnCancel; 93 | } 94 | 95 | /** 96 | * @return the ReactiveResponse correspondent to this request, 97 | * or null if the response is not available yet 98 | */ 99 | public ReactiveResponse getReactiveResponse() { 100 | return response; 101 | } 102 | 103 | /** 104 | * @return the wrapped Jetty request 105 | */ 106 | public Request getRequest() { 107 | return request; 108 | } 109 | 110 | /** 111 | *

Creates a Publisher that sends the request when a Subscriber requests the response 112 | * via {@link Subscription#request(long)}, discarding the response content.

113 | * 114 | * @return a Publisher for the response 115 | */ 116 | public Publisher response() { 117 | return response(ReactiveResponse.Content.discard()); 118 | } 119 | 120 | /** 121 | *

Creates a Publisher that sends the request when a Subscriber requests the response 122 | * via {@link Subscription#request(long)}, processing the response content with the given 123 | * {@code BiFunction}.

124 | *

The given {@code BiFunction} is called by the implementation when the response arrives, 125 | * with the {@link ReactiveResponse} and the response content Publisher as parameters.

126 | *

Applications must subscribe (possibly asynchronously) to the response content Publisher, 127 | * even if it is known that the response has no content, to receive the response success/failure 128 | * events.

129 | *

The response content Publisher emits {@link Chunk} objects that must be eventually 130 | * released (possibly asynchronously at a later time) by calling {@link Chunk#release()}.

131 | * 132 | * @param contentFn the function that processes the response content 133 | * @param the element type of the processed response content 134 | * @return a Publisher for the processed content 135 | */ 136 | public Publisher response(BiFunction, Publisher> contentFn) { 137 | return new ResponseListenerProcessor<>(this, contentFn, abortOnCancel); 138 | } 139 | 140 | /** 141 | * @return a Publisher for request events 142 | */ 143 | public Publisher requestEvents() { 144 | return requestEvents; 145 | } 146 | 147 | public Publisher responseEvents() { 148 | return responseEvents; 149 | } 150 | 151 | @Override 152 | public String toString() { 153 | return String.format("Reactive[%s]", request); 154 | } 155 | 156 | /** 157 | * A Builder for ReactiveRequest. 158 | */ 159 | public static class Builder { 160 | private final Request request; 161 | private boolean abortOnCancel; 162 | 163 | public Builder(HttpClient client, String uri) { 164 | this(client.newRequest(uri)); 165 | } 166 | 167 | public Builder(Request request) { 168 | this.request = request; 169 | } 170 | 171 | /** 172 | *

Provides the request content via a Publisher.

173 | * 174 | * @param content the request content 175 | * @return this instance 176 | */ 177 | public Builder content(Content content) { 178 | request.body(new AdapterRequestContent(content)); 179 | return this; 180 | } 181 | 182 | /** 183 | * @param abortOnCancel whether a request should be aborted when the 184 | * content subscriber cancels the subscription 185 | * @return this instance 186 | */ 187 | public Builder abortOnCancel(boolean abortOnCancel) { 188 | this.abortOnCancel = abortOnCancel; 189 | return this; 190 | } 191 | 192 | /** 193 | * @return a built ReactiveRequest 194 | */ 195 | public ReactiveRequest build() { 196 | return new ReactiveRequest(request, abortOnCancel); 197 | } 198 | } 199 | 200 | /** 201 | * A ReactiveRequest event. 202 | */ 203 | public static class Event { 204 | private final Type type; 205 | private final ReactiveRequest request; 206 | private final ByteBuffer content; 207 | private final Throwable failure; 208 | 209 | public Event(Type type, ReactiveRequest request) { 210 | this(type, request, null, null); 211 | } 212 | 213 | public Event(Type type, ReactiveRequest request, ByteBuffer content) { 214 | this(type, request, content, null); 215 | } 216 | 217 | public Event(Type type, ReactiveRequest request, Throwable failure) { 218 | this(type, request, null, failure); 219 | } 220 | 221 | private Event(Type type, ReactiveRequest request, ByteBuffer content, Throwable failure) { 222 | this.type = type; 223 | this.request = request; 224 | this.content = content; 225 | this.failure = failure; 226 | } 227 | 228 | /** 229 | * @return the event type 230 | */ 231 | public Type getType() { 232 | return type; 233 | } 234 | 235 | /** 236 | * @return the request that generated this event 237 | */ 238 | public ReactiveRequest getRequest() { 239 | return request; 240 | } 241 | 242 | /** 243 | * @return the event content, or null if this is not a content event 244 | */ 245 | public ByteBuffer getContent() { 246 | return content; 247 | } 248 | 249 | /** 250 | * @return the event failure, or null if this is not a failure event 251 | */ 252 | public Throwable getFailure() { 253 | return failure; 254 | } 255 | 256 | /** 257 | * The event types 258 | */ 259 | public enum Type { 260 | /** 261 | * The request has been queued 262 | */ 263 | QUEUED, 264 | /** 265 | * The request is ready to be sent 266 | */ 267 | BEGIN, 268 | /** 269 | * The request headers have been prepared 270 | */ 271 | HEADERS, 272 | /** 273 | * The request headers have been sent 274 | */ 275 | COMMIT, 276 | /** 277 | * A chunk of content has been sent 278 | */ 279 | CONTENT, 280 | /** 281 | * The request succeeded 282 | */ 283 | SUCCESS, 284 | /** 285 | * The request failed 286 | */ 287 | FAILURE 288 | } 289 | } 290 | 291 | /** 292 | * A Publisher of content chunks that also specifies the content length and type. 293 | */ 294 | public interface Content extends Publisher { 295 | /** 296 | * @return the content length 297 | */ 298 | public long getLength(); 299 | 300 | /** 301 | * @return the content type in the form {@code media_type[;charset=]} 302 | */ 303 | public String getContentType(); 304 | 305 | /** 306 | *

Rewinds this content, if possible.

307 | * 308 | * @return whether this request content was rewound 309 | */ 310 | public default boolean rewind() { 311 | return false; 312 | } 313 | 314 | public static Content fromString(String string, String mediaType, Charset charset) { 315 | return new StringContent(string, mediaType, charset); 316 | } 317 | 318 | /** 319 | *

Creates a Content from the given Publisher of {@link Chunk}s.

320 | *

The implementation will call {@link Chunk#release()} on each {@link Chunk}.

321 | * 322 | * @param publisher the request content {@link Chunk}s 323 | * @param contentType the request content type 324 | * @return a Content wrapping the given {@link Chunk}s 325 | */ 326 | public static Content fromPublisher(Publisher publisher, String contentType) { 327 | return new PublisherContent(publisher, contentType); 328 | } 329 | 330 | /** 331 | *

Creates a Content from the given Publisher of {@link Chunk}s.

332 | *

The implementation will call {@link Chunk#release()} on each {@link Chunk}.

333 | * 334 | * @param publisher the request content {@link Chunk}s 335 | * @param mediaType the request content media type 336 | * @param charset the request content charset 337 | * @return a Content wrapping the given {@link Chunk}s 338 | */ 339 | public static Content fromPublisher(Publisher publisher, String mediaType, Charset charset) { 340 | return fromPublisher(publisher, mediaType + ";charset=" + charset.name()); 341 | } 342 | } 343 | } 344 | -------------------------------------------------------------------------------- /src/main/java/org/eclipse/jetty/reactive/client/ReactiveResponse.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017 the original author or authors. 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 org.eclipse.jetty.reactive.client; 17 | 18 | import java.nio.ByteBuffer; 19 | import java.util.Locale; 20 | import java.util.function.BiFunction; 21 | import org.eclipse.jetty.client.Response; 22 | import org.eclipse.jetty.http.HttpFields; 23 | import org.eclipse.jetty.http.HttpHeader; 24 | import org.eclipse.jetty.io.Content.Chunk; 25 | import org.eclipse.jetty.reactive.client.internal.ByteArrayBufferingProcessor; 26 | import org.eclipse.jetty.reactive.client.internal.ByteBufferBufferingProcessor; 27 | import org.eclipse.jetty.reactive.client.internal.DiscardingProcessor; 28 | import org.eclipse.jetty.reactive.client.internal.ResultProcessor; 29 | import org.eclipse.jetty.reactive.client.internal.StringBufferingProcessor; 30 | import org.eclipse.jetty.reactive.client.internal.Transformer; 31 | import org.reactivestreams.Publisher; 32 | 33 | /** 34 | *

A reactive wrapper over Jetty's {@code HttpClient} {@link Response}.

35 | *

A ReactiveResponse is available as soon as the response headers arrived 36 | * to the client. The response content is processed by a response content 37 | * function specified in {@link ReactiveRequest#response(BiFunction)}.

38 | */ 39 | public class ReactiveResponse { 40 | private final ReactiveRequest request; 41 | private final Response response; 42 | private String mediaType; 43 | private String encoding; 44 | 45 | public ReactiveResponse(ReactiveRequest request, Response response) { 46 | this.request = request; 47 | this.response = response; 48 | } 49 | 50 | /** 51 | * @return the ReactiveRequest correspondent to this response 52 | */ 53 | public ReactiveRequest getReactiveRequest() { 54 | return request; 55 | } 56 | 57 | /** 58 | * @return the wrapped Jetty response 59 | */ 60 | public Response getResponse() { 61 | return response; 62 | } 63 | 64 | /** 65 | * @return the HTTP status code 66 | */ 67 | public int getStatus() { 68 | return response.getStatus(); 69 | } 70 | 71 | /** 72 | * @return the HTTP response headers 73 | */ 74 | public HttpFields getHeaders() { 75 | return response.getHeaders(); 76 | } 77 | 78 | /** 79 | * @return the media type specified by the {@code Content-Type} header 80 | * @see #getEncoding() 81 | */ 82 | public String getMediaType() { 83 | resolveContentType(); 84 | return mediaType.isEmpty() ? null : mediaType; 85 | } 86 | 87 | /** 88 | * @return the encoding specified by the {@code Content-Type} header 89 | * @see #getMediaType() 90 | */ 91 | public String getEncoding() { 92 | resolveContentType(); 93 | return encoding.isEmpty() ? null : encoding; 94 | } 95 | 96 | private void resolveContentType() { 97 | if (mediaType == null) { 98 | String contentType = getHeaders().get(HttpHeader.CONTENT_TYPE); 99 | if (contentType != null) { 100 | String media = contentType; 101 | String charset = "charset="; 102 | int index = contentType.toLowerCase(Locale.ENGLISH).indexOf(charset); 103 | if (index > 0) { 104 | media = contentType.substring(0, index); 105 | String encoding = contentType.substring(index + charset.length()); 106 | // Sometimes charsets arrive with an ending semicolon 107 | int semicolon = encoding.indexOf(';'); 108 | if (semicolon > 0) { 109 | encoding = encoding.substring(0, semicolon).trim(); 110 | } 111 | this.encoding = encoding; 112 | } else { 113 | this.encoding = ""; 114 | } 115 | int semicolon = media.indexOf(';'); 116 | if (semicolon > 0) { 117 | media = media.substring(0, semicolon).trim(); 118 | } 119 | this.mediaType = media; 120 | } else { 121 | this.mediaType = ""; 122 | this.encoding = ""; 123 | } 124 | } 125 | } 126 | 127 | @Override 128 | public String toString() { 129 | return String.format("Reactive[%s]", response); 130 | } 131 | 132 | /** 133 | * Collects utility methods to process response content. 134 | */ 135 | public static class Content { 136 | /** 137 | * @return a response content processing function that discards the content 138 | */ 139 | public static BiFunction, Publisher> discard() { 140 | return (response, content) -> { 141 | DiscardingProcessor result = new DiscardingProcessor(response); 142 | content.subscribe(result); 143 | return result; 144 | }; 145 | } 146 | 147 | /** 148 | * @return a response content processing function that converts the content to a string 149 | * up to {@value StringBufferingProcessor#DEFAULT_MAX_CAPACITY} bytes. 150 | */ 151 | public static BiFunction, Publisher> asString() { 152 | return asString(StringBufferingProcessor.DEFAULT_MAX_CAPACITY); 153 | } 154 | 155 | /** 156 | * @return a response content processing function that converts the content to a string 157 | * up to the specified maximum capacity in bytes. 158 | */ 159 | public static BiFunction, Publisher> asString(int maxCapacity) { 160 | return (response, content) -> { 161 | StringBufferingProcessor result = new StringBufferingProcessor(response, maxCapacity); 162 | content.subscribe(result); 163 | return result; 164 | }; 165 | } 166 | 167 | /** 168 | * @return a response content processing function that converts the content to a {@link ByteBuffer} 169 | * up to {@value ByteBufferBufferingProcessor#DEFAULT_MAX_CAPACITY} bytes. 170 | */ 171 | public static BiFunction, Publisher> asByteBuffer() { 172 | return asByteBuffer(ByteBufferBufferingProcessor.DEFAULT_MAX_CAPACITY); 173 | } 174 | 175 | /** 176 | * @return a response content processing function that converts the content to a {@link ByteBuffer} 177 | * up to the specified maximum capacity in bytes. 178 | */ 179 | public static BiFunction, Publisher> asByteBuffer(int maxCapacity) { 180 | return (response, content) -> { 181 | ByteBufferBufferingProcessor result = new ByteBufferBufferingProcessor(response, maxCapacity); 182 | content.subscribe(result); 183 | return result; 184 | }; 185 | } 186 | 187 | /** 188 | * @return a response content processing function that converts the content to a {@code byte[]} 189 | * up to {@value ByteArrayBufferingProcessor#DEFAULT_MAX_CAPACITY} bytes. 190 | */ 191 | public static BiFunction, Publisher> asByteArray() { 192 | return asByteArray(ByteArrayBufferingProcessor.DEFAULT_MAX_CAPACITY); 193 | } 194 | 195 | /** 196 | * @return a response content processing function that converts the content to a {@code byte[]} 197 | * up to the specified maximum capacity in bytes. 198 | */ 199 | public static BiFunction, Publisher> asByteArray(int maxCapacity) { 200 | return (response, content) -> { 201 | ByteArrayBufferingProcessor result = new ByteArrayBufferingProcessor(response, maxCapacity); 202 | content.subscribe(result); 203 | return result; 204 | }; 205 | } 206 | 207 | /** 208 | * @return a response content processing function that discards the content 209 | * and produces a {@link Result} with a {@code null} content of the given type. 210 | * 211 | * @param the type of the content 212 | */ 213 | public static BiFunction, Publisher>> asDiscardResult() { 214 | return (response, content) -> { 215 | ResultProcessor resultProcessor = new ResultProcessor<>(response); 216 | discard().apply(response, content).subscribe(resultProcessor); 217 | Transformer, Result> transformer = new Transformer<>(r -> null); 218 | resultProcessor.subscribe(transformer); 219 | return transformer; 220 | }; 221 | } 222 | 223 | /** 224 | * @return a response content processing function that converts the content to a string 225 | * up to {@value StringBufferingProcessor#DEFAULT_MAX_CAPACITY} bytes, 226 | * and produces a {@link Result} with the string content. 227 | */ 228 | public static BiFunction, Publisher>> asStringResult() { 229 | return asStringResult(StringBufferingProcessor.DEFAULT_MAX_CAPACITY); 230 | } 231 | 232 | /** 233 | * @return a response content processing function that converts the content to a string 234 | * up to the specified maximum capacity in bytes, 235 | * and produces a {@link Result} with the string content. 236 | */ 237 | public static BiFunction, Publisher>> asStringResult(int maxCapacity) { 238 | return (response, content) -> { 239 | ResultProcessor resultProcessor = new ResultProcessor<>(response); 240 | asString(maxCapacity).apply(response, content).subscribe(resultProcessor); 241 | return resultProcessor; 242 | }; 243 | } 244 | 245 | /** 246 | * @return a response content processing function that converts the content to a {@link ByteBuffer} 247 | * up to {@value ByteBufferBufferingProcessor#DEFAULT_MAX_CAPACITY} bytes, 248 | * and produces a {@link Result} with the {@link ByteBuffer} content. 249 | */ 250 | public static BiFunction, Publisher>> asByteBufferResult() { 251 | return asByteBufferResult(ByteBufferBufferingProcessor.DEFAULT_MAX_CAPACITY); 252 | } 253 | 254 | /** 255 | * @return a response content processing function that converts the content to a {@link ByteBuffer} 256 | * up to the specified maximum capacity in bytes, 257 | * and produces a {@link Result} with the {@link ByteBuffer} content. 258 | */ 259 | public static BiFunction, Publisher>> asByteBufferResult(int maxCapacity) { 260 | return (response, content) -> { 261 | ResultProcessor resultProcessor = new ResultProcessor<>(response); 262 | asByteBuffer(maxCapacity).apply(response, content).subscribe(resultProcessor); 263 | return resultProcessor; 264 | }; 265 | } 266 | 267 | /** 268 | * @return a response content processing function that converts the content to a {@code byte[]} 269 | * up to {@value ByteArrayBufferingProcessor#DEFAULT_MAX_CAPACITY} bytes, 270 | * and produces a {@link Result} with the {@code byte[]} content. 271 | */ 272 | public static BiFunction, Publisher>> asByteArrayResult() { 273 | return asByteArrayResult(ByteArrayBufferingProcessor.DEFAULT_MAX_CAPACITY); 274 | } 275 | 276 | /** 277 | * @return a response content processing function that converts the content to a {@code byte[]} 278 | * up to the specified maximum capacity in bytes, 279 | * and produces a {@link Result} with the {@code byte[]} content. 280 | */ 281 | public static BiFunction, Publisher>> asByteArrayResult(int maxCapacity) { 282 | return (response, content) -> { 283 | ResultProcessor resultProcessor = new ResultProcessor<>(response); 284 | asByteArray(maxCapacity).apply(response, content).subscribe(resultProcessor); 285 | return resultProcessor; 286 | }; 287 | } 288 | } 289 | 290 | /** 291 | *

A record holding the {@link ReactiveResponse} and the response content.

292 | * 293 | * @param response the response 294 | * @param content the response content 295 | * @param the type of the response content 296 | */ 297 | public record Result(ReactiveResponse response, T content) { 298 | } 299 | 300 | public static class Event { 301 | private final Type type; 302 | private final ReactiveResponse response; 303 | private final ByteBuffer content; 304 | private final Throwable failure; 305 | 306 | public Event(Type type, ReactiveResponse response) { 307 | this(type, response, null, null); 308 | } 309 | 310 | public Event(Type type, ReactiveResponse response, ByteBuffer content) { 311 | this(type, response, content, null); 312 | } 313 | 314 | public Event(Type type, ReactiveResponse response, Throwable failure) { 315 | this(type, response, null, failure); 316 | } 317 | 318 | private Event(Type type, ReactiveResponse response, ByteBuffer content, Throwable failure) { 319 | this.type = type; 320 | this.response = response; 321 | this.content = content; 322 | this.failure = failure; 323 | } 324 | 325 | /** 326 | * @return the event type 327 | */ 328 | public Type getType() { 329 | return type; 330 | } 331 | 332 | /** 333 | * @return the response that generated this event 334 | */ 335 | public ReactiveResponse getResponse() { 336 | return response; 337 | } 338 | 339 | /** 340 | * @return the event content, or null if this is not a content event 341 | */ 342 | public ByteBuffer getContent() { 343 | return content; 344 | } 345 | 346 | /** 347 | * @return the event failure, or null if this is not a failure event 348 | */ 349 | public Throwable getFailure() { 350 | return failure; 351 | } 352 | 353 | public enum Type { 354 | BEGIN, 355 | HEADERS, 356 | CONTENT, 357 | SUCCESS, 358 | FAILURE, 359 | COMPLETE 360 | } 361 | } 362 | } 363 | -------------------------------------------------------------------------------- /src/main/java/org/eclipse/jetty/reactive/client/internal/AbstractBufferingProcessor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017 the original author or authors. 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 org.eclipse.jetty.reactive.client.internal; 17 | 18 | import java.util.ArrayList; 19 | import java.util.List; 20 | import org.eclipse.jetty.io.Content; 21 | import org.eclipse.jetty.reactive.client.ReactiveResponse; 22 | 23 | /** 24 | *

A {@link org.reactivestreams.Processor} that buffers {@link Content.Chunk}s 25 | * up to a max capacity.

26 | *

When the {@code complete} event is received from upstream, {@link #process(List)} 27 | * is invoked to processes the chunks and produce a single item of type {@code T}, 28 | * that is then published to downstream.

29 | * 30 | * @param the type of the item resulted from processing the chunks 31 | */ 32 | public abstract class AbstractBufferingProcessor extends AbstractSingleProcessor { 33 | public static final int DEFAULT_MAX_CAPACITY = 2 * 1024 * 1024; 34 | 35 | private final List chunks = new ArrayList<>(); 36 | private final ReactiveResponse response; 37 | private final int maxCapacity; 38 | private int capacity; 39 | 40 | public AbstractBufferingProcessor(ReactiveResponse response, int maxCapacity) { 41 | this.response = response; 42 | this.maxCapacity = maxCapacity; 43 | } 44 | 45 | public ReactiveResponse getResponse() { 46 | return response; 47 | } 48 | 49 | @Override 50 | public void onNext(Content.Chunk chunk) { 51 | capacity += chunk.remaining(); 52 | if ((maxCapacity > 0 && capacity > maxCapacity) || capacity < 0) { 53 | upStreamCancel(); 54 | onError(new IllegalStateException("buffering capacity %d exceeded".formatted(maxCapacity))); 55 | return; 56 | } 57 | chunks.add(chunk); 58 | upStreamRequest(1); 59 | } 60 | 61 | @Override 62 | public void onError(Throwable throwable) { 63 | chunks.forEach(Content.Chunk::release); 64 | super.onError(throwable); 65 | } 66 | 67 | @Override 68 | public void onComplete() { 69 | T result = process(chunks); 70 | chunks.clear(); 71 | downStreamOnNext(result); 72 | super.onComplete(); 73 | } 74 | 75 | @Override 76 | public void cancel() { 77 | chunks.forEach(Content.Chunk::release); 78 | super.cancel(); 79 | } 80 | 81 | protected abstract T process(List chunks); 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/org/eclipse/jetty/reactive/client/internal/AbstractEventPublisher.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017 the original author or authors. 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 org.eclipse.jetty.reactive.client.internal; 17 | 18 | import org.eclipse.jetty.util.MathUtils; 19 | import org.eclipse.jetty.util.thread.AutoLock; 20 | import org.reactivestreams.Subscriber; 21 | 22 | public abstract class AbstractEventPublisher extends AbstractSinglePublisher { 23 | private long demand; 24 | private boolean initial; 25 | private boolean terminated; 26 | private Throwable failure; 27 | 28 | @Override 29 | protected void onRequest(Subscriber subscriber, long n) { 30 | boolean notify = false; 31 | Throwable failure = null; 32 | try (AutoLock ignored = lock()) { 33 | demand = MathUtils.cappedAdd(demand, n); 34 | boolean isInitial = initial; 35 | initial = false; 36 | if (isInitial && terminated) { 37 | notify = true; 38 | failure = this.failure; 39 | } 40 | } 41 | if (notify) { 42 | if (failure == null) { 43 | emitOnComplete(subscriber); 44 | } else { 45 | emitOnError(subscriber, failure); 46 | } 47 | } 48 | } 49 | 50 | protected void emit(T event) { 51 | Subscriber subscriber = null; 52 | try (AutoLock ignored = lock()) { 53 | if (demand > 0) { 54 | --demand; 55 | subscriber = subscriber(); 56 | } 57 | } 58 | if (subscriber != null) { 59 | emitOnNext(subscriber, event); 60 | } 61 | } 62 | 63 | protected void succeed() { 64 | Subscriber subscriber; 65 | try (AutoLock ignored = lock()) { 66 | terminated = true; 67 | subscriber = subscriber(); 68 | } 69 | if (subscriber != null) { 70 | emitOnComplete(subscriber); 71 | } 72 | } 73 | 74 | protected void fail(Throwable failure) { 75 | Subscriber subscriber; 76 | try (AutoLock ignored = lock()) { 77 | terminated = true; 78 | this.failure = failure; 79 | subscriber = subscriber(); 80 | } 81 | if (subscriber != null) { 82 | emitOnError(subscriber, failure); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/org/eclipse/jetty/reactive/client/internal/AbstractSingleProcessor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017 the original author or authors. 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 org.eclipse.jetty.reactive.client.internal; 17 | 18 | import java.util.Objects; 19 | import org.eclipse.jetty.util.MathUtils; 20 | import org.eclipse.jetty.util.thread.AutoLock; 21 | import org.reactivestreams.Processor; 22 | import org.reactivestreams.Publisher; 23 | import org.reactivestreams.Subscriber; 24 | import org.reactivestreams.Subscription; 25 | 26 | /** 27 | *

A {@link Processor} that allows a single {@link Subscriber} at a time, 28 | * and can subscribe to only one {@link Publisher} at a time.

29 | *

The implementation acts as a {@link Subscriber} to upstream input, 30 | * and acts as a {@link Publisher} for the downstream output.

31 | *

Subclasses implement the transformation of the input elements into the 32 | * output elements by overriding {@link #onNext(Object)}.

33 | * 34 | * @param the type of the input elements 35 | * @param the type of the output elements 36 | */ 37 | public abstract class AbstractSingleProcessor extends AbstractSinglePublisher implements Processor { 38 | private Subscription upStream; 39 | private long demand; 40 | 41 | protected Subscriber downStream() { 42 | return subscriber(); 43 | } 44 | 45 | @Override 46 | protected void onFailure(Subscriber subscriber, Throwable failure) { 47 | upStreamCancel(); 48 | super.onFailure(subscriber, failure); 49 | } 50 | 51 | @Override 52 | public void cancel() { 53 | upStreamCancel(); 54 | super.cancel(); 55 | } 56 | 57 | protected void upStreamCancel() { 58 | Subscription upStream; 59 | try (AutoLock ignored = lock()) { 60 | upStream = this.upStream; 61 | // Null-out the field to allow re-subscriptions. 62 | this.upStream = null; 63 | } 64 | if (upStream != null) { 65 | upStream.cancel(); 66 | } 67 | } 68 | 69 | @Override 70 | protected void onRequest(Subscriber subscriber, long n) { 71 | long demand; 72 | Subscription upStream; 73 | try (AutoLock ignored = lock()) { 74 | demand = MathUtils.cappedAdd(this.demand, n); 75 | upStream = this.upStream; 76 | // If there is not upStream yet, store the demand. 77 | this.demand = upStream == null ? demand : 0; 78 | } 79 | upStreamRequest(upStream, demand); 80 | } 81 | 82 | protected void upStreamRequest(long n) { 83 | upStreamRequest(upStream(), n); 84 | } 85 | 86 | private void upStreamRequest(Subscription upStream, long demand) { 87 | if (upStream != null) { 88 | upStream.request(demand); 89 | } 90 | } 91 | 92 | @Override 93 | public void onSubscribe(Subscription subscription) { 94 | Objects.requireNonNull(subscription, "invalid 'null' subscription"); 95 | long demand = 0; 96 | Throwable failure = null; 97 | try (AutoLock ignored = lock()) { 98 | if (this.upStream != null) { 99 | failure = new IllegalStateException("multiple subscriptions not supported"); 100 | } else { 101 | this.upStream = subscription; 102 | // The demand stored so far will be forwarded upstream. 103 | demand = this.demand; 104 | this.demand = 0; 105 | } 106 | } 107 | if (failure != null) { 108 | subscription.cancel(); 109 | downStreamOnError(failure); 110 | } else if (demand > 0) { 111 | // Forward upstream any previously stored demand. 112 | subscription.request(demand); 113 | } 114 | } 115 | 116 | private Subscription upStream() { 117 | try (AutoLock ignored = lock()) { 118 | return upStream; 119 | } 120 | } 121 | 122 | protected void downStreamOnNext(O item) { 123 | Subscriber downStream = downStream(); 124 | if (downStream != null) { 125 | emitOnNext(downStream, item); 126 | } 127 | } 128 | 129 | @Override 130 | public void onError(Throwable throwable) { 131 | // This method is called by the upStream 132 | // publisher, forward to downStream. 133 | downStreamOnError(throwable); 134 | } 135 | 136 | private void downStreamOnError(Throwable throwable) { 137 | Subscriber downStream = downStream(); 138 | if (downStream != null) { 139 | emitOnError(downStream, throwable); 140 | } 141 | } 142 | 143 | @Override 144 | public void onComplete() { 145 | // This method is called by the upStream 146 | // publisher, forward to downStream. 147 | downStreamOnComplete(); 148 | } 149 | 150 | private void downStreamOnComplete() { 151 | Subscriber downStream = downStream(); 152 | if (downStream != null) { 153 | emitOnComplete(downStream); 154 | } 155 | } 156 | 157 | @Override 158 | public String toString() { 159 | return String.format("%s@%x", getClass().getSimpleName(), hashCode()); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/main/java/org/eclipse/jetty/reactive/client/internal/AbstractSinglePublisher.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017 the original author or authors. 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 org.eclipse.jetty.reactive.client.internal; 17 | 18 | import java.util.Objects; 19 | 20 | import org.eclipse.jetty.util.thread.AutoLock; 21 | import org.reactivestreams.Publisher; 22 | import org.reactivestreams.Subscriber; 23 | import org.reactivestreams.Subscription; 24 | import org.slf4j.Logger; 25 | import org.slf4j.LoggerFactory; 26 | 27 | /** 28 | *

A {@link Publisher} that supports a single {@link Subscriber} at a time.

29 | * 30 | * @param the type of items emitted by this {@link Publisher} 31 | */ 32 | public abstract class AbstractSinglePublisher implements Publisher, Subscription { 33 | private static final Logger logger = LoggerFactory.getLogger(AbstractSinglePublisher.class); 34 | 35 | private final AutoLock lock = new AutoLock(); 36 | private Subscriber subscriber; 37 | 38 | protected AutoLock lock() { 39 | return lock.lock(); 40 | } 41 | 42 | @Override 43 | public void subscribe(Subscriber subscriber) { 44 | Objects.requireNonNull(subscriber, "invalid 'null' subscriber"); 45 | Throwable failure = null; 46 | try (AutoLock ignored = lock()) { 47 | if (this.subscriber != null) { 48 | failure = new IllegalStateException("multiple subscribers not supported"); 49 | } else { 50 | this.subscriber = subscriber; 51 | } 52 | } 53 | if (logger.isDebugEnabled()) { 54 | logger.debug("{} subscription from {}", this, subscriber); 55 | } 56 | subscriber.onSubscribe(this); 57 | if (failure != null) { 58 | onFailure(subscriber, failure); 59 | } 60 | } 61 | 62 | protected Subscriber subscriber() { 63 | try (AutoLock ignored = lock()) { 64 | return subscriber; 65 | } 66 | } 67 | 68 | @Override 69 | public void request(long n) { 70 | Subscriber subscriber; 71 | Throwable failure = null; 72 | try (AutoLock ignored = lock()) { 73 | subscriber = this.subscriber; 74 | if (n <= 0) { 75 | failure = new IllegalArgumentException("reactive stream violation rule 3.9"); 76 | } 77 | } 78 | if (failure != null) { 79 | onFailure(subscriber, failure); 80 | } else { 81 | onRequest(subscriber, n); 82 | } 83 | } 84 | 85 | protected abstract void onRequest(Subscriber subscriber, long n); 86 | 87 | protected void onFailure(Subscriber subscriber, Throwable failure) { 88 | emitOnError(subscriber, failure); 89 | } 90 | 91 | protected void emitOnNext(Subscriber subscriber, T item) { 92 | subscriber.onNext(item); 93 | } 94 | 95 | protected void emitOnError(Subscriber subscriber, Throwable failure) { 96 | // Reset before emitting the event, otherwise there might be a race. 97 | reset(); 98 | subscriber.onError(failure); 99 | } 100 | 101 | protected void emitOnComplete(Subscriber subscriber) { 102 | // Reset before emitting the event, otherwise there might be a race. 103 | reset(); 104 | subscriber.onComplete(); 105 | } 106 | 107 | @Override 108 | public void cancel() { 109 | Subscriber subscriber; 110 | try (AutoLock ignored = lock()) { 111 | subscriber = subscriber(); 112 | reset(); 113 | } 114 | if (logger.isDebugEnabled()) { 115 | logger.debug("{} cancelled subscription from {}", this, subscriber); 116 | } 117 | } 118 | 119 | private void reset() { 120 | try (AutoLock ignored = lock()) { 121 | // Null-out the field to allow re-subscriptions. 122 | this.subscriber = null; 123 | } 124 | } 125 | 126 | @Override 127 | public String toString() { 128 | return String.format("%s@%x", getClass().getSimpleName(), hashCode()); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/main/java/org/eclipse/jetty/reactive/client/internal/AdapterRequestContent.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017 the original author or authors. 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 org.eclipse.jetty.reactive.client.internal; 17 | 18 | import org.eclipse.jetty.client.HttpClient; 19 | import org.eclipse.jetty.client.Request; 20 | import org.eclipse.jetty.io.Content; 21 | import org.eclipse.jetty.reactive.client.ReactiveRequest; 22 | import org.eclipse.jetty.util.thread.AutoLock; 23 | import org.eclipse.jetty.util.thread.SerializedInvoker; 24 | import org.reactivestreams.Subscriber; 25 | import org.reactivestreams.Subscription; 26 | import org.slf4j.Logger; 27 | import org.slf4j.LoggerFactory; 28 | 29 | /** 30 | *

A {@link Request.Content} whose source is a {@link ReactiveRequest.Content}.

31 | */ 32 | public class AdapterRequestContent implements Request.Content { 33 | private static final Logger logger = LoggerFactory.getLogger(AdapterRequestContent.class); 34 | 35 | private final ReactiveRequest.Content reactiveContent; 36 | private Bridge bridge; 37 | 38 | public AdapterRequestContent(ReactiveRequest.Content content) { 39 | this.reactiveContent = content; 40 | } 41 | 42 | @Override 43 | public long getLength() { 44 | return reactiveContent.getLength(); 45 | } 46 | 47 | @Override 48 | public Content.Chunk read() { 49 | return getOrCreateBridge().read(); 50 | } 51 | 52 | @Override 53 | public void demand(Runnable runnable) { 54 | getOrCreateBridge().demand(runnable); 55 | } 56 | 57 | @Override 58 | public void fail(Throwable failure) { 59 | getOrCreateBridge().fail(failure); 60 | } 61 | 62 | @Override 63 | public boolean rewind() { 64 | boolean rewound = reactiveContent.rewind(); 65 | if (logger.isDebugEnabled()) { 66 | logger.debug("rewinding {} {} on {}", rewound, reactiveContent, bridge); 67 | } 68 | if (rewound) { 69 | bridge = null; 70 | } 71 | return rewound; 72 | } 73 | 74 | private Bridge getOrCreateBridge() { 75 | if (bridge == null) { 76 | bridge = new Bridge(); 77 | } 78 | return bridge; 79 | } 80 | 81 | @Override 82 | public String getContentType() { 83 | return reactiveContent.getContentType(); 84 | } 85 | 86 | @Override 87 | public String toString() { 88 | return String.format("%s@%x", getClass().getSimpleName(), hashCode()); 89 | } 90 | 91 | /** 92 | *

A bridge between the {@link Request.Content} read by the {@link HttpClient} 93 | * implementation and the {@link ReactiveRequest.Content} provided by applications.

94 | *

The first access to the {@link Request.Content} from the {@link HttpClient} 95 | * implementation creates the bridge and forwards the access to it, calling either 96 | * {@link #read()} or {@link #demand(Runnable)}. 97 | * Method {@link #read()} returns the current {@link Content.Chunk}. 98 | * Method {@link #demand(Runnable)} forwards the demand to the {@link ReactiveRequest.Content}, 99 | * which in turns calls {@link #onNext(Content.Chunk)}, providing the current chunk 100 | * returned by {@link #read()}.

101 | */ 102 | private class Bridge implements Subscriber { 103 | private final SerializedInvoker invoker = new SerializedInvoker(); 104 | private final AutoLock lock = new AutoLock(); 105 | private Subscription subscription; 106 | private Content.Chunk chunk; 107 | private Throwable failure; 108 | private boolean complete; 109 | private Runnable demand; 110 | 111 | private Bridge() { 112 | reactiveContent.subscribe(this); 113 | } 114 | 115 | @Override 116 | public void onSubscribe(Subscription s) { 117 | subscription = s; 118 | } 119 | 120 | @Override 121 | public void onNext(Content.Chunk c) { 122 | if (logger.isDebugEnabled()) { 123 | logger.debug("content {} on {}", c, this); 124 | } 125 | 126 | Runnable onDemand; 127 | try (AutoLock ignored = lock.lock()) { 128 | chunk = c; 129 | onDemand = demand; 130 | demand = null; 131 | } 132 | 133 | invoker.run(() -> invokeDemand(onDemand)); 134 | } 135 | 136 | @Override 137 | public void onError(Throwable error) { 138 | if (logger.isDebugEnabled()) { 139 | logger.debug("error on {}", this, error); 140 | } 141 | 142 | Runnable onDemand; 143 | try (AutoLock ignored = lock.lock()) { 144 | failure = error; 145 | onDemand = demand; 146 | demand = null; 147 | } 148 | 149 | invoker.run(() -> invokeDemand(onDemand)); 150 | } 151 | 152 | @Override 153 | public void onComplete() { 154 | if (logger.isDebugEnabled()) { 155 | logger.debug("complete on {}", this); 156 | } 157 | 158 | Runnable onDemand; 159 | try (AutoLock ignored = lock.lock()) { 160 | complete = true; 161 | onDemand = demand; 162 | demand = null; 163 | } 164 | 165 | invoker.run(() -> invokeDemand(onDemand)); 166 | } 167 | 168 | private Content.Chunk read() { 169 | Content.Chunk result; 170 | try (AutoLock ignored = lock.lock()) { 171 | result = chunk; 172 | if (result == null) { 173 | if (complete) { 174 | result = Content.Chunk.EOF; 175 | } else if (failure != null) { 176 | result = Content.Chunk.from(failure); 177 | } 178 | } 179 | chunk = Content.Chunk.next(result); 180 | } 181 | if (logger.isDebugEnabled()) { 182 | logger.debug("read {} on {}", result, this); 183 | } 184 | return result; 185 | } 186 | 187 | private void demand(Runnable onDemand) { 188 | if (logger.isDebugEnabled()) { 189 | logger.debug("demand {} on {}", onDemand, this); 190 | } 191 | 192 | Throwable cause; 193 | try (AutoLock ignored = lock.lock()) { 194 | if (demand != null) { 195 | throw new IllegalStateException("demand already exists"); 196 | } 197 | cause = failure; 198 | if (cause == null) { 199 | demand = onDemand; 200 | } 201 | } 202 | if (cause == null) { 203 | // Forward the demand. 204 | subscription.request(1); 205 | } else { 206 | invoker.run(() -> invokeDemand(onDemand)); 207 | } 208 | } 209 | 210 | private void fail(Throwable cause) { 211 | if (logger.isDebugEnabled()) { 212 | logger.debug("failure while processing request content on {}", this, cause); 213 | } 214 | 215 | subscription.cancel(); 216 | 217 | Runnable onDemand; 218 | try (AutoLock ignored = lock.lock()) { 219 | if (failure == null) { 220 | failure = cause; 221 | } 222 | onDemand = demand; 223 | demand = null; 224 | } 225 | invoker.run(() -> invokeDemand(onDemand)); 226 | } 227 | 228 | private void invokeDemand(Runnable demand) { 229 | try { 230 | if (logger.isDebugEnabled()) { 231 | logger.debug("invoking demand callback {} on {}", demand, this); 232 | } 233 | if (demand != null) { 234 | demand.run(); 235 | } 236 | } catch (Throwable x) { 237 | fail(x); 238 | } 239 | } 240 | 241 | @Override 242 | public String toString() { 243 | return "%s$%s@%x".formatted( 244 | getClass().getEnclosingClass().getSimpleName(), 245 | getClass().getSimpleName(), 246 | hashCode() 247 | ); 248 | } 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /src/main/java/org/eclipse/jetty/reactive/client/internal/ByteArrayBufferingProcessor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017 the original author or authors. 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 org.eclipse.jetty.reactive.client.internal; 17 | 18 | import java.util.List; 19 | import org.eclipse.jetty.io.Content; 20 | import org.eclipse.jetty.reactive.client.ReactiveResponse; 21 | 22 | public class ByteArrayBufferingProcessor extends AbstractBufferingProcessor { 23 | public ByteArrayBufferingProcessor(ReactiveResponse response, int maxCapacity) { 24 | super(response, maxCapacity); 25 | } 26 | 27 | @Override 28 | protected byte[] process(List chunks) { 29 | int length = Math.toIntExact(chunks.stream().mapToLong(Content.Chunk::remaining).sum()); 30 | int offset = 0; 31 | byte[] bytes = new byte[length]; 32 | for (Content.Chunk chunk : chunks) { 33 | int size = chunk.remaining(); 34 | chunk.getByteBuffer().get(bytes, offset, size); 35 | offset += size; 36 | chunk.release(); 37 | } 38 | return bytes; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/org/eclipse/jetty/reactive/client/internal/ByteBufferBufferingProcessor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017 the original author or authors. 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 org.eclipse.jetty.reactive.client.internal; 17 | 18 | import java.nio.ByteBuffer; 19 | import java.util.List; 20 | import org.eclipse.jetty.io.Content; 21 | import org.eclipse.jetty.reactive.client.ReactiveResponse; 22 | 23 | public class ByteBufferBufferingProcessor extends AbstractBufferingProcessor { 24 | public ByteBufferBufferingProcessor(ReactiveResponse response, int maxCapacity) { 25 | super(response, maxCapacity); 26 | } 27 | 28 | @Override 29 | protected ByteBuffer process(List chunks) { 30 | int length = Math.toIntExact(chunks.stream().mapToLong(Content.Chunk::remaining).sum()); 31 | ByteBuffer result = ByteBuffer.allocateDirect(length); 32 | for (Content.Chunk chunk : chunks) { 33 | result.put(chunk.getByteBuffer()); 34 | chunk.release(); 35 | } 36 | return result.flip(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/org/eclipse/jetty/reactive/client/internal/DiscardingProcessor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017 the original author or authors. 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 org.eclipse.jetty.reactive.client.internal; 17 | 18 | import org.eclipse.jetty.io.Content; 19 | import org.eclipse.jetty.reactive.client.ReactiveResponse; 20 | 21 | public class DiscardingProcessor extends AbstractSingleProcessor { 22 | private final ReactiveResponse response; 23 | 24 | public DiscardingProcessor(ReactiveResponse response) { 25 | this.response = response; 26 | } 27 | 28 | @Override 29 | public void onNext(Content.Chunk chunk) { 30 | chunk.release(); 31 | upStreamRequest(1); 32 | } 33 | 34 | @Override 35 | public void onComplete() { 36 | downStreamOnNext(response); 37 | super.onComplete(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/org/eclipse/jetty/reactive/client/internal/PublisherContent.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017 the original author or authors. 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 org.eclipse.jetty.reactive.client.internal; 17 | 18 | import java.util.Objects; 19 | 20 | import org.eclipse.jetty.io.Content; 21 | import org.eclipse.jetty.reactive.client.ReactiveRequest; 22 | import org.reactivestreams.Publisher; 23 | import org.reactivestreams.Subscriber; 24 | 25 | /** 26 | * A {@link ReactiveRequest.Content} that wraps a {@link Publisher}. 27 | */ 28 | public class PublisherContent extends AbstractSingleProcessor implements ReactiveRequest.Content { 29 | private final Publisher publisher; 30 | private final String contentType; 31 | 32 | public PublisherContent(Publisher publisher, String contentType) { 33 | this.publisher = publisher; 34 | this.contentType = Objects.requireNonNull(contentType); 35 | } 36 | 37 | @Override 38 | public void subscribe(Subscriber subscriber) { 39 | super.subscribe(subscriber); 40 | publisher.subscribe(this); 41 | } 42 | 43 | @Override 44 | public void onNext(Content.Chunk chunk) { 45 | downStreamOnNext(chunk); 46 | } 47 | 48 | @Override 49 | public long getLength() { 50 | return -1; 51 | } 52 | 53 | @Override 54 | public String getContentType() { 55 | return contentType; 56 | } 57 | 58 | @Override 59 | public boolean rewind() { 60 | return true; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/org/eclipse/jetty/reactive/client/internal/QueuedSinglePublisher.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017 the original author or authors. 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 org.eclipse.jetty.reactive.client.internal; 17 | 18 | import java.util.ArrayDeque; 19 | import java.util.Queue; 20 | import java.util.concurrent.CompletionException; 21 | 22 | import org.eclipse.jetty.util.MathUtils; 23 | import org.eclipse.jetty.util.thread.AutoLock; 24 | import org.reactivestreams.Subscriber; 25 | import org.slf4j.Logger; 26 | import org.slf4j.LoggerFactory; 27 | 28 | public class QueuedSinglePublisher extends AbstractSinglePublisher { 29 | private static final Logger logger = LoggerFactory.getLogger(QueuedSinglePublisher.class); 30 | 31 | private final Queue items = new ArrayDeque<>(); 32 | private long demand; 33 | private boolean stalled = true; 34 | private boolean active; 35 | private Throwable terminated; 36 | 37 | public void offer(T item) { 38 | if (logger.isDebugEnabled()) { 39 | logger.debug("offered item {} to {}", item, this); 40 | } 41 | process(item); 42 | } 43 | 44 | public void complete() { 45 | if (logger.isDebugEnabled()) { 46 | logger.debug("completed {}", this); 47 | } 48 | process(new Complete()); 49 | } 50 | 51 | public boolean fail(Throwable failure) { 52 | if (logger.isDebugEnabled()) { 53 | logger.debug("failed {}", this, failure); 54 | } 55 | return process(new Failure(failure)); 56 | } 57 | 58 | protected void tryProduce(Runnable producer) { 59 | boolean produce; 60 | try (AutoLock ignored = lock()) { 61 | produce = demand > 0 && stalled; 62 | } 63 | 64 | if (logger.isDebugEnabled()) { 65 | logger.debug("producing {} on {}", produce, this); 66 | } 67 | 68 | if (produce) { 69 | producer.run(); 70 | } 71 | } 72 | 73 | public boolean hasDemand() { 74 | try (AutoLock ignored = lock()) { 75 | return demand > 0; 76 | } 77 | } 78 | 79 | @Override 80 | protected void onRequest(Subscriber subscriber, long n) { 81 | boolean proceed = false; 82 | try (AutoLock ignored = lock()) { 83 | demand = MathUtils.cappedAdd(demand, n); 84 | if (stalled) { 85 | stalled = false; 86 | proceed = true; 87 | } 88 | } 89 | 90 | if (logger.isDebugEnabled()) { 91 | logger.debug("demand {}, proceeding {} on {}", n, proceed, this); 92 | } 93 | 94 | if (proceed) { 95 | proceed(subscriber); 96 | } 97 | } 98 | 99 | private boolean process(Object item) { 100 | Subscriber subscriber; 101 | try (AutoLock ignored = lock()) { 102 | if (terminated != null) { 103 | throw new IllegalStateException(terminated); 104 | } 105 | if (isTerminal(item)) { 106 | terminated = new CompletionException("terminated from " + Thread.currentThread(), null); 107 | } 108 | items.offer(item); 109 | subscriber = subscriber(); 110 | if (subscriber != null) { 111 | if (stalled) { 112 | stalled = false; 113 | } 114 | } 115 | } 116 | if (subscriber != null) { 117 | proceed(subscriber); 118 | return true; 119 | } else { 120 | return false; 121 | } 122 | } 123 | 124 | private void proceed(Subscriber subscriber) { 125 | try (AutoLock ignored = lock()) { 126 | if (active) { 127 | return; 128 | } 129 | active = true; 130 | } 131 | 132 | Object item; 133 | boolean terminal; 134 | while (true) { 135 | try (AutoLock ignored = lock()) { 136 | item = items.peek(); 137 | if (item == null) { 138 | stalled = true; 139 | active = false; 140 | return; 141 | } else { 142 | terminal = isTerminal(item); 143 | if (!terminal) { 144 | if (demand > 0) { 145 | --demand; 146 | } else { 147 | stalled = true; 148 | active = false; 149 | return; 150 | } 151 | } 152 | } 153 | item = items.poll(); 154 | } 155 | 156 | if (logger.isDebugEnabled()) { 157 | logger.debug("processing {} item {} by {}", terminal ? "last" : "next", item, this); 158 | } 159 | 160 | if (terminal) { 161 | @SuppressWarnings("unchecked") 162 | Terminal t = (Terminal)item; 163 | t.notify(subscriber); 164 | } else { 165 | @SuppressWarnings("unchecked") 166 | T t = (T)item; 167 | onNext(subscriber, t); 168 | } 169 | } 170 | } 171 | 172 | protected void onNext(Subscriber subscriber, T item) { 173 | emitOnNext(subscriber, item); 174 | } 175 | 176 | private boolean isTerminal(Object item) { 177 | return item instanceof Terminal; 178 | } 179 | 180 | @FunctionalInterface 181 | private interface Terminal { 182 | public void notify(Subscriber subscriber); 183 | } 184 | 185 | private class Complete implements Terminal { 186 | @Override 187 | public void notify(Subscriber subscriber) { 188 | emitOnComplete(subscriber); 189 | } 190 | } 191 | 192 | private class Failure implements Terminal { 193 | private final Throwable failure; 194 | 195 | private Failure(Throwable failure) { 196 | this.failure = failure; 197 | } 198 | 199 | @Override 200 | public void notify(Subscriber subscriber) { 201 | emitOnError(subscriber, failure); 202 | } 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /src/main/java/org/eclipse/jetty/reactive/client/internal/RequestEventPublisher.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017 the original author or authors. 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 org.eclipse.jetty.reactive.client.internal; 17 | 18 | import java.nio.ByteBuffer; 19 | 20 | import org.eclipse.jetty.client.Request; 21 | import org.eclipse.jetty.reactive.client.ReactiveRequest; 22 | 23 | public class RequestEventPublisher extends AbstractEventPublisher implements Request.Listener { 24 | private final ReactiveRequest request; 25 | 26 | public RequestEventPublisher(ReactiveRequest request) { 27 | this.request = request; 28 | } 29 | 30 | @Override 31 | public void onQueued(Request request) { 32 | emit(new ReactiveRequest.Event(ReactiveRequest.Event.Type.QUEUED, this.request)); 33 | } 34 | 35 | @Override 36 | public void onBegin(Request request) { 37 | emit(new ReactiveRequest.Event(ReactiveRequest.Event.Type.BEGIN, this.request)); 38 | } 39 | 40 | @Override 41 | public void onHeaders(Request request) { 42 | emit(new ReactiveRequest.Event(ReactiveRequest.Event.Type.HEADERS, this.request)); 43 | } 44 | 45 | @Override 46 | public void onCommit(Request request) { 47 | emit(new ReactiveRequest.Event(ReactiveRequest.Event.Type.COMMIT, this.request)); 48 | } 49 | 50 | @Override 51 | public void onContent(Request request, ByteBuffer content) { 52 | emit(new ReactiveRequest.Event(ReactiveRequest.Event.Type.CONTENT, this.request, content)); 53 | } 54 | 55 | @Override 56 | public void onSuccess(Request request) { 57 | emit(new ReactiveRequest.Event(ReactiveRequest.Event.Type.SUCCESS, this.request)); 58 | succeed(); 59 | } 60 | 61 | @Override 62 | public void onFailure(Request request, Throwable failure) { 63 | emit(new ReactiveRequest.Event(ReactiveRequest.Event.Type.FAILURE, this.request, failure)); 64 | fail(failure); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/org/eclipse/jetty/reactive/client/internal/ResponseEventPublisher.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017 the original author or authors. 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 org.eclipse.jetty.reactive.client.internal; 17 | 18 | import java.nio.ByteBuffer; 19 | import org.eclipse.jetty.client.Response; 20 | import org.eclipse.jetty.client.Result; 21 | import org.eclipse.jetty.http.HttpField; 22 | import org.eclipse.jetty.io.Content; 23 | import org.eclipse.jetty.reactive.client.ReactiveRequest; 24 | import org.eclipse.jetty.reactive.client.ReactiveResponse; 25 | import org.eclipse.jetty.util.thread.Invocable; 26 | 27 | public class ResponseEventPublisher extends AbstractEventPublisher implements Response.Listener { 28 | private final ReactiveRequest request; 29 | 30 | public ResponseEventPublisher(ReactiveRequest request) { 31 | this.request = request; 32 | } 33 | 34 | @Override 35 | public void onBegin(Response response) { 36 | emit(new ReactiveResponse.Event(ReactiveResponse.Event.Type.BEGIN, request.getReactiveResponse())); 37 | } 38 | 39 | @Override 40 | public boolean onHeader(Response response, HttpField field) { 41 | return true; 42 | } 43 | 44 | @Override 45 | public void onHeaders(Response response) { 46 | emit(new ReactiveResponse.Event(ReactiveResponse.Event.Type.HEADERS, request.getReactiveResponse())); 47 | } 48 | 49 | @Override 50 | public void onContent(Response response, ByteBuffer content) { 51 | } 52 | 53 | @Override 54 | public void onContent(Response response, Content.Chunk chunk, Runnable demander) { 55 | } 56 | 57 | @Override 58 | public void onContentSource(Response response, Content.Source source) { 59 | Runnable reader = new Invocable.Task.Abstract(Invocable.InvocationType.NON_BLOCKING) { 60 | @Override 61 | public void run() { 62 | while (true) { 63 | Content.Chunk chunk = source.read(); 64 | if (chunk == null) { 65 | source.demand(this); 66 | return; 67 | } 68 | if (Content.Chunk.isFailure(chunk)) { 69 | onFailure(response, chunk.getFailure()); 70 | return; 71 | } 72 | if (chunk.hasRemaining()) { 73 | emit(new ReactiveResponse.Event(ReactiveResponse.Event.Type.CONTENT, request.getReactiveResponse(), chunk.getByteBuffer().asReadOnlyBuffer())); 74 | } 75 | chunk.release(); 76 | if (chunk.isLast()) { 77 | break; 78 | } 79 | } 80 | } 81 | }; 82 | reader.run(); 83 | } 84 | 85 | @Override 86 | public void onSuccess(Response response) { 87 | emit(new ReactiveResponse.Event(ReactiveResponse.Event.Type.SUCCESS, request.getReactiveResponse())); 88 | } 89 | 90 | @Override 91 | public void onFailure(Response response, Throwable failure) { 92 | emit(new ReactiveResponse.Event(ReactiveResponse.Event.Type.FAILURE, request.getReactiveResponse(), failure)); 93 | } 94 | 95 | @Override 96 | public void onComplete(Result result) { 97 | emit(new ReactiveResponse.Event(ReactiveResponse.Event.Type.COMPLETE, request.getReactiveResponse())); 98 | if (result.isSucceeded()) { 99 | succeed(); 100 | } else { 101 | fail(result.getFailure()); 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/main/java/org/eclipse/jetty/reactive/client/internal/ResponseListenerProcessor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017 the original author or authors. 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 org.eclipse.jetty.reactive.client.internal; 17 | 18 | import java.util.concurrent.CancellationException; 19 | import java.util.function.BiFunction; 20 | import org.eclipse.jetty.client.Response; 21 | import org.eclipse.jetty.client.Result; 22 | import org.eclipse.jetty.io.Content; 23 | import org.eclipse.jetty.reactive.client.ReactiveRequest; 24 | import org.eclipse.jetty.reactive.client.ReactiveResponse; 25 | import org.eclipse.jetty.util.thread.AutoLock; 26 | import org.eclipse.jetty.util.thread.Invocable; 27 | import org.reactivestreams.Publisher; 28 | import org.reactivestreams.Subscriber; 29 | import org.slf4j.Logger; 30 | import org.slf4j.LoggerFactory; 31 | 32 | /** 33 | *

A Processor that listens for response events.

34 | *

When this Processor is demanded data, it first sends the request and produces no data. 35 | * When the response arrives, the application is invoked with a Publisher that produces 36 | * response content chunks.

37 | *

The application processes the response content chunks into some other data 38 | * structure (for example, splits them further, or coalesce them into a single chunk) and 39 | * returns the application Processor to this implementation, which then builds this chain:

40 | *
 41 |  * HTTP response content chunks Publisher - (produces Content.Chunks)
 42 |  *   Application Processor                - (processes Content.Chunks and produces Ts)
 43 |  *     ResponseListenerProcessor          - (forwards Ts to application)
 44 |  *       Application Subscriber           - (consumes Ts)
 45 |  * 
46 | *

Data flows from top to bottom, demand from bottom to top.

47 | *

ResponseListenerProcessor acts as a "hot" publisher: it is returned to the application 48 | * before the response content arrives so that the application can subscribe to it.

49 | *

Any further data demand to this Processor is forwarded to the Application Processor, 50 | * which in turn demands response content chunks. 51 | * Response content chunks arrive to the Application Processor, which processes them and 52 | * produces data that is forwarded to this Processor, which forwards it to the Application 53 | * Subscriber.

54 | */ 55 | public class ResponseListenerProcessor extends AbstractSingleProcessor implements Response.Listener { 56 | private static final Logger logger = LoggerFactory.getLogger(ResponseListenerProcessor.class); 57 | 58 | private final ContentPublisher content = new ContentPublisher(); 59 | private final ReactiveRequest request; 60 | private final BiFunction, Publisher> contentFn; 61 | private final boolean abortOnCancel; 62 | private boolean requestSent; 63 | private boolean responseReceived; 64 | 65 | public ResponseListenerProcessor(ReactiveRequest request, BiFunction, Publisher> contentFn, boolean abortOnCancel) { 66 | this.request = request; 67 | this.contentFn = contentFn; 68 | this.abortOnCancel = abortOnCancel; 69 | } 70 | 71 | @Override 72 | public void onHeaders(Response response) { 73 | if (logger.isDebugEnabled()) { 74 | logger.debug("received response headers {} on {}", response, this); 75 | } 76 | } 77 | 78 | @Override 79 | public void onContentSource(Response response, Content.Source source) { 80 | if (logger.isDebugEnabled()) { 81 | logger.debug("received response content source {} {} on {}", response, source, this); 82 | } 83 | 84 | // Link the source of Chunks with the Publisher of Chunks. 85 | content.accept(source); 86 | 87 | responseReceived = true; 88 | 89 | // Call the application to obtain a response content transformer. 90 | Publisher appPublisher = contentFn.apply(request.getReactiveResponse(), content); 91 | 92 | // Links the publisher/subscriber chain. 93 | // Content Chunks flow from top (upstream) to bottom (downstream). 94 | // 95 | // ContentPublisher (reads Chunks) 96 | // `- application Processor (receives Chunks, transforms them and emits Ts) [optional] 97 | // `- application Publisher (emits Ts) 98 | // `- ResponseListenerProcessor (receives Ts, emits Ts) 99 | // `- application Subscriber (receives Ts) 100 | appPublisher.subscribe(this); 101 | } 102 | 103 | @Override 104 | public void onSuccess(Response response) { 105 | if (logger.isDebugEnabled()) { 106 | logger.debug("response complete {} on {}", response, this); 107 | } 108 | } 109 | 110 | @Override 111 | public void onFailure(Response response, Throwable failure) { 112 | if (logger.isDebugEnabled()) { 113 | logger.debug("response failure {} on {}", response, this, failure); 114 | } 115 | } 116 | 117 | @Override 118 | public void onComplete(Result result) { 119 | if (result.isSucceeded()) { 120 | content.complete(); 121 | } else { 122 | Throwable failure = result.getFailure(); 123 | if (!content.fail(failure)) { 124 | if (!responseReceived) { 125 | onError(failure); 126 | } 127 | } 128 | } 129 | } 130 | 131 | @Override 132 | protected void onRequest(Subscriber subscriber, long n) { 133 | boolean send; 134 | try (AutoLock ignored = lock()) { 135 | send = !requestSent; 136 | requestSent = true; 137 | } 138 | if (send) { 139 | send(); 140 | } 141 | super.onRequest(subscriber, n); 142 | } 143 | 144 | @Override 145 | public void onNext(T t) { 146 | downStreamOnNext(t); 147 | } 148 | 149 | private void send() { 150 | if (logger.isDebugEnabled()) { 151 | logger.debug("sending request {} from {}", request, this); 152 | } 153 | request.getRequest().send(this); 154 | } 155 | 156 | @Override 157 | public void cancel() { 158 | if (abortOnCancel) { 159 | request.getRequest().abort(new CancellationException()); 160 | } 161 | super.cancel(); 162 | } 163 | 164 | @Override 165 | public String toString() { 166 | return String.format("%s@%x[%s]", getClass().getSimpleName(), hashCode(), request); 167 | } 168 | 169 | /** 170 | *

Publishes response {@link Content.Chunk}s to the application 171 | * {@code BiFunction} given to {@link ReactiveRequest#response(BiFunction)}.

172 | */ 173 | private static class ContentPublisher extends QueuedSinglePublisher implements Invocable.Task { 174 | private volatile Content.Source contentSource; 175 | 176 | private void accept(Content.Source source) { 177 | contentSource = source; 178 | } 179 | 180 | @Override 181 | protected void onRequest(Subscriber subscriber, long n) { 182 | super.onRequest(subscriber, n); 183 | 184 | // This method is called by: 185 | // 1) An application thread, in case of asynchronous demand => resume production. 186 | // 2) A producer thread, from onNext() + request() => must not resume production. 187 | 188 | tryProduce(this); 189 | } 190 | 191 | @Override 192 | public void run() { 193 | Content.Source source = contentSource; 194 | if (source != null) { 195 | read(source); 196 | } 197 | } 198 | 199 | @Override 200 | public InvocationType getInvocationType() { 201 | return InvocationType.NON_BLOCKING; 202 | } 203 | 204 | private void read(Content.Source source) { 205 | while (true) { 206 | if (!hasDemand()) { 207 | return; 208 | } 209 | 210 | Content.Chunk chunk = source.read(); 211 | if (logger.isDebugEnabled()) { 212 | logger.debug("read {} from {} on {}", chunk, source, this); 213 | } 214 | 215 | if (chunk == null) { 216 | source.demand(this); 217 | return; 218 | } 219 | 220 | if (Content.Chunk.isFailure(chunk)) { 221 | fail(chunk.getFailure()); 222 | return; 223 | } 224 | 225 | if (chunk.hasRemaining()) { 226 | try { 227 | offer(chunk); 228 | } catch (Throwable x) { 229 | chunk.release(); 230 | fail(x); 231 | return; 232 | } 233 | } else { 234 | chunk.release(); 235 | } 236 | 237 | if (chunk.isLast()) { 238 | return; 239 | } 240 | } 241 | } 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /src/main/java/org/eclipse/jetty/reactive/client/internal/ResultProcessor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017 the original author or authors. 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 org.eclipse.jetty.reactive.client.internal; 17 | 18 | import org.eclipse.jetty.reactive.client.ReactiveResponse; 19 | 20 | /** 21 | *

A {@link org.reactivestreams.Processor} that receives (typically) 22 | * a single item of response content of type {@code T} from upstream, 23 | * and produces a single {@link ReactiveResponse.Result} that wraps 24 | * the {@link ReactiveResponse} and the response content item.

25 | * 26 | * @param the type of the response content associated 27 | */ 28 | public class ResultProcessor extends AbstractSingleProcessor> { 29 | private final ReactiveResponse response; 30 | private T item; 31 | 32 | public ResultProcessor(ReactiveResponse response) { 33 | this.response = response; 34 | } 35 | 36 | @Override 37 | public void onNext(T item) { 38 | this.item = item; 39 | } 40 | 41 | @Override 42 | public void onComplete() { 43 | downStreamOnNext(new ReactiveResponse.Result<>(response, item)); 44 | super.onComplete(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/org/eclipse/jetty/reactive/client/internal/StringBufferingProcessor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017 the original author or authors. 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 org.eclipse.jetty.reactive.client.internal; 17 | 18 | import java.nio.charset.Charset; 19 | import java.nio.charset.StandardCharsets; 20 | import java.util.List; 21 | import java.util.Objects; 22 | import org.eclipse.jetty.io.Content; 23 | import org.eclipse.jetty.reactive.client.ReactiveResponse; 24 | 25 | public class StringBufferingProcessor extends AbstractBufferingProcessor { 26 | private final ByteArrayBufferingProcessor bytesProcessor; 27 | 28 | public StringBufferingProcessor(ReactiveResponse response, int maxCapacity) { 29 | super(response, maxCapacity); 30 | bytesProcessor = new ByteArrayBufferingProcessor(response, maxCapacity); 31 | } 32 | 33 | @Override 34 | protected String process(List chunks) { 35 | byte[] bytes = bytesProcessor.process(chunks); 36 | String encoding = Objects.requireNonNullElse(getResponse().getEncoding(), StandardCharsets.UTF_8.name()); 37 | return new String(bytes, Charset.forName(encoding)); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/org/eclipse/jetty/reactive/client/internal/StringContent.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017 the original author or authors. 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 org.eclipse.jetty.reactive.client.internal; 17 | 18 | import java.nio.ByteBuffer; 19 | import java.nio.charset.Charset; 20 | import java.util.Objects; 21 | 22 | import org.eclipse.jetty.io.Content; 23 | import org.eclipse.jetty.reactive.client.ReactiveRequest; 24 | import org.reactivestreams.Subscriber; 25 | 26 | /** 27 | *

Utility class that provides a String as reactive content.

28 | */ 29 | public class StringContent extends AbstractSinglePublisher implements ReactiveRequest.Content { 30 | private final String mediaType; 31 | private final Charset encoding; 32 | private final byte[] bytes; 33 | private State state = State.INITIAL; 34 | 35 | public StringContent(String string, String mediaType, Charset encoding) { 36 | this.mediaType = Objects.requireNonNull(mediaType); 37 | this.encoding = Objects.requireNonNull(encoding); 38 | this.bytes = string.getBytes(encoding); 39 | } 40 | 41 | @Override 42 | public long getLength() { 43 | return bytes.length; 44 | } 45 | 46 | @Override 47 | public String getContentType() { 48 | return mediaType + ";charset=" + encoding.name(); 49 | } 50 | 51 | @Override 52 | public boolean rewind() { 53 | state = State.INITIAL; 54 | return true; 55 | } 56 | 57 | @Override 58 | protected void onRequest(Subscriber subscriber, long n) { 59 | switch (state) { 60 | case INITIAL: { 61 | state = State.CONTENT; 62 | emitOnNext(subscriber, Content.Chunk.from(ByteBuffer.wrap(bytes), false)); 63 | break; 64 | } 65 | case CONTENT: { 66 | state = State.COMPLETE; 67 | emitOnComplete(subscriber); 68 | break; 69 | } 70 | default: { 71 | break; 72 | } 73 | } 74 | } 75 | 76 | private enum State { 77 | INITIAL, CONTENT, COMPLETE 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/org/eclipse/jetty/reactive/client/internal/Transformer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017 the original author or authors. 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 org.eclipse.jetty.reactive.client.internal; 17 | 18 | import java.util.Objects; 19 | import java.util.function.Function; 20 | 21 | /** 22 | *

A {@link org.reactivestreams.Processor} that applies a function 23 | * to transform items from input type {@code I} to output type {@code O}.

24 | * 25 | * @param the input type 26 | * @param the output type 27 | */ 28 | public class Transformer extends AbstractSingleProcessor { 29 | private final Function transformer; 30 | 31 | public Transformer(Function transformer) { 32 | this.transformer = Objects.requireNonNull(transformer); 33 | } 34 | 35 | @Override 36 | public void onNext(I i) { 37 | downStreamOnNext(transformer.apply(i)); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/test/java/org/eclipse/jetty/reactive/client/AbstractTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017 the original author or authors. 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 org.eclipse.jetty.reactive.client; 17 | 18 | import java.util.List; 19 | import org.eclipse.jetty.client.HttpClient; 20 | import org.eclipse.jetty.client.HttpClientTransport; 21 | import org.eclipse.jetty.client.transport.HttpClientTransportOverHTTP; 22 | import org.eclipse.jetty.http2.client.HTTP2Client; 23 | import org.eclipse.jetty.http2.client.transport.HttpClientTransportOverHTTP2; 24 | import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory; 25 | import org.eclipse.jetty.io.ClientConnector; 26 | import org.eclipse.jetty.server.ConnectionFactory; 27 | import org.eclipse.jetty.server.Handler; 28 | import org.eclipse.jetty.server.HttpConfiguration; 29 | import org.eclipse.jetty.server.HttpConnectionFactory; 30 | import org.eclipse.jetty.server.Server; 31 | import org.eclipse.jetty.server.ServerConnector; 32 | import org.eclipse.jetty.util.component.LifeCycle; 33 | import org.eclipse.jetty.util.thread.QueuedThreadPool; 34 | import org.junit.jupiter.api.AfterEach; 35 | import org.junit.jupiter.api.BeforeEach; 36 | import org.junit.jupiter.api.TestInfo; 37 | 38 | public class AbstractTest { 39 | public static void printTestName(TestInfo testInfo) { 40 | System.err.printf("Running %s%n", testInfo.getTestMethod() 41 | .map(m -> "%s.%s() %s".formatted(m.getDeclaringClass().getSimpleName(), m.getName(), testInfo.getDisplayName())) 42 | .orElseThrow()); 43 | } 44 | 45 | public static List protocols() { 46 | return List.of("http", "h2c"); 47 | } 48 | 49 | private final HttpConfiguration httpConfiguration = new HttpConfiguration(); 50 | private HttpClient httpClient; 51 | protected Server server; 52 | private ServerConnector connector; 53 | 54 | @BeforeEach 55 | public void before(TestInfo testInfo) { 56 | printTestName(testInfo); 57 | } 58 | 59 | public void prepare(String protocol, Handler handler) throws Exception { 60 | QueuedThreadPool serverThreads = new QueuedThreadPool(); 61 | serverThreads.setName("server"); 62 | server = new Server(serverThreads); 63 | connector = new ServerConnector(server, 1, 1, createServerConnectionFactory(protocol)); 64 | server.addConnector(connector); 65 | server.setHandler(handler); 66 | server.start(); 67 | 68 | QueuedThreadPool clientThreads = new QueuedThreadPool(); 69 | clientThreads.setName("client"); 70 | ClientConnector clientConnector = new ClientConnector(); 71 | clientConnector.setExecutor(clientThreads); 72 | clientConnector.setSelectors(1); 73 | httpClient = new HttpClient(createClientTransport(clientConnector, protocol)); 74 | httpClient.setExecutor(clientThreads); 75 | httpClient.start(); 76 | } 77 | 78 | private ConnectionFactory createServerConnectionFactory(String protocol) { 79 | return switch (protocol) { 80 | case "h2c" -> new HTTP2CServerConnectionFactory(httpConfiguration); 81 | default -> new HttpConnectionFactory(httpConfiguration); 82 | }; 83 | } 84 | 85 | private HttpClientTransport createClientTransport(ClientConnector clientConnector, String protocol) { 86 | return switch (protocol) { 87 | case "h2c" -> new HttpClientTransportOverHTTP2(new HTTP2Client(clientConnector)); 88 | default -> new HttpClientTransportOverHTTP(clientConnector); 89 | }; 90 | } 91 | 92 | @AfterEach 93 | public void dispose() { 94 | LifeCycle.stop(httpClient); 95 | LifeCycle.stop(server); 96 | } 97 | 98 | protected HttpClient httpClient() { 99 | return httpClient; 100 | } 101 | 102 | protected String uri() { 103 | return "http://localhost:" + connector.getLocalPort(); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/test/java/org/eclipse/jetty/reactive/client/MetricsTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017 the original author or authors. 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 org.eclipse.jetty.reactive.client; 17 | 18 | import java.util.Map; 19 | import java.util.concurrent.ConcurrentHashMap; 20 | import java.util.concurrent.CountDownLatch; 21 | import java.util.concurrent.TimeUnit; 22 | import java.util.concurrent.atomic.AtomicInteger; 23 | import java.util.stream.IntStream; 24 | 25 | import org.HdrHistogram.ConcurrentHistogram; 26 | import org.HdrHistogram.Histogram; 27 | import org.eclipse.jetty.client.Request; 28 | import org.eclipse.jetty.server.Handler; 29 | import org.eclipse.jetty.server.Response; 30 | import org.eclipse.jetty.toolchain.perf.HistogramSnapshot; 31 | import org.eclipse.jetty.util.Callback; 32 | import org.junit.jupiter.params.ParameterizedTest; 33 | import org.junit.jupiter.params.provider.MethodSource; 34 | import org.reactivestreams.Subscriber; 35 | import org.reactivestreams.Subscription; 36 | 37 | import static org.junit.jupiter.api.Assertions.assertTrue; 38 | 39 | public class MetricsTest extends AbstractTest { 40 | @ParameterizedTest 41 | @MethodSource("protocols") 42 | public void testMetrics(String protocol) throws Exception { 43 | prepare(protocol, new Handler.Abstract() { 44 | @Override 45 | public boolean handle(org.eclipse.jetty.server.Request request, Response response, Callback callback) { 46 | callback.succeeded(); 47 | return true; 48 | } 49 | }); 50 | 51 | // Data structure to hold response status codes. 52 | Map responses = new ConcurrentHashMap<>(); 53 | 54 | // Data structure to hold response times. 55 | Histogram responseTimes = new ConcurrentHistogram( 56 | TimeUnit.MICROSECONDS.toNanos(1), 57 | TimeUnit.MINUTES.toNanos(1), 58 | 3 59 | ); 60 | 61 | int count = 100; 62 | CountDownLatch latch = new CountDownLatch(count); 63 | IntStream.range(0, count) 64 | .parallel() 65 | .forEach(i -> { 66 | Request request = httpClient().newRequest(uri() + "/" + i); 67 | 68 | // Collect information about response status codes. 69 | request.onResponseSuccess(rsp -> 70 | { 71 | int key = rsp.getStatus() / 100; 72 | responses.computeIfAbsent(key, k -> new AtomicInteger()).incrementAndGet(); 73 | }); 74 | 75 | // Collect information about response times. 76 | request.onRequestBegin(req -> req.attribute("nanoTime", System.nanoTime())) 77 | .onComplete(result -> { 78 | Long nanoTime = (Long)result.getRequest().getAttributes().get("nanoTime"); 79 | if (nanoTime != null) { 80 | long responseTime = System.nanoTime() - nanoTime; 81 | responseTimes.recordValue(responseTime); 82 | } 83 | }); 84 | 85 | ReactiveRequest.newBuilder(request) 86 | .build() 87 | .response() 88 | .subscribe(new Subscriber<>() { 89 | @Override 90 | public void onSubscribe(Subscription subscription) { 91 | subscription.request(1); 92 | } 93 | 94 | @Override 95 | public void onNext(ReactiveResponse reactiveResponse) { 96 | } 97 | 98 | @Override 99 | public void onError(Throwable throwable) { 100 | } 101 | 102 | @Override 103 | public void onComplete() { 104 | latch.countDown(); 105 | } 106 | }); 107 | }); 108 | 109 | // Wait for all the responses to arrive. 110 | assertTrue(latch.await(10, TimeUnit.SECONDS)); 111 | 112 | System.err.println("responses = " + responses); 113 | 114 | HistogramSnapshot snapshot = new HistogramSnapshot(responseTimes, 32, "Response Times", "us", TimeUnit.NANOSECONDS::toMicros); 115 | System.err.println(snapshot); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/test/java/org/eclipse/jetty/reactive/client/ReactiveTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017 the original author or authors. 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 org.eclipse.jetty.reactive.client; 17 | 18 | import java.util.concurrent.CountDownLatch; 19 | import java.util.concurrent.TimeUnit; 20 | import java.util.concurrent.atomic.AtomicReference; 21 | 22 | import org.eclipse.jetty.http.HttpStatus; 23 | import org.eclipse.jetty.io.Content; 24 | import org.eclipse.jetty.reactive.client.internal.QueuedSinglePublisher; 25 | import org.eclipse.jetty.server.Handler; 26 | import org.eclipse.jetty.server.Request; 27 | import org.eclipse.jetty.server.Response; 28 | import org.eclipse.jetty.util.Callback; 29 | import org.junit.jupiter.params.ParameterizedTest; 30 | import org.junit.jupiter.params.provider.MethodSource; 31 | import org.reactivestreams.Publisher; 32 | import org.reactivestreams.Subscriber; 33 | import org.reactivestreams.Subscription; 34 | 35 | import static org.junit.jupiter.api.Assertions.assertEquals; 36 | import static org.junit.jupiter.api.Assertions.assertNotNull; 37 | import static org.junit.jupiter.api.Assertions.assertTrue; 38 | 39 | public class ReactiveTest extends AbstractTest { 40 | @ParameterizedTest 41 | @MethodSource("protocols") 42 | public void testSimpleReactiveUsage(String protocol) throws Exception { 43 | prepare(protocol, new Handler.Abstract() { 44 | @Override 45 | public boolean handle(org.eclipse.jetty.server.Request request, Response response, Callback callback) { 46 | Content.Sink.write(response, true, "hello world", callback); 47 | return true; 48 | } 49 | }); 50 | 51 | Publisher publisher = ReactiveRequest.newBuilder(httpClient(), uri()).build().response(); 52 | 53 | CountDownLatch latch = new CountDownLatch(1); 54 | AtomicReference responseRef = new AtomicReference<>(); 55 | publisher.subscribe(new Subscriber<>() { 56 | @Override 57 | public void onSubscribe(Subscription subscription) { 58 | subscription.request(1); 59 | } 60 | 61 | @Override 62 | public void onNext(ReactiveResponse response) { 63 | responseRef.set(response); 64 | } 65 | 66 | @Override 67 | public void onError(Throwable failure) { 68 | } 69 | 70 | @Override 71 | public void onComplete() { 72 | latch.countDown(); 73 | } 74 | }); 75 | 76 | assertTrue(latch.await(5, TimeUnit.SECONDS)); 77 | ReactiveResponse response = responseRef.get(); 78 | assertNotNull(response); 79 | assertEquals(response.getStatus(), HttpStatus.OK_200); 80 | } 81 | 82 | @ParameterizedTest 83 | @MethodSource("protocols") 84 | public void testDelayedDemand(String protocol) throws Exception { 85 | prepare(protocol, new Handler.Abstract() { 86 | @Override 87 | public boolean handle(Request request, Response response, Callback callback) { 88 | Content.Sink.write(response, true, "0123456789".repeat(16 * 1024), callback); 89 | return true; 90 | } 91 | }); 92 | 93 | AtomicReference responseRef = new AtomicReference<>(); 94 | AtomicReference> chunkPublisherRef = new AtomicReference<>(); 95 | CountDownLatch responseLatch = new CountDownLatch(1); 96 | CountDownLatch completeLatch = new CountDownLatch(1); 97 | QueuedSinglePublisher publisher = new QueuedSinglePublisher<>(); 98 | ReactiveRequest request = ReactiveRequest.newBuilder(httpClient().newRequest(uri())).build(); 99 | request.response((reactiveResponse, chunkPublisher) -> { 100 | responseRef.set(reactiveResponse); 101 | chunkPublisherRef.set(chunkPublisher); 102 | responseLatch.countDown(); 103 | return publisher; 104 | }).subscribe(new Subscriber<>() { 105 | @Override 106 | public void onSubscribe(Subscription subscription) { 107 | subscription.request(1); 108 | } 109 | 110 | @Override 111 | public void onNext(ReactiveResponse response) { 112 | } 113 | 114 | @Override 115 | public void onError(Throwable failure) { 116 | } 117 | 118 | @Override 119 | public void onComplete() { 120 | completeLatch.countDown(); 121 | } 122 | }); 123 | 124 | assertTrue(responseLatch.await(5, TimeUnit.SECONDS)); 125 | 126 | // Delay the demand of content. 127 | Thread.sleep(100); 128 | chunkPublisherRef.get().subscribe(new Subscriber<>() { 129 | private Subscription subscription; 130 | 131 | @Override 132 | public void onSubscribe(Subscription subscription) { 133 | this.subscription = subscription; 134 | httpClient().getScheduler().schedule(() -> subscription.request(1), 100, TimeUnit.MILLISECONDS); 135 | } 136 | 137 | @Override 138 | public void onNext(Content.Chunk chunk) { 139 | chunk.release(); 140 | httpClient().getScheduler().schedule(() -> subscription.request(1), 100, TimeUnit.MILLISECONDS); 141 | } 142 | 143 | @Override 144 | public void onError(Throwable throwable) { 145 | } 146 | 147 | @Override 148 | public void onComplete() { 149 | publisher.offer(responseRef.get()); 150 | publisher.complete(); 151 | } 152 | }); 153 | 154 | assertTrue(completeLatch.await(15, TimeUnit.SECONDS)); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/test/java/org/eclipse/jetty/reactive/client/ReactorTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017 the original author or authors. 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 org.eclipse.jetty.reactive.client; 17 | 18 | import java.io.InterruptedIOException; 19 | import java.net.URI; 20 | import java.nio.ByteBuffer; 21 | import java.time.Duration; 22 | import java.util.Random; 23 | import java.util.concurrent.TimeoutException; 24 | import org.eclipse.jetty.http.HttpHeader; 25 | import org.eclipse.jetty.http.HttpStatus; 26 | import org.eclipse.jetty.io.Content; 27 | import org.eclipse.jetty.server.Handler; 28 | import org.eclipse.jetty.server.Request; 29 | import org.eclipse.jetty.server.Response; 30 | import org.eclipse.jetty.util.Callback; 31 | import org.junit.jupiter.params.ParameterizedTest; 32 | import org.junit.jupiter.params.provider.MethodSource; 33 | import org.springframework.http.client.reactive.JettyClientHttpConnector; 34 | import org.springframework.web.reactive.function.client.WebClient; 35 | import reactor.core.publisher.Hooks; 36 | import reactor.core.publisher.Mono; 37 | 38 | import static org.junit.jupiter.api.Assertions.assertArrayEquals; 39 | import static org.junit.jupiter.api.Assertions.assertEquals; 40 | import static org.junit.jupiter.api.Assertions.assertNotNull; 41 | 42 | public class ReactorTest extends AbstractTest { 43 | @ParameterizedTest 44 | @MethodSource("protocols") 45 | public void testResponseWithContent(String protocol) throws Exception { 46 | byte[] data = new byte[1024]; 47 | new Random().nextBytes(data); 48 | prepare(protocol, new Handler.Abstract() { 49 | @Override 50 | public boolean handle(Request request, Response response, Callback callback) { 51 | response.write(true, ByteBuffer.wrap(data), callback); 52 | return true; 53 | } 54 | }); 55 | 56 | WebClient client = WebClient.builder().clientConnector(new JettyClientHttpConnector(httpClient())).build(); 57 | byte[] responseContent = client.get() 58 | .uri(uri()) 59 | .retrieve() 60 | .bodyToMono(byte[].class) 61 | .block(); 62 | assertNotNull(responseContent); 63 | assertArrayEquals(data, responseContent); 64 | } 65 | 66 | @ParameterizedTest 67 | @MethodSource("protocols") 68 | public void testTotalTimeout(String protocol) throws Exception { 69 | long timeout = 1000; 70 | String result = "HELLO"; 71 | prepare(protocol, new Handler.Abstract() { 72 | @Override 73 | public boolean handle(Request request, Response response, Callback callback) throws Exception { 74 | try { 75 | Thread.sleep(2 * timeout); 76 | Content.Sink.write(response, true, result, callback); 77 | return true; 78 | } catch (InterruptedException x) { 79 | throw new InterruptedIOException(); 80 | } 81 | } 82 | }); 83 | 84 | // Suppresses weird exception thrown by Reactor. 85 | Hooks.onErrorDropped(t -> {}); 86 | 87 | String timeoutResult = "TIMEOUT"; 88 | String responseContent = WebClient.builder() 89 | .clientConnector(new JettyClientHttpConnector(httpClient())) 90 | .build() 91 | .get() 92 | .uri(new URI(uri())) 93 | .retrieve() 94 | .bodyToMono(String.class) 95 | .timeout(Duration.ofMillis(timeout)) 96 | .onErrorReturn(TimeoutException.class::isInstance, timeoutResult) 97 | .block(); 98 | 99 | assertEquals(timeoutResult, responseContent); 100 | } 101 | 102 | @ParameterizedTest 103 | @MethodSource("protocols") 104 | public void test401(String protocol) throws Exception { 105 | prepare(protocol, new Handler.Abstract() 106 | { 107 | @Override 108 | public boolean handle(Request request, Response response, Callback callback) { 109 | response.setStatus(HttpStatus.UNAUTHORIZED_401); 110 | response.getHeaders().add(HttpHeader.WWW_AUTHENTICATE, "Basic realm=\"test\""); 111 | callback.succeeded(); 112 | return true; 113 | } 114 | }); 115 | 116 | var request = httpClient().newRequest(uri()); 117 | ReactiveRequest reactiveRequest = ReactiveRequest.newBuilder(request).abortOnCancel(true).build(); 118 | Mono responseMono = Mono.fromDirect(reactiveRequest.response()); 119 | ReactiveResponse reactiveResponse = responseMono.block(Duration.ofSeconds(5)); 120 | 121 | assertNotNull(reactiveResponse); 122 | assertEquals(HttpStatus.UNAUTHORIZED_401, reactiveResponse.getStatus()); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/test/java/org/eclipse/jetty/reactive/client/RxJava2Test.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017 the original author or authors. 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 org.eclipse.jetty.reactive.client; 17 | 18 | import java.io.ByteArrayOutputStream; 19 | import java.io.IOException; 20 | import java.net.URI; 21 | import java.nio.ByteBuffer; 22 | import java.nio.CharBuffer; 23 | import java.nio.charset.Charset; 24 | import java.nio.charset.StandardCharsets; 25 | import java.util.ArrayList; 26 | import java.util.List; 27 | import java.util.Objects; 28 | import java.util.Random; 29 | import java.util.concurrent.CompletableFuture; 30 | import java.util.concurrent.CountDownLatch; 31 | import java.util.concurrent.ThreadLocalRandom; 32 | import java.util.concurrent.TimeUnit; 33 | import java.util.concurrent.atomic.AtomicInteger; 34 | import java.util.stream.Collectors; 35 | import java.util.stream.Stream; 36 | import io.reactivex.rxjava3.core.Emitter; 37 | import io.reactivex.rxjava3.core.Flowable; 38 | import io.reactivex.rxjava3.core.Single; 39 | import org.eclipse.jetty.http.HttpField; 40 | import org.eclipse.jetty.http.HttpHeader; 41 | import org.eclipse.jetty.http.HttpMethod; 42 | import org.eclipse.jetty.http.HttpStatus; 43 | import org.eclipse.jetty.io.ByteBufferPool; 44 | import org.eclipse.jetty.io.Content; 45 | import org.eclipse.jetty.io.RetainableByteBuffer; 46 | import org.eclipse.jetty.reactive.client.internal.AbstractSinglePublisher; 47 | import org.eclipse.jetty.server.Handler; 48 | import org.eclipse.jetty.server.Request; 49 | import org.eclipse.jetty.server.Response; 50 | import org.eclipse.jetty.util.Blocker; 51 | import org.eclipse.jetty.util.BufferUtil; 52 | import org.eclipse.jetty.util.Callback; 53 | import org.eclipse.jetty.util.IteratingCallback; 54 | import org.eclipse.jetty.util.MathUtils; 55 | import org.eclipse.jetty.util.thread.AutoLock; 56 | import org.junit.jupiter.api.Tag; 57 | import org.junit.jupiter.api.Test; 58 | import org.junit.jupiter.params.ParameterizedTest; 59 | import org.junit.jupiter.params.provider.MethodSource; 60 | import org.reactivestreams.Publisher; 61 | import org.reactivestreams.Subscriber; 62 | import org.reactivestreams.Subscription; 63 | 64 | import static java.nio.charset.StandardCharsets.UTF_8; 65 | import static org.awaitility.Awaitility.await; 66 | import static org.hamcrest.Matchers.is; 67 | import static org.hamcrest.Matchers.notNullValue; 68 | import static org.junit.jupiter.api.Assertions.assertArrayEquals; 69 | import static org.junit.jupiter.api.Assertions.assertEquals; 70 | import static org.junit.jupiter.api.Assertions.assertTrue; 71 | 72 | public class RxJava2Test extends AbstractTest { 73 | @Test 74 | @Tag("external") 75 | public void testExternalServer() throws Exception { 76 | prepare("http", new Handler.Abstract() { 77 | @Override 78 | public boolean handle(Request request, Response response, Callback callback) { 79 | callback.succeeded(); 80 | return true; 81 | } 82 | }); 83 | 84 | CountDownLatch contentLatch = new CountDownLatch(1); 85 | CountDownLatch responseLatch = new CountDownLatch(1); 86 | URI uri = URI.create("https://example.org"); 87 | ReactiveRequest request = ReactiveRequest.newBuilder(httpClient().newRequest(uri)).build(); 88 | Flowable.fromPublisher(request.response((reactiveResponse, chunkPublisher) -> Flowable.fromPublisher(chunkPublisher) 89 | .map(chunk -> { 90 | ByteBuffer byteBuffer = chunk.getByteBuffer(); 91 | CharBuffer charBuffer = UTF_8.decode(byteBuffer); 92 | chunk.release(); 93 | return charBuffer.toString(); 94 | }).doOnComplete(contentLatch::countDown))) 95 | .doOnComplete(responseLatch::countDown) 96 | .subscribe(); 97 | 98 | assertTrue(responseLatch.await(5, TimeUnit.SECONDS)); 99 | } 100 | 101 | @ParameterizedTest 102 | @MethodSource("protocols") 103 | public void testSimpleUsage(String protocol) throws Exception { 104 | prepare(protocol, new Handler.Abstract() { 105 | @Override 106 | public boolean handle(Request request, Response response, Callback callback) { 107 | callback.succeeded(); 108 | return true; 109 | } 110 | }); 111 | 112 | ReactiveRequest request = ReactiveRequest.newBuilder(httpClient().newRequest(uri())).build(); 113 | int status = Single.fromPublisher(request.response(ReactiveResponse.Content.discard())) 114 | .map(ReactiveResponse::getStatus) 115 | .blockingGet(); 116 | 117 | assertEquals(status, HttpStatus.OK_200); 118 | } 119 | 120 | @ParameterizedTest 121 | @MethodSource("protocols") 122 | public void testRequestEvents(String protocol) throws Exception { 123 | prepare(protocol, new Handler.Abstract() { 124 | @Override 125 | public boolean handle(Request request, Response response, Callback callback) { 126 | callback.succeeded(); 127 | return true; 128 | } 129 | 130 | }); 131 | 132 | ReactiveRequest request = ReactiveRequest.newBuilder(httpClient(), uri()).build(); 133 | Publisher events = request.requestEvents(); 134 | 135 | CountDownLatch latch = new CountDownLatch(1); 136 | List names = new ArrayList<>(); 137 | Flowable.fromPublisher(events) 138 | .map(ReactiveRequest.Event::getType) 139 | .map(ReactiveRequest.Event.Type::name) 140 | .subscribe(new Subscriber<>() { 141 | private Subscription subscription; 142 | 143 | @Override 144 | public void onSubscribe(Subscription subscription) { 145 | this.subscription = subscription; 146 | subscription.request(1); 147 | } 148 | 149 | @Override 150 | public void onNext(String name) { 151 | names.add(name); 152 | subscription.request(1); 153 | } 154 | 155 | @Override 156 | public void onError(Throwable throwable) { 157 | } 158 | 159 | @Override 160 | public void onComplete() { 161 | latch.countDown(); 162 | } 163 | }); 164 | 165 | ReactiveResponse response = Single.fromPublisher(request.response()).blockingGet(); 166 | 167 | assertEquals(response.getStatus(), HttpStatus.OK_200); 168 | assertTrue(latch.await(5, TimeUnit.SECONDS)); 169 | 170 | List expected = Stream.of( 171 | ReactiveRequest.Event.Type.QUEUED, 172 | ReactiveRequest.Event.Type.BEGIN, 173 | ReactiveRequest.Event.Type.HEADERS, 174 | ReactiveRequest.Event.Type.COMMIT, 175 | ReactiveRequest.Event.Type.SUCCESS) 176 | .map(Enum::name) 177 | .collect(Collectors.toList()); 178 | assertEquals(names, expected); 179 | } 180 | 181 | @ParameterizedTest 182 | @MethodSource("protocols") 183 | public void testResponseEvents(String protocol) throws Exception { 184 | prepare(protocol, new Handler.Abstract() { 185 | @Override 186 | public boolean handle(Request request, Response response, Callback callback) { 187 | response.write(true, ByteBuffer.wrap(new byte[]{0}), callback); 188 | return true; 189 | } 190 | }); 191 | 192 | ReactiveRequest request = ReactiveRequest.newBuilder(httpClient(), uri()).build(); 193 | Publisher events = request.responseEvents(); 194 | 195 | CountDownLatch latch = new CountDownLatch(1); 196 | List names = new ArrayList<>(); 197 | Flowable.fromPublisher(events) 198 | .map(ReactiveResponse.Event::getType) 199 | .map(ReactiveResponse.Event.Type::name) 200 | .subscribe(new Subscriber<>() { 201 | private Subscription subscription; 202 | 203 | @Override 204 | public void onSubscribe(Subscription subscription) { 205 | this.subscription = subscription; 206 | subscription.request(1); 207 | } 208 | 209 | @Override 210 | public void onNext(String name) { 211 | names.add(name); 212 | subscription.request(1); 213 | } 214 | 215 | @Override 216 | public void onError(Throwable throwable) { 217 | } 218 | 219 | @Override 220 | public void onComplete() { 221 | latch.countDown(); 222 | } 223 | }); 224 | 225 | ReactiveResponse response = Single.fromPublisher(request.response()).blockingGet(); 226 | 227 | assertEquals(response.getStatus(), HttpStatus.OK_200); 228 | assertTrue(latch.await(5, TimeUnit.SECONDS)); 229 | 230 | List expected = Stream.of( 231 | ReactiveResponse.Event.Type.BEGIN, 232 | ReactiveResponse.Event.Type.HEADERS, 233 | ReactiveResponse.Event.Type.CONTENT, 234 | ReactiveResponse.Event.Type.SUCCESS, 235 | ReactiveResponse.Event.Type.COMPLETE) 236 | .map(Enum::name) 237 | .collect(Collectors.toList()); 238 | assertEquals(names, expected); 239 | } 240 | 241 | @ParameterizedTest 242 | @MethodSource("protocols") 243 | public void testRequestBody(String protocol) throws Exception { 244 | prepare(protocol, new Handler.Abstract() { 245 | @Override 246 | public boolean handle(Request request, Response response, Callback callback) { 247 | HttpField contentType = request.getHeaders().getField(HttpHeader.CONTENT_TYPE); 248 | if (contentType != null) { 249 | response.getHeaders().put(contentType); 250 | } 251 | Content.copy(request, response, callback); 252 | return true; 253 | } 254 | }); 255 | 256 | String text = "Γειά σου Κόσμε"; 257 | ReactiveRequest request = ReactiveRequest.newBuilder(httpClient(), uri()) 258 | .content(ReactiveRequest.Content.fromString(text, "text/plain", UTF_8)) 259 | .build(); 260 | 261 | String content = Single.fromPublisher(request.response(ReactiveResponse.Content.asString())) 262 | .blockingGet(); 263 | 264 | assertEquals(content, text); 265 | } 266 | 267 | @ParameterizedTest 268 | @MethodSource("protocols") 269 | public void testFlowableRequestBody(String protocol) throws Exception { 270 | prepare(protocol, new Handler.Abstract() { 271 | @Override 272 | public boolean handle(Request request, Response response, Callback callback) { 273 | HttpField contentType = request.getHeaders().getField(HttpHeader.CONTENT_TYPE); 274 | if (contentType != null) { 275 | response.getHeaders().put(contentType); 276 | } 277 | Content.copy(request, response, callback); 278 | return true; 279 | } 280 | }); 281 | 282 | String data = "01234"; 283 | // Data from a generic stream (the regexp will split the data into single characters). 284 | Flowable stream = Flowable.fromArray(data.split("(?!^)")); 285 | 286 | // Transform it to chunks, showing what you can use the callback for. 287 | Charset charset = UTF_8; 288 | ByteBufferPool bufferPool = httpClient().getByteBufferPool(); 289 | Flowable chunks = stream.map(item -> item.getBytes(charset)) 290 | .map(bytes -> { 291 | RetainableByteBuffer buffer = bufferPool.acquire(bytes.length, true); 292 | BufferUtil.append(buffer.getByteBuffer(), bytes, 0, bytes.length); 293 | return buffer; 294 | }) 295 | .map(buffer -> Content.Chunk.from(buffer.getByteBuffer(), false, buffer::release)); 296 | 297 | ReactiveRequest request = ReactiveRequest.newBuilder(httpClient(), uri()) 298 | .content(ReactiveRequest.Content.fromPublisher(chunks, "text/plain", charset)) 299 | .build(); 300 | 301 | String content = Single.fromPublisher(request.response(ReactiveResponse.Content.asString())) 302 | .blockingGet(); 303 | 304 | assertEquals(content, data); 305 | } 306 | 307 | @ParameterizedTest 308 | @MethodSource("protocols") 309 | public void testResponseBody(String protocol) throws Exception { 310 | Charset charset = StandardCharsets.UTF_16; 311 | String data = "\u20ac"; 312 | prepare(protocol, new Handler.Abstract() { 313 | @Override 314 | public boolean handle(Request request, Response response, Callback callback) { 315 | response.getHeaders().put(HttpHeader.CONTENT_TYPE, "text/plain;charset=" + charset.name()); 316 | response.write(true, ByteBuffer.wrap(data.getBytes(charset)), callback); 317 | return true; 318 | } 319 | }); 320 | 321 | ReactiveRequest request = ReactiveRequest.newBuilder(httpClient(), uri()).build(); 322 | Publisher publisher = request.response(ReactiveResponse.Content.asString()); 323 | String text = Single.fromPublisher(publisher).blockingGet(); 324 | 325 | assertEquals(text, data); 326 | } 327 | 328 | @ParameterizedTest 329 | @MethodSource("protocols") 330 | public void testFlowableResponseBody(String protocol) throws Exception { 331 | byte[] data = new byte[1024]; 332 | new Random().nextBytes(data); 333 | prepare(protocol, new Handler.Abstract() { 334 | @Override 335 | public boolean handle(Request request, Response response, Callback callback) { 336 | response.write(true, ByteBuffer.wrap(data), callback); 337 | return true; 338 | } 339 | }); 340 | 341 | ReactiveRequest request = ReactiveRequest.newBuilder(httpClient(), uri()).build(); 342 | byte[] bytes = Flowable.fromPublisher(request.response((response, content) -> content)) 343 | .flatMap(chunk -> Flowable.generate((Emitter emitter) -> { 344 | ByteBuffer buffer = chunk.getByteBuffer(); 345 | if (buffer.hasRemaining()) { 346 | emitter.onNext(buffer.get()); 347 | } else { 348 | chunk.release(); 349 | emitter.onComplete(); 350 | } 351 | })) 352 | .reduce(new ByteArrayOutputStream(), (acc, b) -> { 353 | acc.write(b); 354 | return acc; 355 | }) 356 | .map(ByteArrayOutputStream::toByteArray) 357 | .blockingGet(); 358 | 359 | assertArrayEquals(bytes, data); 360 | } 361 | 362 | @ParameterizedTest 363 | @MethodSource("protocols") 364 | public void testFlowableResponseThenBody(String protocol) throws Exception { 365 | String pangram = "quizzical twins proved my hijack bug fix"; 366 | prepare(protocol, new Handler.Abstract() { 367 | @Override 368 | public boolean handle(Request request, Response response, Callback callback) { 369 | response.getHeaders().put(HttpHeader.CONTENT_TYPE, "text/plain"); 370 | Content.Sink.write(response, true, pangram, callback); 371 | return true; 372 | } 373 | }); 374 | 375 | ReactiveRequest request = ReactiveRequest.newBuilder(httpClient(), uri()).build(); 376 | ReactiveResponse.Result> result = Single.fromPublisher(request.response((response, content) -> 377 | Flowable.just(new ReactiveResponse.Result<>(response, content)))).blockingGet(); 378 | 379 | assertEquals(result.response().getStatus(), HttpStatus.OK_200); 380 | 381 | String responseContent = Single.fromPublisher(ReactiveResponse.Content.asString().apply(result.response(), result.content())) 382 | .blockingGet(); 383 | 384 | assertEquals(responseContent, pangram); 385 | } 386 | 387 | @ParameterizedTest 388 | @MethodSource("protocols") 389 | public void testFlowableResponseBodyBackPressure(String protocol) throws Exception { 390 | prepare(protocol, new Handler.Abstract() { 391 | @Override 392 | public boolean handle(Request request, Response response, Callback callback) { 393 | response.getHeaders().put(HttpHeader.CONTENT_TYPE, "text/plain"); 394 | Callback.Completable completable = new Callback.Completable(); 395 | Content.Sink.write(response, false, "hello", completable); 396 | completable.thenRun(() -> Content.Sink.write(response, true, "world", callback)); 397 | return true; 398 | } 399 | }); 400 | 401 | AtomicInteger chunks = new AtomicInteger(); 402 | ReactiveRequest request = ReactiveRequest.newBuilder(httpClient(), uri()).build(); 403 | request.getRequest().onResponseContent((response, chunk) -> chunks.incrementAndGet()); 404 | 405 | Publisher publisher = request.response((response, content) -> content); 406 | 407 | CountDownLatch completeLatch = new CountDownLatch(1); 408 | var subscriber = new Subscriber() { 409 | private Subscription subscription; 410 | private Content.Chunk chunk; 411 | 412 | @Override 413 | public void onSubscribe(Subscription subscription) { 414 | this.subscription = subscription; 415 | } 416 | 417 | @Override 418 | public void onNext(Content.Chunk chunk) { 419 | this.chunk = chunk; 420 | } 421 | 422 | @Override 423 | public void onError(Throwable throwable) { 424 | } 425 | 426 | @Override 427 | public void onComplete() { 428 | completeLatch.countDown(); 429 | } 430 | }; 431 | publisher.subscribe(subscriber); 432 | 433 | // There is no demand yet, so there should be no chunks. 434 | assertEquals(0, chunks.get()); 435 | 436 | // Send the request and demand 1 chunk of content. 437 | subscriber.subscription.request(1); 438 | 439 | // There should be 1 chunk only. 440 | await().during(1, TimeUnit.SECONDS).atMost(5, TimeUnit.SECONDS).until(chunks::get, is(1)); 441 | Content.Chunk chunk = await().atMost(5, TimeUnit.SECONDS).until(() -> subscriber.chunk, notNullValue()); 442 | subscriber.chunk = null; 443 | assertEquals("hello", UTF_8.decode(chunk.getByteBuffer()).toString()); 444 | 445 | // Wait to be sure there is backpressure. 446 | await().during(1, TimeUnit.SECONDS).atMost(5, TimeUnit.SECONDS).until(chunks::get, is(1)); 447 | 448 | // Demand 1 more chunk. 449 | subscriber.subscription.request(1); 450 | chunk = await().atMost(5, TimeUnit.SECONDS).until(() -> subscriber.chunk, notNullValue()); 451 | subscriber.chunk = null; 452 | assertEquals("world", UTF_8.decode(chunk.getByteBuffer()).toString()); 453 | 454 | // Demand completion. 455 | subscriber.subscription.request(1); 456 | 457 | assertTrue(completeLatch.await(5, TimeUnit.SECONDS)); 458 | } 459 | 460 | @ParameterizedTest 461 | @MethodSource("protocols") 462 | public void testFlowableResponsePipedToRequest(String protocol) throws Exception { 463 | String data1 = "data1"; 464 | String data2 = "data2"; 465 | prepare(protocol, new Handler.Abstract() { 466 | @Override 467 | public boolean handle(Request request, Response response, Callback callback) throws Exception { 468 | response.getHeaders().put(HttpHeader.CONTENT_TYPE, "text/plain"); 469 | String target = Request.getPathInContext(request); 470 | if ("/1".equals(target)) { 471 | Content.Sink.write(response, true, data1, callback); 472 | } else if ("/2".equals(target)) { 473 | if (Content.Source.asString(request).equals(data1)) { 474 | Content.Sink.write(response, true, data2, callback); 475 | } 476 | } 477 | return true; 478 | } 479 | }); 480 | 481 | ReactiveRequest request1 = ReactiveRequest.newBuilder(httpClient().newRequest(uri()).path("/1")).build(); 482 | Publisher sender1 = request1.response((response1, content1) -> { 483 | ReactiveRequest request2 = ReactiveRequest.newBuilder(httpClient().newRequest(uri()).path("/2")) 484 | .content(ReactiveRequest.Content.fromPublisher(content1, "text/plain")).build(); 485 | return request2.response(ReactiveResponse.Content.asString()); 486 | }); 487 | String result = Single.fromPublisher(sender1).blockingGet(); 488 | 489 | assertEquals(result, data2); 490 | } 491 | 492 | @ParameterizedTest 493 | @MethodSource("protocols") 494 | public void testFlowableTimeout(String protocol) throws Exception { 495 | long timeout = 500; 496 | prepare(protocol, new Handler.Abstract() { 497 | @Override 498 | public boolean handle(Request request, Response response, Callback callback) throws Exception { 499 | Thread.sleep(timeout * 2); 500 | callback.succeeded(); 501 | return true; 502 | } 503 | }); 504 | 505 | CountDownLatch latch = new CountDownLatch(1); 506 | ReactiveRequest request = ReactiveRequest.newBuilder(httpClient().newRequest(uri())).build(); 507 | Single.fromPublisher(request.response(ReactiveResponse.Content.discard())) 508 | .map(ReactiveResponse::getStatus) 509 | .timeout(timeout, TimeUnit.MILLISECONDS) 510 | .subscribe((status, failure) -> { 511 | if (failure != null) { 512 | latch.countDown(); 513 | } 514 | }); 515 | 516 | assertTrue(latch.await(timeout * 2, TimeUnit.MILLISECONDS)); 517 | } 518 | 519 | @ParameterizedTest 520 | @MethodSource("protocols") 521 | public void testDelayedContentSubscriptionWithoutResponseContent(String protocol) throws Exception { 522 | prepare(protocol, new Handler.Abstract() { 523 | @Override 524 | public boolean handle(Request request, Response response, Callback callback) { 525 | callback.succeeded(); 526 | return true; 527 | } 528 | }); 529 | 530 | long delay = 1000; 531 | CountDownLatch latch = new CountDownLatch(1); 532 | ReactiveRequest request = ReactiveRequest.newBuilder(httpClient().newRequest(uri())).build(); 533 | Single.fromPublisher(request.response((response, content) -> 534 | // Subscribe to the content after a delay, 535 | // discard the content and emit the response. 536 | Flowable.fromPublisher(content) 537 | .delaySubscription(delay, TimeUnit.MILLISECONDS) 538 | .doOnNext(Content.Chunk::release) 539 | .filter(chunk -> false) 540 | .isEmpty() 541 | .map(empty -> response) 542 | .toFlowable())) 543 | .subscribe(response -> latch.countDown()); 544 | 545 | assertTrue(latch.await(delay * 2, TimeUnit.MILLISECONDS)); 546 | } 547 | 548 | @ParameterizedTest 549 | @MethodSource("protocols") 550 | public void testConnectTimeout(String protocol) throws Exception { 551 | prepare(protocol, new Handler.Abstract() { 552 | @Override 553 | public boolean handle(Request request, Response response, Callback callback) { 554 | callback.succeeded(); 555 | return true; 556 | } 557 | }); 558 | String uri = uri(); 559 | server.stop(); 560 | 561 | long connectTimeout = 500; 562 | httpClient().setConnectTimeout(connectTimeout); 563 | CountDownLatch latch = new CountDownLatch(1); 564 | ReactiveRequest request = ReactiveRequest.newBuilder(httpClient().newRequest(uri)).build(); 565 | Single.fromPublisher(request.response(ReactiveResponse.Content.discard())) 566 | .map(ReactiveResponse::getStatus) 567 | .subscribe((status, failure) -> { 568 | if (failure != null) { 569 | latch.countDown(); 570 | } 571 | }); 572 | 573 | assertTrue(latch.await(connectTimeout * 2, TimeUnit.MILLISECONDS)); 574 | } 575 | 576 | @ParameterizedTest 577 | @MethodSource("protocols") 578 | public void testResponseContentTimeout(String protocol) throws Exception { 579 | long timeout = 500; 580 | byte[] data = "hello".getBytes(UTF_8); 581 | prepare(protocol, new Handler.Abstract() { 582 | @Override 583 | public boolean handle(Request request, Response response, Callback callback) throws Exception { 584 | response.getHeaders().put(HttpHeader.CONTENT_LENGTH, data.length); 585 | response.write(false, null, Callback.NOOP); 586 | Thread.sleep(timeout * 2); 587 | response.write(true, ByteBuffer.wrap(data), callback); 588 | return true; 589 | } 590 | }); 591 | 592 | CountDownLatch latch = new CountDownLatch(1); 593 | ReactiveRequest request = ReactiveRequest.newBuilder(httpClient().newRequest(uri()).timeout(timeout, TimeUnit.MILLISECONDS)).build(); 594 | Single.fromPublisher(request.response((response, content) -> { 595 | int status = response.getStatus(); 596 | if (status != HttpStatus.OK_200) { 597 | ReactiveResponse.Content.discard().apply(response, content); 598 | return Flowable.error(new IOException(String.valueOf(status))); 599 | } else { 600 | return ReactiveResponse.Content.asString().apply(response, content); 601 | } 602 | })).subscribe((content, failure) -> { 603 | if (failure != null) { 604 | latch.countDown(); 605 | } 606 | }); 607 | 608 | assertTrue(latch.await(timeout * 2, TimeUnit.MILLISECONDS)); 609 | } 610 | 611 | @ParameterizedTest 612 | @MethodSource("protocols") 613 | public void testBufferedResponseContent(String protocol) throws Exception { 614 | Random random = new Random(); 615 | byte[] content1 = new byte[1024]; 616 | random.nextBytes(content1); 617 | byte[] content2 = new byte[2048]; 618 | random.nextBytes(content2); 619 | byte[] original = new byte[content1.length + content2.length]; 620 | System.arraycopy(content1, 0, original, 0, content1.length); 621 | System.arraycopy(content2, 0, original, content1.length, content2.length); 622 | 623 | prepare(protocol, new Handler.Abstract() { 624 | @Override 625 | public boolean handle(Request request, Response response, Callback callback) throws Exception { 626 | response.getHeaders().put(HttpHeader.CONTENT_LENGTH, original.length); 627 | try (Blocker.Callback c = Blocker.callback()) { 628 | response.write(false, ByteBuffer.wrap(content1), c); 629 | c.block(); 630 | } 631 | Thread.sleep(500); 632 | response.write(true, ByteBuffer.wrap(content2), callback); 633 | return true; 634 | } 635 | }); 636 | 637 | ReactiveRequest request = ReactiveRequest.newBuilder(httpClient().newRequest(uri())).build(); 638 | 639 | Publisher> publisher = request.response((response, content) -> { 640 | if (response.getStatus() == HttpStatus.OK_200) { 641 | return ReactiveResponse.Content.asStringResult().apply(response, content); 642 | } else { 643 | return ReactiveResponse.Content.asDiscardResult().apply(response, content); 644 | } 645 | }); 646 | 647 | ReactiveResponse.Result result = Single.fromPublisher(publisher) 648 | .blockingGet(); 649 | 650 | assertEquals(result.response().getStatus(), HttpStatus.OK_200); 651 | 652 | String expected = UTF_8.decode(ByteBuffer.allocate(original.length) 653 | .put(original) 654 | .flip()).toString(); 655 | assertEquals(expected, result.content()); 656 | } 657 | 658 | @ParameterizedTest 659 | @MethodSource("protocols") 660 | public void testSlowDownload(String protocol) throws Exception { 661 | byte[] bytes = new byte[512 * 1024]; 662 | ThreadLocalRandom.current().nextBytes(bytes); 663 | ByteBuffer byteBuffer = ByteBuffer.wrap(bytes); 664 | prepare(protocol, new Handler.Abstract() { 665 | @Override 666 | public boolean handle(Request request, Response response, Callback callback) { 667 | new IteratingCallback() { 668 | private boolean last; 669 | 670 | @Override 671 | protected Action process() throws Exception { 672 | if (last) { 673 | return Action.SUCCEEDED; 674 | } 675 | // Write slowly 1 byte at a time. 676 | if (byteBuffer.position() % 256 == 0) { 677 | Thread.sleep(1); 678 | } 679 | ByteBuffer data = ByteBuffer.wrap(new byte[]{byteBuffer.get()}); 680 | last = !byteBuffer.hasRemaining(); 681 | response.write(last, data, this); 682 | return Action.SCHEDULED; 683 | } 684 | 685 | @Override 686 | protected void onCompleteSuccess() { 687 | callback.succeeded(); 688 | } 689 | 690 | @Override 691 | protected void onCompleteFailure(Throwable cause) { 692 | callback.failed(cause); 693 | } 694 | }.iterate(); 695 | return true; 696 | } 697 | }); 698 | 699 | ReactiveRequest request = ReactiveRequest.newBuilder(httpClient().newRequest(uri())).build(); 700 | 701 | ReactiveResponse.Result result = Single.fromPublisher(request.response(ReactiveResponse.Content.asByteBufferResult())) 702 | .blockingGet(); 703 | assertEquals(HttpStatus.OK_200, result.response().getStatus()); 704 | assertArrayEquals(bytes, BufferUtil.toArray(result.content())); 705 | } 706 | 707 | @ParameterizedTest 708 | @MethodSource("protocols") 709 | public void testReproducibleContent(String protocol) throws Exception { 710 | prepare(protocol, new Handler.Abstract() { 711 | @Override 712 | public boolean handle(Request request, Response response, Callback callback) { 713 | String target = Request.getPathInContext(request); 714 | if (!target.equals("/ok")) { 715 | Response.sendRedirect(request, response, callback, HttpStatus.TEMPORARY_REDIRECT_307, "/ok", true); 716 | } else { 717 | Content.copy(request, response, callback); 718 | } 719 | return true; 720 | } 721 | }); 722 | 723 | String text = "hello world"; 724 | ReactiveRequest request = ReactiveRequest.newBuilder(httpClient().newRequest(uri()).method(HttpMethod.POST)) 725 | .content(ReactiveRequest.Content.fromString(text, "text/plain", UTF_8)) 726 | .build(); 727 | String content = Single.fromPublisher(request.response(ReactiveResponse.Content.asString())) 728 | .blockingGet(); 729 | 730 | assertEquals(text, content); 731 | } 732 | 733 | @ParameterizedTest 734 | @MethodSource("protocols") 735 | public void testReproducibleContentSplitAndDelayed(String protocol) throws Exception { 736 | prepare(protocol, new Handler.Abstract() { 737 | @Override 738 | public boolean handle(Request request, Response response, Callback callback) { 739 | String target = Request.getPathInContext(request); 740 | if (!target.equals("/ok")) { 741 | Response.sendRedirect(request, response, callback, HttpStatus.TEMPORARY_REDIRECT_307, "/ok", true); 742 | } else { 743 | Content.copy(request, response, callback); 744 | } 745 | return true; 746 | } 747 | }); 748 | 749 | String text1 = "hello"; 750 | String text2 = "world"; 751 | ChunkListSinglePublisher publisher = new ChunkListSinglePublisher(); 752 | // Offer content to trigger the sending of the request and the processing on the server. 753 | publisher.offer(UTF_8.encode(text1)); 754 | ReactiveRequest request = ReactiveRequest.newBuilder(httpClient().newRequest(uri()).method(HttpMethod.POST)) 755 | .content(ReactiveRequest.Content.fromPublisher(publisher, "text/plain", UTF_8)) 756 | .build(); 757 | Single flow = Single.fromPublisher(request.response(ReactiveResponse.Content.asString())); 758 | // Send the request by subscribing as a CompletableFuture. 759 | CompletableFuture completable = flow.toCompletionStage().toCompletableFuture(); 760 | 761 | // Allow the redirect to happen. 762 | Thread.sleep(1000); 763 | 764 | publisher.offer(UTF_8.encode(text2)); 765 | publisher.complete(); 766 | 767 | assertEquals(text1 + text2, completable.get(5, TimeUnit.SECONDS)); 768 | } 769 | 770 | private static class ChunkListSinglePublisher extends AbstractSinglePublisher { 771 | private final List byteBuffers = new ArrayList<>(); 772 | private boolean complete; 773 | private boolean stalled; 774 | private long demand; 775 | private int index; 776 | 777 | private ChunkListSinglePublisher() { 778 | reset(); 779 | } 780 | 781 | private void reset() { 782 | try (AutoLock ignored = lock()) { 783 | complete = false; 784 | stalled = true; 785 | demand = 0; 786 | index = 0; 787 | } 788 | } 789 | 790 | private void offer(ByteBuffer byteBuffer) { 791 | Subscriber subscriber; 792 | try (AutoLock ignored = lock()) { 793 | byteBuffers.add(Objects.requireNonNull(byteBuffer)); 794 | subscriber = subscriber(); 795 | if (subscriber != null) { 796 | stalled = false; 797 | } 798 | } 799 | if (subscriber != null) { 800 | proceed(subscriber); 801 | } 802 | } 803 | 804 | private void complete() { 805 | complete = true; 806 | Subscriber subscriber = subscriber(); 807 | if (subscriber != null) { 808 | proceed(subscriber); 809 | } 810 | } 811 | 812 | @Override 813 | protected void onRequest(Subscriber subscriber, long n) { 814 | boolean proceed = false; 815 | try (AutoLock ignored = lock()) { 816 | demand = MathUtils.cappedAdd(demand, n); 817 | if (stalled) { 818 | stalled = false; 819 | proceed = true; 820 | } 821 | } 822 | if (proceed) { 823 | proceed(subscriber); 824 | } 825 | } 826 | 827 | private void proceed(Subscriber subscriber) { 828 | while (true) { 829 | ByteBuffer byteBuffer = null; 830 | boolean notify = false; 831 | try (AutoLock ignored = lock()) { 832 | if (index < byteBuffers.size()) { 833 | if (demand > 0) { 834 | byteBuffer = byteBuffers.get(index); 835 | ++index; 836 | --demand; 837 | notify = true; 838 | } else { 839 | stalled = true; 840 | } 841 | } else { 842 | if (complete) { 843 | notify = true; 844 | } else { 845 | stalled = true; 846 | } 847 | } 848 | } 849 | if (notify) { 850 | if (byteBuffer != null) { 851 | emitOnNext(subscriber, Content.Chunk.from(byteBuffer.slice(), false)); 852 | continue; 853 | } else { 854 | emitOnComplete(subscriber); 855 | } 856 | } 857 | break; 858 | } 859 | } 860 | 861 | @Override 862 | public void cancel() { 863 | reset(); 864 | super.cancel(); 865 | } 866 | } 867 | } 868 | -------------------------------------------------------------------------------- /src/test/java/org/eclipse/jetty/reactive/client/internal/QueuedSinglePublisherTCKTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017 the original author or authors. 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 org.eclipse.jetty.reactive.client.internal; 17 | 18 | import org.eclipse.jetty.reactive.client.AbstractTest; 19 | import org.junit.jupiter.api.BeforeEach; 20 | import org.junit.jupiter.api.TestInfo; 21 | import org.reactivestreams.Publisher; 22 | import org.reactivestreams.tck.PublisherVerification; 23 | import org.reactivestreams.tck.TestEnvironment; 24 | 25 | public class QueuedSinglePublisherTCKTest extends PublisherVerification { 26 | public QueuedSinglePublisherTCKTest() { 27 | super(new TestEnvironment()); 28 | } 29 | 30 | @BeforeEach 31 | public void before(TestInfo testInfo) { 32 | AbstractTest.printTestName(testInfo); 33 | } 34 | 35 | @Override 36 | public Publisher createPublisher(long elements) { 37 | QueuedSinglePublisher publisher = new QueuedSinglePublisher<>(); 38 | for (int i = 0; i < elements; ++i) { 39 | publisher.offer("element_" + i); 40 | } 41 | publisher.complete(); 42 | return publisher; 43 | } 44 | 45 | @Override 46 | public Publisher createFailedPublisher() { 47 | QueuedSinglePublisher publisher = new QueuedSinglePublisher<>(); 48 | publisher.cancel(); 49 | return publisher; 50 | } 51 | 52 | @Override 53 | public long maxElementsFromPublisher() { 54 | return 1024; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/test/java/org/eclipse/jetty/reactive/client/internal/QueuedSinglePublisherTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017 the original author or authors. 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 org.eclipse.jetty.reactive.client.internal; 17 | 18 | import java.util.concurrent.CountDownLatch; 19 | import java.util.concurrent.TimeUnit; 20 | import java.util.concurrent.atomic.AtomicInteger; 21 | 22 | import org.eclipse.jetty.reactive.client.AbstractTest; 23 | import org.junit.jupiter.api.BeforeEach; 24 | import org.junit.jupiter.api.Test; 25 | import org.junit.jupiter.api.TestInfo; 26 | import org.reactivestreams.Subscriber; 27 | import org.reactivestreams.Subscription; 28 | 29 | import static org.junit.jupiter.api.Assertions.assertThrows; 30 | import static org.junit.jupiter.api.Assertions.assertTrue; 31 | 32 | public class QueuedSinglePublisherTest { 33 | @BeforeEach 34 | public void before(TestInfo testInfo) { 35 | AbstractTest.printTestName(testInfo); 36 | } 37 | 38 | @Test 39 | public void testReentrancyFromOnNextToOnComplete() throws Exception { 40 | QueuedSinglePublisher publisher = new QueuedSinglePublisher<>(); 41 | 42 | CountDownLatch latch = new CountDownLatch(1); 43 | AtomicInteger next = new AtomicInteger(); 44 | publisher.subscribe(new Subscriber<>() { 45 | @Override 46 | public void onSubscribe(Subscription subscription) { 47 | subscription.request(1); 48 | } 49 | 50 | @Override 51 | public void onNext(Runnable item) { 52 | item.run(); 53 | next.incrementAndGet(); 54 | } 55 | 56 | @Override 57 | public void onError(Throwable throwable) { 58 | } 59 | 60 | @Override 61 | public void onComplete() { 62 | if (next.get() > 0) { 63 | latch.countDown(); 64 | } 65 | } 66 | }); 67 | 68 | // We offer a Runnable that will call complete(). 69 | publisher.offer(publisher::complete); 70 | 71 | assertTrue(latch.await(5, TimeUnit.SECONDS)); 72 | } 73 | 74 | @Test 75 | public void testReentrancyFromOnNextToOnError() throws Exception { 76 | QueuedSinglePublisher publisher = new QueuedSinglePublisher<>(); 77 | 78 | CountDownLatch latch = new CountDownLatch(1); 79 | AtomicInteger next = new AtomicInteger(); 80 | publisher.subscribe(new Subscriber<>() { 81 | @Override 82 | public void onSubscribe(Subscription subscription) { 83 | subscription.request(1); 84 | } 85 | 86 | @Override 87 | public void onNext(Runnable item) { 88 | item.run(); 89 | next.incrementAndGet(); 90 | } 91 | 92 | @Override 93 | public void onError(Throwable throwable) { 94 | if (next.get() > 0) { 95 | latch.countDown(); 96 | } 97 | } 98 | 99 | @Override 100 | public void onComplete() { 101 | } 102 | }); 103 | 104 | // We offer a Runnable that will call complete(). 105 | publisher.offer(() -> publisher.fail(new Exception())); 106 | 107 | assertTrue(latch.await(5, TimeUnit.SECONDS)); 108 | } 109 | 110 | @Test 111 | public void testOfferAfterComplete() { 112 | QueuedSinglePublisher publisher = new QueuedSinglePublisher<>(); 113 | publisher.offer(() -> {}); 114 | publisher.complete(); 115 | assertThrows(IllegalStateException.class, () -> publisher.offer(() -> {})); 116 | } 117 | 118 | @Test 119 | public void testCompleteWithoutDemand() throws Exception { 120 | QueuedSinglePublisher publisher = new QueuedSinglePublisher<>(); 121 | 122 | CountDownLatch latch = new CountDownLatch(1); 123 | publisher.subscribe(new Subscriber<>() { 124 | @Override 125 | public void onSubscribe(Subscription subscription) { 126 | } 127 | 128 | @Override 129 | public void onNext(Runnable runnable) { 130 | } 131 | 132 | @Override 133 | public void onError(Throwable throwable) { 134 | } 135 | 136 | @Override 137 | public void onComplete() { 138 | latch.countDown(); 139 | } 140 | }); 141 | 142 | publisher.complete(); 143 | 144 | assertTrue(latch.await(5, TimeUnit.SECONDS)); 145 | } 146 | 147 | @Test 148 | public void testOfferCompleteWithDemandOne() throws Exception { 149 | QueuedSinglePublisher publisher = new QueuedSinglePublisher<>(); 150 | 151 | CountDownLatch nextLatch = new CountDownLatch(1); 152 | CountDownLatch completeLatch = new CountDownLatch(1); 153 | publisher.subscribe(new Subscriber<>() { 154 | @Override 155 | public void onSubscribe(Subscription subscription) { 156 | subscription.request(1); 157 | } 158 | 159 | @Override 160 | public void onNext(Runnable runnable) { 161 | nextLatch.countDown(); 162 | } 163 | 164 | @Override 165 | public void onError(Throwable throwable) { 166 | } 167 | 168 | @Override 169 | public void onComplete() { 170 | completeLatch.countDown(); 171 | } 172 | }); 173 | 174 | publisher.offer(() -> {}); 175 | assertTrue(nextLatch.await(5, TimeUnit.SECONDS)); 176 | 177 | publisher.complete(); 178 | assertTrue(completeLatch.await(5, TimeUnit.SECONDS)); 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/test/java/org/eclipse/jetty/reactive/client/internal/SingleProcessorTCKTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017 the original author or authors. 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 org.eclipse.jetty.reactive.client.internal; 17 | 18 | import java.nio.ByteBuffer; 19 | import java.nio.charset.StandardCharsets; 20 | import java.util.Objects; 21 | import java.util.concurrent.ExecutorService; 22 | import java.util.concurrent.Executors; 23 | 24 | import org.eclipse.jetty.io.Content; 25 | import org.eclipse.jetty.reactive.client.AbstractTest; 26 | import org.junit.jupiter.api.BeforeEach; 27 | import org.junit.jupiter.api.TestInfo; 28 | import org.reactivestreams.Processor; 29 | import org.reactivestreams.Publisher; 30 | import org.reactivestreams.tck.IdentityProcessorVerification; 31 | import org.reactivestreams.tck.TestEnvironment; 32 | 33 | public class SingleProcessorTCKTest extends IdentityProcessorVerification { 34 | public SingleProcessorTCKTest() { 35 | super(new TestEnvironment()); 36 | } 37 | 38 | @BeforeEach 39 | public void before(TestInfo testInfo) { 40 | AbstractTest.printTestName(testInfo); 41 | } 42 | 43 | @Override 44 | public Processor createIdentityProcessor(int bufferSize) { 45 | return new AbstractSingleProcessor<>() { 46 | @Override 47 | public void onNext(Content.Chunk chunk) { 48 | downStreamOnNext(Objects.requireNonNull(chunk)); 49 | } 50 | }; 51 | } 52 | 53 | @Override 54 | public Publisher createFailedPublisher() { 55 | return null; 56 | } 57 | 58 | @Override 59 | public ExecutorService publisherExecutorService() { 60 | return Executors.newCachedThreadPool(); 61 | } 62 | 63 | @Override 64 | public Content.Chunk createElement(int element) { 65 | return newChunk("element_" + element); 66 | } 67 | 68 | private Content.Chunk newChunk(String data) { 69 | return Content.Chunk.from(ByteBuffer.wrap(data.getBytes(StandardCharsets.UTF_8)), false); 70 | } 71 | 72 | @Override 73 | public long maxSupportedSubscribers() { 74 | return 1; 75 | } 76 | 77 | @Override 78 | public boolean skipStochasticTests() { 79 | return true; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/test/java/org/eclipse/jetty/reactive/client/internal/SingleProcessorTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017 the original author or authors. 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 org.eclipse.jetty.reactive.client.internal; 17 | 18 | import java.util.ArrayList; 19 | import java.util.List; 20 | import java.util.concurrent.CountDownLatch; 21 | import java.util.concurrent.TimeUnit; 22 | import java.util.stream.Collectors; 23 | import java.util.stream.IntStream; 24 | 25 | import io.reactivex.rxjava3.core.Flowable; 26 | import org.eclipse.jetty.reactive.client.AbstractTest; 27 | import org.junit.jupiter.api.BeforeEach; 28 | import org.junit.jupiter.api.Test; 29 | import org.junit.jupiter.api.TestInfo; 30 | import org.reactivestreams.Subscriber; 31 | import org.reactivestreams.Subscription; 32 | 33 | import static org.junit.jupiter.api.Assertions.assertEquals; 34 | import static org.junit.jupiter.api.Assertions.assertTrue; 35 | 36 | public class SingleProcessorTest { 37 | @BeforeEach 38 | public void before(TestInfo testInfo) { 39 | AbstractTest.printTestName(testInfo); 40 | } 41 | 42 | @Test 43 | public void testDemandWithoutUpStreamIsRemembered() throws Exception { 44 | AbstractSingleProcessor processor = new AbstractSingleProcessor<>() { 45 | @Override 46 | public void onNext(String item) { 47 | downStreamOnNext(item); 48 | } 49 | }; 50 | 51 | // First link a Subscriber, calling request(1) - no upStream yet. 52 | CountDownLatch latch = new CountDownLatch(1); 53 | List items = new ArrayList<>(); 54 | processor.subscribe(new Subscriber<>() { 55 | private Subscription subscription; 56 | 57 | @Override 58 | public void onSubscribe(Subscription subscription) { 59 | this.subscription = subscription; 60 | subscription.request(1); 61 | } 62 | 63 | @Override 64 | public void onNext(String item) { 65 | items.add(item); 66 | subscription.request(1); 67 | } 68 | 69 | @Override 70 | public void onError(Throwable throwable) { 71 | } 72 | 73 | @Override 74 | public void onComplete() { 75 | latch.countDown(); 76 | } 77 | }); 78 | 79 | // Now create an upStream Publisher and subscribe the processor. 80 | int count = 16; 81 | Flowable.range(0, count) 82 | .map(String::valueOf) 83 | .subscribe(processor); 84 | 85 | assertTrue(latch.await(5, TimeUnit.SECONDS)); 86 | 87 | List expected = IntStream.range(0, count) 88 | .mapToObj(String::valueOf) 89 | .collect(Collectors.toList()); 90 | assertEquals(items, expected); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/test/java/org/eclipse/jetty/reactive/client/internal/StringContentTCKTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2017 the original author or authors. 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 org.eclipse.jetty.reactive.client.internal; 17 | 18 | import java.nio.charset.StandardCharsets; 19 | 20 | import org.eclipse.jetty.io.Content; 21 | import org.eclipse.jetty.reactive.client.AbstractTest; 22 | import org.junit.jupiter.api.BeforeEach; 23 | import org.junit.jupiter.api.TestInfo; 24 | import org.reactivestreams.Publisher; 25 | import org.reactivestreams.tck.PublisherVerification; 26 | import org.reactivestreams.tck.TestEnvironment; 27 | 28 | public class StringContentTCKTest extends PublisherVerification { 29 | public StringContentTCKTest() { 30 | super(new TestEnvironment()); 31 | } 32 | 33 | @BeforeEach 34 | public void before(TestInfo testInfo) { 35 | AbstractTest.printTestName(testInfo); 36 | } 37 | 38 | @Override 39 | public Publisher createPublisher(long elements) { 40 | return new StringContent("data", "text/plain", StandardCharsets.UTF_8); 41 | } 42 | 43 | @Override 44 | public Publisher createFailedPublisher() { 45 | return null; 46 | } 47 | 48 | @Override 49 | public long maxElementsFromPublisher() { 50 | return 1; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/test/resources/jetty-logging.properties: -------------------------------------------------------------------------------- 1 | # Logging Levels: ALL, TRACE, DEBUG, INFO, WARN, ERROR, OFF. 2 | #org.eclipse.jetty.LEVEL=DEBUG 3 | #org.eclipse.jetty.reactive.LEVEL=DEBUG 4 | --------------------------------------------------------------------------------