├── .gitignore ├── .mvn ├── jvm.config ├── maven.config └── wrapper │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── .travis.yml ├── LICENSE ├── README.md ├── feign-reactor-core ├── pom.xml └── src │ ├── main │ └── java │ │ └── feign │ │ └── reactor │ │ ├── DefaultMethodHandler.java │ │ ├── ReactiveClientMethodHandler.java │ │ ├── ReactiveDelegatingContract.java │ │ ├── ReactiveFeign.java │ │ ├── ReactiveInvocationHandler.java │ │ ├── ReactiveMethodHandler.java │ │ ├── ReactiveMethodHandlerFactory.java │ │ ├── ReactiveOptions.java │ │ ├── ReactiveRetryPolicy.java │ │ ├── ReactiveRetryers.java │ │ ├── client │ │ ├── DelegatingReactiveHttpResponse.java │ │ ├── InterceptorReactiveHttpClient.java │ │ ├── LoggerReactiveHttpClient.java │ │ ├── ReactiveClientFactory.java │ │ ├── ReactiveHttpClient.java │ │ ├── ReactiveHttpRequest.java │ │ ├── ReactiveHttpRequestInterceptor.java │ │ ├── ReactiveHttpResponse.java │ │ ├── ReadTimeoutException.java │ │ ├── ResponseMappers.java │ │ ├── RetryReactiveHttpClient.java │ │ ├── StatusHandlerReactiveHttpClient.java │ │ └── statushandler │ │ │ ├── CompositeStatusHandler.java │ │ │ ├── ReactiveStatusHandler.java │ │ │ └── ReactiveStatusHandlers.java │ │ └── utils │ │ ├── FeignUtils.java │ │ ├── HttpUtils.java │ │ ├── MultiValueMapUtils.java │ │ └── Pair.java │ └── test │ └── java │ └── feign │ └── reactor │ ├── CompressionTest.java │ ├── ConnectionTimeoutTest.java │ ├── ContractTest.java │ ├── DefaultMethodTest.java │ ├── LoggerTest.java │ ├── NotFoundTest.java │ ├── ReactivityTest.java │ ├── ReadTimeoutTest.java │ ├── RequestInterceptorTest.java │ ├── RetryingTest.java │ ├── SmokeTest.java │ ├── StatusHandlerTest.java │ ├── TestUtils.java │ └── testcase │ ├── IcecreamServiceApi.java │ ├── IcecreamServiceApiBroken.java │ └── domain │ ├── Bill.java │ ├── Flavor.java │ ├── IceCreamOrder.java │ ├── Mixin.java │ └── OrderGenerator.java ├── feign-reactor-fake-impl ├── pom.xml └── src │ └── test │ ├── java │ └── feign │ │ └── reactor │ │ └── resttemplate │ │ ├── CompressionTest.java │ │ ├── ConnectionTimeoutTest.java │ │ ├── ContractTest.java │ │ ├── DefaultMethodTest.java │ │ ├── LoggerTest.java │ │ ├── NotFoundTest.java │ │ ├── ReactivityTest.java │ │ ├── ReadTimeoutTest.java │ │ ├── RequestInterceptorTest.java │ │ ├── RetryingTest.java │ │ ├── SmokeTest.java │ │ ├── StatusHandlerTest.java │ │ └── client │ │ ├── RestTemplateFakeReactiveFeign.java │ │ └── RestTemplateFakeReactiveHttpClient.java │ └── resources │ └── log4j.properties ├── mvnw ├── mvnw.cmd ├── pom.xml └── src ├── config └── eclipse-java-style.xml └── etc └── header.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled source # 2 | ################### 3 | *.com 4 | *.class 5 | *.dll 6 | *.exe 7 | *.o 8 | *.so 9 | 10 | # Packages # 11 | ############ 12 | # it's better to unpack these files and commit the raw source 13 | # git has its own built in compression methods 14 | *.7z 15 | *.dmg 16 | *.gz 17 | *.iso 18 | *.jar 19 | *.rar 20 | *.tar 21 | *.zip 22 | !.mvn/wrapper/*.jar 23 | 24 | # Logs and databases # 25 | ###################### 26 | *.log 27 | 28 | # OS generated files # 29 | ###################### 30 | .DS_Store* 31 | ehthumbs.db 32 | Icon? 33 | Thumbs.db 34 | 35 | # Editor Files # 36 | ################ 37 | *~ 38 | *.swp 39 | 40 | # Build output directies 41 | /target 42 | **/test-output 43 | **/target 44 | **/bin 45 | build 46 | */build 47 | .m2 48 | 49 | # IntelliJ specific files/directories 50 | out 51 | .idea 52 | *.ipr 53 | *.iws 54 | *.iml 55 | atlassian-ide-plugin.xml 56 | 57 | # Eclipse specific files/directories 58 | .classpath 59 | .project 60 | .settings 61 | .metadata 62 | .factorypath 63 | .generated 64 | 65 | # NetBeans specific files/directories 66 | .nbattrs 67 | -------------------------------------------------------------------------------- /.mvn/jvm.config: -------------------------------------------------------------------------------- 1 | -Xmx1024m -XX:CICompilerCount=1 -XX:TieredStopAtLevel=1 -Djava.security.egd=file:/dev/./urandom -------------------------------------------------------------------------------- /.mvn/maven.config: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenFeign/feign-reactive/39cd2978ace7c998e868c4ac696fb55c1d6c9f93/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.5.0/apache-maven-3.5.0-bin.zip -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: oraclejdk8 3 | -------------------------------------------------------------------------------- /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 | # feign-reactor - incubating 2 | Reactive Streams Specification compatible implementation of Feign [![Build Status](https://travis-ci.com/OpenFeign/feign-reactive.svg?branch=master)](https://travis-ci.com/OpenFeign/feign-reactive) 3 | 4 | Status: incubating 5 | 6 | This project is currently under active development. Interfaces, contracts, and APIs may change dramatically during this time. Please consider this when using this project. 7 | -------------------------------------------------------------------------------- /feign-reactor-core/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 20 | 4.0.0 21 | 22 | 23 | io.github.openfeign.incubating 24 | feign-reactor 25 | 0.1.0 26 | 27 | 28 | feign-reactor-core 29 | jar 30 | Feign Reactive Core 31 | 32 | 33 | ${project.basedir}/.. 34 | 35 | 36 | 37 | 38 | io.projectreactor 39 | reactor-core 40 | 41 | 42 | 43 | io.github.openfeign 44 | feign-core 45 | 46 | 47 | 48 | org.slf4j 49 | slf4j-api 50 | 51 | 52 | commons-httpclient 53 | commons-httpclient 54 | 55 | 56 | 57 | 58 | io.projectreactor 59 | reactor-test 60 | test 61 | 62 | 63 | junit 64 | junit 65 | test 66 | 67 | 68 | 69 | org.assertj 70 | assertj-core 71 | test 72 | 73 | 74 | 75 | com.github.tomakehurst 76 | wiremock 77 | test 78 | 79 | 80 | 81 | org.hamcrest 82 | hamcrest-library 83 | test 84 | 85 | 86 | 87 | org.awaitility 88 | awaitility 89 | test 90 | 91 | 92 | 93 | com.fasterxml.jackson.datatype 94 | jackson-datatype-jsr310 95 | test 96 | 97 | 98 | 99 | org.mockito 100 | mockito-all 101 | test 102 | 103 | 104 | 105 | org.apache.logging.log4j 106 | log4j-core 107 | test 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | org.apache.maven.plugins 116 | maven-jar-plugin 117 | 3.1.0 118 | 119 | 120 | 121 | test-jar 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | -------------------------------------------------------------------------------- /feign-reactor-core/src/main/java/feign/reactor/DefaultMethodHandler.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Feign Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package feign.reactor; 15 | 16 | import feign.InvocationHandlerFactory.MethodHandler; 17 | import java.lang.invoke.MethodHandle; 18 | import java.lang.invoke.MethodHandles.Lookup; 19 | import java.lang.reflect.Field; 20 | import java.lang.reflect.Method; 21 | 22 | /** 23 | * Handles default methods by directly invoking the default method code on the interface. The bindTo 24 | * method must be called on the result before invoke is called. 25 | */ 26 | final class DefaultMethodHandler implements MethodHandler { 27 | // Uses Java 7 MethodHandle based reflection. As default methods will only exist when 28 | // run on a Java 8 JVM this will not affect use on legacy JVMs. 29 | private final MethodHandle unboundHandle; 30 | 31 | // handle is effectively final after bindTo has been called. 32 | private MethodHandle handle; 33 | 34 | public DefaultMethodHandler(Method defaultMethod) { 35 | try { 36 | Class declaringClass = defaultMethod.getDeclaringClass(); 37 | Field field = Lookup.class.getDeclaredField("IMPL_LOOKUP"); 38 | field.setAccessible(true); 39 | Lookup lookup = (Lookup) field.get(null); 40 | 41 | this.unboundHandle = lookup.unreflectSpecial(defaultMethod, declaringClass); 42 | } catch (NoSuchFieldException | IllegalAccessException ex) { 43 | throw new IllegalStateException(ex); 44 | } 45 | } 46 | 47 | /** 48 | * Bind this handler to a proxy object. After bound, DefaultMethodHandler#invoke will act as if it 49 | * was called on the proxy object. Must be called once and only once for a given instance of 50 | * DefaultMethodHandler 51 | */ 52 | public void bindTo(Object proxy) { 53 | if (handle != null) { 54 | throw new IllegalStateException( 55 | "Attempted to rebind a default method handler that was already bound"); 56 | } 57 | handle = unboundHandle.bindTo(proxy); 58 | } 59 | 60 | /** 61 | * Invoke this method. DefaultMethodHandler#bindTo must be called before the first time invoke is 62 | * called. 63 | */ 64 | @Override 65 | public Object invoke(Object[] argv) throws Throwable { 66 | if (handle == null) { 67 | throw new IllegalStateException( 68 | "Default method handler invoked before proxy has been bound."); 69 | } 70 | return handle.invokeWithArguments(argv); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /feign-reactor-core/src/main/java/feign/reactor/ReactiveClientMethodHandler.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Feign Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package feign.reactor; 15 | 16 | import feign.MethodMetadata; 17 | import feign.Target; 18 | import org.reactivestreams.Publisher; 19 | import feign.reactor.client.ReactiveClientFactory; 20 | import feign.reactor.client.ReactiveHttpClient; 21 | import feign.reactor.client.ReactiveHttpRequest; 22 | import feign.reactor.utils.Pair; 23 | import reactor.core.publisher.Mono; 24 | import java.lang.reflect.Type; 25 | import java.net.URI; 26 | import java.net.URISyntaxException; 27 | import java.util.*; 28 | import java.util.function.Function; 29 | import java.util.regex.Matcher; 30 | import java.util.regex.Pattern; 31 | import java.util.stream.Collectors; 32 | import java.util.stream.Stream; 33 | import static feign.Util.checkNotNull; 34 | import static java.util.stream.Collectors.*; 35 | import static feign.reactor.utils.FeignUtils.returnPublisherType; 36 | import static feign.reactor.utils.MultiValueMapUtils.*; 37 | 38 | /** 39 | * Method handler for asynchronous HTTP requests via {@link ReactiveHttpClient}. 40 | * 41 | * @author Sergii Karpenko 42 | */ 43 | public class ReactiveClientMethodHandler implements ReactiveMethodHandler { 44 | 45 | private final Target target; 46 | private final MethodMetadata methodMetadata; 47 | private final ReactiveHttpClient reactiveClient; 48 | private final Function, String> pathExpander; 49 | private final Map, String>>> headerExpanders; 50 | private final Map> queriesAll; 51 | private final Map, String>>> queryExpanders; 52 | private final Type returnPublisherType; 53 | 54 | private ReactiveClientMethodHandler(Target target, 55 | MethodMetadata methodMetadata, 56 | ReactiveHttpClient reactiveClient) { 57 | this.target = checkNotNull(target, "target must be not null"); 58 | this.methodMetadata = checkNotNull(methodMetadata, 59 | "methodMetadata must be not null"); 60 | this.reactiveClient = checkNotNull(reactiveClient, "client must be not null"); 61 | this.pathExpander = buildExpandFunction(methodMetadata.template().url()); 62 | this.headerExpanders = buildExpanders(methodMetadata.template().headers()); 63 | 64 | this.queriesAll = new HashMap<>(methodMetadata.template().queries()); 65 | if (methodMetadata.formParams() != null) { 66 | methodMetadata.formParams() 67 | .forEach(param -> add(queriesAll, param, "{" + param + "}")); 68 | } 69 | this.queryExpanders = buildExpanders(queriesAll); 70 | 71 | this.returnPublisherType = returnPublisherType(methodMetadata); 72 | } 73 | 74 | @Override 75 | @SuppressWarnings("unchecked") 76 | public Publisher invoke(final Object[] argv) { 77 | 78 | final ReactiveHttpRequest request = buildRequest(argv); 79 | 80 | return reactiveClient.executeRequest(request, returnPublisherType); 81 | } 82 | 83 | protected ReactiveHttpRequest buildRequest(Object[] argv) { 84 | 85 | Map substitutionsMap = methodMetadata.indexToName().entrySet().stream() 86 | .flatMap(e -> e.getValue().stream() 87 | .map(v -> new AbstractMap.SimpleImmutableEntry<>(e.getKey(), v))) 88 | .collect(Collectors.toMap(Map.Entry::getValue, 89 | entry -> argv[entry.getKey()])); 90 | 91 | try { 92 | String path = pathExpander.apply(substitutionsMap); 93 | Map> queries = queries(argv, substitutionsMap); 94 | Map> headers = headers(argv, substitutionsMap); 95 | 96 | URI uri = new URI(target.url() + path + queryLine(queries)); 97 | 98 | return new ReactiveHttpRequest(methodMetadata.template().method(), uri, headers, body(argv)); 99 | 100 | } catch (URISyntaxException e) { 101 | throw new RuntimeException(e); 102 | } 103 | } 104 | 105 | private String queryLine(Map> queries) { 106 | if (queries.isEmpty()) { 107 | return ""; 108 | } 109 | 110 | StringBuilder queryBuilder = new StringBuilder(); 111 | for (Map.Entry> query : queries.entrySet()) { 112 | String field = query.getKey(); 113 | for (String value : query.getValue()) { 114 | queryBuilder.append('&'); 115 | queryBuilder.append(field); 116 | if (value != null) { 117 | queryBuilder.append('='); 118 | if (!value.isEmpty()) { 119 | queryBuilder.append(value); 120 | } 121 | } 122 | } 123 | } 124 | queryBuilder.deleteCharAt(0); 125 | return queryBuilder.insert(0, '?').toString(); 126 | } 127 | 128 | protected Map> queries(Object[] argv, 129 | Map substitutionsMap) { 130 | Map> queries = new LinkedHashMap<>(); 131 | 132 | // queries from template 133 | queriesAll.keySet() 134 | .forEach(queryName -> addAll(queries, queryName, 135 | queryExpanders.get(queryName).stream() 136 | .map(expander -> expander.apply(substitutionsMap)) 137 | .collect(toList()))); 138 | 139 | // queries from args 140 | if (methodMetadata.queryMapIndex() != null) { 141 | ((Map) argv[methodMetadata.queryMapIndex()]) 142 | .forEach((key, value) -> { 143 | if (value instanceof Iterable) { 144 | ((Iterable) value).forEach(element -> add(queries, key, element.toString())); 145 | } else { 146 | add(queries, key, value.toString()); 147 | } 148 | }); 149 | } 150 | 151 | return queries; 152 | } 153 | 154 | protected Map> headers(Object[] argv, Map substitutionsMap) { 155 | 156 | Map> headers = new LinkedHashMap<>(); 157 | 158 | // headers from template 159 | methodMetadata.template().headers().keySet() 160 | .forEach(headerName -> addAllOrdered(headers, headerName, 161 | headerExpanders.get(headerName).stream() 162 | .map(expander -> expander.apply(substitutionsMap)) 163 | .collect(toList()))); 164 | 165 | // headers from args 166 | if (methodMetadata.headerMapIndex() != null) { 167 | ((Map) argv[methodMetadata.headerMapIndex()]) 168 | .forEach((key, value) -> { 169 | if (value instanceof Iterable) { 170 | ((Iterable) value) 171 | .forEach(element -> addOrdered(headers, key, element.toString())); 172 | } else { 173 | addOrdered(headers, key, value.toString()); 174 | } 175 | }); 176 | } 177 | 178 | return headers; 179 | } 180 | 181 | protected Publisher body(Object[] argv) { 182 | if (methodMetadata.bodyIndex() != null) { 183 | Object body = argv[methodMetadata.bodyIndex()]; 184 | if (body instanceof Publisher) { 185 | return (Publisher) body; 186 | } else { 187 | return Mono.just(body); 188 | } 189 | } else { 190 | return Mono.empty(); 191 | } 192 | } 193 | 194 | private static Map, String>>> buildExpanders( 195 | Map> templates) { 196 | Stream> headersFlattened = templates.entrySet().stream() 197 | .flatMap(e -> e.getValue().stream() 198 | .map(v -> new Pair<>(e.getKey(), v))); 199 | return headersFlattened.collect(groupingBy( 200 | entry -> entry.left, 201 | mapping(entry -> buildExpandFunction(entry.right), toList()))); 202 | } 203 | 204 | /** 205 | * 206 | * @param template 207 | * @return function that able to map substitutions map to actual value for specified template 208 | */ 209 | private static final Pattern PATTERN = Pattern.compile("\\{([^}]+)\\}"); 210 | 211 | private static Function, String> buildExpandFunction(String template) { 212 | List, String>> chunks = new ArrayList<>(); 213 | Matcher matcher = PATTERN.matcher(template); 214 | int previousMatchEnd = 0; 215 | while (matcher.find()) { 216 | String textChunk = template.substring(previousMatchEnd, matcher.start()); 217 | if (textChunk.length() > 0) { 218 | chunks.add(data -> textChunk); 219 | } 220 | 221 | String substitute = matcher.group(1); 222 | chunks.add(data -> { 223 | Object substitution = data.get(substitute); 224 | if (substitution != null) { 225 | return substitution.toString(); 226 | } else { 227 | return substitute; 228 | } 229 | }); 230 | previousMatchEnd = matcher.end(); 231 | } 232 | 233 | String textChunk = template.substring(previousMatchEnd, template.length()); 234 | if (textChunk.length() > 0) { 235 | chunks.add(data -> textChunk); 236 | } 237 | 238 | return traceData -> chunks.stream().map(chunk -> chunk.apply(traceData)) 239 | .collect(Collectors.joining()); 240 | } 241 | 242 | public static class Factory implements ReactiveMethodHandlerFactory { 243 | private final ReactiveClientFactory reactiveClientFactory; 244 | 245 | public Factory(final ReactiveClientFactory reactiveClientFactory) { 246 | this.reactiveClientFactory = checkNotNull(reactiveClientFactory, "client must not be null"); 247 | } 248 | 249 | @Override 250 | public ReactiveClientMethodHandler create(Target target, 251 | final MethodMetadata metadata) { 252 | 253 | return new ReactiveClientMethodHandler(target, metadata, 254 | reactiveClientFactory.apply(metadata)); 255 | } 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /feign-reactor-core/src/main/java/feign/reactor/ReactiveDelegatingContract.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Feign Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package feign.reactor; 15 | 16 | import feign.Contract; 17 | import feign.MethodMetadata; 18 | import reactor.core.publisher.Flux; 19 | import reactor.core.publisher.Mono; 20 | import java.lang.reflect.ParameterizedType; 21 | import java.lang.reflect.Type; 22 | import java.util.List; 23 | import static feign.Util.checkNotNull; 24 | 25 | /** 26 | * Contract allowing only {@link Mono} and {@link Flux} return type. 27 | * 28 | * @author Sergii Karpenko 29 | */ 30 | public class ReactiveDelegatingContract implements Contract { 31 | 32 | private final Contract delegate; 33 | 34 | public ReactiveDelegatingContract(final Contract delegate) { 35 | this.delegate = checkNotNull(delegate, "delegate must not be null"); 36 | } 37 | 38 | @Override 39 | public List parseAndValidatateMetadata(final Class targetType) { 40 | final List methodsMetadata = 41 | this.delegate.parseAndValidatateMetadata(targetType); 42 | 43 | for (final MethodMetadata metadata : methodsMetadata) { 44 | final Type type = metadata.returnType(); 45 | if (!isMonoOrFlux(type)) { 46 | throw new IllegalArgumentException(String.format( 47 | "Method %s of contract %s doesn't returns reactor.core.publisher.Mono or reactor.core.publisher.Flux", 48 | metadata.configKey(), targetType.getSimpleName())); 49 | } 50 | } 51 | 52 | return methodsMetadata; 53 | } 54 | 55 | private boolean isMonoOrFlux(final Type type) { 56 | return (type instanceof ParameterizedType) 57 | && (((ParameterizedType) type).getRawType() == Mono.class 58 | || ((ParameterizedType) type).getRawType() == Flux.class); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /feign-reactor-core/src/main/java/feign/reactor/ReactiveFeign.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Feign Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package feign.reactor; 15 | 16 | import feign.*; 17 | import feign.InvocationHandlerFactory.MethodHandler; 18 | import feign.codec.ErrorDecoder; 19 | import feign.reactor.client.ReactiveClientFactory; 20 | import feign.reactor.client.ReactiveHttpClient; 21 | import feign.reactor.client.ReactiveHttpRequestInterceptor; 22 | import feign.reactor.client.ReactiveHttpResponse; 23 | import feign.reactor.client.statushandler.ReactiveStatusHandler; 24 | import feign.reactor.client.statushandler.ReactiveStatusHandlers; 25 | import org.reactivestreams.Publisher; 26 | import reactor.core.publisher.Flux; 27 | import reactor.core.publisher.Mono; 28 | 29 | import java.lang.reflect.InvocationHandler; 30 | import java.lang.reflect.Method; 31 | import java.lang.reflect.Proxy; 32 | import java.util.LinkedHashMap; 33 | import java.util.LinkedList; 34 | import java.util.List; 35 | import java.util.Map; 36 | import java.util.function.BiFunction; 37 | import java.util.function.Function; 38 | 39 | import static feign.Util.checkNotNull; 40 | import static feign.Util.isDefault; 41 | import static feign.reactor.client.InterceptorReactiveHttpClient.intercept; 42 | import static feign.reactor.client.LoggerReactiveHttpClient.log; 43 | import static feign.reactor.client.ResponseMappers.ignore404; 44 | import static feign.reactor.client.ResponseMappers.mapResponse; 45 | import static feign.reactor.client.RetryReactiveHttpClient.retry; 46 | import static feign.reactor.client.StatusHandlerReactiveHttpClient.handleStatus; 47 | 48 | /** 49 | * Allows Feign interfaces to accept {@link Publisher} as body and return reactive {@link Mono} or 50 | * {@link Flux}. 51 | * 52 | * @author Sergii Karpenko 53 | */ 54 | public class ReactiveFeign { 55 | 56 | private final ParseHandlersByName targetToHandlersByName; 57 | private final InvocationHandlerFactory factory; 58 | 59 | protected ReactiveFeign( 60 | final ParseHandlersByName targetToHandlersByName, 61 | final InvocationHandlerFactory factory) { 62 | this.targetToHandlersByName = targetToHandlersByName; 63 | this.factory = factory; 64 | } 65 | 66 | public static Builder builder() { 67 | return new Builder<>(); 68 | } 69 | 70 | @SuppressWarnings("unchecked") 71 | public T newInstance(Target target) { 72 | final Map nameToHandler = targetToHandlersByName 73 | .apply(target); 74 | final Map methodToHandler = new LinkedHashMap<>(); 75 | final List defaultMethodHandlers = new LinkedList<>(); 76 | 77 | for (final Method method : target.type().getMethods()) { 78 | if (isDefault(method)) { 79 | final DefaultMethodHandler handler = new DefaultMethodHandler(method); 80 | defaultMethodHandlers.add(handler); 81 | methodToHandler.put(method, handler); 82 | } else { 83 | methodToHandler.put(method, 84 | nameToHandler.get(Feign.configKey(target.type(), method))); 85 | } 86 | } 87 | 88 | final InvocationHandler handler = factory.create(target, methodToHandler); 89 | T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(), 90 | new Class[] {target.type()}, handler); 91 | 92 | for (final DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) { 93 | defaultMethodHandler.bindTo(proxy); 94 | } 95 | 96 | return proxy; 97 | } 98 | 99 | /** 100 | * ReactiveFeign builder. 101 | */ 102 | public static class Builder { 103 | protected Contract contract = new ReactiveDelegatingContract(new Contract.Default()); 104 | protected Function clientFactory; 105 | protected ReactiveHttpRequestInterceptor requestInterceptor; 106 | protected BiFunction responseMapper; 107 | protected ReactiveStatusHandler statusHandler = 108 | ReactiveStatusHandlers.defaultFeign(new ErrorDecoder.Default()); 109 | protected InvocationHandlerFactory invocationHandlerFactory = 110 | new ReactiveInvocationHandler.Factory(); 111 | protected boolean decode404 = false; 112 | protected Target target; 113 | 114 | private Function, Flux> retryFunction; 115 | 116 | protected Builder(){ 117 | } 118 | 119 | public Builder clientFactory(Function clientFactory) { 120 | this.clientFactory = clientFactory; 121 | return this; 122 | } 123 | 124 | /** 125 | * Sets contract. Provided contract will be wrapped in {@link ReactiveDelegatingContract} 126 | * 127 | * @param contract contract. 128 | * @return this builder 129 | */ 130 | public Builder contract(final Contract contract) { 131 | this.contract = new ReactiveDelegatingContract(contract); 132 | return this; 133 | } 134 | 135 | public Builder requestInterceptor(ReactiveHttpRequestInterceptor requestInterceptor) { 136 | this.requestInterceptor = requestInterceptor; 137 | return this; 138 | } 139 | 140 | /** 141 | * This flag indicates that the reactive feign client should process responses with 404 status, 142 | * specifically returning empty {@link Mono} or {@link Flux} instead of throwing 143 | * {@link FeignException}. 144 | *

