diff --git a/src/it/java/org/seedstack/feign/FeignIT.java b/src/it/java/org/seedstack/feign/FeignIT.java index 2a23826..3dcae25 100644 --- a/src/it/java/org/seedstack/feign/FeignIT.java +++ b/src/it/java/org/seedstack/feign/FeignIT.java @@ -14,7 +14,12 @@ import org.jboss.shrinkwrap.api.spec.WebArchive; import org.junit.Test; import org.seedstack.feign.fixtures.Message; -import org.seedstack.feign.fixtures.TestAPI; +import org.seedstack.feign.fixtures.TestContract; +import org.seedstack.feign.fixtures.apis.HystrixDisabledAPI; +import org.seedstack.feign.fixtures.apis.HystrixEnabledAPI; +import org.seedstack.feign.fixtures.apis.TargetableAPI; +import org.seedstack.feign.fixtures.apis.TestAPI; +import org.seedstack.feign.fixtures.apis.TestContractAPI; import org.seedstack.seed.it.AbstractSeedWebIT; import javax.inject.Inject; @@ -29,6 +34,18 @@ public class FeignIT extends AbstractSeedWebIT { @Inject private TestAPI testAPI; + @Inject + private TestContractAPI contractAPI; + + @Inject + private HystrixEnabledAPI hystrixEnabledAPI; + + @Inject + private HystrixDisabledAPI hystrixDisabledAPI; + + @Inject + private TargetableAPI targetableAPI; + @Deployment public static WebArchive createDeployment() { return ShrinkWrap.create(WebArchive.class, "feign.war"); @@ -40,6 +57,30 @@ public void feignClientIsInjectable() throws Exception { assertThat(testAPI).isNotNull(); } + @Test + @RunAsClient + public void feignContractClientIsInjectable() throws Exception { + assertThat(contractAPI).isNotNull(); + } + + @Test + @RunAsClient + public void feignHystrixEnabledClientIsInjectable() throws Exception { + assertThat(hystrixEnabledAPI).isNotNull(); + } + + @Test + @RunAsClient + public void feignHystrixDisabledClientIsInjectable() throws Exception { + assertThat(hystrixDisabledAPI).isNotNull(); + } + + @Test + @RunAsClient + public void feignTargetableClientIsInjectable() throws Exception { + assertThat(targetableAPI).isNotNull(); + } + @Test @RunAsClient public void testNominalCall() { @@ -55,4 +96,38 @@ public void testFallback() { assertThat(message.getBody()).isEqualTo("Error code: 404 !"); assertThat(message.getAuthor()).isEqualTo("fallback"); } + + @Test + @RunAsClient + public void testContractNominalCall() { + Message message = contractAPI.getMessage(); + assertThat(message.getBody()).isEqualTo("Hello World !"); + assertThat(message.getAuthor()).isEqualTo("computer"); + assertThat(TestContract.hasBeenUsed()).isTrue(); + } + + @Test + @RunAsClient + public void testHystrixEnabledNominalCall() { + Message message = hystrixEnabledAPI.getMessage(); + assertThat(message.getBody()).isEqualTo("Hello World !"); + assertThat(message.getAuthor()).isEqualTo("computer"); + } + + @Test + @RunAsClient + public void testHystrixDisabledNominalCall() { + Message message = hystrixDisabledAPI.getMessage(); + assertThat(message.getBody()).isEqualTo("Hello World !"); + assertThat(message.getAuthor()).isEqualTo("computer"); + } + + @Test + @RunAsClient + public void testTargetableNominalCall() { + Message message = targetableAPI.getMessage(); + assertThat(message.getBody()).isEqualTo("I was routed trough a custom target"); + assertThat(message.getAuthor()).isEqualTo("or i thought so"); + } + } diff --git a/src/it/java/org/seedstack/feign/fixtures/TestContract.java b/src/it/java/org/seedstack/feign/fixtures/TestContract.java new file mode 100644 index 0000000..2a0f385 --- /dev/null +++ b/src/it/java/org/seedstack/feign/fixtures/TestContract.java @@ -0,0 +1,175 @@ +/** + * Copyright (c) 2013-2016, The SeedStack authors + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.seedstack.feign.fixtures; + +import static feign.Util.checkState; +import static feign.Util.emptyToNull; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Map; + +import feign.Body; +import feign.Contract; +import feign.HeaderMap; +import feign.Headers; +import feign.MethodMetadata; +import feign.Param; +import feign.QueryMap; +import feign.RequestLine; + +public class TestContract extends Contract.BaseContract { + + private static boolean used = false; + + public static boolean hasBeenUsed() { + return used; + } + private static void use() { + used = true; + } + + // Copycat of DefaultContract with test-flag + @Override + protected void processAnnotationOnClass(MethodMetadata data, Class targetType) { + use(); + if (targetType.isAnnotationPresent(Headers.class)) { + String[] headersOnType = targetType.getAnnotation(Headers.class).value(); + checkState(headersOnType.length > 0, "Headers annotation was empty on type %s.", + targetType.getName()); + Map> headers = toMap(headersOnType); + headers.putAll(data.template().headers()); + data.template().headers(null); // to clear + data.template().headers(headers); + } + } + + @Override + protected void processAnnotationOnMethod(MethodMetadata data, Annotation methodAnnotation, + Method method) { + use(); + Class annotationType = methodAnnotation.annotationType(); + if (annotationType == RequestLine.class) { + String requestLine = RequestLine.class.cast(methodAnnotation).value(); + checkState(emptyToNull(requestLine) != null, + "RequestLine annotation was empty on method %s.", method.getName()); + if (requestLine.indexOf(' ') == -1) { + checkState(requestLine.indexOf('/') == -1, + "RequestLine annotation didn't start with an HTTP verb on method %s.", + method.getName()); + data.template().method(requestLine); + return; + } + data.template().method(requestLine.substring(0, requestLine.indexOf(' '))); + if (requestLine.indexOf(' ') == requestLine.lastIndexOf(' ')) { + // no HTTP version is ok + data.template().append(requestLine.substring(requestLine.indexOf(' ') + 1)); + } else { + // skip HTTP version + data.template().append( + requestLine.substring(requestLine.indexOf(' ') + 1, + requestLine.lastIndexOf(' '))); + } + + data.template().decodeSlash(RequestLine.class.cast(methodAnnotation).decodeSlash()); + + } else if (annotationType == Body.class) { + String body = Body.class.cast(methodAnnotation).value(); + checkState(emptyToNull(body) != null, "Body annotation was empty on method %s.", + method.getName()); + if (body.indexOf('{') == -1) { + data.template().body(body); + } else { + data.template().bodyTemplate(body); + } + } else if (annotationType == Headers.class) { + String[] headersOnMethod = Headers.class.cast(methodAnnotation).value(); + checkState(headersOnMethod.length > 0, "Headers annotation was empty on method %s.", + method.getName()); + data.template().headers(toMap(headersOnMethod)); + } + + } + + @Override + protected boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[] annotations, + int paramIndex) { + use(); + boolean isHttpAnnotation = false; + for (Annotation annotation : annotations) { + Class annotationType = annotation.annotationType(); + if (annotationType == Param.class) { + String name = ((Param) annotation).value(); + checkState(emptyToNull(name) != null, "Param annotation was empty on param %s.", + paramIndex); + nameParam(data, name, paramIndex); + if (annotationType == Param.class) { + Class expander = ((Param) annotation).expander(); + if (expander != Param.ToStringExpander.class) { + data.indexToExpanderClass().put(paramIndex, expander); + } + } + isHttpAnnotation = true; + String varName = '{' + name + '}'; + if (data.template().url().indexOf(varName) == -1 && + !searchMapValuesContainsSubstring(data.template().queries(), varName) && + !searchMapValuesContainsSubstring(data.template().headers(), varName)) { + data.formParams().add(name); + } + } else if (annotationType == QueryMap.class) { + checkState(data.queryMapIndex() == null, + "QueryMap annotation was present on multiple parameters."); + data.queryMapIndex(paramIndex); + data.queryMapEncoded(QueryMap.class.cast(annotation).encoded()); + isHttpAnnotation = true; + } else if (annotationType == HeaderMap.class) { + checkState(data.headerMapIndex() == null, + "HeaderMap annotation was present on multiple parameters."); + data.headerMapIndex(paramIndex); + isHttpAnnotation = true; + } + } + return isHttpAnnotation; + } + + private static boolean searchMapValuesContainsSubstring(Map> map, + String search) { + Collection> values = map.values(); + if (values == null) { + return false; + } + + for (Collection entry : values) { + for (String value : entry) { + if (value.indexOf(search) != -1) { + return true; + } + } + } + + return false; + } + + private static Map> toMap(String[] input) { + Map> result = new LinkedHashMap>( + input.length); + for (String header : input) { + int colon = header.indexOf(':'); + String name = header.substring(0, colon); + if (!result.containsKey(name)) { + result.put(name, new ArrayList(1)); + } + result.get(name).add(header.substring(colon + 2)); + } + return result; + } + +} diff --git a/src/it/java/org/seedstack/feign/fixtures/TestFallback.java b/src/it/java/org/seedstack/feign/fixtures/TestFallback.java index 21ab86c..3d235a5 100644 --- a/src/it/java/org/seedstack/feign/fixtures/TestFallback.java +++ b/src/it/java/org/seedstack/feign/fixtures/TestFallback.java @@ -7,6 +7,8 @@ */ package org.seedstack.feign.fixtures; +import org.seedstack.feign.fixtures.apis.TestAPI; + public class TestFallback implements TestAPI { @Override public Message getMessage() { diff --git a/src/it/java/org/seedstack/feign/fixtures/TestResource.java b/src/it/java/org/seedstack/feign/fixtures/TestResource.java index e6b4ec9..2345328 100644 --- a/src/it/java/org/seedstack/feign/fixtures/TestResource.java +++ b/src/it/java/org/seedstack/feign/fixtures/TestResource.java @@ -12,12 +12,20 @@ import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; -@Path("/message") +@Path("/") public class TestResource { @GET @Produces(MediaType.APPLICATION_JSON) + @Path("/message") public Message say() { return new Message("Hello World !", "computer"); } + + @GET + @Produces(MediaType.APPLICATION_JSON) + @Path("/target/message") + public Message targetSay() { + return new Message("I was routed trough a custom target","or i thought so"); + } } \ No newline at end of file diff --git a/src/it/java/org/seedstack/feign/fixtures/TestTarget.java b/src/it/java/org/seedstack/feign/fixtures/TestTarget.java new file mode 100644 index 0000000..ff28ff7 --- /dev/null +++ b/src/it/java/org/seedstack/feign/fixtures/TestTarget.java @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2013-2016, The SeedStack authors + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +/** + * + */ +package org.seedstack.feign.fixtures; + +import javax.inject.Inject; + +import org.seedstack.feign.fixtures.apis.TargetableAPI; +import org.seedstack.seed.Application; + +import feign.Target.HardCodedTarget; + +public class TestTarget extends HardCodedTarget { + + @Inject + public TestTarget(Application application) { + super(TargetableAPI.class, String.format("http://localhost:%s/feign/target/", + application.getConfiguration().getMandatory(String.class, + "integrationTest.reservedPort"))); + } + + @Override + public String toString() { + return "TestTarget []" + super.toString(); + } + +} diff --git a/src/it/java/org/seedstack/feign/fixtures/apis/HystrixDisabledAPI.java b/src/it/java/org/seedstack/feign/fixtures/apis/HystrixDisabledAPI.java new file mode 100644 index 0000000..5e98dd2 --- /dev/null +++ b/src/it/java/org/seedstack/feign/fixtures/apis/HystrixDisabledAPI.java @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2013-2016, The SeedStack authors + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.seedstack.feign.fixtures.apis; + +import org.seedstack.feign.FeignApi; +import org.seedstack.feign.fixtures.Message; + +import feign.RequestLine; + + +@FeignApi +public interface HystrixDisabledAPI { + + @RequestLine("GET /message") + Message getMessage(); + +} diff --git a/src/it/java/org/seedstack/feign/fixtures/apis/HystrixEnabledAPI.java b/src/it/java/org/seedstack/feign/fixtures/apis/HystrixEnabledAPI.java new file mode 100644 index 0000000..6927383 --- /dev/null +++ b/src/it/java/org/seedstack/feign/fixtures/apis/HystrixEnabledAPI.java @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2013-2016, The SeedStack authors + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.seedstack.feign.fixtures.apis; + +import org.seedstack.feign.FeignApi; +import org.seedstack.feign.fixtures.Message; + +import feign.RequestLine; + + +@FeignApi +public interface HystrixEnabledAPI { + + @RequestLine("GET /message") + Message getMessage(); + +} diff --git a/src/it/java/org/seedstack/feign/fixtures/apis/TargetableAPI.java b/src/it/java/org/seedstack/feign/fixtures/apis/TargetableAPI.java new file mode 100644 index 0000000..c2b184d --- /dev/null +++ b/src/it/java/org/seedstack/feign/fixtures/apis/TargetableAPI.java @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2013-2016, The SeedStack authors + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.seedstack.feign.fixtures.apis; + +import org.seedstack.feign.FeignApi; +import org.seedstack.feign.fixtures.Message; + +import feign.RequestLine; + + +@FeignApi +public interface TargetableAPI { + + @RequestLine("GET /message") + Message getMessage(); + +} diff --git a/src/it/java/org/seedstack/feign/fixtures/TestAPI.java b/src/it/java/org/seedstack/feign/fixtures/apis/TestAPI.java similarity index 86% rename from src/it/java/org/seedstack/feign/fixtures/TestAPI.java rename to src/it/java/org/seedstack/feign/fixtures/apis/TestAPI.java index 90304e5..b080d25 100644 --- a/src/it/java/org/seedstack/feign/fixtures/TestAPI.java +++ b/src/it/java/org/seedstack/feign/fixtures/apis/TestAPI.java @@ -5,11 +5,12 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -package org.seedstack.feign.fixtures; +package org.seedstack.feign.fixtures.apis; import feign.Headers; import feign.RequestLine; import org.seedstack.feign.FeignApi; +import org.seedstack.feign.fixtures.Message; @FeignApi @Headers("Accept: application/json") diff --git a/src/it/java/org/seedstack/feign/fixtures/apis/TestContractAPI.java b/src/it/java/org/seedstack/feign/fixtures/apis/TestContractAPI.java new file mode 100644 index 0000000..26d3dae --- /dev/null +++ b/src/it/java/org/seedstack/feign/fixtures/apis/TestContractAPI.java @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2013-2016, The SeedStack authors + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.seedstack.feign.fixtures.apis; + +import org.seedstack.feign.FeignApi; +import org.seedstack.feign.fixtures.Message; + +import feign.RequestLine; + + +@FeignApi +public interface TestContractAPI { + + @RequestLine("GET /message") + Message getMessage(); + +} diff --git a/src/it/resources/application.yaml b/src/it/resources/application.yaml index 359039e..d1e23d3 100644 --- a/src/it/resources/application.yaml +++ b/src/it/resources/application.yaml @@ -6,9 +6,35 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. # +integrationTest: + reservedPort: ${sys.tomcat\.http\.port:'9090'} + feign: endpoints: - org.seedstack.feign.fixtures.TestAPI: + org.seedstack.feign.fixtures.apis.TestAPI: + baseUrl: http://localhost:${sys.tomcat\.http\.port:'9090'}/feign + logLevel: BASIC + fallback: org.seedstack.feign.fixtures.TestFallback + logger: feign.slf4j.Slf4jLogger + hystrixWrapper: AUTO + org.seedstack.feign.fixtures.apis.TestContractAPI: + baseUrl: http://localhost:${sys.tomcat\.http\.port:'9090'}/feign + contract: org.seedstack.feign.fixtures.TestContract + hystrixWrapper: DISABLED + org.seedstack.feign.fixtures.apis.HystrixEnabledAPI: baseUrl: http://localhost:${sys.tomcat\.http\.port:'9090'}/feign logLevel: BASIC - fallback: org.seedstack.feign.fixtures.TestFallback \ No newline at end of file + fallback: org.seedstack.feign.fixtures.TestFallback + hystrixWrapper: ENABLED + org.seedstack.feign.fixtures.apis.HystrixDisabledAPI: + baseUrl: http://localhost:${sys.tomcat\.http\.port:'9090'}/feign + logLevel: BASIC + hystrixWrapper: DISABLED + org.seedstack.feign.fixtures.apis.TargetableAPI: + baseUrl: http://ignore.is.overriden + logLevel: BASIC + encoder: feign.jackson.JacksonEncoder + decoder: feign.jackson.JacksonDecoder + target: org.seedstack.feign.fixtures.TestTarget + hystrixWrapper: DISABLED + \ No newline at end of file diff --git a/src/main/java/org/seedstack/feign/FeignConfig.java b/src/main/java/org/seedstack/feign/FeignConfig.java index 086a176..f1fcd46 100644 --- a/src/main/java/org/seedstack/feign/FeignConfig.java +++ b/src/main/java/org/seedstack/feign/FeignConfig.java @@ -7,20 +7,25 @@ */ package org.seedstack.feign; +import java.net.URL; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import javax.validation.constraints.NotNull; + +import org.seedstack.coffig.Config; +import org.seedstack.coffig.SingleValue; + +import feign.Contract; import feign.Logger; +import feign.Target; +import feign.Target.HardCodedTarget; import feign.codec.Decoder; import feign.codec.Encoder; import feign.jackson.JacksonDecoder; import feign.jackson.JacksonEncoder; import feign.slf4j.Slf4jLogger; -import org.seedstack.coffig.Config; -import org.seedstack.coffig.SingleValue; - -import javax.validation.constraints.NotNull; -import java.net.URL; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; @Config("feign") public class FeignConfig { @@ -34,11 +39,16 @@ public void addEndpoint(Class endpointClass, EndpointConfig endpoint) { endpoints.put(endpointClass, endpoint); } + @SuppressWarnings("rawtypes") public static class EndpointConfig { @SingleValue @NotNull private URL baseUrl; + private Class contract; + + private Class target = HardCodedTarget.class; + private Class encoder = JacksonEncoder.class; private Class decoder = JacksonDecoder.class; @@ -69,10 +79,28 @@ public EndpointConfig setEncoder(Class encoder) { return this; } + public Class getContract() { + return contract; + } + + public EndpointConfig setContract(Class contract) { + this.contract = contract; + return this; + } + public Class getDecoder() { return decoder; } + public Class getTarget() { + return target; + } + + public EndpointConfig setTarget(Class target) { + this.target = target; + return this; + } + public EndpointConfig setDecoder(Class decoder) { this.decoder = decoder; return this; @@ -116,8 +144,6 @@ public EndpointConfig setFallback(Class fallback) { } public enum HystrixWrapperMode { - AUTO, - ENABLED, - DISABLED, + AUTO, ENABLED, DISABLED, } } diff --git a/src/main/java/org/seedstack/feign/internal/FeignErrorCode.java b/src/main/java/org/seedstack/feign/internal/FeignErrorCode.java index 52d9c65..7a0856f 100644 --- a/src/main/java/org/seedstack/feign/internal/FeignErrorCode.java +++ b/src/main/java/org/seedstack/feign/internal/FeignErrorCode.java @@ -11,9 +11,12 @@ enum FeignErrorCode implements ErrorCode { ERROR_BUILDING_HYSTRIX_CLIENT, + ERROR_INSTANTIATING_CONTRACT, ERROR_INSTANTIATING_DECODER, ERROR_INSTANTIATING_ENCODER, + ERROR_INSTANTIATING_TARGET, + ERROR_INSTANTIATING_TARGET_BAD_TARGET_CLASS, ERROR_INSTANTIATING_FALLBACK, ERROR_INSTANTIATING_LOGGER, - HYSTRIX_NOT_PRESENT + HYSTRIX_NOT_PRESENT } diff --git a/src/main/java/org/seedstack/feign/internal/FeignModule.java b/src/main/java/org/seedstack/feign/internal/FeignModule.java index 2e021f0..9e91708 100644 --- a/src/main/java/org/seedstack/feign/internal/FeignModule.java +++ b/src/main/java/org/seedstack/feign/internal/FeignModule.java @@ -7,22 +7,43 @@ */ package org.seedstack.feign.internal; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + import com.google.inject.AbstractModule; +import com.google.inject.multibindings.Multibinder; -import java.util.Collection; +import feign.Target; class FeignModule extends AbstractModule { private final Collection> feignApis; + private final List>> feignTargets = new ArrayList<>(); - FeignModule(Collection> feignApis) { + FeignModule(Collection> feignApis, Collection> targetClasses) { this.feignApis = feignApis; + resolveFeignTargets(targetClasses); + } - @Override @SuppressWarnings("unchecked") + private void resolveFeignTargets(Collection> targetClasses) { + targetClasses.stream() + .map(x -> (Class>) x) + .forEach(feignTargets::add); + } + + @Override + @SuppressWarnings({ "unchecked", "rawtypes" }) protected void configure() { + Multibinder targetMultibinder = Multibinder.newSetBinder(binder(), Target.class); + for (Class targetClass : feignTargets) { + targetMultibinder.addBinding().to((Class) targetClass); + } + for (Class feignApi : feignApis) { bind(feignApi).toProvider((javax.inject.Provider) new FeignProvider(feignApi)); } } + } diff --git a/src/main/java/org/seedstack/feign/internal/FeignPlugin.java b/src/main/java/org/seedstack/feign/internal/FeignPlugin.java index b89f389..fb47b10 100644 --- a/src/main/java/org/seedstack/feign/internal/FeignPlugin.java +++ b/src/main/java/org/seedstack/feign/internal/FeignPlugin.java @@ -7,19 +7,22 @@ */ package org.seedstack.feign.internal; -import io.nuun.kernel.api.plugin.InitState; -import io.nuun.kernel.api.plugin.context.InitContext; -import io.nuun.kernel.api.plugin.request.ClasspathScanRequest; -import org.kametic.specifications.Specification; -import org.seedstack.seed.core.internal.AbstractSeedPlugin; - import java.util.ArrayList; import java.util.Collection; import java.util.Map; +import org.kametic.specifications.Specification; +import org.seedstack.seed.core.internal.AbstractSeedPlugin; + +import io.nuun.kernel.api.plugin.InitState; +import io.nuun.kernel.api.plugin.context.InitContext; +import io.nuun.kernel.api.plugin.request.ClasspathScanRequest; + public class FeignPlugin extends AbstractSeedPlugin { private final Specification> FEIGN_INTERFACE_SPECIFICATION = new FeignInterfaceSpecification(); - private Collection> feignApis = new ArrayList<>(); + private final Specification> FEIGN_TARGET_SPECIFICATION = new FeignTargetInterfaceSpecification(); + private final Collection> feignApis = new ArrayList<>(); + private final Collection> feignTargets = new ArrayList<>(); @Override public String name() { @@ -30,19 +33,22 @@ public String name() { public Collection classpathScanRequests() { return classpathScanRequestBuilder() .specification(FEIGN_INTERFACE_SPECIFICATION) + .specification(FEIGN_TARGET_SPECIFICATION) .build(); } + @SuppressWarnings("rawtypes") @Override protected InitState initialize(InitContext initContext) { Map>> scannedClasses = initContext.scannedTypesBySpecification(); feignApis.addAll(scannedClasses.get(FEIGN_INTERFACE_SPECIFICATION)); + feignTargets.addAll(scannedClasses.get(FEIGN_TARGET_SPECIFICATION)); return InitState.INITIALIZED; } @Override public Object nativeUnitModule() { - return new FeignModule(feignApis); + return new FeignModule(feignApis,feignTargets); } } diff --git a/src/main/java/org/seedstack/feign/internal/FeignProvider.java b/src/main/java/org/seedstack/feign/internal/FeignProvider.java index 163c291..58b71a7 100644 --- a/src/main/java/org/seedstack/feign/internal/FeignProvider.java +++ b/src/main/java/org/seedstack/feign/internal/FeignProvider.java @@ -7,71 +7,100 @@ */ package org.seedstack.feign.internal; -import feign.Feign; -import feign.Logger; -import feign.codec.Decoder; -import feign.codec.Encoder; -import feign.hystrix.HystrixFeign; +import java.lang.reflect.Method; +import java.util.Optional; +import java.util.Set; + +import javax.inject.Inject; +import javax.inject.Provider; + import org.seedstack.feign.FeignConfig; +import org.seedstack.feign.FeignConfig.EndpointConfig; import org.seedstack.seed.Configuration; import org.seedstack.seed.SeedException; import org.seedstack.shed.reflect.Classes; -import javax.inject.Provider; -import java.lang.reflect.Method; -import java.util.Optional; +import feign.Contract; +import feign.Feign; +import feign.Logger; +import feign.Target; +import feign.Target.HardCodedTarget; +import feign.codec.Decoder; +import feign.codec.Encoder; +import feign.hystrix.HystrixFeign; class FeignProvider implements Provider { - private static final Optional> HYSTRIX_OPTIONAL = Classes.optional("com.netflix.hystrix.Hystrix"); + + private static final String FAILURE_CLASS_TEXT = "class"; + private static final Optional> HYSTRIX_OPTIONAL = Classes + .optional("com.netflix.hystrix.Hystrix"); @Configuration private FeignConfig config; private Class feignApi; + @Inject + @SuppressWarnings("rawtypes") + private Set targets; + FeignProvider(Class feignApi) { this.feignApi = feignApi; } + @SuppressWarnings("unchecked") @Override public Object get() { FeignConfig.EndpointConfig endpointConfig = config.getEndpoints().get(feignApi); Feign.Builder builder = createBuilder(endpointConfig); builder.encoder(instantiateEncoder(endpointConfig.getEncoder())); builder.decoder(instantiateDecoder(endpointConfig.getDecoder())); + + if (endpointConfig.getContract() != null) { + builder.contract(instantiateContract(endpointConfig.getContract())); + } + builder.logger(instantiateLogger(endpointConfig.getLogger())); builder.logLevel(endpointConfig.getLogLevel()); if (endpointConfig.getFallback() != null) { if (builder instanceof HystrixFeign.Builder) { - return buildHystrixClient(endpointConfig, builder, instantiateFallback(endpointConfig.getFallback())); + return buildHystrixClient(endpointConfig, builder, + instantiateFallback(endpointConfig.getFallback())); } else { throw SeedException.createNew(FeignErrorCode.HYSTRIX_NOT_PRESENT) .put("endpoint", feignApi.getName()); } } else { - return builder.target(feignApi, endpointConfig.getBaseUrl().toExternalForm()); + return builder.target(instantiateTarget(endpointConfig)); } } private Feign.Builder createBuilder(FeignConfig.EndpointConfig endpointConfig) { switch (endpointConfig.getHystrixWrapper()) { - case AUTO: - return HYSTRIX_OPTIONAL.map(dummy -> (Feign.Builder) HystrixFeign.builder()).orElse(Feign.builder()); - case ENABLED: - return HYSTRIX_OPTIONAL.map(dummy -> (Feign.Builder) HystrixFeign.builder()).orElseThrow(() -> (SeedException) SeedException.createNew(FeignErrorCode.HYSTRIX_NOT_PRESENT).put("endpoint", feignApi.getName())); - case DISABLED: - return Feign.builder(); - default: - throw new IllegalArgumentException("Unsupported Hystrix mode " + endpointConfig.getHystrixWrapper()); + case AUTO: + return HYSTRIX_OPTIONAL.map(dummy -> (Feign.Builder) HystrixFeign.builder()) + .orElse(Feign.builder()); + case ENABLED: + return HYSTRIX_OPTIONAL.map(dummy -> (Feign.Builder) HystrixFeign.builder()) + .orElseThrow(() -> (SeedException) SeedException + .createNew(FeignErrorCode.HYSTRIX_NOT_PRESENT) + .put("endpoint", feignApi.getName())); + case DISABLED: + return Feign.builder(); + default: + throw new IllegalArgumentException( + "Unsupported Hystrix mode " + endpointConfig.getHystrixWrapper()); } } - private Object buildHystrixClient(FeignConfig.EndpointConfig endpointConfig, Feign.Builder builder, Object fallback) { + private Object buildHystrixClient(FeignConfig.EndpointConfig endpointConfig, + Feign.Builder builder, Object fallback) { try { - Method target = HystrixFeign.Builder.class.getMethod("target", Class.class, String.class, Object.class); - return target.invoke(builder, feignApi, endpointConfig.getBaseUrl().toExternalForm(), fallback); + Method target = HystrixFeign.Builder.class.getMethod("target", Target.class, + Object.class); + return target.invoke(builder, instantiateTarget(endpointConfig), fallback); } catch (Exception e) { throw SeedException.wrap(e, FeignErrorCode.ERROR_BUILDING_HYSTRIX_CLIENT) - .put("class", fallback); + .put(FAILURE_CLASS_TEXT, fallback); } } @@ -80,7 +109,16 @@ private Object instantiateFallback(Class fallback) { return fallback.newInstance(); } catch (Exception e) { throw SeedException.wrap(e, FeignErrorCode.ERROR_INSTANTIATING_FALLBACK) - .put("class", fallback); + .put(FAILURE_CLASS_TEXT, fallback); + } + } + + private Contract instantiateContract(Class contractClass) { + try { + return contractClass.newInstance(); + } catch (Exception e) { + throw SeedException.wrap(e, FeignErrorCode.ERROR_INSTANTIATING_CONTRACT) + .put(FAILURE_CLASS_TEXT, contractClass); } } @@ -89,7 +127,7 @@ private Encoder instantiateEncoder(Class encoderClass) { return encoderClass.newInstance(); } catch (Exception e) { throw SeedException.wrap(e, FeignErrorCode.ERROR_INSTANTIATING_ENCODER) - .put("class", encoderClass); + .put(FAILURE_CLASS_TEXT, encoderClass); } } @@ -98,8 +136,45 @@ private Decoder instantiateDecoder(Class decoderClass) { return decoderClass.newInstance(); } catch (Exception e) { throw SeedException.wrap(e, FeignErrorCode.ERROR_INSTANTIATING_DECODER) - .put("class", decoderClass); + .put(FAILURE_CLASS_TEXT, decoderClass); + } + } + + @SuppressWarnings("rawtypes") + private Target getInjectedTarget(Class targetClass) { + Optional target = this.targets.stream() + .filter(x -> x.getClass().equals(targetClass)) + .findFirst(); + + if (!target.isPresent()) { + throw SeedException + .createNew(FeignErrorCode.ERROR_INSTANTIATING_TARGET_BAD_TARGET_CLASS) + .put(FAILURE_CLASS_TEXT, targetClass); + } + return target.get(); + } + + @SuppressWarnings({ "rawtypes" }) + private Target instantiateTarget(EndpointConfig endpointConfig) { + Class targetClass = endpointConfig.getTarget(); + Target target; + + if (targetClass.equals(HardCodedTarget.class)) { + target = new HardCodedTarget<>(feignApi, endpointConfig.getBaseUrl().toExternalForm()); + } else { + try { + target = getInjectedTarget(targetClass); + } catch (Exception e) { + throw SeedException.wrap(e, FeignErrorCode.ERROR_INSTANTIATING_TARGET) + .put(FAILURE_CLASS_TEXT, targetClass); + } + } + if (!target.type().isAssignableFrom(feignApi)) { + throw SeedException + .createNew(FeignErrorCode.ERROR_INSTANTIATING_TARGET_BAD_TARGET_CLASS) + .put(FAILURE_CLASS_TEXT, targetClass); } + return target; } private Logger instantiateLogger(Class loggerClass) { @@ -107,7 +182,7 @@ private Logger instantiateLogger(Class loggerClass) { return loggerClass.newInstance(); } catch (Exception e) { throw SeedException.wrap(e, FeignErrorCode.ERROR_INSTANTIATING_LOGGER) - .put("class", loggerClass); + .put(FAILURE_CLASS_TEXT, loggerClass); } } } diff --git a/src/main/java/org/seedstack/feign/internal/FeignTargetInterfaceSpecification.java b/src/main/java/org/seedstack/feign/internal/FeignTargetInterfaceSpecification.java new file mode 100644 index 0000000..38d40a8 --- /dev/null +++ b/src/main/java/org/seedstack/feign/internal/FeignTargetInterfaceSpecification.java @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2013-2016, The SeedStack authors + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.seedstack.feign.internal; + +import org.kametic.specifications.AbstractSpecification; +import org.seedstack.shed.reflect.ClassPredicates; + +import feign.Target; + +class FeignTargetInterfaceSpecification extends AbstractSpecification> { + + @Override + public boolean isSatisfiedBy(Class candidate) { + return ClassPredicates + .classIsDescendantOf(Target.class) + .test(candidate); + } + +} diff --git a/src/main/resources/org/seedstack/feign/internal/FeignErrorCode.properties b/src/main/resources/org/seedstack/feign/internal/FeignErrorCode.properties index d79a029..d3940cc 100644 --- a/src/main/resources/org/seedstack/feign/internal/FeignErrorCode.properties +++ b/src/main/resources/org/seedstack/feign/internal/FeignErrorCode.properties @@ -7,10 +7,16 @@ # ERROR_BUILDING_HYSTRIX_CLIENT=Cannot build the Feign Hystrix client. +ERROR_INSTANTIATING_CONTRACT=The class ${class} cannot be instantiated. +ERROR_INSTANTIATING_CONTRACT.fix=Check the property "contract", it must be the fully qualified name of a valid contract. ERROR_INSTANTIATING_DECODER=The class ${class} cannot be instantiated. ERROR_INSTANTIATING_DECODER.fix=Check the property "decoder", it must be the fully qualified name of a valid decoder. ERROR_INSTANTIATING_ENCODER=The class ${class} cannot be instantiated. ERROR_INSTANTIATING_ENCODER.fix=Check the property "encoder", it must be the fully qualified name of a valid encoder. +ERROR_INSTANTIATING_TARGET=The class ${class} cannot be instantiated. +ERROR_INSTANTIATING_TARGET.fix=Check the property "target", it must be the fully qualified name of a valid target. Target must have a no-args constructor. +ERROR_INSTANTIATING_TARGET_BAD_TARGET_CLASS=The class ${class} cannot handle the Api type specified. +ERROR_INSTANTIATING_TARGET_BAD_TARGET_CLASS.fix=Check that target, implements Target. ERROR_INSTANTIATING_FALLBACK=The class ${class} cannot be instantiated. ERROR_INSTANTIATING_FALLBACK.fix=Check the property "fallback", it must be the fully qualified name of a valid fallback class implementing your API. ERROR_INSTANTIATING_LOGGER=The class ${class} cannot be instantiated.