145 | *

146 | * This flag only works with 404, as opposed to all or arbitrary status codes. This was an 147 | * explicit decision: 404 - empty is safe, common and doesn't complicate redirection, retry or 148 | * fallback policy. 149 | * 150 | * @return this builder 151 | */ 152 | public Builder decode404() { 153 | this.decode404 = true; 154 | return this; 155 | } 156 | 157 | public Builder statusHandler(ReactiveStatusHandler statusHandler) { 158 | this.statusHandler = statusHandler; 159 | return this; 160 | } 161 | 162 | /** 163 | * The most common way to introduce custom logic on handling http response 164 | * 165 | * @param responseMapper 166 | * @return 167 | */ 168 | public Builder responseMapper(BiFunction responseMapper) { 169 | this.responseMapper = responseMapper; 170 | return this; 171 | } 172 | 173 | public Builder retryWhen( 174 | Function, Flux> retryFunction) { 175 | this.retryFunction = retryFunction; 176 | return this; 177 | } 178 | 179 | public Builder retryWhen(ReactiveRetryPolicy retryPolicy) { 180 | return retryWhen(retryPolicy.toRetryFunction()); 181 | } 182 | 183 | /** 184 | * Defines target and builds client. 185 | * 186 | * @param apiType API interface 187 | * @param url base URL 188 | * @return built client 189 | */ 190 | public T target(final Class apiType, final String url) { 191 | return target(new Target.HardCodedTarget<>(apiType, url)); 192 | } 193 | 194 | /** 195 | * Defines target and builds client. 196 | * 197 | * @param target target instance 198 | * @return built client 199 | */ 200 | public T target(final Target target) { 201 | this.target = target; 202 | return build().newInstance(target); 203 | } 204 | 205 | protected ReactiveFeign build() { 206 | final ParseHandlersByName handlersByName = new ParseHandlersByName( 207 | contract, buildReactiveMethodHandlerFactory()); 208 | return new ReactiveFeign(handlersByName, invocationHandlerFactory); 209 | } 210 | 211 | protected ReactiveMethodHandlerFactory buildReactiveMethodHandlerFactory() { 212 | return new ReactiveClientMethodHandler.Factory(buildReactiveClientFactory()); 213 | } 214 | 215 | protected ReactiveClientFactory buildReactiveClientFactory() { 216 | return methodMetadata -> { 217 | 218 | checkNotNull(clientFactory, 219 | "clientFactory wasn't provided in ReactiveFeign builder"); 220 | 221 | ReactiveHttpClient reactiveClient = clientFactory.apply(methodMetadata); 222 | 223 | if (requestInterceptor != null) { 224 | reactiveClient = intercept(reactiveClient, requestInterceptor); 225 | } 226 | 227 | reactiveClient = log(reactiveClient, methodMetadata); 228 | 229 | if (responseMapper != null) { 230 | reactiveClient = mapResponse(reactiveClient, methodMetadata, responseMapper); 231 | } 232 | 233 | if (decode404) { 234 | reactiveClient = mapResponse(reactiveClient, methodMetadata, ignore404()); 235 | } 236 | 237 | if (statusHandler != null) { 238 | reactiveClient = handleStatus(reactiveClient, methodMetadata, statusHandler); 239 | } 240 | 241 | if (retryFunction != null) { 242 | reactiveClient = retry(reactiveClient, methodMetadata, retryFunction); 243 | } 244 | 245 | return reactiveClient; 246 | }; 247 | } 248 | } 249 | 250 | public static final class ParseHandlersByName { 251 | private final Contract contract; 252 | private final ReactiveMethodHandlerFactory factory; 253 | 254 | ParseHandlersByName(final Contract contract, 255 | final ReactiveMethodHandlerFactory factory) { 256 | this.contract = contract; 257 | this.factory = factory; 258 | } 259 | 260 | Map apply(final Target target) { 261 | final List metadata = contract 262 | .parseAndValidatateMetadata(target.type()); 263 | final Map result = new LinkedHashMap<>(); 264 | 265 | for (final MethodMetadata md : metadata) { 266 | ReactiveMethodHandler methodHandler = factory.create(target, md); 267 | result.put(md.configKey(), methodHandler); 268 | } 269 | 270 | return result; 271 | } 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /feign-reactor-core/src/main/java/feign/reactor/ReactiveInvocationHandler.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Feign Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package feign.reactor; 15 | 16 | import feign.InvocationHandlerFactory; 17 | import feign.InvocationHandlerFactory.MethodHandler; 18 | import feign.Target; 19 | import feign.reactor.client.ReactiveHttpClient; 20 | import org.reactivestreams.Publisher; 21 | import reactor.core.publisher.Flux; 22 | import reactor.core.publisher.Mono; 23 | import java.lang.reflect.InvocationHandler; 24 | import java.lang.reflect.Method; 25 | import java.lang.reflect.Proxy; 26 | import java.util.Map; 27 | import static feign.Util.checkNotNull; 28 | 29 | /** 30 | * {@link InvocationHandler} implementation that transforms calls to methods of feign contract into 31 | * asynchronous HTTP requests via spring WebClient. 32 | * 33 | * @author Sergii Karpenko 34 | */ 35 | public final class ReactiveInvocationHandler implements InvocationHandler { 36 | private final Target target; 37 | private final Map dispatch; 38 | 39 | private ReactiveInvocationHandler(final Target target, 40 | final Map dispatch) { 41 | this.target = checkNotNull(target, "target must not be null"); 42 | this.dispatch = checkNotNull(dispatch, "dispatch must not be null"); 43 | defineObjectMethodsHandlers(); 44 | } 45 | 46 | private void defineObjectMethodsHandlers() { 47 | try { 48 | dispatch.put(Object.class.getMethod("equals", Object.class), 49 | args -> { 50 | Object otherHandler = args.length > 0 && args[0] != null 51 | ? Proxy.getInvocationHandler(args[0]) 52 | : null; 53 | return equals(otherHandler); 54 | }); 55 | dispatch.put(Object.class.getMethod("hashCode"), 56 | args -> hashCode()); 57 | dispatch.put(Object.class.getMethod("toString"), 58 | args -> toString()); 59 | } catch (NoSuchMethodException e) { 60 | throw new RuntimeException(e); 61 | } 62 | } 63 | 64 | @Override 65 | public Object invoke(final Object proxy, final Method method, final Object[] args) 66 | throws Throwable { 67 | if (method.getDeclaringClass().equals(Object.class)) { 68 | return dispatch.get(method).invoke(args); 69 | } else { 70 | return invokeRequestMethod(method, args); 71 | } 72 | } 73 | 74 | /** 75 | * Transforms method invocation into request that executed by {@link ReactiveHttpClient}. 76 | * 77 | * @param method invoked method 78 | * @param args provided arguments to method 79 | * @return Publisher with decoded result 80 | */ 81 | private Publisher invokeRequestMethod(final Method method, final Object[] args) { 82 | try { 83 | return (Publisher) dispatch.get(method).invoke(args); 84 | } catch (Throwable throwable) { 85 | return method.getReturnType() == Mono.class ? Mono.error(throwable) 86 | : Flux.error(throwable); 87 | } 88 | } 89 | 90 | @Override 91 | public boolean equals(final Object other) { 92 | if (other instanceof ReactiveInvocationHandler) { 93 | final ReactiveInvocationHandler otherHandler = (ReactiveInvocationHandler) other; 94 | return this.target.equals(otherHandler.target); 95 | } 96 | return false; 97 | } 98 | 99 | @Override 100 | public int hashCode() { 101 | return target.hashCode(); 102 | } 103 | 104 | @Override 105 | public String toString() { 106 | return target.toString(); 107 | } 108 | 109 | /** 110 | * Factory for ReactiveInvocationHandler. 111 | */ 112 | public static final class Factory implements InvocationHandlerFactory { 113 | 114 | @Override 115 | public InvocationHandler create(final Target target, 116 | final Map dispatch) { 117 | return new ReactiveInvocationHandler(target, dispatch); 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /feign-reactor-core/src/main/java/feign/reactor/ReactiveMethodHandler.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Feign Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package feign.reactor; 15 | 16 | import feign.InvocationHandlerFactory; 17 | import org.reactivestreams.Publisher; 18 | 19 | /** 20 | * @author Sergii Karpenko 21 | */ 22 | public interface ReactiveMethodHandler extends InvocationHandlerFactory.MethodHandler { 23 | 24 | @Override 25 | Publisher invoke(final Object[] argv); 26 | 27 | } 28 | -------------------------------------------------------------------------------- /feign-reactor-core/src/main/java/feign/reactor/ReactiveMethodHandlerFactory.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Feign Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package feign.reactor; 15 | 16 | import feign.MethodMetadata; 17 | import feign.Target; 18 | 19 | public interface ReactiveMethodHandlerFactory { 20 | 21 | ReactiveMethodHandler create(final Target target, final MethodMetadata metadata); 22 | } 23 | -------------------------------------------------------------------------------- /feign-reactor-core/src/main/java/feign/reactor/ReactiveOptions.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Feign Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package feign.reactor; 15 | 16 | /** 17 | * @author Sergii Karpenko 18 | */ 19 | public class ReactiveOptions { 20 | 21 | private final Integer connectTimeoutMillis; 22 | private final Integer readTimeoutMillis; 23 | private final Boolean tryUseCompression; 24 | 25 | private ReactiveOptions(Integer connectTimeoutMillis, Integer readTimeoutMillis, 26 | Boolean tryUseCompression) { 27 | this.connectTimeoutMillis = connectTimeoutMillis; 28 | this.readTimeoutMillis = readTimeoutMillis; 29 | this.tryUseCompression = tryUseCompression; 30 | } 31 | 32 | public Integer getConnectTimeoutMillis() { 33 | return connectTimeoutMillis; 34 | } 35 | 36 | public Integer getReadTimeoutMillis() { 37 | return readTimeoutMillis; 38 | } 39 | 40 | public Boolean isTryUseCompression() { 41 | return tryUseCompression; 42 | } 43 | 44 | public boolean isEmpty() { 45 | return connectTimeoutMillis == null && readTimeoutMillis == null 46 | && tryUseCompression == null; 47 | } 48 | 49 | public static class Builder { 50 | private Integer connectTimeoutMillis; 51 | private Integer readTimeoutMillis; 52 | private Boolean tryUseCompression; 53 | 54 | public Builder() {} 55 | 56 | public Builder setConnectTimeoutMillis(int connectTimeoutMillis) { 57 | this.connectTimeoutMillis = connectTimeoutMillis; 58 | return this; 59 | } 60 | 61 | public Builder setReadTimeoutMillis(int readTimeoutMillis) { 62 | this.readTimeoutMillis = readTimeoutMillis; 63 | return this; 64 | } 65 | 66 | public Builder setTryUseCompression(boolean tryUseCompression) { 67 | this.tryUseCompression = tryUseCompression; 68 | return this; 69 | } 70 | 71 | public ReactiveOptions build() { 72 | return new ReactiveOptions(connectTimeoutMillis, readTimeoutMillis, 73 | tryUseCompression); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /feign-reactor-core/src/main/java/feign/reactor/ReactiveRetryPolicy.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Feign Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package feign.reactor; 15 | 16 | import reactor.core.Exceptions; 17 | import reactor.core.publisher.Flux; 18 | import reactor.core.publisher.Mono; 19 | import reactor.util.function.Tuples; 20 | 21 | import java.time.Duration; 22 | import java.util.function.Function; 23 | 24 | public interface ReactiveRetryPolicy { 25 | /** 26 | * @param error 27 | * @param attemptNo 28 | * @return -1 if should not be retried, 0 if retry immediately 29 | */ 30 | long retryDelay(Throwable error, int attemptNo); 31 | 32 | default Function, Flux> toRetryFunction() { 33 | return errors -> errors 34 | .zipWith(Flux.range(1, Integer.MAX_VALUE), (error, index) -> { 35 | long delay = retryDelay(error, index); 36 | if (delay >= 0) { 37 | return Tuples.of(delay, error); 38 | } else { 39 | throw Exceptions.propagate(error); 40 | } 41 | }).flatMap( 42 | tuple2 -> tuple2.getT1() > 0 43 | ? Mono.delay(Duration.ofMillis(tuple2.getT1())) 44 | .map(time -> tuple2.getT2()) 45 | : Mono.just(tuple2.getT2())); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /feign-reactor-core/src/main/java/feign/reactor/ReactiveRetryers.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Feign Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package feign.reactor; 15 | 16 | import feign.RetryableException; 17 | import java.util.Date; 18 | 19 | /** 20 | * @author Sergii Karpenko 21 | */ 22 | public class ReactiveRetryers { 23 | 24 | public static ReactiveRetryPolicy retry(int maxRetries) { 25 | return (error, attemptNo) -> attemptNo <= maxRetries ? 0 : -1; 26 | } 27 | 28 | public static ReactiveRetryPolicy retryWithBackoff(int maxRetries, long periodInMs) { 29 | return (error, attemptNo) -> { 30 | if (attemptNo <= maxRetries) { 31 | long delay; 32 | Date retryAfter; 33 | // "Retry-After" header set 34 | if (error instanceof RetryableException 35 | && (retryAfter = ((RetryableException) error) 36 | .retryAfter()) != null) { 37 | delay = retryAfter.getTime() - System.currentTimeMillis(); 38 | delay = Math.min(delay, periodInMs); 39 | delay = Math.max(delay, 0); 40 | } else { 41 | delay = periodInMs; 42 | } 43 | return delay; 44 | } else { 45 | return -1; 46 | } 47 | }; 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /feign-reactor-core/src/main/java/feign/reactor/client/DelegatingReactiveHttpResponse.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Feign Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package feign.reactor.client; 15 | 16 | import reactor.core.publisher.Mono; 17 | import java.util.List; 18 | import java.util.Map; 19 | 20 | /** 21 | * @author Sergii Karpenko 22 | */ 23 | abstract public class DelegatingReactiveHttpResponse implements ReactiveHttpResponse { 24 | 25 | private final ReactiveHttpResponse response; 26 | 27 | protected DelegatingReactiveHttpResponse(ReactiveHttpResponse response) { 28 | this.response = response; 29 | } 30 | 31 | protected ReactiveHttpResponse getResponse() { 32 | return response; 33 | } 34 | 35 | @Override 36 | public int status() { 37 | return response.status(); 38 | } 39 | 40 | @Override 41 | public Map> headers() { 42 | return response.headers(); 43 | } 44 | 45 | @Override 46 | public Mono bodyData() { 47 | throw new UnsupportedOperationException(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /feign-reactor-core/src/main/java/feign/reactor/client/InterceptorReactiveHttpClient.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Feign Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package feign.reactor.client; 15 | 16 | /** 17 | * Used to modify request before call. May be used to set auth headers to all requests. 18 | * 19 | * @author Sergii Karpenko 20 | */ 21 | public class InterceptorReactiveHttpClient { 22 | 23 | public static ReactiveHttpClient intercept(ReactiveHttpClient reactiveHttpClient, 24 | ReactiveHttpRequestInterceptor interceptor) { 25 | return request -> reactiveHttpClient.executeRequest(interceptor.apply(request)); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /feign-reactor-core/src/main/java/feign/reactor/client/LoggerReactiveHttpClient.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Feign Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package feign.reactor.client; 15 | 16 | import feign.MethodMetadata; 17 | import feign.reactor.utils.Pair; 18 | import org.reactivestreams.Publisher; 19 | import org.slf4j.LoggerFactory; 20 | import reactor.core.publisher.Flux; 21 | import reactor.core.publisher.Mono; 22 | 23 | import java.util.concurrent.atomic.AtomicLong; 24 | import java.util.function.Consumer; 25 | import java.util.function.Supplier; 26 | import java.util.stream.Collectors; 27 | 28 | import static feign.reactor.utils.FeignUtils.methodTag; 29 | import static reactor.core.publisher.Mono.just; 30 | 31 | /** 32 | * Wraps {@link ReactiveHttpClient} with log logic 33 | * 34 | * @author Sergii Karpenko 35 | */ 36 | public class LoggerReactiveHttpClient implements ReactiveHttpClient { 37 | 38 | private final org.slf4j.Logger logger = LoggerFactory.getLogger(LoggerReactiveHttpClient.class); 39 | 40 | private final ReactiveHttpClient reactiveClient; 41 | private final String methodTag; 42 | 43 | public static ReactiveHttpClient log(ReactiveHttpClient reactiveClient, MethodMetadata methodMetadata) { 44 | return new LoggerReactiveHttpClient(reactiveClient, methodMetadata); 45 | } 46 | 47 | private LoggerReactiveHttpClient(ReactiveHttpClient reactiveClient, 48 | MethodMetadata methodMetadata) { 49 | this.reactiveClient = reactiveClient; 50 | this.methodTag = methodTag(methodMetadata); 51 | } 52 | 53 | @Override 54 | public Mono executeRequest(ReactiveHttpRequest request) { 55 | 56 | AtomicLong start = new AtomicLong(-1); 57 | return Mono 58 | .defer(() -> { 59 | start.set(System.currentTimeMillis()); 60 | return just(request); 61 | }) 62 | .flatMap(req -> { 63 | req = logRequest(methodTag, req); 64 | 65 | return reactiveClient.executeRequest(req) 66 | .doOnNext(resp -> logResponseHeaders(methodTag, resp, 67 | System.currentTimeMillis() - start.get())); 68 | }) 69 | .map(resp -> new LoggerReactiveHttpResponse(resp, start)); 70 | } 71 | 72 | private ReactiveHttpRequest logRequest( 73 | String feignMethodTag, ReactiveHttpRequest request) { 74 | if (logger.isDebugEnabled()) { 75 | logger.debug("[{}]--->{} {} HTTP/1.1", feignMethodTag, request.method(), 76 | request.uri()); 77 | } 78 | 79 | if (logger.isTraceEnabled()) { 80 | logger.trace("[{}] REQUEST HEADERS\n{}", feignMethodTag, 81 | msg(() -> request.headers().entrySet().stream() 82 | .map(entry -> String.format("%s:%s", entry.getKey(), 83 | entry.getValue())) 84 | .collect(Collectors.joining("\n")))); 85 | 86 | if(request.body() != null) { 87 | Publisher bodyLogged; 88 | if (request.body() instanceof Mono) { 89 | bodyLogged = ((Mono) request.body()).doOnNext(body -> logger.trace( 90 | "[{}] REQUEST BODY\n{}", feignMethodTag, body)); 91 | } else if (request.body() instanceof Flux) { 92 | bodyLogged = ((Flux) request.body()).doOnNext(body -> logger.trace( 93 | "[{}] REQUEST BODY ELEMENT\n{}", feignMethodTag, body)); 94 | } else { 95 | throw new IllegalArgumentException("Unsupported publisher type: " + request.body().getClass()); 96 | } 97 | return new ReactiveHttpRequest(request, bodyLogged); 98 | } 99 | } 100 | 101 | return request; 102 | } 103 | 104 | private void logResponseHeaders(String feignMethodTag, 105 | ReactiveHttpResponse httpResponse, 106 | long elapsedTime) { 107 | if (logger.isTraceEnabled()) { 108 | logger.trace("[{}] RESPONSE HEADERS\n{}", feignMethodTag, 109 | msg(() -> httpResponse.headers().entrySet().stream() 110 | .flatMap(entry -> entry.getValue().stream() 111 | .map(value -> new Pair<>(entry.getKey(), value))) 112 | .map(pair -> String.format("%s:%s", pair.left, pair.right)) 113 | .collect(Collectors.joining("\n")))); 114 | } 115 | if (logger.isDebugEnabled()) { 116 | logger.debug("[{}]<--- headers takes {} milliseconds", feignMethodTag, 117 | elapsedTime); 118 | } 119 | } 120 | 121 | private void logResponseBodyAndTime(String feignMethodTag, Object response, long elapsedTime) { 122 | if (logger.isTraceEnabled()) { 123 | logger.trace("[{}] RESPONSE BODY\n{}", feignMethodTag, response); 124 | } 125 | 126 | if (logger.isDebugEnabled()) { 127 | logger.debug("[{}]<--- body takes {} milliseconds", feignMethodTag, elapsedTime); 128 | } 129 | } 130 | 131 | private class LoggerReactiveHttpResponse extends DelegatingReactiveHttpResponse { 132 | 133 | private final AtomicLong start; 134 | 135 | private LoggerReactiveHttpResponse(ReactiveHttpResponse response, AtomicLong start) { 136 | super(response); 137 | this.start = start; 138 | } 139 | 140 | @Override 141 | public Publisher body() { 142 | Publisher publisher = getResponse().body(); 143 | 144 | if (publisher instanceof Mono) { 145 | return ((Mono) publisher).doOnNext(responseBodyLogger(start)); 146 | } else { 147 | return ((Flux) publisher).doOnNext(responseBodyLogger(start)); 148 | } 149 | } 150 | 151 | @Override 152 | public Mono bodyData() { 153 | Mono publisher = getResponse().bodyData(); 154 | 155 | return publisher.doOnNext(responseBodyLogger(start)); 156 | } 157 | 158 | private Consumer responseBodyLogger(AtomicLong start) { 159 | return result -> logResponseBodyAndTime(methodTag, result, 160 | System.currentTimeMillis() - start.get()); 161 | } 162 | } 163 | 164 | private static MessageSupplier msg(Supplier supplier) { 165 | return new MessageSupplier(supplier); 166 | } 167 | 168 | static class MessageSupplier { 169 | private Supplier supplier; 170 | 171 | MessageSupplier(Supplier supplier) { 172 | this.supplier = supplier; 173 | } 174 | 175 | @Override 176 | public String toString() { 177 | return supplier.get().toString(); 178 | } 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /feign-reactor-core/src/main/java/feign/reactor/client/ReactiveClientFactory.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Feign Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package feign.reactor.client; 15 | 16 | import feign.MethodMetadata; 17 | import java.util.function.Function; 18 | 19 | /** 20 | * @author Sergii Karpenko 21 | */ 22 | 23 | public interface ReactiveClientFactory extends Function { 24 | } 25 | -------------------------------------------------------------------------------- /feign-reactor-core/src/main/java/feign/reactor/client/ReactiveHttpClient.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Feign Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package feign.reactor.client; 15 | 16 | import org.reactivestreams.Publisher; 17 | import reactor.core.publisher.Mono; 18 | import java.lang.reflect.Type; 19 | 20 | /** 21 | * Client that execute http requests reactively 22 | * 23 | * @author Sergii Karpenko 24 | */ 25 | public interface ReactiveHttpClient { 26 | 27 | Mono executeRequest(ReactiveHttpRequest request); 28 | 29 | default Publisher executeRequest(ReactiveHttpRequest request, Type returnPublisherType) { 30 | Mono response = executeRequest(request); 31 | if (returnPublisherType == Mono.class) { 32 | return response.flatMap(resp -> (Mono) resp.body()); 33 | } else { 34 | return response.flatMapMany(ReactiveHttpResponse::body); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /feign-reactor-core/src/main/java/feign/reactor/client/ReactiveHttpRequest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Feign Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package feign.reactor.client; 15 | 16 | import org.reactivestreams.Publisher; 17 | import java.net.URI; 18 | import java.util.List; 19 | import java.util.Map; 20 | import static feign.Util.checkNotNull; 21 | 22 | /** 23 | * An immutable reactive request to an http server. 24 | * 25 | * @author Sergii Karpenko 26 | */ 27 | public final class ReactiveHttpRequest { 28 | 29 | private final String method; 30 | private final URI uri; 31 | private final Map> headers; 32 | private final Publisher body; 33 | 34 | /** 35 | * No parameters can be null except {@code body}. All parameters must be effectively immutable, 36 | * via safe copies, not mutating or otherwise. 37 | */ 38 | public ReactiveHttpRequest(String method, URI uri, 39 | Map> headers, Publisher body) { 40 | this.method = checkNotNull(method, "method of %s", uri); 41 | this.uri = checkNotNull(uri, "url"); 42 | this.headers = checkNotNull(headers, "headers of %s %s", method, uri); 43 | this.body = body; // nullable 44 | } 45 | 46 | public ReactiveHttpRequest(ReactiveHttpRequest request, Publisher body){ 47 | this(request.method, request.uri, request.headers, body); 48 | } 49 | 50 | /* Method to invoke on the server. */ 51 | public String method() { 52 | return method; 53 | } 54 | 55 | /* Fully resolved URL including query. */ 56 | public URI uri() { 57 | return uri; 58 | } 59 | 60 | /* Ordered list of headers that will be sent to the server. */ 61 | public Map> headers() { 62 | return headers; 63 | } 64 | 65 | /** 66 | * If present, this is the replayable body to send to the server. 67 | */ 68 | public Publisher body() { 69 | return body; 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /feign-reactor-core/src/main/java/feign/reactor/client/ReactiveHttpRequestInterceptor.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Feign Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package feign.reactor.client; 15 | 16 | import java.util.function.Function; 17 | 18 | /** 19 | * Used to modify request before call. May be used to set auth headers to all requests. 20 | * 21 | * @author Sergii Karpenko 22 | * 23 | */ 24 | public interface ReactiveHttpRequestInterceptor 25 | extends Function { 26 | } 27 | -------------------------------------------------------------------------------- /feign-reactor-core/src/main/java/feign/reactor/client/ReactiveHttpResponse.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Feign Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package feign.reactor.client; 15 | 16 | import org.reactivestreams.Publisher; 17 | import reactor.core.publisher.Mono; 18 | import java.util.List; 19 | import java.util.Map; 20 | 21 | /** 22 | * Reactive response from an http server. 23 | * 24 | * @author Sergii Karpenko 25 | */ 26 | public interface ReactiveHttpResponse { 27 | 28 | int status(); 29 | 30 | Map> headers(); 31 | 32 | Publisher body(); 33 | 34 | /** 35 | * used by error decoders 36 | * 37 | * @return error message data 38 | */ 39 | Mono bodyData(); 40 | } 41 | -------------------------------------------------------------------------------- /feign-reactor-core/src/main/java/feign/reactor/client/ReadTimeoutException.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Feign Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package feign.reactor.client; 15 | 16 | public class ReadTimeoutException extends RuntimeException { 17 | 18 | public ReadTimeoutException(Throwable cause) { 19 | super(cause); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /feign-reactor-core/src/main/java/feign/reactor/client/ResponseMappers.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Feign Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package feign.reactor.client; 15 | 16 | import feign.MethodMetadata; 17 | import org.apache.commons.httpclient.HttpStatus; 18 | import org.reactivestreams.Publisher; 19 | import reactor.core.publisher.Mono; 20 | import java.util.function.BiFunction; 21 | 22 | /** 23 | * Maps 404 error response to successful empty response 24 | * 25 | * @author Sergii Karpenko 26 | */ 27 | public class ResponseMappers { 28 | 29 | public static BiFunction ignore404() { 30 | return (MethodMetadata methodMetadata, ReactiveHttpResponse response) -> { 31 | if (response.status() == HttpStatus.SC_NOT_FOUND) { 32 | return new DelegatingReactiveHttpResponse(response) { 33 | @Override 34 | public int status() { 35 | return HttpStatus.SC_OK; 36 | } 37 | 38 | @Override 39 | public Publisher body() { 40 | return Mono.empty(); 41 | } 42 | }; 43 | } 44 | return response; 45 | }; 46 | } 47 | 48 | public static ReactiveHttpClient mapResponse( 49 | ReactiveHttpClient reactiveHttpClient, 50 | MethodMetadata methodMetadata, 51 | BiFunction responseMapper) { 52 | return request -> reactiveHttpClient.executeRequest(request) 53 | .map(response -> responseMapper.apply(methodMetadata, response)); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /feign-reactor-core/src/main/java/feign/reactor/client/RetryReactiveHttpClient.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Feign Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package feign.reactor.client; 15 | 16 | import feign.MethodMetadata; 17 | import org.reactivestreams.Publisher; 18 | import org.slf4j.LoggerFactory; 19 | import reactor.core.publisher.Flux; 20 | import reactor.core.publisher.Mono; 21 | 22 | import java.lang.reflect.Type; 23 | import java.util.function.Function; 24 | 25 | import static feign.reactor.utils.FeignUtils.methodTag; 26 | 27 | /** 28 | * Wraps {@link ReactiveHttpClient} with retry logic provided by retryFunction 29 | * 30 | * @author Sergii Karpenko 31 | */ 32 | public class RetryReactiveHttpClient implements ReactiveHttpClient { 33 | 34 | private static final org.slf4j.Logger logger = LoggerFactory 35 | .getLogger(RetryReactiveHttpClient.class); 36 | 37 | private final String feignMethodTag; 38 | private final ReactiveHttpClient reactiveClient; 39 | private final Function, Flux> retryFunction; 40 | 41 | public static ReactiveHttpClient retry( 42 | ReactiveHttpClient reactiveClient, 43 | MethodMetadata methodMetadata, 44 | Function, Flux> retryFunction) { 45 | return new RetryReactiveHttpClient<>(reactiveClient, methodMetadata, retryFunction); 46 | } 47 | 48 | private RetryReactiveHttpClient(ReactiveHttpClient reactiveClient, 49 | MethodMetadata methodMetadata, 50 | Function, Flux> retryFunction) { 51 | this.reactiveClient = reactiveClient; 52 | this.feignMethodTag = methodTag(methodMetadata); 53 | this.retryFunction = wrapWithLog(retryFunction, feignMethodTag); 54 | } 55 | 56 | @Override 57 | public Publisher executeRequest(ReactiveHttpRequest request, Type returnPublisherType) { 58 | Publisher response = reactiveClient.executeRequest(request, returnPublisherType); 59 | if (returnPublisherType == Mono.class) { 60 | return ((Mono) response).retryWhen(retryFunction).onErrorMap(outOfRetries()); 61 | } else { 62 | return ((Flux) response).retryWhen(retryFunction).onErrorMap(outOfRetries()); 63 | } 64 | } 65 | 66 | @Override 67 | public Mono executeRequest(ReactiveHttpRequest request) { 68 | return reactiveClient.executeRequest(request); 69 | } 70 | 71 | private Function outOfRetries() { 72 | return throwable -> { 73 | logger.debug("[{}]---> USED ALL RETRIES", feignMethodTag, throwable); 74 | return new OutOfRetriesException(throwable, feignMethodTag); 75 | }; 76 | } 77 | 78 | private static Function, Flux> wrapWithLog( 79 | Function, Flux> retryFunction, 80 | String feignMethodTag) { 81 | return throwableFlux -> retryFunction.apply(throwableFlux) 82 | .doOnNext(throwable -> { 83 | if (logger.isDebugEnabled()) { 84 | logger.debug("[{}]---> RETRYING on error", feignMethodTag, throwable); 85 | } 86 | }); 87 | } 88 | 89 | public static class OutOfRetriesException extends Exception { 90 | OutOfRetriesException(Throwable cause, String feignMethodTag) { 91 | super("All retries used for: " + feignMethodTag, cause); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /feign-reactor-core/src/main/java/feign/reactor/client/StatusHandlerReactiveHttpClient.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Feign Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package feign.reactor.client; 15 | 16 | import feign.MethodMetadata; 17 | import org.reactivestreams.Publisher; 18 | import feign.reactor.client.statushandler.ReactiveStatusHandler; 19 | import reactor.core.publisher.Flux; 20 | import reactor.core.publisher.Mono; 21 | import static feign.reactor.utils.FeignUtils.methodTag; 22 | 23 | /** 24 | * Uses statusHandlers to process status of http response 25 | * 26 | * @author Sergii Karpenko 27 | */ 28 | 29 | public class StatusHandlerReactiveHttpClient implements ReactiveHttpClient { 30 | 31 | private final ReactiveHttpClient reactiveClient; 32 | private final String methodTag; 33 | 34 | private final ReactiveStatusHandler statusHandler; 35 | 36 | public static ReactiveHttpClient handleStatus( 37 | ReactiveHttpClient reactiveClient, 38 | MethodMetadata methodMetadata, 39 | ReactiveStatusHandler statusHandler) { 40 | return new StatusHandlerReactiveHttpClient(reactiveClient, methodMetadata, statusHandler); 41 | } 42 | 43 | private StatusHandlerReactiveHttpClient(ReactiveHttpClient reactiveClient, 44 | MethodMetadata methodMetadata, 45 | ReactiveStatusHandler statusHandler) { 46 | this.reactiveClient = reactiveClient; 47 | this.methodTag = methodTag(methodMetadata); 48 | this.statusHandler = statusHandler; 49 | } 50 | 51 | @Override 52 | public Mono executeRequest(ReactiveHttpRequest request) { 53 | return reactiveClient.executeRequest(request).map(response -> { 54 | if (statusHandler.shouldHandle(response.status())) { 55 | return new ErrorReactiveHttpResponse(response, statusHandler.decode(methodTag, response)); 56 | } else { 57 | return response; 58 | } 59 | }); 60 | } 61 | 62 | private class ErrorReactiveHttpResponse extends DelegatingReactiveHttpResponse { 63 | 64 | private final Mono error; 65 | 66 | protected ErrorReactiveHttpResponse(ReactiveHttpResponse response, Mono error) { 67 | super(response); 68 | this.error = error; 69 | } 70 | 71 | @Override 72 | public Publisher body() { 73 | if (getResponse().body() instanceof Mono) { 74 | return error.flatMap(Mono::error); 75 | } else { 76 | return error.flatMapMany(Flux::error); 77 | } 78 | } 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /feign-reactor-core/src/main/java/feign/reactor/client/statushandler/CompositeStatusHandler.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Feign Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package feign.reactor.client.statushandler; 15 | 16 | import feign.reactor.client.ReactiveHttpResponse; 17 | import reactor.core.publisher.Mono; 18 | import java.util.List; 19 | import static java.util.Arrays.asList; 20 | 21 | /** 22 | * @author Sergii Karpenko 23 | */ 24 | public class CompositeStatusHandler implements ReactiveStatusHandler { 25 | 26 | private final List handlers; 27 | 28 | public static CompositeStatusHandler compose(ReactiveStatusHandler... handlers) { 29 | return new CompositeStatusHandler(asList(handlers)); 30 | } 31 | 32 | private CompositeStatusHandler(List handlers) { 33 | this.handlers = handlers; 34 | } 35 | 36 | @Override 37 | public boolean shouldHandle(int status) { 38 | return handlers.stream().anyMatch(handler -> handler.shouldHandle(status)); 39 | } 40 | 41 | @Override 42 | public Mono decode(String methodKey, ReactiveHttpResponse response) { 43 | return handlers.stream() 44 | .filter(statusHandler -> statusHandler 45 | .shouldHandle(response.status())) 46 | .findFirst() 47 | .map(statusHandler -> statusHandler.decode(methodKey, response)) 48 | .orElse(null); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /feign-reactor-core/src/main/java/feign/reactor/client/statushandler/ReactiveStatusHandler.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Feign Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package feign.reactor.client.statushandler; 15 | 16 | import feign.reactor.client.ReactiveHttpResponse; 17 | import reactor.core.publisher.Mono; 18 | 19 | /** 20 | * @author Sergii Karpenko 21 | */ 22 | public interface ReactiveStatusHandler { 23 | 24 | boolean shouldHandle(int status); 25 | 26 | Mono decode(String methodKey, ReactiveHttpResponse response); 27 | } 28 | -------------------------------------------------------------------------------- /feign-reactor-core/src/main/java/feign/reactor/client/statushandler/ReactiveStatusHandlers.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Feign Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package feign.reactor.client.statushandler; 15 | 16 | import feign.Response; 17 | import feign.codec.ErrorDecoder; 18 | import org.apache.commons.httpclient.HttpStatus; 19 | import feign.reactor.client.ReactiveHttpResponse; 20 | import reactor.core.publisher.Mono; 21 | import java.util.Map; 22 | import java.util.function.BiFunction; 23 | import java.util.function.Predicate; 24 | import java.util.stream.Collectors; 25 | import static feign.reactor.utils.HttpUtils.familyOf; 26 | 27 | public class ReactiveStatusHandlers { 28 | 29 | public static ReactiveStatusHandler defaultFeign(ErrorDecoder errorDecoder) { 30 | return new ReactiveStatusHandler() { 31 | 32 | @Override 33 | public boolean shouldHandle(int status) { 34 | return familyOf(status).isError(); 35 | } 36 | 37 | @Override 38 | public Mono decode(String methodTag, ReactiveHttpResponse response) { 39 | return response.bodyData().map(bodyData -> errorDecoder.decode(methodTag, 40 | Response.builder().status(response.status()) 41 | .reason(HttpStatus.getStatusText(response.status())) 42 | .headers(response.headers().entrySet() 43 | .stream() 44 | .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))) 45 | .body(bodyData).build())); 46 | } 47 | }; 48 | } 49 | 50 | public static ReactiveStatusHandler throwOnStatus(Predicate statusPredicate, 51 | BiFunction errorFunction) { 52 | return new ReactiveStatusHandler() { 53 | @Override 54 | public boolean shouldHandle(int status) { 55 | return statusPredicate.test(status); 56 | } 57 | 58 | @Override 59 | public Mono decode(String methodKey, ReactiveHttpResponse response) { 60 | return Mono.just(errorFunction.apply(methodKey, response)); 61 | } 62 | }; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /feign-reactor-core/src/main/java/feign/reactor/utils/FeignUtils.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Feign Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package feign.reactor.utils; 15 | 16 | import feign.MethodMetadata; 17 | import org.reactivestreams.Publisher; 18 | import java.lang.reflect.ParameterizedType; 19 | import java.lang.reflect.Type; 20 | import static feign.Util.resolveLastTypeParameter; 21 | import static java.util.Optional.ofNullable; 22 | 23 | public class FeignUtils { 24 | 25 | public static String methodTag(MethodMetadata methodMetadata) { 26 | return methodMetadata.configKey().substring(0, 27 | methodMetadata.configKey().indexOf('(')); 28 | } 29 | 30 | public static Type returnPublisherType(MethodMetadata methodMetadata) { 31 | final Type returnType = methodMetadata.returnType(); 32 | return ((ParameterizedType) returnType).getRawType(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /feign-reactor-core/src/main/java/feign/reactor/utils/HttpUtils.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Feign Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package feign.reactor.utils; 15 | 16 | import static feign.reactor.utils.HttpUtils.StatusCodeFamily.*; 17 | 18 | public class HttpUtils { 19 | 20 | public static StatusCodeFamily familyOf(final int statusCode) { 21 | switch (statusCode / 100) { 22 | case 1: 23 | return INFORMATIONAL; 24 | case 2: 25 | return SUCCESSFUL; 26 | case 3: 27 | return REDIRECTION; 28 | case 4: 29 | return CLIENT_ERROR; 30 | case 5: 31 | return SERVER_ERROR; 32 | default: 33 | return OTHER; 34 | } 35 | } 36 | 37 | public enum StatusCodeFamily { 38 | INFORMATIONAL(false), SUCCESSFUL(false), REDIRECTION(false), CLIENT_ERROR(true), SERVER_ERROR( 39 | true), OTHER(false); 40 | 41 | private final boolean error; 42 | 43 | StatusCodeFamily(boolean error) { 44 | this.error = error; 45 | } 46 | 47 | public boolean isError() { 48 | return error; 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /feign-reactor-core/src/main/java/feign/reactor/utils/MultiValueMapUtils.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Feign Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package feign.reactor.utils; 15 | 16 | import java.util.ArrayList; 17 | import java.util.Collection; 18 | import java.util.List; 19 | import java.util.Map; 20 | 21 | public class MultiValueMapUtils { 22 | 23 | public static void addAllOrdered(Map> multiMap, K key, List values) { 24 | multiMap.compute(key, (key_, values_) -> { 25 | List valuesMerged = values_ != null ? values_ : new ArrayList<>(values.size()); 26 | valuesMerged.addAll(values); 27 | return valuesMerged; 28 | }); 29 | } 30 | 31 | public static void addOrdered(Map> multiMap, K key, V value) { 32 | multiMap.compute(key, (key_, values_) -> { 33 | List valuesMerged = values_ != null ? values_ : new ArrayList<>(1); 34 | valuesMerged.add(value); 35 | return valuesMerged; 36 | }); 37 | } 38 | 39 | public static void addAll(Map> multiMap, K key, Collection values) { 40 | multiMap.compute(key, (key_, values_) -> { 41 | Collection valuesMerged = values_ != null ? values_ : new ArrayList<>(values.size()); 42 | valuesMerged.addAll(values); 43 | return valuesMerged; 44 | }); 45 | } 46 | 47 | public static void add(Map> multiMap, K key, V value) { 48 | multiMap.compute(key, (key_, values_) -> { 49 | Collection valuesMerged = values_ != null ? values_ : new ArrayList<>(1); 50 | valuesMerged.add(value); 51 | return valuesMerged; 52 | }); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /feign-reactor-core/src/main/java/feign/reactor/utils/Pair.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Feign Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package feign.reactor.utils; 15 | 16 | public class Pair { 17 | public final L left; 18 | public final R right; 19 | 20 | public Pair(L left, R right) { 21 | this.left = left; 22 | this.right = right; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /feign-reactor-core/src/test/java/feign/reactor/CompressionTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Feign Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package feign.reactor; 15 | 16 | import com.fasterxml.jackson.core.JsonProcessingException; 17 | import com.github.tomakehurst.wiremock.common.Gzip; 18 | import com.github.tomakehurst.wiremock.junit.WireMockClassRule; 19 | import feign.reactor.testcase.IcecreamServiceApi; 20 | import feign.reactor.testcase.domain.Bill; 21 | import feign.reactor.testcase.domain.IceCreamOrder; 22 | import feign.reactor.testcase.domain.OrderGenerator; 23 | import org.junit.ClassRule; 24 | import org.junit.Test; 25 | import reactor.core.publisher.Mono; 26 | import reactor.test.StepVerifier; 27 | import static com.github.tomakehurst.wiremock.client.WireMock.*; 28 | import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; 29 | import static feign.reactor.TestUtils.equalsComparingFieldByFieldRecursively; 30 | 31 | /** 32 | * Test the new capability of Reactive Feign client to support both Feign Request.Options 33 | * (regression) and the new ReactiveOptions configuration. 34 | * 35 | * @author Sergii Karpenko 36 | */ 37 | 38 | abstract public class CompressionTest { 39 | 40 | @ClassRule 41 | public static WireMockClassRule wireMockRule = new WireMockClassRule( 42 | wireMockConfig().dynamicPort()); 43 | 44 | abstract protected ReactiveFeign.Builder builder(ReactiveOptions options); 45 | 46 | @Test 47 | public void testCompression() throws JsonProcessingException { 48 | 49 | IceCreamOrder order = new OrderGenerator().generate(20); 50 | Bill billExpected = Bill.makeBill(order); 51 | 52 | wireMockRule.stubFor(post(urlEqualTo("/icecream/orders")) 53 | .withHeader("Accept-Encoding", containing("gzip")) 54 | .withRequestBody(equalTo(TestUtils.MAPPER.writeValueAsString(order))) 55 | .willReturn(aResponse().withStatus(200) 56 | .withHeader("Content-Type", "application/json") 57 | .withHeader("Content-Encoding", "gzip") 58 | .withBody(Gzip.gzip(TestUtils.MAPPER.writeValueAsString(billExpected))))); 59 | 60 | IcecreamServiceApi client = builder( 61 | new ReactiveOptions.Builder() 62 | .setTryUseCompression(true) 63 | .build()) 64 | .target(IcecreamServiceApi.class, "http://localhost:" + wireMockRule.port()); 65 | 66 | Mono bill = client.makeOrder(order); 67 | StepVerifier.create(bill) 68 | .expectNextMatches(equalsComparingFieldByFieldRecursively(billExpected)) 69 | .verifyComplete(); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /feign-reactor-core/src/test/java/feign/reactor/ConnectionTimeoutTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Feign Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package feign.reactor; 15 | 16 | import feign.reactor.testcase.IcecreamServiceApi; 17 | import org.hamcrest.Matchers; 18 | import org.junit.*; 19 | import org.junit.rules.ExpectedException; 20 | import java.io.IOException; 21 | import java.net.ConnectException; 22 | import java.net.ServerSocket; 23 | import java.net.Socket; 24 | 25 | /** 26 | * @author Sergii Karpenko 27 | */ 28 | abstract public class ConnectionTimeoutTest { 29 | 30 | @Rule 31 | public ExpectedException expectedException = ExpectedException.none(); 32 | 33 | private ServerSocket serverSocket; 34 | private Socket socket; 35 | private int port; 36 | 37 | abstract protected ReactiveFeign.Builder builder(ReactiveOptions options); 38 | 39 | @Before 40 | public void before() throws IOException { 41 | // server socket with single element backlog queue (1) and dynamicaly allocated 42 | // port (0) 43 | serverSocket = new ServerSocket(0, 1); 44 | // just get the allocated port 45 | port = serverSocket.getLocalPort(); 46 | // fill backlog queue by this request so consequent requests will be blocked 47 | socket = new Socket(); 48 | socket.connect(serverSocket.getLocalSocketAddress()); 49 | } 50 | 51 | @After 52 | public void after() throws IOException { 53 | // some cleanup 54 | if (serverSocket != null && !serverSocket.isClosed()) { 55 | serverSocket.close(); 56 | } 57 | } 58 | 59 | // TODO investigate why doesn't work on codecov.io but works locally 60 | @Ignore 61 | @Test 62 | public void shouldFailOnConnectionTimeout() { 63 | 64 | expectedException.expectCause( 65 | 66 | Matchers.any(ConnectException.class)); 67 | 68 | IcecreamServiceApi client = builder( 69 | new ReactiveOptions.Builder() 70 | .setConnectTimeoutMillis(300) 71 | .setReadTimeoutMillis(100) 72 | .build()) 73 | .target(IcecreamServiceApi.class, "http://localhost:" + port); 74 | 75 | client.findOrder(1).block(); 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /feign-reactor-core/src/test/java/feign/reactor/ContractTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Feign Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package feign.reactor; 15 | 16 | import feign.reactor.testcase.IcecreamServiceApi; 17 | import feign.reactor.testcase.IcecreamServiceApiBroken; 18 | import org.junit.Rule; 19 | import org.junit.Test; 20 | import org.junit.rules.ExpectedException; 21 | import static org.hamcrest.Matchers.containsString; 22 | 23 | /** 24 | * @author Sergii Karpenko 25 | */ 26 | 27 | abstract public class ContractTest { 28 | 29 | @Rule 30 | public ExpectedException expectedException = ExpectedException.none(); 31 | 32 | abstract protected ReactiveFeign.Builder builder(); 33 | 34 | @Test 35 | public void shouldFailOnBrokenContract() { 36 | 37 | expectedException.expect(IllegalArgumentException.class); 38 | expectedException.expectMessage(containsString("Broken Contract")); 39 | 40 | this.builder() 41 | .contract(targetType -> { 42 | throw new IllegalArgumentException("Broken Contract"); 43 | }) 44 | .target(IcecreamServiceApi.class, "http://localhost:8888"); 45 | } 46 | 47 | @Test 48 | public void shouldFailIfNotReactiveContract() { 49 | 50 | expectedException.expect(IllegalArgumentException.class); 51 | expectedException.expectMessage(containsString("IcecreamServiceApiBroken#findOrder(int)")); 52 | 53 | this.builder() 54 | .target(IcecreamServiceApiBroken.class, "http://localhost:8888"); 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /feign-reactor-core/src/test/java/feign/reactor/DefaultMethodTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Feign Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package feign.reactor; 15 | 16 | import com.fasterxml.jackson.core.JsonProcessingException; 17 | import com.github.tomakehurst.wiremock.junit.WireMockClassRule; 18 | import feign.RequestLine; 19 | import feign.reactor.testcase.IcecreamServiceApi; 20 | import feign.reactor.testcase.domain.IceCreamOrder; 21 | import feign.reactor.testcase.domain.OrderGenerator; 22 | import org.junit.Before; 23 | import org.junit.ClassRule; 24 | import org.junit.Test; 25 | import reactor.core.publisher.Flux; 26 | import reactor.core.publisher.Mono; 27 | import reactor.test.StepVerifier; 28 | import static com.github.tomakehurst.wiremock.client.WireMock.*; 29 | import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; 30 | import static feign.reactor.TestUtils.equalsComparingFieldByFieldRecursively; 31 | import static org.assertj.core.api.Assertions.assertThat; 32 | 33 | /** 34 | * @author Sergii Karpenko 35 | */ 36 | abstract public class DefaultMethodTest { 37 | 38 | @ClassRule 39 | public static WireMockClassRule wireMockRule = new WireMockClassRule( 40 | wireMockConfig().dynamicPort()); 41 | 42 | @Before 43 | public void resetServers() { 44 | wireMockRule.resetAll(); 45 | } 46 | 47 | abstract protected ReactiveFeign.Builder builder(); 48 | 49 | abstract protected ReactiveFeign.Builder builder(Class apiClass); 50 | 51 | abstract protected ReactiveFeign.Builder builder(ReactiveOptions options); 52 | 53 | @Test 54 | public void shouldProcessDefaultMethodOnProxy() throws JsonProcessingException { 55 | IceCreamOrder orderGenerated = new OrderGenerator().generate(1); 56 | String orderStr = TestUtils.MAPPER.writeValueAsString(orderGenerated); 57 | 58 | wireMockRule.stubFor(get(urlEqualTo("/icecream/orders/1")) 59 | .withHeader("Accept", equalTo("application/json")) 60 | .willReturn(aResponse().withStatus(200) 61 | .withHeader("Content-Type", "application/json") 62 | .withBody(orderStr))); 63 | 64 | IcecreamServiceApi client = builder() 65 | .target(IcecreamServiceApi.class, "http://localhost:" + wireMockRule.port()); 66 | 67 | StepVerifier.create(client.findFirstOrder()) 68 | .expectNextMatches(equalsComparingFieldByFieldRecursively(orderGenerated)) 69 | .verifyComplete(); 70 | } 71 | 72 | @Test 73 | public void shouldWrapExceptionWithMono() { 74 | IceCreamOrder orderGenerated = new OrderGenerator().generate(1); 75 | 76 | IcecreamServiceApi client = builder() 77 | .target(IcecreamServiceApi.class, "http://localhost:" + wireMockRule.port()); 78 | 79 | Mono errorOrder = client.throwExceptionMono().onErrorReturn( 80 | throwable -> throwable.equals(IcecreamServiceApi.RUNTIME_EXCEPTION), 81 | orderGenerated); 82 | 83 | StepVerifier.create(errorOrder) 84 | .expectNextMatches(equalsComparingFieldByFieldRecursively(orderGenerated)) 85 | .verifyComplete(); 86 | } 87 | 88 | @Test 89 | public void shouldWrapExceptionWithFlux() { 90 | IceCreamOrder orderGenerated = new OrderGenerator().generate(1); 91 | 92 | IcecreamServiceApi client = builder() 93 | .target(IcecreamServiceApi.class, "http://localhost:" + wireMockRule.port()); 94 | 95 | Flux errorOrder = client.throwExceptionFlux().onErrorReturn( 96 | throwable -> throwable.equals(IcecreamServiceApi.RUNTIME_EXCEPTION), 97 | orderGenerated); 98 | 99 | StepVerifier.create(errorOrder) 100 | .expectNextMatches(equalsComparingFieldByFieldRecursively(orderGenerated)) 101 | .verifyComplete(); 102 | } 103 | 104 | @Test 105 | public void shouldOverrideEquals() { 106 | 107 | IcecreamServiceApi client = builder( 108 | new ReactiveOptions.Builder() 109 | .setConnectTimeoutMillis(300) 110 | .setReadTimeoutMillis(100).build()) 111 | .target(IcecreamServiceApi.class, 112 | "http://localhost:" + wireMockRule.port()); 113 | 114 | IcecreamServiceApi clientWithSameTarget = builder() 115 | .target(IcecreamServiceApi.class, "http://localhost:" + wireMockRule.port()); 116 | assertThat(client).isEqualTo(clientWithSameTarget); 117 | 118 | IcecreamServiceApi clientWithOtherPort = builder() 119 | .target(IcecreamServiceApi.class, "http://localhost:" + (wireMockRule.port() + 1)); 120 | assertThat(client).isNotEqualTo(clientWithOtherPort); 121 | 122 | OtherApi clientWithOtherInterface = builder(OtherApi.class) 123 | .target(OtherApi.class, "http://localhost:" + wireMockRule.port()); 124 | assertThat(client).isNotEqualTo(clientWithOtherInterface); 125 | } 126 | 127 | interface OtherApi { 128 | @RequestLine("GET /icecream/flavors") 129 | Mono method(String arg); 130 | } 131 | 132 | @Test 133 | public void shouldOverrideHashcode() { 134 | 135 | IcecreamServiceApi client = builder() 136 | .target(IcecreamServiceApi.class, "http://localhost:" + wireMockRule.port()); 137 | 138 | IcecreamServiceApi otherClientWithSameTarget = builder() 139 | .target(IcecreamServiceApi.class, "http://localhost:" + wireMockRule.port()); 140 | 141 | assertThat(client.hashCode()).isEqualTo(otherClientWithSameTarget.hashCode()); 142 | } 143 | 144 | @Test 145 | public void shouldOverrideToString() { 146 | 147 | IcecreamServiceApi client = builder() 148 | .target(IcecreamServiceApi.class, "http://localhost:" + wireMockRule.port()); 149 | 150 | assertThat(client.toString()) 151 | .isEqualTo("HardCodedTarget(type=IcecreamServiceApi, " 152 | + "url=http://localhost:" + wireMockRule.port() + ")"); 153 | } 154 | 155 | } 156 | -------------------------------------------------------------------------------- /feign-reactor-core/src/test/java/feign/reactor/LoggerTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Feign Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | 15 | package feign.reactor; 16 | 17 | import com.github.tomakehurst.wiremock.junit.WireMockClassRule; 18 | import feign.reactor.client.LoggerReactiveHttpClient; 19 | import feign.reactor.testcase.IcecreamServiceApi; 20 | import feign.reactor.testcase.domain.Bill; 21 | import feign.reactor.testcase.domain.IceCreamOrder; 22 | import feign.reactor.testcase.domain.OrderGenerator; 23 | import org.apache.logging.log4j.Level; 24 | import org.apache.logging.log4j.LogManager; 25 | import org.apache.logging.log4j.core.Appender; 26 | import org.apache.logging.log4j.core.LogEvent; 27 | import org.apache.logging.log4j.core.LoggerContext; 28 | import org.apache.logging.log4j.core.config.Configuration; 29 | import org.apache.logging.log4j.core.config.LoggerConfig; 30 | import org.assertj.core.api.Condition; 31 | import org.junit.Before; 32 | import org.junit.ClassRule; 33 | import org.junit.Test; 34 | import org.mockito.ArgumentCaptor; 35 | import org.mockito.Mockito; 36 | import reactor.core.publisher.Mono; 37 | import java.util.List; 38 | import static com.github.tomakehurst.wiremock.client.WireMock.*; 39 | import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; 40 | import static org.assertj.core.api.Assertions.assertThat; 41 | import static org.mockito.Mockito.never; 42 | import static org.mockito.Mockito.times; 43 | import static org.mockito.Mockito.when; 44 | 45 | /** 46 | * @author Sergii Karpenko 47 | */ 48 | abstract public class LoggerTest { 49 | 50 | public static final String LOGGER_NAME = LoggerReactiveHttpClient.class.getName(); 51 | @ClassRule 52 | public static WireMockClassRule wireMockRule = new WireMockClassRule( 53 | wireMockConfig() 54 | .asynchronousResponseEnabled(true) 55 | .dynamicPort()); 56 | 57 | abstract protected ReactiveFeign.Builder builder(); 58 | 59 | protected Appender appender; 60 | 61 | @Test 62 | public void shouldLog() throws Exception { 63 | 64 | setLogLevel(Level.TRACE); 65 | 66 | IceCreamOrder order = new OrderGenerator().generate(20); 67 | Bill billExpected = Bill.makeBill(order); 68 | 69 | wireMockRule.stubFor(post(urlEqualTo("/icecream/orders")) 70 | .withRequestBody(equalTo(TestUtils.MAPPER.writeValueAsString(order))) 71 | .willReturn(aResponse().withStatus(200) 72 | .withHeader("Content-Type", "application/json") 73 | .withBody(TestUtils.MAPPER.writeValueAsString(billExpected)))); 74 | 75 | IcecreamServiceApi client = builder() 76 | .target(IcecreamServiceApi.class, 77 | "http://localhost:" + wireMockRule.port()); 78 | 79 | Mono billMono = client.makeOrder(order); 80 | 81 | // no logs before subscription 82 | ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(LogEvent.class); 83 | Mockito.verify(appender, never()).append(argumentCaptor.capture()); 84 | 85 | billMono.block(); 86 | 87 | Mockito.verify(appender, times(7)).append(argumentCaptor.capture()); 88 | 89 | List logEvents = argumentCaptor.getAllValues(); 90 | assertLogEvent(logEvents, 0, Level.DEBUG, 91 | "[IcecreamServiceApi#makeOrder]--->POST http://localhost"); 92 | assertLogEvent(logEvents, 1, Level.TRACE, 93 | "[IcecreamServiceApi#makeOrder] REQUEST HEADERS\n" + 94 | "Accept:[application/json]"); 95 | assertLogEvent(logEvents, 2, Level.TRACE, 96 | "[IcecreamServiceApi#makeOrder] REQUEST BODY\n" + 97 | "IceCreamOrder{ id=20, balls="); 98 | assertLogEvent(logEvents, 3, Level.TRACE, 99 | "[IcecreamServiceApi#makeOrder] RESPONSE HEADERS\n" + 100 | "Content-Type:application/json"); 101 | assertLogEvent(logEvents, 4, Level.DEBUG, 102 | "[IcecreamServiceApi#makeOrder]<--- headers takes"); 103 | assertLogEvent(logEvents, 5, Level.TRACE, 104 | "[IcecreamServiceApi#makeOrder] RESPONSE BODY\n" + 105 | "feign.reactor.testcase.domain.Bill"); 106 | assertLogEvent(logEvents, 6, Level.DEBUG, 107 | "[IcecreamServiceApi#makeOrder]<--- body takes"); 108 | } 109 | 110 | private void assertLogEvent(List events, int index, Level level, String message) { 111 | assertThat(events).element(index) 112 | .hasFieldOrPropertyWithValue("level", level) 113 | .extracting("message") 114 | .extractingResultOf("getFormattedMessage") 115 | .have(new Condition<>(o -> ((String) o).contains(message), "check message")); 116 | } 117 | 118 | @Before 119 | public void before() { 120 | appender = Mockito.mock(Appender.class); 121 | when(appender.getName()).thenReturn("TestAppender"); 122 | when(appender.isStarted()).thenReturn(true); 123 | getLoggerConfig().addAppender(appender, Level.ALL, null); 124 | } 125 | 126 | private static void setLogLevel(Level logLevel) { 127 | LoggerContext loggerContext = (LoggerContext) LogManager.getContext(false); 128 | Configuration configuration = loggerContext.getConfiguration(); 129 | configuration.getLoggerConfig(LOGGER_NAME).setLevel(logLevel); 130 | loggerContext.updateLoggers(); 131 | } 132 | 133 | private static LoggerConfig getLoggerConfig() { 134 | LoggerContext loggerContext = (LoggerContext) LogManager.getContext(false); 135 | Configuration configuration = loggerContext.getConfiguration(); 136 | configuration.addLogger(LOGGER_NAME, new LoggerConfig()); 137 | return configuration.getLoggerConfig(LOGGER_NAME); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /feign-reactor-core/src/test/java/feign/reactor/NotFoundTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Feign Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package feign.reactor; 15 | 16 | import com.github.tomakehurst.wiremock.junit.WireMockClassRule; 17 | import feign.reactor.testcase.IcecreamServiceApi; 18 | import org.apache.http.HttpStatus; 19 | import org.junit.ClassRule; 20 | import org.junit.Test; 21 | import reactor.test.StepVerifier; 22 | import static com.github.tomakehurst.wiremock.client.WireMock.*; 23 | import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; 24 | 25 | /** 26 | * @author Sergii Karpenko 27 | */ 28 | public abstract class NotFoundTest { 29 | 30 | @ClassRule 31 | public static WireMockClassRule wireMockRule = new WireMockClassRule( 32 | wireMockConfig().dynamicPort()); 33 | 34 | abstract protected ReactiveFeign.Builder builder(); 35 | 36 | @Test 37 | public void shouldReturnEmptyMono() { 38 | 39 | String orderUrl = "/icecream/orders/2"; 40 | wireMockRule.stubFor(get(urlEqualTo(orderUrl)) 41 | .withHeader("Accept", equalTo("application/json")) 42 | .willReturn(aResponse().withStatus(HttpStatus.SC_NOT_FOUND))); 43 | 44 | IcecreamServiceApi client = builder() 45 | .decode404() 46 | .target(IcecreamServiceApi.class, "http://localhost:" + wireMockRule.port()); 47 | 48 | StepVerifier.create(client.findOrder(2)) 49 | .expectNextCount(0) 50 | .verifyComplete(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /feign-reactor-core/src/test/java/feign/reactor/ReactivityTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Feign Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package feign.reactor; 15 | 16 | import com.fasterxml.jackson.core.JsonProcessingException; 17 | import com.github.tomakehurst.wiremock.junit.WireMockClassRule; 18 | import feign.reactor.testcase.IcecreamServiceApi; 19 | import feign.reactor.testcase.domain.IceCreamOrder; 20 | import feign.reactor.testcase.domain.OrderGenerator; 21 | import org.awaitility.Duration; 22 | import org.junit.ClassRule; 23 | import org.junit.Test; 24 | import java.util.concurrent.TimeUnit; 25 | import java.util.concurrent.atomic.AtomicInteger; 26 | import static com.github.tomakehurst.wiremock.client.WireMock.*; 27 | import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; 28 | import static org.assertj.core.api.Assertions.assertThat; 29 | import static org.awaitility.Awaitility.await; 30 | import static org.awaitility.Awaitility.waitAtMost; 31 | 32 | /** 33 | * @author Sergii Karpenko 34 | */ 35 | abstract public class ReactivityTest { 36 | 37 | public static final int DELAY_IN_MILLIS = 500; 38 | public static final int CALLS_NUMBER = 100; 39 | public static final int REACTIVE_GAIN_RATIO = 20; 40 | @ClassRule 41 | public static WireMockClassRule wireMockRule = new WireMockClassRule( 42 | wireMockConfig() 43 | .asynchronousResponseEnabled(true) 44 | .dynamicPort()); 45 | 46 | abstract protected ReactiveFeign.Builder builder(); 47 | 48 | @Test 49 | public void shouldRunReactively() throws JsonProcessingException { 50 | 51 | IceCreamOrder orderGenerated = new OrderGenerator().generate(1); 52 | String orderStr = TestUtils.MAPPER.writeValueAsString(orderGenerated); 53 | 54 | wireMockRule.stubFor(get(urlEqualTo("/icecream/orders/1")) 55 | .withHeader("Accept", equalTo("application/json")) 56 | .willReturn(aResponse().withStatus(200) 57 | .withHeader("Content-Type", "application/json") 58 | .withBody(orderStr) 59 | .withFixedDelay(DELAY_IN_MILLIS))); 60 | 61 | IcecreamServiceApi client = builder() 62 | .target(IcecreamServiceApi.class, 63 | "http://localhost:" + wireMockRule.port()); 64 | 65 | AtomicInteger counter = new AtomicInteger(); 66 | 67 | new Thread(() -> { 68 | for (int i = 0; i < CALLS_NUMBER; i++) { 69 | client.findFirstOrder() 70 | .doOnNext(order -> counter.incrementAndGet()) 71 | .subscribe(); 72 | } 73 | }).start(); 74 | 75 | int timeToCompleteReactively = CALLS_NUMBER * DELAY_IN_MILLIS / REACTIVE_GAIN_RATIO; 76 | waitAtMost(new Duration(timeToCompleteReactively, TimeUnit.MILLISECONDS)) 77 | .until(() -> counter.get() == CALLS_NUMBER); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /feign-reactor-core/src/test/java/feign/reactor/ReadTimeoutTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Feign Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package feign.reactor; 15 | 16 | import com.github.tomakehurst.wiremock.junit.WireMockClassRule; 17 | import feign.reactor.client.ReadTimeoutException; 18 | import feign.reactor.testcase.IcecreamServiceApi; 19 | import org.junit.ClassRule; 20 | import org.junit.Test; 21 | import reactor.test.StepVerifier; 22 | import static com.github.tomakehurst.wiremock.client.WireMock.*; 23 | import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; 24 | 25 | /** 26 | * @author Sergii Karpenko 27 | */ 28 | abstract public class ReadTimeoutTest { 29 | 30 | @ClassRule 31 | public static WireMockClassRule wireMockRule = new WireMockClassRule( 32 | wireMockConfig().dynamicPort()); 33 | 34 | abstract protected ReactiveFeign.Builder builder(ReactiveOptions options); 35 | 36 | @Test 37 | public void shouldFailOnReadTimeout() { 38 | 39 | String orderUrl = "/icecream/orders/1"; 40 | 41 | wireMockRule.stubFor(get(urlEqualTo(orderUrl)) 42 | .withHeader("Accept", equalTo("application/json")) 43 | .willReturn(aResponse().withFixedDelay(200))); 44 | 45 | IcecreamServiceApi client = builder( 46 | new ReactiveOptions.Builder() 47 | .setConnectTimeoutMillis(300) 48 | .setReadTimeoutMillis(100) 49 | .build()) 50 | .target(IcecreamServiceApi.class, 51 | "http://localhost:" + wireMockRule.port()); 52 | 53 | StepVerifier.create(client.findOrder(1)) 54 | .expectError(ReadTimeoutException.class) 55 | .verify(); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /feign-reactor-core/src/test/java/feign/reactor/RequestInterceptorTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Feign Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package feign.reactor; 15 | 16 | import com.fasterxml.jackson.core.JsonProcessingException; 17 | import com.github.tomakehurst.wiremock.junit.WireMockClassRule; 18 | import feign.FeignException; 19 | import feign.reactor.testcase.IcecreamServiceApi; 20 | import feign.reactor.testcase.domain.IceCreamOrder; 21 | import feign.reactor.testcase.domain.OrderGenerator; 22 | import org.apache.http.HttpStatus; 23 | import org.junit.ClassRule; 24 | import org.junit.Test; 25 | import reactor.test.StepVerifier; 26 | import static com.github.tomakehurst.wiremock.client.WireMock.*; 27 | import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; 28 | import static feign.reactor.TestUtils.equalsComparingFieldByFieldRecursively; 29 | import static feign.reactor.utils.MultiValueMapUtils.addOrdered; 30 | 31 | /** 32 | * @author Sergii Karpenko 33 | */ 34 | abstract public class RequestInterceptorTest { 35 | 36 | @ClassRule 37 | public static WireMockClassRule wireMockRule = new WireMockClassRule( 38 | wireMockConfig().dynamicPort()); 39 | 40 | abstract protected ReactiveFeign.Builder builder(); 41 | 42 | @Test 43 | public void shouldInterceptRequestAndSetAuthHeader() throws JsonProcessingException { 44 | 45 | String orderUrl = "/icecream/orders/1"; 46 | 47 | IceCreamOrder orderGenerated = new OrderGenerator().generate(1); 48 | String orderStr = TestUtils.MAPPER.writeValueAsString(orderGenerated); 49 | 50 | wireMockRule.stubFor(get(urlEqualTo(orderUrl)) 51 | .withHeader("Accept", equalTo("application/json")) 52 | .willReturn(aResponse().withStatus(HttpStatus.SC_UNAUTHORIZED))) 53 | .setPriority(100); 54 | 55 | wireMockRule.stubFor(get(urlEqualTo(orderUrl)) 56 | .withHeader("Accept", equalTo("application/json")) 57 | .withHeader("Authorization", equalTo("Bearer mytoken123")) 58 | .willReturn(aResponse().withStatus(200) 59 | .withHeader("Content-Type", "application/json") 60 | .withBody(orderStr))) 61 | .setPriority(1); 62 | 63 | IcecreamServiceApi clientWithoutAuth = builder() 64 | .target(IcecreamServiceApi.class, "http://localhost:" + wireMockRule.port()); 65 | 66 | StepVerifier.create(clientWithoutAuth.findFirstOrder()) 67 | .expectError(FeignException.class) 68 | .verify(); 69 | 70 | IcecreamServiceApi clientWithAuth = builder() 71 | .requestInterceptor(request -> { 72 | addOrdered(request.headers(), "Authorization", "Bearer mytoken123"); 73 | return request; 74 | }) 75 | .target(IcecreamServiceApi.class, 76 | "http://localhost:" + wireMockRule.port()); 77 | 78 | StepVerifier.create(clientWithAuth.findFirstOrder()) 79 | .expectNextMatches(equalsComparingFieldByFieldRecursively(orderGenerated)) 80 | .expectComplete(); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /feign-reactor-core/src/test/java/feign/reactor/RetryingTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Feign Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package feign.reactor; 15 | 16 | import com.fasterxml.jackson.core.JsonProcessingException; 17 | import com.github.tomakehurst.wiremock.client.ResponseDefinitionBuilder; 18 | import com.github.tomakehurst.wiremock.junit.WireMockClassRule; 19 | import feign.RetryableException; 20 | import feign.reactor.client.RetryReactiveHttpClient; 21 | import feign.reactor.testcase.IcecreamServiceApi; 22 | import feign.reactor.testcase.domain.IceCreamOrder; 23 | import feign.reactor.testcase.domain.Mixin; 24 | import feign.reactor.testcase.domain.OrderGenerator; 25 | import org.junit.Before; 26 | import org.junit.ClassRule; 27 | import org.junit.Test; 28 | import reactor.test.StepVerifier; 29 | import java.util.Arrays; 30 | import static com.github.tomakehurst.wiremock.client.WireMock.*; 31 | import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; 32 | import static com.github.tomakehurst.wiremock.stubbing.Scenario.STARTED; 33 | import static feign.reactor.TestUtils.equalsComparingFieldByFieldRecursively; 34 | import static org.apache.http.HttpHeaders.RETRY_AFTER; 35 | import static org.apache.http.HttpStatus.SC_OK; 36 | import static org.apache.http.HttpStatus.SC_SERVICE_UNAVAILABLE; 37 | import static org.hamcrest.CoreMatchers.allOf; 38 | import static org.hamcrest.Matchers.hasProperty; 39 | import static org.hamcrest.Matchers.isA; 40 | 41 | /** 42 | * @author Sergii Karpenko 43 | */ 44 | public abstract class RetryingTest { 45 | 46 | @ClassRule 47 | public static WireMockClassRule wireMockRule = new WireMockClassRule( 48 | wireMockConfig().dynamicPort()); 49 | 50 | abstract protected ReactiveFeign.Builder builder(); 51 | 52 | @Before 53 | public void resetServers() { 54 | wireMockRule.resetAll(); 55 | } 56 | 57 | @Test 58 | public void shouldSuccessOnRetriesMono() throws JsonProcessingException { 59 | 60 | IceCreamOrder orderGenerated = new OrderGenerator().generate(1); 61 | String orderStr = TestUtils.MAPPER.writeValueAsString(orderGenerated); 62 | 63 | mockResponseAfterSeveralAttempts(wireMockRule, 2, "testRetrying_success", 64 | "/icecream/orders/1", 65 | aResponse().withStatus(503).withHeader(RETRY_AFTER, "1"), 66 | aResponse().withStatus(200).withHeader("Content-Type", "application/json") 67 | .withBody(orderStr)); 68 | 69 | IcecreamServiceApi client = builder() 70 | .retryWhen(ReactiveRetryers.retryWithBackoff(3, 0)) 71 | .target(IcecreamServiceApi.class, 72 | "http://localhost:" + wireMockRule.port()); 73 | 74 | StepVerifier.create(client.findOrder(1)) 75 | .expectNextMatches(equalsComparingFieldByFieldRecursively(orderGenerated)) 76 | .verifyComplete(); 77 | } 78 | 79 | @Test 80 | public void shouldSuccessOnRetriesFlux() throws JsonProcessingException { 81 | 82 | String mixinsStr = TestUtils.MAPPER.writeValueAsString(Mixin.values()); 83 | 84 | mockResponseAfterSeveralAttempts(wireMockRule, 2, "testRetrying_success", 85 | "/icecream/mixins", 86 | aResponse().withStatus(SC_SERVICE_UNAVAILABLE).withHeader(RETRY_AFTER, 87 | "1"), 88 | aResponse().withStatus(SC_OK) 89 | .withHeader("Content-Type", "application/json") 90 | .withBody(mixinsStr)); 91 | 92 | IcecreamServiceApi client = builder() 93 | .retryWhen(ReactiveRetryers.retryWithBackoff(3, 0)) 94 | .target(IcecreamServiceApi.class, "http://localhost:" + wireMockRule.port()); 95 | 96 | StepVerifier.create(client.getAvailableMixins()) 97 | .expectNextSequence(Arrays.asList(Mixin.values())) 98 | .verifyComplete(); 99 | } 100 | 101 | @Test 102 | public void shouldSuccessOnRetriesWoRetryAfter() throws JsonProcessingException { 103 | 104 | IceCreamOrder orderGenerated = new OrderGenerator().generate(1); 105 | String orderStr = TestUtils.MAPPER.writeValueAsString(orderGenerated); 106 | 107 | mockResponseAfterSeveralAttempts(wireMockRule, 2, "testRetrying_success", 108 | "/icecream/orders/1", aResponse().withStatus(SC_SERVICE_UNAVAILABLE), 109 | aResponse().withStatus(SC_OK) 110 | .withHeader("Content-Type", "application/json") 111 | .withBody(orderStr)); 112 | 113 | IcecreamServiceApi client = builder() 114 | .retryWhen(ReactiveRetryers.retryWithBackoff(3, 0)) 115 | .target(IcecreamServiceApi.class, "http://localhost:" + wireMockRule.port()); 116 | 117 | StepVerifier.create(client.findOrder(1)) 118 | .expectNextMatches(equalsComparingFieldByFieldRecursively(orderGenerated)) 119 | .verifyComplete(); 120 | } 121 | 122 | private static void mockResponseAfterSeveralAttempts(WireMockClassRule rule, 123 | int failedAttemptsNo, 124 | String scenario, 125 | String url, 126 | ResponseDefinitionBuilder failResponse, 127 | ResponseDefinitionBuilder response) { 128 | String state = STARTED; 129 | for (int attempt = 0; attempt < failedAttemptsNo; attempt++) { 130 | String nextState = "attempt" + attempt; 131 | rule.stubFor( 132 | get(urlEqualTo(url)).withHeader("Accept", equalTo("application/json")) 133 | .inScenario(scenario).whenScenarioStateIs(state) 134 | .willReturn(failResponse).willSetStateTo(nextState)); 135 | 136 | state = nextState; 137 | } 138 | 139 | rule.stubFor(get(urlEqualTo(url)) 140 | .withHeader("Accept", equalTo("application/json")).inScenario(scenario) 141 | .whenScenarioStateIs(state).willReturn(response)); 142 | } 143 | 144 | @Test 145 | public void shouldFailAsNoMoreRetries() { 146 | 147 | String orderUrl = "/icecream/orders/1"; 148 | 149 | wireMockRule.stubFor(get(urlEqualTo(orderUrl)) 150 | .withHeader("Accept", equalTo("application/json")) 151 | .willReturn(aResponse().withStatus(503).withHeader(RETRY_AFTER, "1"))); 152 | 153 | IcecreamServiceApi client = builder() 154 | .retryWhen(ReactiveRetryers.retry(3)) 155 | .target(IcecreamServiceApi.class, "http://localhost:" + wireMockRule.port()); 156 | 157 | StepVerifier.create(client.findOrder(1)) 158 | .expectErrorMatches(throwable -> throwable instanceof RetryReactiveHttpClient.OutOfRetriesException 159 | && hasProperty("cause", isA(RetryableException.class)).matches(throwable)) 160 | .verify(); 161 | } 162 | 163 | @Test 164 | public void shouldFailAsNoMoreRetriesWithBackoff() { 165 | 166 | String orderUrl = "/icecream/orders/1"; 167 | 168 | wireMockRule.stubFor(get(urlEqualTo(orderUrl)) 169 | .withHeader("Accept", equalTo("application/json")) 170 | .willReturn(aResponse().withStatus(503).withHeader(RETRY_AFTER, "1"))); 171 | 172 | IcecreamServiceApi client = builder() 173 | .retryWhen(ReactiveRetryers.retryWithBackoff(3, 5)) 174 | .target(IcecreamServiceApi.class, "http://localhost:" + wireMockRule.port()); 175 | 176 | StepVerifier.create(client.findOrder(1)) 177 | .expectErrorMatches(throwable -> throwable instanceof RetryReactiveHttpClient.OutOfRetriesException 178 | && hasProperty("cause", isA(RetryableException.class)).matches(throwable)) 179 | .verify(); 180 | } 181 | 182 | } 183 | -------------------------------------------------------------------------------- /feign-reactor-core/src/test/java/feign/reactor/SmokeTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Feign Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package feign.reactor; 15 | 16 | import com.fasterxml.jackson.core.JsonProcessingException; 17 | import com.github.tomakehurst.wiremock.junit.WireMockClassRule; 18 | import feign.reactor.testcase.IcecreamServiceApi; 19 | import feign.reactor.testcase.domain.*; 20 | import org.junit.Before; 21 | import org.junit.ClassRule; 22 | import org.junit.Rule; 23 | import org.junit.Test; 24 | import org.junit.rules.ExpectedException; 25 | import reactor.core.publisher.Flux; 26 | import reactor.core.publisher.Mono; 27 | import reactor.test.StepVerifier; 28 | import java.util.Map; 29 | import java.util.stream.Collectors; 30 | import static com.github.tomakehurst.wiremock.client.WireMock.*; 31 | import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; 32 | import static feign.reactor.TestUtils.equalsComparingFieldByFieldRecursively; 33 | import static java.util.Arrays.asList; 34 | 35 | /** 36 | * @author Sergii Karpenko 37 | */ 38 | 39 | abstract public class SmokeTest { 40 | 41 | @ClassRule 42 | public static WireMockClassRule wireMockRule = new WireMockClassRule( 43 | wireMockConfig().dynamicPort()); 44 | 45 | @Before 46 | public void resetServers() { 47 | wireMockRule.resetAll(); 48 | } 49 | 50 | abstract protected ReactiveFeign.Builder builder(); 51 | 52 | private IcecreamServiceApi client; 53 | 54 | private OrderGenerator generator = new OrderGenerator(); 55 | private Map orders = generator.generateRange(10).stream() 56 | .collect(Collectors.toMap(IceCreamOrder::getId, o -> o)); 57 | 58 | @Rule 59 | public ExpectedException expectedException = ExpectedException.none(); 60 | 61 | @Before 62 | public void setUp() { 63 | String targetUrl = "http://localhost:" + wireMockRule.port(); 64 | client = builder() 65 | .decode404() 66 | .target(IcecreamServiceApi.class, targetUrl); 67 | } 68 | 69 | @Test 70 | public void testSimpleGet_success() throws JsonProcessingException { 71 | 72 | wireMockRule.stubFor(get(urlEqualTo("/icecream/flavors")) 73 | .willReturn(aResponse().withStatus(200) 74 | .withHeader("Content-Type", "application/json") 75 | .withBody(TestUtils.MAPPER.writeValueAsString(Flavor.values())))); 76 | 77 | wireMockRule.stubFor(get(urlEqualTo("/icecream/mixins")) 78 | .willReturn(aResponse().withStatus(200) 79 | .withHeader("Content-Type", "application/json") 80 | .withBody(TestUtils.MAPPER.writeValueAsString(Mixin.values())))); 81 | 82 | Flux flavors = client.getAvailableFlavors(); 83 | Flux mixins = client.getAvailableMixins(); 84 | 85 | StepVerifier.create(flavors) 86 | .expectNextSequence(asList(Flavor.values())) 87 | .verifyComplete(); 88 | StepVerifier.create(mixins) 89 | .expectNextSequence(asList(Mixin.values())) 90 | .verifyComplete(); 91 | 92 | } 93 | 94 | @Test 95 | public void testFindOrder_success() throws JsonProcessingException { 96 | 97 | IceCreamOrder orderExpected = orders.get(1); 98 | wireMockRule.stubFor(get(urlEqualTo("/icecream/orders/1")) 99 | .willReturn(aResponse().withStatus(200) 100 | .withHeader("Content-Type", "application/json") 101 | .withBody(TestUtils.MAPPER.writeValueAsString(orderExpected)))); 102 | 103 | Mono order = client.findOrder(1); 104 | 105 | StepVerifier.create(order) 106 | .expectNextMatches(equalsComparingFieldByFieldRecursively(orderExpected)) 107 | .verifyComplete(); 108 | } 109 | 110 | @Test 111 | public void testFindOrder_empty() { 112 | 113 | Mono orderEmpty = client.findOrder(123); 114 | 115 | StepVerifier.create(orderEmpty) 116 | .expectNextCount(0) 117 | .verifyComplete(); 118 | } 119 | 120 | @Test 121 | public void testMakeOrder_success() throws JsonProcessingException { 122 | 123 | IceCreamOrder order = new OrderGenerator().generate(20); 124 | Bill billExpected = Bill.makeBill(order); 125 | 126 | wireMockRule.stubFor(post(urlEqualTo("/icecream/orders")) 127 | .withRequestBody(equalTo(TestUtils.MAPPER.writeValueAsString(order))) 128 | .willReturn(aResponse().withStatus(200) 129 | .withHeader("Content-Type", "application/json") 130 | .withBody(TestUtils.MAPPER.writeValueAsString(billExpected)))); 131 | 132 | Mono bill = client.makeOrder(order); 133 | StepVerifier.create(bill) 134 | .expectNextMatches(equalsComparingFieldByFieldRecursively(billExpected)) 135 | .verifyComplete(); 136 | } 137 | 138 | @Test 139 | public void testPayBill_success() { 140 | 141 | Bill bill = Bill.makeBill(new OrderGenerator().generate(30)); 142 | 143 | Mono result = client.payBill(bill); 144 | StepVerifier.create(result) 145 | .verifyComplete(); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /feign-reactor-core/src/test/java/feign/reactor/StatusHandlerTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Feign Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package feign.reactor; 15 | 16 | import com.github.tomakehurst.wiremock.junit.WireMockClassRule; 17 | import feign.RetryableException; 18 | import feign.reactor.testcase.IcecreamServiceApi; 19 | import org.apache.http.HttpStatus; 20 | import org.junit.Before; 21 | import org.junit.ClassRule; 22 | import org.junit.Test; 23 | import reactor.test.StepVerifier; 24 | import static com.github.tomakehurst.wiremock.client.WireMock.*; 25 | import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; 26 | import static feign.reactor.client.statushandler.CompositeStatusHandler.compose; 27 | import static feign.reactor.client.statushandler.ReactiveStatusHandlers.throwOnStatus; 28 | 29 | /** 30 | * @author Sergii Karpenko 31 | */ 32 | public abstract class StatusHandlerTest { 33 | 34 | @ClassRule 35 | public static WireMockClassRule wireMockRule = new WireMockClassRule( 36 | wireMockConfig().dynamicPort()); 37 | 38 | abstract protected ReactiveFeign.Builder builder(); 39 | 40 | @Before 41 | public void resetServers() { 42 | wireMockRule.resetAll(); 43 | } 44 | 45 | @Test 46 | public void shouldThrowRetryException() { 47 | 48 | wireMockRule.stubFor(get(urlEqualTo("/icecream/orders/1")) 49 | .withHeader("Accept", equalTo("application/json")) 50 | .willReturn(aResponse().withStatus(HttpStatus.SC_SERVICE_UNAVAILABLE))); 51 | IcecreamServiceApi client = builder() 52 | .statusHandler(throwOnStatus( 53 | status -> status == HttpStatus.SC_SERVICE_UNAVAILABLE, 54 | (methodTag, response) -> new RetryableException("Should retry on next node", null))) 55 | .target(IcecreamServiceApi.class, "http://localhost:" + wireMockRule.port()); 56 | 57 | StepVerifier.create(client.findFirstOrder()) 58 | .expectError(RetryableException.class); 59 | } 60 | 61 | @Test 62 | public void shouldThrowOnStatusCode() { 63 | wireMockRule.stubFor(get(urlEqualTo("/icecream/orders/1")) 64 | .withHeader("Accept", equalTo("application/json")) 65 | .willReturn(aResponse().withStatus(HttpStatus.SC_SERVICE_UNAVAILABLE))); 66 | 67 | wireMockRule.stubFor(get(urlEqualTo("/icecream/orders/2")) 68 | .withHeader("Accept", equalTo("application/json")) 69 | .willReturn(aResponse().withStatus(HttpStatus.SC_UNAUTHORIZED))); 70 | 71 | 72 | IcecreamServiceApi client = builder() 73 | .statusHandler(compose( 74 | throwOnStatus( 75 | status -> status == HttpStatus.SC_SERVICE_UNAVAILABLE, 76 | (methodTag, response) -> new RetryableException("Should retry on next node", null)), 77 | throwOnStatus( 78 | status -> status == HttpStatus.SC_UNAUTHORIZED, 79 | (methodTag, response) -> new RuntimeException("Should login", null)))) 80 | .target(IcecreamServiceApi.class, "http://localhost:" + wireMockRule.port()); 81 | 82 | StepVerifier.create(client.findFirstOrder()) 83 | .expectError(RetryableException.class) 84 | .verify(); 85 | 86 | StepVerifier.create(client.findOrder(2)) 87 | .expectError(RuntimeException.class) 88 | .verify(); 89 | 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /feign-reactor-core/src/test/java/feign/reactor/TestUtils.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Feign Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package feign.reactor; 15 | 16 | import com.fasterxml.jackson.core.JsonProcessingException; 17 | import com.fasterxml.jackson.databind.ObjectMapper; 18 | import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; 19 | import java.util.function.Predicate; 20 | 21 | /** 22 | * Helper methods for tests. 23 | */ 24 | class TestUtils { 25 | static final ObjectMapper MAPPER; 26 | 27 | static { 28 | MAPPER = new ObjectMapper(); 29 | MAPPER.registerModule(new JavaTimeModule()); 30 | } 31 | 32 | public static Predicate equalsComparingFieldByFieldRecursively(T rhs) { 33 | return lhs -> { 34 | try { 35 | return MAPPER.writeValueAsString(lhs).equals(MAPPER.writeValueAsString(rhs)); 36 | } catch (JsonProcessingException e) { 37 | throw new RuntimeException(e); 38 | } 39 | }; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /feign-reactor-core/src/test/java/feign/reactor/testcase/IcecreamServiceApi.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Feign Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package feign.reactor.testcase; 15 | 16 | import feign.Headers; 17 | import feign.Param; 18 | import feign.RequestLine; 19 | import feign.reactor.testcase.domain.Bill; 20 | import feign.reactor.testcase.domain.Flavor; 21 | import feign.reactor.testcase.domain.IceCreamOrder; 22 | import feign.reactor.testcase.domain.Mixin; 23 | import reactor.core.publisher.Flux; 24 | import reactor.core.publisher.Mono; 25 | 26 | /** 27 | * API of an iceream web service. 28 | * 29 | * @author Sergii Karpenko 30 | */ 31 | @Headers({"Accept: application/json"}) 32 | public interface IcecreamServiceApi { 33 | 34 | RuntimeException RUNTIME_EXCEPTION = new RuntimeException("tests exception"); 35 | 36 | @RequestLine("GET /icecream/flavors") 37 | Flux getAvailableFlavors(); 38 | 39 | @RequestLine("GET /icecream/mixins") 40 | Flux getAvailableMixins(); 41 | 42 | @RequestLine("POST /icecream/orders") 43 | @Headers("Content-Type: application/json") 44 | Mono makeOrder(IceCreamOrder order); 45 | 46 | @RequestLine("GET /icecream/orders/{orderId}") 47 | Mono findOrder(@Param("orderId") int orderId); 48 | 49 | @RequestLine("POST /icecream/bills/pay") 50 | @Headers("Content-Type: application/json") 51 | Mono payBill(Bill bill); 52 | 53 | default Mono findFirstOrder() { 54 | return findOrder(1); 55 | } 56 | 57 | default Mono throwExceptionMono() { 58 | throw RUNTIME_EXCEPTION; 59 | } 60 | 61 | default Flux throwExceptionFlux() { 62 | throw RUNTIME_EXCEPTION; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /feign-reactor-core/src/test/java/feign/reactor/testcase/IcecreamServiceApiBroken.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Feign Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package feign.reactor.testcase; 15 | 16 | import feign.Headers; 17 | import feign.Param; 18 | import feign.RequestLine; 19 | import feign.reactor.ReactiveDelegatingContract; 20 | import feign.reactor.testcase.domain.Bill; 21 | import feign.reactor.testcase.domain.Flavor; 22 | import feign.reactor.testcase.domain.IceCreamOrder; 23 | import feign.reactor.testcase.domain.Mixin; 24 | import reactor.core.publisher.Flux; 25 | import reactor.core.publisher.Mono; 26 | import java.util.Collection; 27 | 28 | /** 29 | * API of an iceream web service with one method that doesn't returns {@link Mono} or {@link Flux} 30 | * and violates {@link ReactiveDelegatingContract}s rules. 31 | * 32 | * @author Sergii Karpenko 33 | */ 34 | public interface IcecreamServiceApiBroken { 35 | 36 | @RequestLine("GET /icecream/flavors") 37 | Mono> getAvailableFlavors(); 38 | 39 | @RequestLine("GET /icecream/mixins") 40 | Mono> getAvailableMixins(); 41 | 42 | @RequestLine("POST /icecream/orders") 43 | @Headers("Content-Type: application/json") 44 | Mono makeOrder(IceCreamOrder order); 45 | 46 | /** 47 | * Method that doesn't respects contract. 48 | */ 49 | @RequestLine("GET /icecream/orders/{orderId}") 50 | IceCreamOrder findOrder(@Param("orderId") int orderId); 51 | } 52 | -------------------------------------------------------------------------------- /feign-reactor-core/src/test/java/feign/reactor/testcase/domain/Bill.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Feign Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package feign.reactor.testcase.domain; 15 | 16 | import java.util.HashMap; 17 | import java.util.Map; 18 | 19 | /** 20 | * Bill for consumed ice cream. 21 | */ 22 | public class Bill { 23 | private static final Map PRICES = new HashMap<>(); 24 | 25 | static { 26 | PRICES.put(1, (float) 2.00); // two euros for one ball (expensive!) 27 | PRICES.put(3, (float) 2.85); // 2.85€ for 3 balls 28 | PRICES.put(5, (float) 4.30); // 4.30€ for 5 balls 29 | PRICES.put(7, (float) 5); // only five euros for seven balls! Wow 30 | } 31 | 32 | private static final float MIXIN_PRICE = (float) 0.6; // price per mixin 33 | 34 | private Float price; 35 | 36 | public Bill() {} 37 | 38 | public Bill(final Float price) { 39 | this.price = price; 40 | } 41 | 42 | public Float getPrice() { 43 | return price; 44 | } 45 | 46 | public void setPrice(final Float price) { 47 | this.price = price; 48 | } 49 | 50 | /** 51 | * Makes a bill from an order. 52 | * 53 | * @param order ice cream order 54 | * @return bill 55 | */ 56 | public static Bill makeBill(final IceCreamOrder order) { 57 | int nbBalls = order.getBalls().values().stream().mapToInt(Integer::intValue) 58 | .sum(); 59 | Float price = PRICES.get(nbBalls) + order.getMixins().size() * MIXIN_PRICE; 60 | return new Bill(price); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /feign-reactor-core/src/test/java/feign/reactor/testcase/domain/Flavor.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Feign Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package feign.reactor.testcase.domain; 15 | 16 | /** 17 | * Ice cream flavors. 18 | */ 19 | public enum Flavor { 20 | STRAWBERRY, CHOCOLATE, BANANA, PISTACHIO, MELON, VANILLA 21 | } 22 | -------------------------------------------------------------------------------- /feign-reactor-core/src/test/java/feign/reactor/testcase/domain/IceCreamOrder.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Feign Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package feign.reactor.testcase.domain; 15 | 16 | import java.time.Instant; 17 | import java.util.*; 18 | 19 | /** 20 | * Give me some ice-cream! :p 21 | */ 22 | public class IceCreamOrder { 23 | private static Random random = new Random(); 24 | 25 | private int id; // order id 26 | private Map balls; // how much balls of flavor 27 | private Set mixins; // and some mixins ... 28 | private Instant orderTimestamp; // and give it to me right now ! 29 | 30 | IceCreamOrder() {} 31 | 32 | IceCreamOrder(int id) { 33 | this(id, Instant.now()); 34 | } 35 | 36 | IceCreamOrder(int id, final Instant orderTimestamp) { 37 | this.id = id; 38 | this.balls = new HashMap<>(); 39 | this.mixins = new HashSet<>(); 40 | this.orderTimestamp = orderTimestamp; 41 | } 42 | 43 | IceCreamOrder addBall(final Flavor ballFlavor) { 44 | final Integer ballCount = balls.containsKey(ballFlavor) 45 | ? balls.get(ballFlavor) + 1 46 | : 1; 47 | balls.put(ballFlavor, ballCount); 48 | return this; 49 | } 50 | 51 | IceCreamOrder addMixin(final Mixin mixin) { 52 | mixins.add(mixin); 53 | return this; 54 | } 55 | 56 | IceCreamOrder withOrderTimestamp(final Instant orderTimestamp) { 57 | this.orderTimestamp = orderTimestamp; 58 | return this; 59 | } 60 | 61 | public int getId() { 62 | return id; 63 | } 64 | 65 | public Map getBalls() { 66 | return balls; 67 | } 68 | 69 | public Set getMixins() { 70 | return mixins; 71 | } 72 | 73 | public Instant getOrderTimestamp() { 74 | return orderTimestamp; 75 | } 76 | 77 | @Override 78 | public String toString() { 79 | return "IceCreamOrder{" + " id=" + id + ", balls=" + balls + ", mixins=" + mixins 80 | + ", orderTimestamp=" + orderTimestamp + '}'; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /feign-reactor-core/src/test/java/feign/reactor/testcase/domain/Mixin.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Feign Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package feign.reactor.testcase.domain; 15 | 16 | /** 17 | * Ice cream mix-ins. 18 | */ 19 | public enum Mixin { 20 | COOKIES, MNMS, CHOCOLATE_SIROP, STRAWBERRY_SIROP, NUTS, RAINBOW 21 | } 22 | -------------------------------------------------------------------------------- /feign-reactor-core/src/test/java/feign/reactor/testcase/domain/OrderGenerator.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Feign Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package feign.reactor.testcase.domain; 15 | 16 | import java.time.Instant; 17 | import java.time.temporal.ChronoUnit; 18 | import java.util.Collection; 19 | import java.util.List; 20 | import java.util.Random; 21 | import java.util.stream.Collectors; 22 | import java.util.stream.IntStream; 23 | 24 | /** 25 | * Generator of random ice cream orders. 26 | */ 27 | public class OrderGenerator { 28 | private static final int[] BALLS_NUMBER = {1, 3, 5, 7}; 29 | private static final int[] MIXIN_NUMBER = {1, 2, 3}; 30 | 31 | private static final Random random = new Random(); 32 | 33 | public IceCreamOrder generate(int id) { 34 | final IceCreamOrder order = new IceCreamOrder(id); 35 | final int nbBalls = peekBallsNumber(); 36 | final int nbMixins = peekMixinNumber(); 37 | 38 | IntStream.rangeClosed(1, nbBalls).mapToObj(i -> this.peekFlavor()) 39 | .forEach(order::addBall); 40 | 41 | IntStream.rangeClosed(1, nbMixins).mapToObj(i -> this.peekMixin()) 42 | .forEach(order::addMixin); 43 | 44 | return order; 45 | } 46 | 47 | public Collection generateRange(int n) { 48 | Instant now = Instant.now(); 49 | 50 | List orderTimestamps = IntStream.range(0, n) 51 | .mapToObj(minutes -> now.minus(minutes, ChronoUnit.MINUTES)) 52 | .collect(Collectors.toList()); 53 | 54 | return IntStream.range(0, n) 55 | .mapToObj( 56 | i -> this.generate(i).withOrderTimestamp(orderTimestamps.get(i))) 57 | .collect(Collectors.toList()); 58 | } 59 | 60 | private int peekBallsNumber() { 61 | return BALLS_NUMBER[random.nextInt(BALLS_NUMBER.length)]; 62 | } 63 | 64 | private int peekMixinNumber() { 65 | return MIXIN_NUMBER[random.nextInt(MIXIN_NUMBER.length)]; 66 | } 67 | 68 | private Flavor peekFlavor() { 69 | return Flavor.values()[random.nextInt(Flavor.values().length)]; 70 | } 71 | 72 | private Mixin peekMixin() { 73 | return Mixin.values()[random.nextInt(Mixin.values().length)]; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /feign-reactor-fake-impl/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 20 | 4.0.0 21 | 22 | 23 | io.github.openfeign.incubating 24 | feign-reactor 25 | 0.1.0 26 | 27 | 28 | feign-reactor-fake-impl 29 | jar 30 | Feign Reactive Fake Implementation 31 | 32 | 33 | ${project.basedir}/.. 34 | 35 | 5.0.5.RELEASE 36 | 37 | 38 | 39 | 40 | 41 | io.github.openfeign.incubating 42 | feign-reactor-core 43 | 0.1.0 44 | test 45 | 46 | 47 | 48 | io.github.openfeign.incubating 49 | feign-reactor-core 50 | 0.1.0 51 | test-jar 52 | test 53 | 54 | 55 | 56 | io.projectreactor 57 | reactor-core 58 | 59 | 60 | 61 | io.github.openfeign 62 | feign-core 63 | 64 | 65 | 66 | org.slf4j 67 | slf4j-api 68 | 69 | 70 | commons-httpclient 71 | commons-httpclient 72 | 73 | 74 | 75 | 76 | io.projectreactor 77 | reactor-test 78 | test 79 | 80 | 81 | junit 82 | junit 83 | test 84 | 85 | 86 | 87 | org.assertj 88 | assertj-core 89 | test 90 | 91 | 92 | 93 | com.github.tomakehurst 94 | wiremock 95 | test 96 | 97 | 98 | 99 | org.awaitility 100 | awaitility 101 | test 102 | 103 | 104 | 105 | org.hamcrest 106 | hamcrest-library 107 | test 108 | 109 | 110 | 111 | 112 | org.springframework 113 | spring-core 114 | ${spring.framework.version} 115 | test 116 | 117 | 118 | org.springframework 119 | spring-context 120 | ${spring.framework.version} 121 | test 122 | 123 | 124 | org.springframework 125 | spring-web 126 | ${spring.framework.version} 127 | test 128 | 129 | 130 | com.fasterxml.jackson.core 131 | jackson-core 132 | ${jackson.version} 133 | test 134 | 135 | 136 | com.fasterxml.jackson.core 137 | jackson-databind 138 | ${jackson.version} 139 | test 140 | 141 | 142 | com.fasterxml.jackson.core 143 | jackson-annotations 144 | ${jackson.version} 145 | test 146 | 147 | 148 | com.fasterxml.jackson.datatype 149 | jackson-datatype-jsr310 150 | test 151 | 152 | 153 | org.apache.logging.log4j 154 | log4j-slf4j-impl 155 | ${log4j.version} 156 | test 157 | 158 | 159 | org.mockito 160 | mockito-all 161 | test 162 | 163 | 164 | 165 | 166 | -------------------------------------------------------------------------------- /feign-reactor-fake-impl/src/test/java/feign/reactor/resttemplate/CompressionTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Feign Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package feign.reactor.resttemplate; 15 | 16 | import feign.reactor.ReactiveFeign; 17 | import feign.reactor.ReactiveOptions; 18 | import feign.reactor.resttemplate.client.RestTemplateFakeReactiveFeign; 19 | import feign.reactor.testcase.IcecreamServiceApi; 20 | 21 | /** 22 | * @author Sergii Karpenko 23 | */ 24 | public class CompressionTest extends feign.reactor.CompressionTest { 25 | 26 | @Override 27 | protected ReactiveFeign.Builder builder(ReactiveOptions options) { 28 | return RestTemplateFakeReactiveFeign.builder(options); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /feign-reactor-fake-impl/src/test/java/feign/reactor/resttemplate/ConnectionTimeoutTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Feign Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package feign.reactor.resttemplate; 15 | 16 | import feign.reactor.ReactiveFeign; 17 | import feign.reactor.ReactiveOptions; 18 | import feign.reactor.resttemplate.client.RestTemplateFakeReactiveFeign; 19 | import feign.reactor.testcase.IcecreamServiceApi; 20 | 21 | /** 22 | * @author Sergii Karpenko 23 | */ 24 | public class ConnectionTimeoutTest extends feign.reactor.ConnectionTimeoutTest { 25 | 26 | @Override 27 | protected ReactiveFeign.Builder builder(ReactiveOptions options) { 28 | return RestTemplateFakeReactiveFeign.builder(options); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /feign-reactor-fake-impl/src/test/java/feign/reactor/resttemplate/ContractTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Feign Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package feign.reactor.resttemplate; 15 | 16 | import feign.reactor.ReactiveFeign; 17 | import feign.reactor.resttemplate.client.RestTemplateFakeReactiveFeign; 18 | 19 | /** 20 | * @author Sergii Karpenko 21 | */ 22 | public class ContractTest extends feign.reactor.ContractTest { 23 | 24 | @Override 25 | protected ReactiveFeign.Builder builder() { 26 | return RestTemplateFakeReactiveFeign.builder(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /feign-reactor-fake-impl/src/test/java/feign/reactor/resttemplate/DefaultMethodTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Feign Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package feign.reactor.resttemplate; 15 | 16 | import feign.reactor.ReactiveFeign; 17 | import feign.reactor.ReactiveOptions; 18 | import feign.reactor.resttemplate.client.RestTemplateFakeReactiveFeign; 19 | import feign.reactor.testcase.IcecreamServiceApi; 20 | 21 | /** 22 | * @author Sergii Karpenko 23 | */ 24 | public class DefaultMethodTest extends feign.reactor.DefaultMethodTest { 25 | 26 | @Override 27 | protected ReactiveFeign.Builder builder() { 28 | return RestTemplateFakeReactiveFeign.builder(); 29 | } 30 | 31 | @Override 32 | protected ReactiveFeign.Builder builder(Class apiClass) { 33 | return RestTemplateFakeReactiveFeign.builder(); 34 | } 35 | 36 | @Override 37 | protected ReactiveFeign.Builder builder(ReactiveOptions options) { 38 | return RestTemplateFakeReactiveFeign.builder(options); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /feign-reactor-fake-impl/src/test/java/feign/reactor/resttemplate/LoggerTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Feign Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package feign.reactor.resttemplate; 15 | 16 | import feign.reactor.ReactiveFeign; 17 | import feign.reactor.resttemplate.client.RestTemplateFakeReactiveFeign; 18 | import feign.reactor.testcase.IcecreamServiceApi; 19 | 20 | /** 21 | * @author Sergii Karpenko 22 | */ 23 | public class LoggerTest extends feign.reactor.LoggerTest { 24 | 25 | @Override 26 | protected ReactiveFeign.Builder builder() { 27 | return RestTemplateFakeReactiveFeign.builder(); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /feign-reactor-fake-impl/src/test/java/feign/reactor/resttemplate/NotFoundTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Feign Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package feign.reactor.resttemplate; 15 | 16 | import feign.reactor.ReactiveFeign; 17 | import feign.reactor.resttemplate.client.RestTemplateFakeReactiveFeign; 18 | import feign.reactor.testcase.IcecreamServiceApi; 19 | 20 | /** 21 | * @author Sergii Karpenko 22 | */ 23 | public class NotFoundTest extends feign.reactor.NotFoundTest { 24 | 25 | @Override 26 | protected ReactiveFeign.Builder builder() { 27 | return RestTemplateFakeReactiveFeign.builder(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /feign-reactor-fake-impl/src/test/java/feign/reactor/resttemplate/ReactivityTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Feign Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package feign.reactor.resttemplate; 15 | 16 | import com.fasterxml.jackson.core.JsonProcessingException; 17 | import feign.reactor.ReactiveFeign; 18 | import feign.reactor.resttemplate.client.RestTemplateFakeReactiveFeign; 19 | import feign.reactor.testcase.IcecreamServiceApi; 20 | import org.awaitility.core.ConditionTimeoutException; 21 | import org.junit.Test; 22 | 23 | public class ReactivityTest extends feign.reactor.ReactivityTest { 24 | 25 | @Override 26 | protected ReactiveFeign.Builder builder() { 27 | return RestTemplateFakeReactiveFeign.builder(); 28 | } 29 | 30 | @Test(expected = ConditionTimeoutException.class) 31 | @Override 32 | public void shouldRunReactively() throws JsonProcessingException { 33 | super.shouldRunReactively(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /feign-reactor-fake-impl/src/test/java/feign/reactor/resttemplate/ReadTimeoutTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Feign Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package feign.reactor.resttemplate; 15 | 16 | import feign.reactor.ReactiveFeign; 17 | import feign.reactor.ReactiveOptions; 18 | import feign.reactor.resttemplate.client.RestTemplateFakeReactiveFeign; 19 | import feign.reactor.testcase.IcecreamServiceApi; 20 | 21 | /** 22 | * @author Sergii Karpenko 23 | */ 24 | public class ReadTimeoutTest extends feign.reactor.ReadTimeoutTest { 25 | 26 | @Override 27 | protected ReactiveFeign.Builder builder(ReactiveOptions options) { 28 | return RestTemplateFakeReactiveFeign.builder(options); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /feign-reactor-fake-impl/src/test/java/feign/reactor/resttemplate/RequestInterceptorTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Feign Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package feign.reactor.resttemplate; 15 | 16 | import feign.reactor.ReactiveFeign; 17 | import feign.reactor.resttemplate.client.RestTemplateFakeReactiveFeign; 18 | import feign.reactor.testcase.IcecreamServiceApi; 19 | 20 | /** 21 | * @author Sergii Karpenko 22 | */ 23 | public class RequestInterceptorTest extends feign.reactor.RequestInterceptorTest { 24 | 25 | @Override 26 | protected ReactiveFeign.Builder builder() { 27 | return RestTemplateFakeReactiveFeign.builder(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /feign-reactor-fake-impl/src/test/java/feign/reactor/resttemplate/RetryingTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Feign Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package feign.reactor.resttemplate; 15 | 16 | import feign.reactor.ReactiveFeign; 17 | import feign.reactor.resttemplate.client.RestTemplateFakeReactiveFeign; 18 | import feign.reactor.testcase.IcecreamServiceApi; 19 | 20 | /** 21 | * @author Sergii Karpenko 22 | */ 23 | public class RetryingTest extends feign.reactor.RetryingTest { 24 | 25 | @Override 26 | protected ReactiveFeign.Builder builder() { 27 | return RestTemplateFakeReactiveFeign.builder(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /feign-reactor-fake-impl/src/test/java/feign/reactor/resttemplate/SmokeTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Feign Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package feign.reactor.resttemplate; 15 | 16 | import feign.reactor.ReactiveFeign; 17 | import feign.reactor.resttemplate.client.RestTemplateFakeReactiveFeign; 18 | import feign.reactor.testcase.IcecreamServiceApi; 19 | 20 | /** 21 | * @author Sergii Karpenko 22 | */ 23 | public class SmokeTest extends feign.reactor.SmokeTest { 24 | 25 | @Override 26 | protected ReactiveFeign.Builder builder() { 27 | return RestTemplateFakeReactiveFeign.builder(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /feign-reactor-fake-impl/src/test/java/feign/reactor/resttemplate/StatusHandlerTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Feign Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package feign.reactor.resttemplate; 15 | 16 | import feign.reactor.ReactiveFeign; 17 | import feign.reactor.resttemplate.client.RestTemplateFakeReactiveFeign; 18 | import feign.reactor.testcase.IcecreamServiceApi; 19 | 20 | /** 21 | * @author Sergii Karpenko 22 | */ 23 | public class StatusHandlerTest extends feign.reactor.StatusHandlerTest { 24 | 25 | @Override 26 | protected ReactiveFeign.Builder builder() { 27 | return RestTemplateFakeReactiveFeign.builder(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /feign-reactor-fake-impl/src/test/java/feign/reactor/resttemplate/client/RestTemplateFakeReactiveFeign.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Feign Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package feign.reactor.resttemplate.client; 15 | 16 | import feign.reactor.ReactiveFeign; 17 | import feign.reactor.ReactiveOptions; 18 | import feign.reactor.client.ReactiveHttpClient; 19 | import org.apache.http.impl.client.HttpClientBuilder; 20 | import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; 21 | import org.springframework.web.client.RestTemplate; 22 | 23 | /** 24 | * {@link RestTemplate} based implementation 25 | * 26 | * @author Sergii Karpenko 27 | */ 28 | public class RestTemplateFakeReactiveFeign { 29 | 30 | public static ReactiveFeign.Builder builder() { 31 | return builder(new RestTemplate(), false); 32 | } 33 | 34 | public static ReactiveFeign.Builder builder(ReactiveOptions options) { 35 | HttpComponentsClientHttpRequestFactory requestFactory = 36 | new HttpComponentsClientHttpRequestFactory( 37 | HttpClientBuilder.create().build()); 38 | if (options.getConnectTimeoutMillis() != null) { 39 | requestFactory.setConnectTimeout(options.getConnectTimeoutMillis()); 40 | } 41 | if (options.getReadTimeoutMillis() != null) { 42 | requestFactory.setReadTimeout(options.getReadTimeoutMillis()); 43 | } 44 | return builder(new RestTemplate(requestFactory), 45 | options.isTryUseCompression() != null && options.isTryUseCompression()); 46 | } 47 | 48 | public static ReactiveFeign.Builder builder(RestTemplate restTemplate, 49 | boolean acceptGzip) { 50 | return new ReactiveFeign.Builder(){}.clientFactory( 51 | methodMetadata -> new RestTemplateFakeReactiveHttpClient( 52 | methodMetadata, restTemplate, acceptGzip)); 53 | } 54 | } 55 | 56 | 57 | -------------------------------------------------------------------------------- /feign-reactor-fake-impl/src/test/java/feign/reactor/resttemplate/client/RestTemplateFakeReactiveHttpClient.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2018 The Feign Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package feign.reactor.resttemplate.client; 15 | 16 | import com.fasterxml.jackson.databind.ObjectMapper; 17 | import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; 18 | import feign.MethodMetadata; 19 | import feign.reactor.client.ReactiveHttpClient; 20 | import feign.reactor.client.ReactiveHttpRequest; 21 | import feign.reactor.client.ReactiveHttpResponse; 22 | import feign.reactor.client.ReadTimeoutException; 23 | import org.reactivestreams.Publisher; 24 | import org.springframework.core.ParameterizedTypeReference; 25 | import org.springframework.http.HttpEntity; 26 | import org.springframework.http.HttpMethod; 27 | import org.springframework.http.ResponseEntity; 28 | import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; 29 | import org.springframework.util.LinkedMultiValueMap; 30 | import org.springframework.util.MultiValueMap; 31 | import org.springframework.web.client.HttpStatusCodeException; 32 | import org.springframework.web.client.ResourceAccessException; 33 | import org.springframework.web.client.RestTemplate; 34 | import reactor.core.publisher.Flux; 35 | import reactor.core.publisher.Mono; 36 | 37 | import java.lang.reflect.ParameterizedType; 38 | import java.lang.reflect.Type; 39 | import java.net.SocketTimeoutException; 40 | import java.util.List; 41 | import java.util.Map; 42 | 43 | import static feign.Util.resolveLastTypeParameter; 44 | import static org.springframework.core.ParameterizedTypeReference.forType; 45 | 46 | public class RestTemplateFakeReactiveHttpClient implements ReactiveHttpClient { 47 | 48 | private final RestTemplate restTemplate; 49 | private final boolean acceptGzip; 50 | private final Type returnPublisherType; 51 | private final ParameterizedTypeReference returnActualType; 52 | 53 | RestTemplateFakeReactiveHttpClient(MethodMetadata methodMetadata, 54 | RestTemplate restTemplate, 55 | boolean acceptGzip) { 56 | this.restTemplate = restTemplate; 57 | this.acceptGzip = acceptGzip; 58 | 59 | ObjectMapper mapper = new ObjectMapper(); 60 | mapper.registerModule(new JavaTimeModule()); 61 | 62 | MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); 63 | converter.setObjectMapper(mapper); 64 | restTemplate.getMessageConverters().add(0, converter); 65 | 66 | 67 | final Type returnType = methodMetadata.returnType(); 68 | returnPublisherType = ((ParameterizedType) returnType).getRawType(); 69 | returnActualType = forType( 70 | resolveLastTypeParameter(returnType, (Class) returnPublisherType)); 71 | } 72 | 73 | @Override 74 | public Mono executeRequest(ReactiveHttpRequest request) { 75 | 76 | Mono bodyMono; 77 | if (request.body() instanceof Mono) { 78 | bodyMono = ((Mono) request.body()); 79 | } else if (request.body() instanceof Flux) { 80 | bodyMono = ((Flux) request.body()).collectList(); 81 | } else { 82 | bodyMono = Mono.just(request.body()); 83 | } 84 | bodyMono = bodyMono.switchIfEmpty(Mono.just(new byte[0])); 85 | 86 | return bodyMono.flatMap(body -> { 87 | MultiValueMap headers = new LinkedMultiValueMap<>(request.headers()); 88 | if (acceptGzip) { 89 | headers.add("Accept-Encoding", "gzip"); 90 | } 91 | 92 | ResponseEntity response = 93 | restTemplate.exchange(request.uri().toString(), HttpMethod.valueOf(request.method()), 94 | new HttpEntity<>(body, headers), responseType()); 95 | 96 | return Mono.just(new FakeReactiveHttpResponse(response, returnPublisherType)); 97 | }) 98 | .onErrorMap(ex -> ex instanceof ResourceAccessException 99 | && ex.getCause() instanceof SocketTimeoutException, 100 | ReadTimeoutException::new) 101 | .onErrorResume(HttpStatusCodeException.class, 102 | ex -> Mono.just(new ErrorReactiveHttpResponse(ex))); 103 | } 104 | 105 | private ParameterizedTypeReference responseType(){ 106 | if (returnPublisherType == Mono.class) { 107 | return returnActualType; 108 | } else { 109 | return forType(new ParameterizedType() { 110 | @Override 111 | public Type[] getActualTypeArguments() { 112 | return new Type[] {returnActualType.getType()}; 113 | } 114 | 115 | @Override 116 | public Type getRawType() { 117 | return List.class; 118 | } 119 | 120 | @Override 121 | public Type getOwnerType() { 122 | return null; 123 | } 124 | }); 125 | } 126 | } 127 | 128 | private static class FakeReactiveHttpResponse implements ReactiveHttpResponse{ 129 | 130 | private final ResponseEntity response; 131 | private final Type returnPublisherType; 132 | 133 | private FakeReactiveHttpResponse(ResponseEntity response, Type returnPublisherType) { 134 | this.response = response; 135 | this.returnPublisherType = returnPublisherType; 136 | } 137 | 138 | @Override 139 | public int status() { 140 | return response.getStatusCodeValue(); 141 | } 142 | 143 | @Override 144 | public Map> headers() { 145 | return response.getHeaders(); 146 | } 147 | 148 | @Override 149 | public Publisher body() { 150 | if (returnPublisherType == Mono.class) { 151 | return Mono.just(response.getBody()); 152 | } else { 153 | return Flux.fromIterable((List)response.getBody()); 154 | } 155 | } 156 | 157 | @Override 158 | public Mono bodyData() { 159 | return Mono.just(new byte[0]); 160 | } 161 | } 162 | 163 | private static class ErrorReactiveHttpResponse implements ReactiveHttpResponse{ 164 | 165 | private final HttpStatusCodeException ex; 166 | 167 | private ErrorReactiveHttpResponse(HttpStatusCodeException ex) { 168 | this.ex = ex; 169 | } 170 | 171 | @Override 172 | public int status() { 173 | return ex.getStatusCode().value(); 174 | } 175 | 176 | @Override 177 | public Map> headers() { 178 | return ex.getResponseHeaders(); 179 | } 180 | 181 | @Override 182 | public Publisher body() { 183 | return Mono.empty(); 184 | } 185 | 186 | @Override 187 | public Mono bodyData() { 188 | return Mono.just(ex.getResponseBodyAsByteArray()); 189 | } 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /feign-reactor-fake-impl/src/test/resources/log4j.properties: -------------------------------------------------------------------------------- 1 | log4j.rootLogger=TRACE, stdout 2 | log4j.appender.stdout=org.apache.log4j.ConsoleAppender 3 | log4j.appender.stdout.Target=System.out 4 | log4j.appender.stdout.layout=org.apache.log4j.PatternLayout 5 | log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven2 Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /etc/mavenrc ] ; then 40 | . /etc/mavenrc 41 | fi 42 | 43 | if [ -f "$HOME/.mavenrc" ] ; then 44 | . "$HOME/.mavenrc" 45 | fi 46 | 47 | fi 48 | 49 | # OS specific support. $var _must_ be set to either true or false. 50 | cygwin=false; 51 | darwin=false; 52 | mingw=false 53 | case "`uname`" in 54 | CYGWIN*) cygwin=true ;; 55 | MINGW*) mingw=true;; 56 | Darwin*) darwin=true 57 | # 58 | # Look for the Apple JDKs first to preserve the existing behaviour, and then look 59 | # for the new JDKs provided by Oracle. 60 | # 61 | if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK ] ; then 62 | # 63 | # Apple JDKs 64 | # 65 | export JAVA_HOME=/System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK/Home 66 | fi 67 | 68 | if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Java/JavaVirtualMachines/CurrentJDK ] ; then 69 | # 70 | # Apple JDKs 71 | # 72 | export JAVA_HOME=/System/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home 73 | fi 74 | 75 | if [ -z "$JAVA_HOME" ] && [ -L "/Library/Java/JavaVirtualMachines/CurrentJDK" ] ; then 76 | # 77 | # Oracle JDKs 78 | # 79 | export JAVA_HOME=/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home 80 | fi 81 | 82 | if [ -z "$JAVA_HOME" ] && [ -x "/usr/libexec/java_home" ]; then 83 | # 84 | # Apple JDKs 85 | # 86 | export JAVA_HOME=`/usr/libexec/java_home` 87 | fi 88 | ;; 89 | esac 90 | 91 | if [ -z "$JAVA_HOME" ] ; then 92 | if [ -r /etc/gentoo-release ] ; then 93 | JAVA_HOME=`java-config --jre-home` 94 | fi 95 | fi 96 | 97 | if [ -z "$M2_HOME" ] ; then 98 | ## resolve links - $0 may be a link to maven's home 99 | PRG="$0" 100 | 101 | # need this for relative symlinks 102 | while [ -h "$PRG" ] ; do 103 | ls=`ls -ld "$PRG"` 104 | link=`expr "$ls" : '.*-> \(.*\)$'` 105 | if expr "$link" : '/.*' > /dev/null; then 106 | PRG="$link" 107 | else 108 | PRG="`dirname "$PRG"`/$link" 109 | fi 110 | done 111 | 112 | saveddir=`pwd` 113 | 114 | M2_HOME=`dirname "$PRG"`/.. 115 | 116 | # make it fully qualified 117 | M2_HOME=`cd "$M2_HOME" && pwd` 118 | 119 | cd "$saveddir" 120 | # echo Using m2 at $M2_HOME 121 | fi 122 | 123 | # For Cygwin, ensure paths are in UNIX format before anything is touched 124 | if $cygwin ; then 125 | [ -n "$M2_HOME" ] && 126 | M2_HOME=`cygpath --unix "$M2_HOME"` 127 | [ -n "$JAVA_HOME" ] && 128 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 129 | [ -n "$CLASSPATH" ] && 130 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 131 | fi 132 | 133 | # For Migwn, ensure paths are in UNIX format before anything is touched 134 | if $mingw ; then 135 | [ -n "$M2_HOME" ] && 136 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 137 | [ -n "$JAVA_HOME" ] && 138 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 139 | # TODO classpath? 140 | fi 141 | 142 | if [ -z "$JAVA_HOME" ]; then 143 | javaExecutable="`which javac`" 144 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 145 | # readlink(1) is not available as standard on Solaris 10. 146 | readLink=`which readlink` 147 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 148 | if $darwin ; then 149 | javaHome="`dirname \"$javaExecutable\"`" 150 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 151 | else 152 | javaExecutable="`readlink -f \"$javaExecutable\"`" 153 | fi 154 | javaHome="`dirname \"$javaExecutable\"`" 155 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 156 | JAVA_HOME="$javaHome" 157 | export JAVA_HOME 158 | fi 159 | fi 160 | fi 161 | 162 | if [ -z "$JAVACMD" ] ; then 163 | if [ -n "$JAVA_HOME" ] ; then 164 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 165 | # IBM's JDK on AIX uses strange locations for the executables 166 | JAVACMD="$JAVA_HOME/jre/sh/java" 167 | else 168 | JAVACMD="$JAVA_HOME/bin/java" 169 | fi 170 | else 171 | JAVACMD="`which java`" 172 | fi 173 | fi 174 | 175 | if [ ! -x "$JAVACMD" ] ; then 176 | echo "Error: JAVA_HOME is not defined correctly." >&2 177 | echo " We cannot execute $JAVACMD" >&2 178 | exit 1 179 | fi 180 | 181 | if [ -z "$JAVA_HOME" ] ; then 182 | echo "Warning: JAVA_HOME environment variable is not set." 183 | fi 184 | 185 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 186 | 187 | # traverses directory structure from process work directory to filesystem root 188 | # first directory with .mvn subdirectory is considered project base directory 189 | find_maven_basedir() { 190 | local basedir=$(pwd) 191 | local wdir=$(pwd) 192 | while [ "$wdir" != '/' ] ; do 193 | if [ -d "$wdir"/.mvn ] ; then 194 | basedir=$wdir 195 | break 196 | fi 197 | wdir=$(cd "$wdir/.."; pwd) 198 | done 199 | echo "${basedir}" 200 | } 201 | 202 | # concatenates all lines of a file 203 | concat_lines() { 204 | if [ -f "$1" ]; then 205 | echo "$(tr -s '\n' ' ' < "$1")" 206 | fi 207 | } 208 | 209 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-$(find_maven_basedir)} 210 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 211 | 212 | # For Cygwin, switch paths to Windows format before running java 213 | if $cygwin; then 214 | [ -n "$M2_HOME" ] && 215 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 216 | [ -n "$JAVA_HOME" ] && 217 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 218 | [ -n "$CLASSPATH" ] && 219 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 220 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 221 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 222 | fi 223 | 224 | # Provide a "standardized" way to retrieve the CLI args that will 225 | # work with both Windows and non-Windows executions. 226 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" 227 | export MAVEN_CMD_LINE_ARGS 228 | 229 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 230 | 231 | # avoid using MAVEN_CMD_LINE_ARGS below since that would loose parameter escaping in $@ 232 | exec "$JAVACMD" \ 233 | $MAVEN_OPTS \ 234 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 235 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 236 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 237 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM http://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven2 Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' 39 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 40 | 41 | @REM set %HOME% to equivalent of $HOME 42 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 43 | 44 | @REM Execute a user defined script before this one 45 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 46 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 47 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 48 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 49 | :skipRcPre 50 | 51 | @setlocal 52 | 53 | set ERROR_CODE=0 54 | 55 | @REM To isolate internal variables from possible post scripts, we use another setlocal 56 | @setlocal 57 | 58 | @REM ==== START VALIDATION ==== 59 | if not "%JAVA_HOME%" == "" goto OkJHome 60 | 61 | echo. 62 | echo Error: JAVA_HOME not found in your environment. >&2 63 | echo Please set the JAVA_HOME variable in your environment to match the >&2 64 | echo location of your Java installation. >&2 65 | echo. 66 | goto error 67 | 68 | :OkJHome 69 | if exist "%JAVA_HOME%\bin\java.exe" goto init 70 | 71 | echo. 72 | echo Error: JAVA_HOME is set to an invalid directory. >&2 73 | echo JAVA_HOME = "%JAVA_HOME%" >&2 74 | echo Please set the JAVA_HOME variable in your environment to match the >&2 75 | echo location of your Java installation. >&2 76 | echo. 77 | goto error 78 | 79 | @REM ==== END VALIDATION ==== 80 | 81 | :init 82 | 83 | set MAVEN_CMD_LINE_ARGS=%MAVEN_CONFIG% %* 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | 121 | set WRAPPER_JAR=""%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"" 122 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 123 | 124 | # avoid using MAVEN_CMD_LINE_ARGS below since that would loose parameter escaping in %* 125 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 126 | if ERRORLEVEL 1 goto error 127 | goto end 128 | 129 | :error 130 | set ERROR_CODE=1 131 | 132 | :end 133 | @endlocal & set ERROR_CODE=%ERROR_CODE% 134 | 135 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 136 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 137 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 138 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 139 | :skipRcPost 140 | 141 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 142 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 143 | 144 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 145 | 146 | exit /B %ERROR_CODE% 147 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 4.0.0 19 | 20 | io.github.openfeign.incubating 21 | feign-reactor 22 | 0.1.0 23 | pom 24 | 25 | 26 | feign-reactor-core 27 | feign-reactor-fake-impl 28 | 29 | 30 | Feign Reactive 31 | https://github.com/OpenFeign/feign-reactive 32 | 2018 33 | 34 | 35 | OpenFeign 36 | https://github.com/openfeign 37 | 38 | 39 | 40 | Github 41 | https://github.com/OpenFeign/feign-reactive/issues 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | The Apache Software License, Version 2.0 54 | http://www.apache.org/licenses/LICENSE-2.0.txt 55 | repo 56 | 57 | 58 | 59 | 60 | 61 | kptfh 62 | Sergii Karpenko 63 | sergey.karpenko@gmail.com 64 | 65 | 66 | 67 | 68 | 1.8 69 | ${java.version} 70 | ${java.version} 71 | UTF-8 72 | UTF-8 73 | 74 | ${project.basedir} 75 | 76 | 3.1.8.RELEASE 77 | 9.5.1 78 | 1.7.25 79 | 3.1 80 | 81 | 82 | 4.12 83 | 3.9.0 84 | 2.15.0 85 | 3.6.0 86 | 2.9.3 87 | 1.8.0-beta0 88 | 20.0 89 | 1.3 90 | 91 | 92 | 0.7.7.201606060606 93 | 4.1.0 94 | 95 | 3.5.1 96 | 2.19.1 97 | 2.3 98 | 3.0.0 99 | 1.9.5 100 | 2.11.0 101 | 102 | 103 | 104 | 105 | 106 | io.github.openfeign 107 | feign-reactor-core 108 | ${project.version} 109 | 110 | 111 | io.projectreactor 112 | reactor-core 113 | ${reactor.version} 114 | 115 | 116 | io.github.openfeign 117 | feign-core 118 | ${feign.version} 119 | 120 | 121 | org.slf4j 122 | slf4j-api 123 | ${slf4j.version} 124 | 125 | 126 | commons-httpclient 127 | commons-httpclient 128 | ${commons-httpclient.version} 129 | 130 | 131 | 132 | io.github.openfeign 133 | feign-reactor-core 134 | test-jar 135 | ${project.version} 136 | test 137 | 138 | 139 | io.projectreactor 140 | reactor-test 141 | ${reactor.version} 142 | test 143 | 144 | 145 | junit 146 | junit 147 | ${junit.version} 148 | test 149 | 150 | 151 | 152 | org.assertj 153 | assertj-core 154 | ${assertj.version} 155 | test 156 | 157 | 158 | 159 | com.github.tomakehurst 160 | wiremock 161 | ${wiremock.version} 162 | test 163 | 164 | 165 | 166 | org.awaitility 167 | awaitility 168 | ${awaitility.version} 169 | test 170 | 171 | 172 | 173 | org.hamcrest 174 | hamcrest-library 175 | ${hamcrest.version} 176 | test 177 | 178 | 179 | 180 | com.fasterxml.jackson.core 181 | jackson-databind 182 | ${jackson.version} 183 | test 184 | 185 | 186 | com.fasterxml.jackson.core 187 | jackson-annotations 188 | ${jackson.version} 189 | test 190 | 191 | 192 | com.fasterxml.jackson.datatype 193 | jackson-datatype-jsr310 194 | ${jackson.version} 195 | test 196 | 197 | 198 | 199 | org.mockito 200 | mockito-all 201 | ${mockito.version} 202 | test 203 | 204 | 205 | 206 | org.apache.logging.log4j 207 | log4j-core 208 | ${log4j.version} 209 | test 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | org.springframework.boot 223 | spring-boot-maven-plugin 224 | 225 | 226 | 227 | org.apache.maven.plugins 228 | maven-compiler-plugin 229 | ${maven-compiler-plugin.version} 230 | 231 | ${java.version} 232 | ${java.version} 233 | 234 | 235 | 236 | 237 | 238 | org.apache.maven.plugins 239 | maven-surefire-plugin 240 | ${maven-surefire-plugin.version} 241 | 242 | 243 | 244 | 245 | org.jacoco 246 | jacoco-maven-plugin 247 | ${jacoco-plugin.version} 248 | 249 | 250 | 251 | 252 | org.apache.maven.plugins 253 | maven-source-plugin 254 | 255 | 256 | attach-sources 257 | 258 | jar 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | org.eluder.coveralls 267 | coveralls-maven-plugin 268 | ${coveralls-plugin.version} 269 | 270 | 271 | 272 | 273 | org.apache.maven.plugins 274 | maven-javadoc-plugin 275 | 276 | 277 | attach-javadocs 278 | 279 | jar 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | com.mycila 290 | license-maven-plugin 291 | 3.0 292 | 293 | 294 |
${main.basedir}/src/etc/header.txt
295 | 296 | .travis.yml 297 | .editorconfig 298 | .gitattributes 299 | .gitignore 300 | .mvn/** 301 | mvnw* 302 | etc/header.txt 303 | **/.idea/** 304 | LICENSE 305 | **/*.md 306 | bnd.bnd 307 | src/test/resources/** 308 | src/main/resources/** 309 | 310 | true 311 |
312 | 313 | 314 | com.mycila 315 | license-maven-plugin-git 316 | 3.0 317 | 318 | 319 | 320 | 321 | 322 | check 323 | 324 | compile 325 | 326 | 327 |
328 | 329 | com.marvinformatics.formatter 330 | formatter-maven-plugin 331 | 2.2.0 332 | 333 | LF 334 | ${main.basedir}/src/config/eclipse-java-style.xml 335 | 336 | 337 | 338 | 339 | format 340 | 341 | verify 342 | 343 | 344 | 345 | 346 | maven-release-plugin 347 | 2.4.1 348 | 349 | false 350 | release 351 | true 352 | 353 | 354 |
355 |
356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 |
369 | -------------------------------------------------------------------------------- /src/etc/header.txt: -------------------------------------------------------------------------------- 1 | Copyright ${license.git.copyrightYears} The Feign Authors 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 4 | in compliance with the License. You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software distributed under the License 9 | is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 10 | or implied. See the License for the specific language governing permissions and limitations under 11 | the License. 12 | --------------------------------------------------------------------------------