From 9e9413067e8db8c25c9c1887e9de51ac446964d3 Mon Sep 17 00:00:00 2001 From: Marvin Froeder Date: Mon, 22 Apr 2019 20:41:20 +1200 Subject: [PATCH 1/9] Initial hand made implementation for feign client --- apt-generator/pom.xml | 63 ++++ .../aptgenerator/github/GitHubFactory.java | 142 ++++++++ .../github/GitHubFactoryExample.java | 58 ++++ .../aptgenerator/github/TypeReference.java | 70 ++++ core/pom.xml | 5 + core/src/main/java/feign/Feign.java | 70 ++-- core/src/main/java/feign/FeignConfig.java | 64 ++++ .../java/feign/InvocationHandlerFactory.java | 2 +- core/src/main/java/feign/MethodMetadata.java | 18 +- core/src/main/java/feign/ReflectiveFeign.java | 149 ++++----- .../java/feign/SynchronousMethodHandler.java | 130 +++----- .../main/java/feign/codec/ErrorDecoder.java | 2 - core/src/test/java/feign/FeignTest.java | 304 +++++++++--------- .../feign/codec/RetryAfterDecoderTest.java | 3 - .../java/example/github/GitHubExample.java | 30 +- .../main/java/feign/hystrix/HystrixFeign.java | 34 +- pom.xml | 21 +- .../java/feign/reactive/ReactiveFeign.java | 6 +- .../java/feign/reactive/ReactorFeign.java | 12 +- .../main/java/feign/reactive/RxJavaFeign.java | 11 +- 20 files changed, 763 insertions(+), 431 deletions(-) create mode 100644 apt-generator/pom.xml create mode 100644 apt-generator/src/test/java/feign/aptgenerator/github/GitHubFactory.java create mode 100644 apt-generator/src/test/java/feign/aptgenerator/github/GitHubFactoryExample.java create mode 100644 apt-generator/src/test/java/feign/aptgenerator/github/TypeReference.java create mode 100644 core/src/main/java/feign/FeignConfig.java diff --git a/apt-generator/pom.xml b/apt-generator/pom.xml new file mode 100644 index 0000000000..8ccf6e0c22 --- /dev/null +++ b/apt-generator/pom.xml @@ -0,0 +1,63 @@ + + + + 4.0.0 + + + io.github.openfeign + parent + 10.3.0-SNAPSHOT + + + io.github.openfeign.experimental + feign-apt-generator + + + ${project.basedir}/.. + + + + + + io.github.openfeign + feign-bom + ${project.version} + pom + import + + + + + + + io.github.openfeign + feign-core + + + io.github.openfeign + feign-gson + test + + + io.github.openfeign + feign-example-github + ${project.version} + test + + + + diff --git a/apt-generator/src/test/java/feign/aptgenerator/github/GitHubFactory.java b/apt-generator/src/test/java/feign/aptgenerator/github/GitHubFactory.java new file mode 100644 index 0000000000..55690e10c5 --- /dev/null +++ b/apt-generator/src/test/java/feign/aptgenerator/github/GitHubFactory.java @@ -0,0 +1,142 @@ +/** + * Copyright 2012-2019 The Feign Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package feign.aptgenerator.github; + +import java.util.Arrays; +import java.util.List; +import example.github.GitHubExample.*; +import feign.*; +import feign.InvocationHandlerFactory.MethodHandler; +import feign.Request.HttpMethod; +import feign.Target.HardCodedTarget; + +public class GitHubFactory implements GitHub { + + private static final MethodMetadata __repos_metadata; + static { + final MethodMetadata md = new MethodMetadata(); + __repos_metadata = md; + md.returnType(new TypeReference>() {}.getType()); + md.configKey("GitHub#repos(String)"); + + md.template().method(HttpMethod.GET); + md.template().uri("/users/{username}/repos?sort=full_name"); + md.template().decodeSlash(true); + md.template().collectionFormat(CollectionFormat.EXPLODED); + + md.indexToName().put(0, Arrays.asList("username")); + md.indexToEncoded().put(0, false); + } + + private static final MethodMetadata __contributors_metadata; + static { + final MethodMetadata md = new MethodMetadata(); + __contributors_metadata = md; + md.returnType(new TypeReference>() {}.getType()); + md.configKey("GitHub#contributors(String,String)"); + + md.template().method(HttpMethod.GET); + md.template().uri("/repos/{owner}/{repo}/contributors"); + md.template().decodeSlash(true); + md.template().collectionFormat(CollectionFormat.EXPLODED); + + md.indexToName().put(0, Arrays.asList("owner")); + md.indexToEncoded().put(0, false); + + md.indexToName().put(1, Arrays.asList("repo")); + md.indexToEncoded().put(1, false); + } + + private static final MethodMetadata __createIssue_metadata; + static { + final MethodMetadata md = new MethodMetadata(); + __createIssue_metadata = md; + md.returnType(new TypeReference>() {}.getType()); + md.configKey("GitHub#createIssue(Issue,String,String)"); + + md.template().method(HttpMethod.POST); + md.template().uri("/repos/{owner}/{repo}/issues"); + md.template().decodeSlash(true); + md.template().collectionFormat(CollectionFormat.EXPLODED); + + md.indexToName().put(0, Arrays.asList("owner")); + md.indexToEncoded().put(0, false); + + md.indexToName().put(1, Arrays.asList("repo")); + md.indexToEncoded().put(1, false); + } + + private final MethodHandler __repos_handler; + private final MethodHandler __contributors_handler; + private SynchronousMethodHandler __createIssue_handler; + + public GitHubFactory(FeignConfig feignConfig) { + final HardCodedTarget target = + new HardCodedTarget(GitHub.class, feignConfig.url); + + __repos_handler = new SynchronousMethodHandler( + target, + feignConfig, + __repos_metadata, + ReflectiveFeign.from(__repos_metadata, feignConfig)); + + __contributors_handler = new SynchronousMethodHandler( + target, + feignConfig, + __contributors_metadata, + ReflectiveFeign.from(__contributors_metadata, feignConfig)); + + __createIssue_handler = new SynchronousMethodHandler( + target, + feignConfig, + __contributors_metadata, + ReflectiveFeign.from(__contributors_metadata, feignConfig)); + + } + + @Override + public List repos(String owner) { + try { + return __repos_handler.invoke(owner); + } catch (final FeignException e) { + throw e; + } catch (final Throwable e) { + throw new FeignException(-1, "", e) {}; + } + } + + @Override + public List contributors(String owner, String repo) { + try { + return __contributors_handler.invoke(owner, repo); + } catch (final RuntimeException e) { + throw e; + } catch (final Throwable e) { + throw new FeignException(-1, "", e) {}; + } + } + + @Override + public void createIssue(Issue issue, String owner, String repo) { + try { + __createIssue_handler.invoke(issue, owner, repo); + } catch (final RuntimeException e) { + throw e; + } catch (final Throwable e) { + throw new FeignException(-1, "", e) {}; + } + + } + +} diff --git a/apt-generator/src/test/java/feign/aptgenerator/github/GitHubFactoryExample.java b/apt-generator/src/test/java/feign/aptgenerator/github/GitHubFactoryExample.java new file mode 100644 index 0000000000..d85d2139fc --- /dev/null +++ b/apt-generator/src/test/java/feign/aptgenerator/github/GitHubFactoryExample.java @@ -0,0 +1,58 @@ +/** + * Copyright 2012-2019 The Feign Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package feign.aptgenerator.github; + +import java.util.List; +import example.github.GitHubExample.GitHub; +import example.github.GitHubExample.GitHubClientError; +import example.github.GitHubExample.GitHubErrorDecoder; +import feign.FeignConfig; +import feign.Logger; +import feign.codec.Decoder; +import feign.gson.GsonDecoder; + +/** + * Inspired by {@code com.example.retrofit.GitHubClient} + */ +public class GitHubFactoryExample { + + static FeignConfig config() { + final Decoder decoder = new GsonDecoder(); + return FeignConfig.builder() + .decoder(decoder) + .errorDecoder(new GitHubErrorDecoder(decoder)) + .logger(new Logger.ErrorLogger()) + .logLevel(Logger.Level.BASIC) + .url("https://api.github.com") + .build(); + } + + public static void main(String... args) { + final GitHub github = new GitHubFactory(config()); + + System.out.println("Let's fetch and print a list of the contributors to this org."); + final List contributors = github.contributors("openfeign"); + for (final String contributor : contributors) { + System.out.println(contributor); + } + + System.out.println("Now, let's cause an error."); + try { + github.contributors("openfeign", "some-unknown-project"); + } catch (final GitHubClientError e) { + System.out.println(e.getMessage()); + } + } + +} diff --git a/apt-generator/src/test/java/feign/aptgenerator/github/TypeReference.java b/apt-generator/src/test/java/feign/aptgenerator/github/TypeReference.java new file mode 100644 index 0000000000..de99aefd09 --- /dev/null +++ b/apt-generator/src/test/java/feign/aptgenerator/github/TypeReference.java @@ -0,0 +1,70 @@ +/** + * Copyright 2012-2019 The Feign Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package feign.aptgenerator.github; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; + +/** + * This generic abstract class is used for obtaining full generics type information by sub-classing; + * it must be converted to {@link ResolvedType} implementation (implemented by JavaType + * from "databind" bundle) to be used. Class is based on ideas from + * http://gafter.blogspot.com/2006/12/super-type-tokens.html, Additional idea (from a + * suggestion made in comments of the article) is to require bogus implementation of + * Comparable (any such generic interface would do, as long as it forces a method with + * generic type to be implemented). to ensure that a Type argument is indeed given. + *

+ * Usage is by sub-classing: here is one way to instantiate reference to generic type + * List<Integer>: + * + *

+ * TypeReference ref = new TypeReference<List<Integer>>() {};
+ * 
+ * + * which can be passed to methods that accept TypeReference, or resolved using + * TypeFactory to obtain {@link ResolvedType}. + */ +public abstract class TypeReference implements Comparable> { + protected final Type _type; + + protected TypeReference() { + final Type superClass = getClass().getGenericSuperclass(); + if (superClass instanceof Class) { // sanity check, should never happen + throw new IllegalArgumentException( + "Internal error: TypeReference constructed without actual type information"); + } + /* + * 22-Dec-2008, tatu: Not sure if this case is safe -- I suspect it is possible to make it fail? + * But let's deal with specific case when we know an actual use case, and thereby suitable + * workarounds for valid case(s) and/or error to throw on invalid one(s). + */ + _type = ((ParameterizedType) superClass).getActualTypeArguments()[0]; + } + + public Type getType() { + return _type; + } + + /** + * The only reason we define this method (and require implementation of Comparable) + * is to prevent constructing a reference without type information. + */ + @Override + public int compareTo(TypeReference o) { + return 0; + } + // just need an implementation, not a good one... hence ^^^ +} + diff --git a/core/pom.xml b/core/pom.xml index ee2184e81c..864b76d216 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -32,6 +32,11 @@ + + org.projectlombok + lombok + + org.jvnet animal-sniffer-annotation diff --git a/core/src/main/java/feign/Feign.java b/core/src/main/java/feign/Feign.java index bd6cc0b9e1..6047cd4fc3 100644 --- a/core/src/main/java/feign/Feign.java +++ b/core/src/main/java/feign/Feign.java @@ -18,14 +18,13 @@ import java.lang.reflect.Type; import java.util.ArrayList; import java.util.List; -import feign.Logger.NoOpLogger; +import feign.FeignConfig.FeignConfigBuilder; import feign.ReflectiveFeign.ParseHandlersByName; import feign.Request.Options; import feign.Target.HardCodedTarget; import feign.codec.Decoder; import feign.codec.Encoder; import feign.codec.ErrorDecoder; -import static feign.ExceptionPropagationPolicy.NONE; /** * Feign's purpose is to ease development against http apis that feign restfulness.
@@ -35,7 +34,7 @@ public abstract class Feign { public static Builder builder() { - return new Builder(); + return new Builder(FeignConfig.builder()); } /** @@ -65,7 +64,7 @@ public static Builder builder() { * @see MethodMetadata#configKey() */ public static String configKey(Class targetType, Method method) { - StringBuilder builder = new StringBuilder(); + final StringBuilder builder = new StringBuilder(); builder.append(targetType.getSimpleName()); builder.append('#').append(method.getName()).append('('); for (Type param : method.getGenericParameterTypes()) { @@ -96,24 +95,17 @@ public static class Builder { private final List requestInterceptors = new ArrayList(); - private Logger.Level logLevel = Logger.Level.NONE; private Contract contract = new Contract.Default(); - private Client client = new Client.Default(null, null); - private Retryer retryer = new Retryer.Default(); - private Logger logger = new NoOpLogger(); - private Encoder encoder = new Encoder.Default(); - private Decoder decoder = new Decoder.Default(); - private QueryMapEncoder queryMapEncoder = new QueryMapEncoder.Default(); - private ErrorDecoder errorDecoder = new ErrorDecoder.Default(); - private Options options = new Options(); private InvocationHandlerFactory invocationHandlerFactory = new InvocationHandlerFactory.Default(); - private boolean decode404; - private boolean closeAfterDecode = true; - private ExceptionPropagationPolicy propagationPolicy = NONE; + private final FeignConfigBuilder feignConfigBuilder; + + protected Builder(FeignConfigBuilder feignConfigBuilder) { + this.feignConfigBuilder = feignConfigBuilder; + } public Builder logLevel(Logger.Level logLevel) { - this.logLevel = logLevel; + feignConfigBuilder.logLevel(logLevel); return this; } @@ -123,32 +115,32 @@ public Builder contract(Contract contract) { } public Builder client(Client client) { - this.client = client; + feignConfigBuilder.client(client); return this; } public Builder retryer(Retryer retryer) { - this.retryer = retryer; + feignConfigBuilder.retryer(retryer); return this; } public Builder logger(Logger logger) { - this.logger = logger; + feignConfigBuilder.logger(logger); return this; } public Builder encoder(Encoder encoder) { - this.encoder = encoder; + feignConfigBuilder.encoder(encoder); return this; } public Builder decoder(Decoder decoder) { - this.decoder = decoder; + feignConfigBuilder.decoder(decoder); return this; } public Builder queryMapEncoder(QueryMapEncoder queryMapEncoder) { - this.queryMapEncoder = queryMapEncoder; + feignConfigBuilder.queryMapEncoder(queryMapEncoder); return this; } @@ -156,7 +148,7 @@ public Builder queryMapEncoder(QueryMapEncoder queryMapEncoder) { * Allows to map the response before passing it to the decoder. */ public Builder mapAndDecode(ResponseMapper mapper, Decoder decoder) { - this.decoder = new ResponseMappingDecoder(mapper, decoder); + feignConfigBuilder.decoder(new ResponseMappingDecoder(mapper, decoder)); return this; } @@ -178,17 +170,17 @@ public Builder mapAndDecode(ResponseMapper mapper, Decoder decoder) { * @since 8.12 */ public Builder decode404() { - this.decode404 = true; + feignConfigBuilder.decode404(true); return this; } public Builder errorDecoder(ErrorDecoder errorDecoder) { - this.errorDecoder = errorDecoder; + feignConfigBuilder.errorDecoder(errorDecoder); return this; } public Builder options(Options options) { - this.options = options; + feignConfigBuilder.options(options); return this; } @@ -196,7 +188,8 @@ public Builder options(Options options) { * Adds a single request interceptor to the builder. */ public Builder requestInterceptor(RequestInterceptor requestInterceptor) { - this.requestInterceptors.add(requestInterceptor); + requestInterceptors.add(requestInterceptor); + feignConfigBuilder.requestInterceptors(requestInterceptors); return this; } @@ -206,9 +199,10 @@ public Builder requestInterceptor(RequestInterceptor requestInterceptor) { */ public Builder requestInterceptors(Iterable requestInterceptors) { this.requestInterceptors.clear(); - for (RequestInterceptor requestInterceptor : requestInterceptors) { + for (final RequestInterceptor requestInterceptor : requestInterceptors) { this.requestInterceptors.add(requestInterceptor); } + feignConfigBuilder.requestInterceptors(this.requestInterceptors); return this; } @@ -234,12 +228,12 @@ public Builder invocationHandlerFactory(InvocationHandlerFactory invocationHandl * */ public Builder doNotCloseAfterDecode() { - this.closeAfterDecode = false; + feignConfigBuilder.closeAfterDecode(false); return this; } public Builder exceptionPropagationPolicy(ExceptionPropagationPolicy propagationPolicy) { - this.propagationPolicy = propagationPolicy; + feignConfigBuilder.propagationPolicy(propagationPolicy); return this; } @@ -252,13 +246,13 @@ public T target(Target target) { } public Feign build() { - SynchronousMethodHandler.Factory synchronousMethodHandlerFactory = - new SynchronousMethodHandler.Factory(client, retryer, requestInterceptors, logger, - logLevel, decode404, closeAfterDecode, propagationPolicy); - ParseHandlersByName handlersByName = - new ParseHandlersByName(contract, options, encoder, decoder, queryMapEncoder, - errorDecoder, synchronousMethodHandlerFactory); - return new ReflectiveFeign(handlersByName, invocationHandlerFactory, queryMapEncoder); + final FeignConfig feignConfig = feignConfigBuilder.build(); + final SynchronousMethodHandler.Factory synchronousMethodHandlerFactory = + new SynchronousMethodHandler.Factory(feignConfig); + final ParseHandlersByName handlersByName = + new ParseHandlersByName(contract, feignConfig, synchronousMethodHandlerFactory); + return new ReflectiveFeign(handlersByName, invocationHandlerFactory, + feignConfig.queryMapEncoder); } } diff --git a/core/src/main/java/feign/FeignConfig.java b/core/src/main/java/feign/FeignConfig.java new file mode 100644 index 0000000000..52d410e498 --- /dev/null +++ b/core/src/main/java/feign/FeignConfig.java @@ -0,0 +1,64 @@ +/** + * Copyright 2012-2019 The Feign Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package feign; + +import static feign.ExceptionPropagationPolicy.NONE; +import java.util.ArrayList; +import java.util.List; +import feign.Logger.NoOpLogger; +import feign.Request.Options; +import feign.codec.Decoder; +import feign.codec.Encoder; +import feign.codec.ErrorDecoder; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; + +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Builder +public class FeignConfig { + + @Builder.Default + public final List requestInterceptors = new ArrayList(); + @Builder.Default + public final Logger.Level logLevel = Logger.Level.NONE; + @Builder.Default + public final Contract contract = new Contract.Default(); + @Builder.Default + public final Client client = new Client.Default(null, null); + @Builder.Default + public final Retryer retryer = new Retryer.Default(); + @Builder.Default + public final Logger logger = new NoOpLogger(); + @Builder.Default + public final Encoder encoder = new Encoder.Default(); + @Builder.Default + public final Decoder decoder = new Decoder.Default(); + @Builder.Default + public final QueryMapEncoder queryMapEncoder = new QueryMapEncoder.Default(); + @Builder.Default + public final ErrorDecoder errorDecoder = new ErrorDecoder.Default(); + @Builder.Default + public final Options options = new Options(); + @Builder.Default + public final InvocationHandlerFactory invocationHandlerFactory = + new InvocationHandlerFactory.Default(); + public final boolean decode404; + @Builder.Default + public final boolean closeAfterDecode = true; + @Builder.Default + public final ExceptionPropagationPolicy propagationPolicy = NONE; + public final String url; + +} diff --git a/core/src/main/java/feign/InvocationHandlerFactory.java b/core/src/main/java/feign/InvocationHandlerFactory.java index 73f4a84e2a..9b35542536 100644 --- a/core/src/main/java/feign/InvocationHandlerFactory.java +++ b/core/src/main/java/feign/InvocationHandlerFactory.java @@ -30,7 +30,7 @@ public interface InvocationHandlerFactory { */ interface MethodHandler { - Object invoke(Object[] argv) throws Throwable; + E invoke(Object... argv) throws Throwable; } static final class Default implements InvocationHandlerFactory { diff --git a/core/src/main/java/feign/MethodMetadata.java b/core/src/main/java/feign/MethodMetadata.java index a4ae6e9bd6..c4746939d5 100644 --- a/core/src/main/java/feign/MethodMetadata.java +++ b/core/src/main/java/feign/MethodMetadata.java @@ -15,11 +15,7 @@ import java.io.Serializable; import java.lang.reflect.Type; -import java.util.ArrayList; -import java.util.Collection; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import feign.Param.Expander; public final class MethodMetadata implements Serializable { @@ -33,16 +29,16 @@ public final class MethodMetadata implements Serializable { private Integer queryMapIndex; private boolean queryMapEncoded; private transient Type bodyType; - private RequestTemplate template = new RequestTemplate(); - private List formParams = new ArrayList(); - private Map> indexToName = + private final RequestTemplate template = new RequestTemplate(); + private final List formParams = new ArrayList(); + private final Map> indexToName = new LinkedHashMap>(); - private Map> indexToExpanderClass = + private final Map> indexToExpanderClass = new LinkedHashMap>(); - private Map indexToEncoded = new LinkedHashMap(); + private final Map indexToEncoded = new LinkedHashMap(); private transient Map indexToExpander; - MethodMetadata() {} + public MethodMetadata() {} /** * Used as a reference to this method. For example, {@link Logger#log(String, String, Object...) diff --git a/core/src/main/java/feign/ReflectiveFeign.java b/core/src/main/java/feign/ReflectiveFeign.java index a71bed0e68..b8a937baec 100644 --- a/core/src/main/java/feign/ReflectiveFeign.java +++ b/core/src/main/java/feign/ReflectiveFeign.java @@ -13,7 +13,8 @@ */ package feign; -import feign.template.UriUtils; +import static feign.Util.checkArgument; +import static feign.Util.checkNotNull; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; @@ -21,13 +22,9 @@ import java.util.Map.Entry; import feign.InvocationHandlerFactory.MethodHandler; import feign.Param.Expander; -import feign.Request.Options; -import feign.codec.Decoder; import feign.codec.EncodeException; import feign.codec.Encoder; -import feign.codec.ErrorDecoder; -import static feign.Util.checkArgument; -import static feign.Util.checkNotNull; +import feign.template.UriUtils; public class ReflectiveFeign extends Feign { @@ -49,26 +46,26 @@ public class ReflectiveFeign extends Feign { @SuppressWarnings("unchecked") @Override public T newInstance(Target target) { - Map nameToHandler = targetToHandlersByName.apply(target); - Map methodToHandler = new LinkedHashMap(); - List defaultMethodHandlers = new LinkedList(); + final Map nameToHandler = targetToHandlersByName.apply(target); + final Map methodToHandler = new LinkedHashMap(); + final List defaultMethodHandlers = new LinkedList(); - for (Method method : target.type().getMethods()) { + for (final Method method : target.type().getMethods()) { if (method.getDeclaringClass() == Object.class) { continue; } else if (Util.isDefault(method)) { - DefaultMethodHandler handler = new DefaultMethodHandler(method); + final DefaultMethodHandler handler = new DefaultMethodHandler(method); defaultMethodHandlers.add(handler); methodToHandler.put(method, handler); } else { methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method))); } } - InvocationHandler handler = factory.create(target, methodToHandler); - T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(), + final InvocationHandler handler = factory.create(target, methodToHandler); + final T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(), new Class[] {target.type()}, handler); - for (DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) { + for (final DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) { defaultMethodHandler.bindTo(proxy); } return proxy; @@ -88,10 +85,10 @@ static class FeignInvocationHandler implements InvocationHandler { public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if ("equals".equals(method.getName())) { try { - Object otherHandler = + final Object otherHandler = args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null; return equals(otherHandler); - } catch (IllegalArgumentException e) { + } catch (final IllegalArgumentException e) { return false; } } else if ("hashCode".equals(method.getName())) { @@ -106,7 +103,7 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl @Override public boolean equals(Object obj) { if (obj instanceof FeignInvocationHandler) { - FeignInvocationHandler other = (FeignInvocationHandler) obj; + final FeignInvocationHandler other = (FeignInvocationHandler) obj; return target.equals(other.target); } return false; @@ -126,47 +123,43 @@ public String toString() { static final class ParseHandlersByName { private final Contract contract; - private final Options options; - private final Encoder encoder; - private final Decoder decoder; - private final ErrorDecoder errorDecoder; - private final QueryMapEncoder queryMapEncoder; + private final FeignConfig feignConfig; private final SynchronousMethodHandler.Factory factory; ParseHandlersByName( Contract contract, - Options options, - Encoder encoder, - Decoder decoder, - QueryMapEncoder queryMapEncoder, - ErrorDecoder errorDecoder, + FeignConfig feignConfig, SynchronousMethodHandler.Factory factory) { - this.contract = contract; - this.options = options; - this.factory = factory; - this.errorDecoder = errorDecoder; - this.queryMapEncoder = queryMapEncoder; - this.encoder = checkNotNull(encoder, "encoder"); - this.decoder = checkNotNull(decoder, "decoder"); + this.contract = checkNotNull(contract, "contract"); + this.feignConfig = checkNotNull(feignConfig, "feignConfig"); + this.factory = checkNotNull(factory, "factory"); } public Map apply(Target key) { - List metadata = contract.parseAndValidatateMetadata(key.type()); - Map result = new LinkedHashMap(); - for (MethodMetadata md : metadata) { - BuildTemplateByResolvingArgs buildTemplate; - if (!md.formParams().isEmpty() && md.template().bodyTemplate() == null) { - buildTemplate = new BuildFormEncodedTemplateFromArgs(md, encoder, queryMapEncoder); - } else if (md.bodyIndex() != null) { - buildTemplate = new BuildEncodedTemplateFromArgs(md, encoder, queryMapEncoder); - } else { - buildTemplate = new BuildTemplateByResolvingArgs(md, queryMapEncoder); - } + final List metadata = contract.parseAndValidatateMetadata(key.type()); + final Map result = new LinkedHashMap(); + for (final MethodMetadata md : metadata) { + final RequestTemplate.Factory buildTemplate = from(md, feignConfig); result.put(md.configKey(), - factory.create(key, md, buildTemplate, options, decoder, errorDecoder)); + factory.create(key, md, buildTemplate, feignConfig.options, feignConfig.decoder, + feignConfig.errorDecoder)); } return result; } + + } + + public static RequestTemplate.Factory from(MethodMetadata md, FeignConfig feignConfig) { + + if (!md.formParams().isEmpty() && md.template().bodyTemplate() == null) { + return new BuildFormEncodedTemplateFromArgs(md, feignConfig.encoder, + feignConfig.queryMapEncoder); + } else if (md.bodyIndex() != null) { + return new BuildEncodedTemplateFromArgs(md, feignConfig.encoder, + feignConfig.queryMapEncoder); + } else { + return new BuildTemplateByResolvingArgs(md, feignConfig.queryMapEncoder); + } } private static class BuildTemplateByResolvingArgs implements RequestTemplate.Factory { @@ -186,14 +179,14 @@ private BuildTemplateByResolvingArgs(MethodMetadata metadata, QueryMapEncoder qu if (metadata.indexToExpanderClass().isEmpty()) { return; } - for (Entry> indexToExpanderClass : metadata + for (final Entry> indexToExpanderClass : metadata .indexToExpanderClass().entrySet()) { try { indexToExpander .put(indexToExpanderClass.getKey(), indexToExpanderClass.getValue().newInstance()); - } catch (InstantiationException e) { + } catch (final InstantiationException e) { throw new IllegalStateException(e); - } catch (IllegalAccessException e) { + } catch (final IllegalAccessException e) { throw new IllegalStateException(e); } } @@ -201,21 +194,21 @@ private BuildTemplateByResolvingArgs(MethodMetadata metadata, QueryMapEncoder qu @Override public RequestTemplate create(Object[] argv) { - RequestTemplate mutable = RequestTemplate.from(metadata.template()); + final RequestTemplate mutable = RequestTemplate.from(metadata.template()); if (metadata.urlIndex() != null) { - int urlIndex = metadata.urlIndex(); + final int urlIndex = metadata.urlIndex(); checkArgument(argv[urlIndex] != null, "URI parameter %s was null", urlIndex); mutable.target(String.valueOf(argv[urlIndex])); } - Map varBuilder = new LinkedHashMap(); - for (Entry> entry : metadata.indexToName().entrySet()) { - int i = entry.getKey(); + final Map varBuilder = new LinkedHashMap(); + for (final Entry> entry : metadata.indexToName().entrySet()) { + final int i = entry.getKey(); Object value = argv[entry.getKey()]; if (value != null) { // Null values are skipped. if (indexToExpander.containsKey(i)) { value = expandElements(indexToExpander.get(i), value); } - for (String name : entry.getValue()) { + for (final String name : entry.getValue()) { varBuilder.put(name, value); } } @@ -225,8 +218,8 @@ public RequestTemplate create(Object[] argv) { if (metadata.queryMapIndex() != null) { // add query map parameters after initial resolve so that they take // precedence over any predefined values - Object value = argv[metadata.queryMapIndex()]; - Map queryMap = toQueryMap(value); + final Object value = argv[metadata.queryMapIndex()]; + final Map queryMap = toQueryMap(value); template = addQueryMapQueryParameters(queryMap, template); } @@ -244,7 +237,7 @@ private Map toQueryMap(Object value) { } try { return queryMapEncoder.encode(value); - } catch (EncodeException e) { + } catch (final EncodeException e) { throw new IllegalStateException(e); } } @@ -257,8 +250,8 @@ private Object expandElements(Expander expander, Object value) { } private List expandIterable(Expander expander, Iterable value) { - List values = new ArrayList(); - for (Object element : value) { + final List values = new ArrayList(); + for (final Object element : value) { if (element != null) { values.add(expander.expand(element)); } @@ -269,14 +262,14 @@ private List expandIterable(Expander expander, Iterable value) { @SuppressWarnings("unchecked") private RequestTemplate addHeaderMapHeaders(Map headerMap, RequestTemplate mutable) { - for (Entry currEntry : headerMap.entrySet()) { - Collection values = new ArrayList(); + for (final Entry currEntry : headerMap.entrySet()) { + final Collection values = new ArrayList(); - Object currValue = currEntry.getValue(); + final Object currValue = currEntry.getValue(); if (currValue instanceof Iterable) { - Iterator iter = ((Iterable) currValue).iterator(); + final Iterator iter = ((Iterable) currValue).iterator(); while (iter.hasNext()) { - Object nextObject = iter.next(); + final Object nextObject = iter.next(); values.add(nextObject == null ? null : nextObject.toString()); } } else { @@ -291,15 +284,15 @@ private RequestTemplate addHeaderMapHeaders(Map headerMap, @SuppressWarnings("unchecked") private RequestTemplate addQueryMapQueryParameters(Map queryMap, RequestTemplate mutable) { - for (Entry currEntry : queryMap.entrySet()) { - Collection values = new ArrayList(); + for (final Entry currEntry : queryMap.entrySet()) { + final Collection values = new ArrayList(); - boolean encoded = metadata.queryMapEncoded(); - Object currValue = currEntry.getValue(); + final boolean encoded = metadata.queryMapEncoded(); + final Object currValue = currEntry.getValue(); if (currValue instanceof Iterable) { - Iterator iter = ((Iterable) currValue).iterator(); + final Iterator iter = ((Iterable) currValue).iterator(); while (iter.hasNext()) { - Object nextObject = iter.next(); + final Object nextObject = iter.next(); values.add(nextObject == null ? null : encoded ? nextObject.toString() : UriUtils.encode(nextObject.toString())); @@ -335,17 +328,17 @@ private BuildFormEncodedTemplateFromArgs(MethodMetadata metadata, Encoder encode protected RequestTemplate resolve(Object[] argv, RequestTemplate mutable, Map variables) { - Map formVariables = new LinkedHashMap(); - for (Entry entry : variables.entrySet()) { + final Map formVariables = new LinkedHashMap(); + for (final Entry entry : variables.entrySet()) { if (metadata.formParams().contains(entry.getKey())) { formVariables.put(entry.getKey(), entry.getValue()); } } try { encoder.encode(formVariables, Encoder.MAP_STRING_WILDCARD, mutable); - } catch (EncodeException e) { + } catch (final EncodeException e) { throw e; - } catch (RuntimeException e) { + } catch (final RuntimeException e) { throw new EncodeException(e.getMessage(), e); } return super.resolve(argv, mutable, variables); @@ -366,13 +359,13 @@ private BuildEncodedTemplateFromArgs(MethodMetadata metadata, Encoder encoder, protected RequestTemplate resolve(Object[] argv, RequestTemplate mutable, Map variables) { - Object body = argv[metadata.bodyIndex()]; + final Object body = argv[metadata.bodyIndex()]; checkArgument(body != null, "Body parameter %s was null", metadata.bodyIndex()); try { encoder.encode(body, metadata.bodyType(), mutable); - } catch (EncodeException e) { + } catch (final EncodeException e) { throw e; - } catch (RuntimeException e) { + } catch (final RuntimeException e) { throw new EncodeException(e.getMessage(), e); } return super.resolve(argv, mutable, variables); diff --git a/core/src/main/java/feign/SynchronousMethodHandler.java b/core/src/main/java/feign/SynchronousMethodHandler.java index a6371491d3..685c7a0f65 100644 --- a/core/src/main/java/feign/SynchronousMethodHandler.java +++ b/core/src/main/java/feign/SynchronousMethodHandler.java @@ -13,8 +13,12 @@ */ package feign; +import static feign.ExceptionPropagationPolicy.UNWRAP; +import static feign.FeignException.errorExecuting; +import static feign.FeignException.errorReading; +import static feign.Util.checkNotNull; +import static feign.Util.ensureClosed; import java.io.IOException; -import java.util.List; import java.util.concurrent.TimeUnit; import java.util.stream.Stream; import feign.InvocationHandlerFactory.MethodHandler; @@ -22,59 +26,33 @@ import feign.codec.DecodeException; import feign.codec.Decoder; import feign.codec.ErrorDecoder; -import static feign.ExceptionPropagationPolicy.UNWRAP; -import static feign.FeignException.errorExecuting; -import static feign.FeignException.errorReading; -import static feign.Util.checkNotNull; -import static feign.Util.ensureClosed; -final class SynchronousMethodHandler implements MethodHandler { +public final class SynchronousMethodHandler implements MethodHandler { private static final long MAX_RESPONSE_BUFFER_SIZE = 8192L; private final MethodMetadata metadata; private final Target target; - private final Client client; - private final Retryer retryer; - private final List requestInterceptors; - private final Logger logger; - private final Logger.Level logLevel; private final RequestTemplate.Factory buildTemplateFromArgs; - private final Options options; - private final Decoder decoder; - private final ErrorDecoder errorDecoder; - private final boolean decode404; - private final boolean closeAfterDecode; - private final ExceptionPropagationPolicy propagationPolicy; - - private SynchronousMethodHandler(Target target, Client client, Retryer retryer, - List requestInterceptors, Logger logger, - Logger.Level logLevel, MethodMetadata metadata, - RequestTemplate.Factory buildTemplateFromArgs, Options options, - Decoder decoder, ErrorDecoder errorDecoder, boolean decode404, - boolean closeAfterDecode, ExceptionPropagationPolicy propagationPolicy) { + + private final FeignConfig feignConfig; + + public SynchronousMethodHandler( + Target target, + FeignConfig feignConfig, + MethodMetadata metadata, + RequestTemplate.Factory buildTemplateFromArgs) { this.target = checkNotNull(target, "target"); - this.client = checkNotNull(client, "client for %s", target); - this.retryer = checkNotNull(retryer, "retryer for %s", target); - this.requestInterceptors = - checkNotNull(requestInterceptors, "requestInterceptors for %s", target); - this.logger = checkNotNull(logger, "logger for %s", target); - this.logLevel = checkNotNull(logLevel, "logLevel for %s", target); + this.feignConfig = checkNotNull(feignConfig, "feignConfig for %s", target); this.metadata = checkNotNull(metadata, "metadata for %s", target); this.buildTemplateFromArgs = checkNotNull(buildTemplateFromArgs, "metadata for %s", target); - this.options = checkNotNull(options, "options for %s", target); - this.errorDecoder = checkNotNull(errorDecoder, "errorDecoder for %s", target); - this.decoder = checkNotNull(decoder, "decoder for %s", target); - this.decode404 = decode404; - this.closeAfterDecode = closeAfterDecode; - this.propagationPolicy = propagationPolicy; } @Override - public Object invoke(Object[] argv) throws Throwable { + public Object invoke(Object... argv) throws Throwable { RequestTemplate template = buildTemplateFromArgs.create(argv); Options options = findOptions(argv); - Retryer retryer = this.retryer.clone(); + Retryer retryer = feignConfig.retryer.clone(); while (true) { try { return executeAndDecode(template, options); @@ -83,14 +61,14 @@ public Object invoke(Object[] argv) throws Throwable { retryer.continueOrPropagate(e); } catch (RetryableException th) { Throwable cause = th.getCause(); - if (propagationPolicy == UNWRAP && cause != null) { + if (feignConfig.propagationPolicy == UNWRAP && cause != null) { throw cause; } else { throw th; } } - if (logLevel != Logger.Level.NONE) { - logger.logRetry(metadata.configKey(), logLevel); + if (feignConfig.logLevel != Logger.Level.NONE) { + feignConfig.logger.logRetry(metadata.configKey(), feignConfig.logLevel); } continue; } @@ -98,19 +76,20 @@ public Object invoke(Object[] argv) throws Throwable { } Object executeAndDecode(RequestTemplate template, Options options) throws Throwable { - Request request = targetRequest(template); + final Request request = targetRequest(template); - if (logLevel != Logger.Level.NONE) { - logger.logRequest(metadata.configKey(), logLevel, request); + if (feignConfig.logLevel != Logger.Level.NONE) { + feignConfig.logger.logRequest(metadata.configKey(), feignConfig.logLevel, request); } Response response; long start = System.nanoTime(); try { - response = client.execute(request, options); + response = feignConfig.client.execute(request, feignConfig.options); } catch (IOException e) { - if (logLevel != Logger.Level.NONE) { - logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime(start)); + if (feignConfig.logLevel != Logger.Level.NONE) { + feignConfig.logger.logIOException(metadata.configKey(), feignConfig.logLevel, e, + elapsedTime(start)); } throw errorExecuting(request, e); } @@ -118,9 +97,10 @@ Object executeAndDecode(RequestTemplate template, Options options) throws Throwa boolean shouldClose = true; try { - if (logLevel != Logger.Level.NONE) { + if (feignConfig.logLevel != Logger.Level.NONE) { response = - logger.logAndRebufferResponse(metadata.configKey(), logLevel, response, elapsedTime); + feignConfig.logger.logAndRebufferResponse(metadata.configKey(), feignConfig.logLevel, + response, elapsedTime); } if (Response.class == metadata.returnType()) { if (response.body() == null) { @@ -140,19 +120,21 @@ Object executeAndDecode(RequestTemplate template, Options options) throws Throwa return null; } else { Object result = decode(response); - shouldClose = closeAfterDecode; + shouldClose = feignConfig.closeAfterDecode; return result; } - } else if (decode404 && response.status() == 404 && void.class != metadata.returnType()) { + } else if (feignConfig.decode404 && response.status() == 404 + && void.class != metadata.returnType()) { Object result = decode(response); - shouldClose = closeAfterDecode; + shouldClose = feignConfig.closeAfterDecode; return result; } else { - throw errorDecoder.decode(metadata.configKey(), response); + throw feignConfig.errorDecoder.decode(metadata.configKey(), response); } } catch (IOException e) { - if (logLevel != Logger.Level.NONE) { - logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime); + if (feignConfig.logLevel != Logger.Level.NONE) { + feignConfig.logger.logIOException(metadata.configKey(), feignConfig.logLevel, e, + elapsedTime); } throw errorReading(request, response, e); } finally { @@ -167,7 +149,7 @@ long elapsedTime(long start) { } Request targetRequest(RequestTemplate template) { - for (RequestInterceptor interceptor : requestInterceptors) { + for (RequestInterceptor interceptor : feignConfig.requestInterceptors) { interceptor.apply(template); } return target.apply(template); @@ -175,7 +157,7 @@ Request targetRequest(RequestTemplate template) { Object decode(Response response) throws Throwable { try { - return decoder.decode(response, metadata.returnType()); + return feignConfig.decoder.decode(response, metadata.returnType()); } catch (FeignException e) { throw e; } catch (RuntimeException e) { @@ -185,36 +167,20 @@ Object decode(Response response) throws Throwable { Options findOptions(Object[] argv) { if (argv == null || argv.length == 0) { - return this.options; + return feignConfig.options; } return (Options) Stream.of(argv) .filter(o -> o instanceof Options) .findFirst() - .orElse(this.options); + .orElse(feignConfig.options); } static class Factory { - private final Client client; - private final Retryer retryer; - private final List requestInterceptors; - private final Logger logger; - private final Logger.Level logLevel; - private final boolean decode404; - private final boolean closeAfterDecode; - private final ExceptionPropagationPolicy propagationPolicy; - - Factory(Client client, Retryer retryer, List requestInterceptors, - Logger logger, Logger.Level logLevel, boolean decode404, boolean closeAfterDecode, - ExceptionPropagationPolicy propagationPolicy) { - this.client = checkNotNull(client, "client"); - this.retryer = checkNotNull(retryer, "retryer"); - this.requestInterceptors = checkNotNull(requestInterceptors, "requestInterceptors"); - this.logger = checkNotNull(logger, "logger"); - this.logLevel = checkNotNull(logLevel, "logLevel"); - this.decode404 = decode404; - this.closeAfterDecode = closeAfterDecode; - this.propagationPolicy = propagationPolicy; + private final FeignConfig feignConfig; + + Factory(FeignConfig feignConfig) { + this.feignConfig = checkNotNull(feignConfig, "feignConfig"); } public MethodHandler create(Target target, @@ -223,9 +189,7 @@ public MethodHandler create(Target target, Options options, Decoder decoder, ErrorDecoder errorDecoder) { - return new SynchronousMethodHandler(target, client, retryer, requestInterceptors, logger, - logLevel, md, buildTemplateFromArgs, options, decoder, - errorDecoder, decode404, closeAfterDecode, propagationPolicy); + return new SynchronousMethodHandler(target, feignConfig, md, buildTemplateFromArgs); } } } diff --git a/core/src/main/java/feign/codec/ErrorDecoder.java b/core/src/main/java/feign/codec/ErrorDecoder.java index 8f1552502c..ee6ff6fec0 100644 --- a/core/src/main/java/feign/codec/ErrorDecoder.java +++ b/core/src/main/java/feign/codec/ErrorDecoder.java @@ -18,11 +18,9 @@ import static feign.Util.checkNotNull; import static java.util.Locale.US; import static java.util.concurrent.TimeUnit.SECONDS; - import feign.FeignException; import feign.Response; import feign.RetryableException; - import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; diff --git a/core/src/test/java/feign/FeignTest.java b/core/src/test/java/feign/FeignTest.java index 0a8b1ab02e..10124ab4b6 100644 --- a/core/src/test/java/feign/FeignTest.java +++ b/core/src/test/java/feign/FeignTest.java @@ -13,17 +13,17 @@ */ package feign; +import static feign.ExceptionPropagationPolicy.UNWRAP; +import static feign.Util.UTF_8; +import static feign.assertj.MockWebServerAssertions.assertThat; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.data.MapEntry.entry; +import static org.hamcrest.CoreMatchers.isA; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; -import feign.Feign.ResponseMappingDecoder; -import feign.Request.HttpMethod; -import feign.Target.HardCodedTarget; -import feign.querymap.BeanQueryMapEncoder; import feign.querymap.FieldQueryMapEncoder; -import okhttp3.mockwebserver.MockResponse; -import okhttp3.mockwebserver.MockWebServer; -import okhttp3.mockwebserver.SocketPolicy; -import okio.Buffer; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; @@ -32,19 +32,15 @@ import java.net.URI; import java.util.*; import java.util.concurrent.atomic.AtomicReference; -import feign.codec.DecodeException; -import feign.codec.Decoder; -import feign.codec.EncodeException; -import feign.codec.Encoder; -import feign.codec.ErrorDecoder; -import feign.codec.StringDecoder; -import static feign.ExceptionPropagationPolicy.UNWRAP; -import static feign.Util.UTF_8; -import static feign.assertj.MockWebServerAssertions.assertThat; -import static org.assertj.core.data.MapEntry.entry; -import static org.hamcrest.CoreMatchers.isA; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; +import feign.Feign.ResponseMappingDecoder; +import feign.Request.HttpMethod; +import feign.Target.HardCodedTarget; +import feign.codec.*; +import feign.querymap.BeanQueryMapEncoder; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.SocketPolicy; +import okio.Buffer; public class FeignTest { @@ -57,7 +53,8 @@ public class FeignTest { public void iterableQueryParams() throws Exception { server.enqueue(new MockResponse().setBody("foo")); - TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort()); + final TestInterface api = + new TestInterfaceBuilder().target("http://localhost:" + server.getPort()); api.queryParams("user", Arrays.asList("apple", "pear")); @@ -69,7 +66,8 @@ public void iterableQueryParams() throws Exception { public void postTemplateParamsResolve() throws Exception { server.enqueue(new MockResponse().setBody("foo")); - TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort()); + final TestInterface api = + new TestInterfaceBuilder().target("http://localhost:" + server.getPort()); api.login("netflix", "denominator", "password"); @@ -82,9 +80,10 @@ public void postTemplateParamsResolve() throws Exception { public void responseCoercesToStringBody() throws Exception { server.enqueue(new MockResponse().setBody("foo")); - TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort()); + final TestInterface api = + new TestInterfaceBuilder().target("http://localhost:" + server.getPort()); - Response response = api.response(); + final Response response = api.response(); assertTrue(response.body().isRepeatable()); assertEquals("foo", response.body().toString()); } @@ -93,7 +92,8 @@ public void responseCoercesToStringBody() throws Exception { public void postFormParams() throws Exception { server.enqueue(new MockResponse().setBody("foo")); - TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort()); + final TestInterface api = + new TestInterfaceBuilder().target("http://localhost:" + server.getPort()); api.form("netflix", "denominator", "password"); @@ -106,7 +106,8 @@ public void postFormParams() throws Exception { public void postBodyParam() throws Exception { server.enqueue(new MockResponse().setBody("foo")); - TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort()); + final TestInterface api = + new TestInterfaceBuilder().target("http://localhost:" + server.getPort()); api.body(Arrays.asList("netflix", "denominator", "password")); @@ -124,7 +125,7 @@ public void bodyTypeCorrespondsWithParameterType() throws Exception { server.enqueue(new MockResponse().setBody("foo")); final AtomicReference encodedType = new AtomicReference(); - TestInterface api = new TestInterfaceBuilder() + final TestInterface api = new TestInterfaceBuilder() .encoder(new Encoder.Default() { @Override public void encode(Object object, Type bodyType, RequestTemplate template) { @@ -144,7 +145,8 @@ public void encode(Object object, Type bodyType, RequestTemplate template) { public void postGZIPEncodedBodyParam() throws Exception { server.enqueue(new MockResponse().setBody("foo")); - TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort()); + final TestInterface api = + new TestInterfaceBuilder().target("http://localhost:" + server.getPort()); api.gzipBody(Arrays.asList("netflix", "denominator", "password")); @@ -157,7 +159,8 @@ public void postGZIPEncodedBodyParam() throws Exception { public void postDeflateEncodedBodyParam() throws Exception { server.enqueue(new MockResponse().setBody("foo")); - TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort()); + final TestInterface api = + new TestInterfaceBuilder().target("http://localhost:" + server.getPort()); api.deflateBody(Arrays.asList("netflix", "denominator", "password")); @@ -170,7 +173,7 @@ public void postDeflateEncodedBodyParam() throws Exception { public void singleInterceptor() throws Exception { server.enqueue(new MockResponse().setBody("foo")); - TestInterface api = new TestInterfaceBuilder() + final TestInterface api = new TestInterfaceBuilder() .requestInterceptor(new ForwardedForInterceptor()) .target("http://localhost:" + server.getPort()); @@ -184,7 +187,7 @@ public void singleInterceptor() throws Exception { public void multipleInterceptor() throws Exception { server.enqueue(new MockResponse().setBody("foo")); - TestInterface api = new TestInterfaceBuilder() + final TestInterface api = new TestInterfaceBuilder() .requestInterceptor(new ForwardedForInterceptor()) .requestInterceptor(new UserAgentInterceptor()) .target("http://localhost:" + server.getPort()); @@ -200,7 +203,8 @@ public void multipleInterceptor() throws Exception { public void customExpander() throws Exception { server.enqueue(new MockResponse()); - TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort()); + final TestInterface api = + new TestInterfaceBuilder().target("http://localhost:" + server.getPort()); api.expand(new Date(1234l)); @@ -212,7 +216,8 @@ public void customExpander() throws Exception { public void customExpanderListParam() throws Exception { server.enqueue(new MockResponse()); - TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort()); + final TestInterface api = + new TestInterfaceBuilder().target("http://localhost:" + server.getPort()); api.expandList(Arrays.asList(new Date(1234l), new Date(12345l))); @@ -224,7 +229,8 @@ public void customExpanderListParam() throws Exception { public void customExpanderNullParam() throws Exception { server.enqueue(new MockResponse()); - TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort()); + final TestInterface api = + new TestInterfaceBuilder().target("http://localhost:" + server.getPort()); api.expandList(Arrays.asList(new Date(1234l), null)); @@ -236,9 +242,10 @@ public void customExpanderNullParam() throws Exception { public void headerMap() throws Exception { server.enqueue(new MockResponse()); - TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort()); + final TestInterface api = + new TestInterfaceBuilder().target("http://localhost:" + server.getPort()); - Map headerMap = new LinkedHashMap(); + final Map headerMap = new LinkedHashMap(); headerMap.put("Content-Type", "myContent"); headerMap.put("Custom-Header", "fooValue"); api.headerMap(headerMap); @@ -253,9 +260,10 @@ public void headerMap() throws Exception { public void headerMapWithHeaderAnnotations() throws Exception { server.enqueue(new MockResponse()); - TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort()); + final TestInterface api = + new TestInterfaceBuilder().target("http://localhost:" + server.getPort()); - Map headerMap = new LinkedHashMap(); + final Map headerMap = new LinkedHashMap(); headerMap.put("Custom-Header", "fooValue"); api.headerMapWithHeaderAnnotations(headerMap); @@ -284,9 +292,10 @@ public void headerMapWithHeaderAnnotations() throws Exception { public void queryMap() throws Exception { server.enqueue(new MockResponse()); - TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort()); + final TestInterface api = + new TestInterfaceBuilder().target("http://localhost:" + server.getPort()); - Map queryMap = new LinkedHashMap(); + final Map queryMap = new LinkedHashMap(); queryMap.put("name", "alice"); queryMap.put("fooKey", "fooValue"); api.queryMap(queryMap); @@ -299,9 +308,10 @@ public void queryMap() throws Exception { public void queryMapIterableValuesExpanded() throws Exception { server.enqueue(new MockResponse()); - TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort()); + final TestInterface api = + new TestInterfaceBuilder().target("http://localhost:" + server.getPort()); - Map queryMap = new LinkedHashMap(); + final Map queryMap = new LinkedHashMap(); queryMap.put("name", Arrays.asList("Alice", "Bob")); queryMap.put("fooKey", "fooValue"); queryMap.put("emptyListKey", new ArrayList()); @@ -314,7 +324,7 @@ public void queryMapIterableValuesExpanded() throws Exception { @Test public void queryMapWithQueryParams() throws Exception { - TestInterface api = new TestInterfaceBuilder() + final TestInterface api = new TestInterfaceBuilder() .target("http://localhost:" + server.getPort()); server.enqueue(new MockResponse()); @@ -344,7 +354,8 @@ public void queryMapWithQueryParams() throws Exception { @Test public void queryMapValueStartingWithBrace() throws Exception { - TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort()); + final TestInterface api = + new TestInterfaceBuilder().target("http://localhost:" + server.getPort()); server.enqueue(new MockResponse()); Map queryMap = new LinkedHashMap(); @@ -377,9 +388,10 @@ public void queryMapValueStartingWithBrace() throws Exception { @Test public void queryMapPojoWithFullParams() throws Exception { - TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort()); + final TestInterface api = + new TestInterfaceBuilder().target("http://localhost:" + server.getPort()); - CustomPojo customPojo = new CustomPojo("Name", 3); + final CustomPojo customPojo = new CustomPojo("Name", 3); server.enqueue(new MockResponse()); api.queryMapPojo(customPojo); @@ -389,9 +401,10 @@ public void queryMapPojoWithFullParams() throws Exception { @Test public void queryMapPojoWithPartialParams() throws Exception { - TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort()); + final TestInterface api = + new TestInterfaceBuilder().target("http://localhost:" + server.getPort()); - CustomPojo customPojo = new CustomPojo("Name", null); + final CustomPojo customPojo = new CustomPojo("Name", null); server.enqueue(new MockResponse()); api.queryMapPojo(customPojo); @@ -401,9 +414,10 @@ public void queryMapPojoWithPartialParams() throws Exception { @Test public void queryMapPojoWithEmptyParams() throws Exception { - TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort()); + final TestInterface api = + new TestInterfaceBuilder().target("http://localhost:" + server.getPort()); - CustomPojo customPojo = new CustomPojo(null, null); + final CustomPojo customPojo = new CustomPojo(null, null); server.enqueue(new MockResponse()); api.queryMapPojo(customPojo); @@ -433,7 +447,7 @@ public void canOverrideErrorDecoder() throws Exception { thrown.expect(IllegalArgumentException.class); thrown.expectMessage("bad zone name"); - TestInterface api = new TestInterfaceBuilder() + final TestInterface api = new TestInterfaceBuilder() .errorDecoder(new IllegalArgumentExceptionOn400()) .target("http://localhost:" + server.getPort()); @@ -445,7 +459,8 @@ public void retriesLostConnectionBeforeRead() throws Exception { server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AT_START)); server.enqueue(new MockResponse().setBody("success!")); - TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort()); + final TestInterface api = + new TestInterfaceBuilder().target("http://localhost:" + server.getPort()); api.post(); @@ -456,13 +471,8 @@ public void retriesLostConnectionBeforeRead() throws Exception { public void overrideTypeSpecificDecoder() throws Exception { server.enqueue(new MockResponse().setBody("success!")); - TestInterface api = new TestInterfaceBuilder() - .decoder(new Decoder() { - @Override - public Object decode(Response response, Type type) { - return "fail"; - } - }).target("http://localhost:" + server.getPort()); + final TestInterface api = new TestInterfaceBuilder() + .decoder((response, type) -> "fail").target("http://localhost:" + server.getPort()); assertEquals(api.post(), "fail"); } @@ -475,11 +485,11 @@ public void retryableExceptionInDecoder() throws Exception { server.enqueue(new MockResponse().setBody("retry!")); server.enqueue(new MockResponse().setBody("success!")); - TestInterface api = new TestInterfaceBuilder() + final TestInterface api = new TestInterfaceBuilder() .decoder(new StringDecoder() { @Override public Object decode(Response response, Type type) throws IOException { - String string = super.decode(response, type).toString(); + final String string = super.decode(response, type).toString(); if ("retry!".equals(string)) { throw new RetryableException(response.status(), string, HttpMethod.POST, null); } @@ -497,12 +507,9 @@ public void doesntRetryAfterResponseIsSent() throws Exception { thrown.expect(FeignException.class); thrown.expectMessage("timeout reading POST http://"); - TestInterface api = new TestInterfaceBuilder() - .decoder(new Decoder() { - @Override - public Object decode(Response response, Type type) throws IOException { - throw new IOException("timeout"); - } + final TestInterface api = new TestInterfaceBuilder() + .decoder((response, type) -> { + throw new IOException("timeout"); }).target("http://localhost:" + server.getPort()); api.post(); @@ -512,7 +519,7 @@ public Object decode(Response response, Type type) throws IOException { public void throwsFeignExceptionIncludingBody() { server.enqueue(new MockResponse().setBody("success!")); - TestInterface api = Feign.builder() + final TestInterface api = Feign.builder() .decoder((response, type) -> { throw new IOException("timeout"); }) @@ -520,7 +527,7 @@ public void throwsFeignExceptionIncludingBody() { try { api.body("Request body"); - } catch (FeignException e) { + } catch (final FeignException e) { assertThat(e.getMessage()) .isEqualTo("timeout reading POST http://localhost:" + server.getPort() + "/"); assertThat(e.contentUTF8()).isEqualTo("Request body"); @@ -531,7 +538,7 @@ public void throwsFeignExceptionIncludingBody() { public void throwsFeignExceptionWithoutBody() { server.enqueue(new MockResponse().setBody("success!")); - TestInterface api = Feign.builder() + final TestInterface api = Feign.builder() .decoder((response, type) -> { throw new IOException("timeout"); }) @@ -539,7 +546,7 @@ public void throwsFeignExceptionWithoutBody() { try { api.noContent(); - } catch (FeignException e) { + } catch (final FeignException e) { assertThat(e.getMessage()) .isEqualTo("timeout reading POST http://localhost:" + server.getPort() + "/"); assertThat(e.contentUTF8()).isEqualTo(""); @@ -553,17 +560,14 @@ public void ensureRetryerClonesItself() throws Exception { server.enqueue(new MockResponse().setResponseCode(503).setBody("foo 3")); server.enqueue(new MockResponse().setResponseCode(200).setBody("foo 4")); - MockRetryer retryer = new MockRetryer(); + final MockRetryer retryer = new MockRetryer(); - TestInterface api = Feign.builder() + final TestInterface api = Feign.builder() .retryer(retryer) - .errorDecoder(new ErrorDecoder() { - @Override - public Exception decode(String methodKey, Response response) { - return new RetryableException(response.status(), "play it again sam!", HttpMethod.POST, - null); - } - }).target(TestInterface.class, "http://localhost:" + server.getPort()); + .errorDecoder((methodKey, response) -> new RetryableException(response.status(), + "play it again sam!", HttpMethod.POST, + null)) + .target(TestInterface.class, "http://localhost:" + server.getPort()); api.post(); api.post(); // if retryer instance was reused, this statement will throw an exception @@ -579,16 +583,13 @@ public void throwsOriginalExceptionAfterFailedRetries() throws Exception { thrown.expect(TestInterfaceException.class); thrown.expectMessage(message); - TestInterface api = Feign.builder() + final TestInterface api = Feign.builder() .exceptionPropagationPolicy(UNWRAP) .retryer(new Retryer.Default(1, 1, 2)) - .errorDecoder(new ErrorDecoder() { - @Override - public Exception decode(String methodKey, Response response) { - return new RetryableException(response.status(), "play it again sam!", HttpMethod.POST, - new TestInterfaceException(message), null); - } - }).target(TestInterface.class, "http://localhost:" + server.getPort()); + .errorDecoder((methodKey, response) -> new RetryableException(response.status(), + "play it again sam!", HttpMethod.POST, + new TestInterfaceException(message), null)) + .target(TestInterface.class, "http://localhost:" + server.getPort()); api.post(); } @@ -598,26 +599,23 @@ public void throwsRetryableExceptionIfNoUnderlyingCause() throws Exception { server.enqueue(new MockResponse().setResponseCode(503).setBody("foo 1")); server.enqueue(new MockResponse().setResponseCode(503).setBody("foo 2")); - String message = "play it again sam!"; + final String message = "play it again sam!"; thrown.expect(RetryableException.class); thrown.expectMessage(message); - TestInterface api = Feign.builder() + final TestInterface api = Feign.builder() .exceptionPropagationPolicy(UNWRAP) .retryer(new Retryer.Default(1, 1, 2)) - .errorDecoder(new ErrorDecoder() { - @Override - public Exception decode(String methodKey, Response response) { - return new RetryableException(response.status(), message, HttpMethod.POST, null); - } - }).target(TestInterface.class, "http://localhost:" + server.getPort()); + .errorDecoder((methodKey, response) -> new RetryableException(response.status(), message, + HttpMethod.POST, null)) + .target(TestInterface.class, "http://localhost:" + server.getPort()); api.post(); } @Test public void whenReturnTypeIsResponseNoErrorHandling() { - Map> headers = new LinkedHashMap>(); + final Map> headers = new LinkedHashMap>(); headers.put("Location", Arrays.asList("http://bar.com")); final Response response = Response.builder() .status(302) @@ -628,7 +626,7 @@ public void whenReturnTypeIsResponseNoErrorHandling() { .build(); // fake client as Client.Default follows redirects. - TestInterface api = Feign.builder() + final TestInterface api = Feign.builder() .client((request, options) -> response) .target(TestInterface.class, "http://localhost:" + server.getPort()); @@ -659,12 +657,9 @@ public void okIfDecodeRootCauseHasNoMessage() throws Exception { server.enqueue(new MockResponse().setBody("success!")); thrown.expect(DecodeException.class); - TestInterface api = new TestInterfaceBuilder() - .decoder(new Decoder() { - @Override - public Object decode(Response response, Type type) throws IOException { - throw new RuntimeException(); - } + final TestInterface api = new TestInterfaceBuilder() + .decoder((response, type) -> { + throw new RuntimeException(); }).target("http://localhost:" + server.getPort()); api.post(); @@ -676,14 +671,11 @@ public void decodingExceptionGetWrappedInDecode404Mode() throws Exception { thrown.expect(DecodeException.class); thrown.expectCause(isA(NoSuchElementException.class));; - TestInterface api = new TestInterfaceBuilder() + final TestInterface api = new TestInterfaceBuilder() .decode404() - .decoder(new Decoder() { - @Override - public Object decode(Response response, Type type) throws IOException { - assertEquals(404, response.status()); - throw new NoSuchElementException(); - } + .decoder((response, type) -> { + assertEquals(404, response.status()); + throw new NoSuchElementException(); }).target("http://localhost:" + server.getPort()); api.post(); } @@ -693,7 +685,7 @@ public void decodingDoesNotSwallow404ErrorsInDecode404Mode() throws Exception { server.enqueue(new MockResponse().setResponseCode(404)); thrown.expect(IllegalArgumentException.class); - TestInterface api = new TestInterfaceBuilder() + final TestInterface api = new TestInterfaceBuilder() .decode404() .errorDecoder(new IllegalArgumentExceptionOn404()) .target("http://localhost:" + server.getPort()); @@ -705,12 +697,9 @@ public void okIfEncodeRootCauseHasNoMessage() throws Exception { server.enqueue(new MockResponse().setBody("success!")); thrown.expect(EncodeException.class); - TestInterface api = new TestInterfaceBuilder() - .encoder(new Encoder() { - @Override - public void encode(Object object, Type bodyType, RequestTemplate template) { - throw new RuntimeException(); - } + final TestInterface api = new TestInterfaceBuilder() + .encoder((object, bodyType, template) -> { + throw new RuntimeException(); }).target("http://localhost:" + server.getPort()); api.body(Arrays.asList("foo")); @@ -718,16 +707,16 @@ public void encode(Object object, Type bodyType, RequestTemplate template) { @Test public void equalsHashCodeAndToStringWork() { - Target t1 = + final Target t1 = new HardCodedTarget(TestInterface.class, "http://localhost:8080"); - Target t2 = + final Target t2 = new HardCodedTarget(TestInterface.class, "http://localhost:8888"); - Target t3 = + final Target t3 = new HardCodedTarget(OtherTestInterface.class, "http://localhost:8080"); - TestInterface i1 = Feign.builder().target(t1); - TestInterface i2 = Feign.builder().target(t1); - TestInterface i3 = Feign.builder().target(t2); - OtherTestInterface i4 = Feign.builder().target(t3); + final TestInterface i1 = Feign.builder().target(t1); + final TestInterface i2 = Feign.builder().target(t1); + final TestInterface i3 = Feign.builder().target(t2); + final OtherTestInterface i4 = Feign.builder().target(t3); assertThat(i1) .isEqualTo(i2) @@ -756,10 +745,10 @@ public void equalsHashCodeAndToStringWork() { @Test public void decodeLogicSupportsByteArray() throws Exception { - byte[] expectedResponse = {12, 34, 56}; + final byte[] expectedResponse = {12, 34, 56}; server.enqueue(new MockResponse().setBody(new Buffer().write(expectedResponse))); - OtherTestInterface api = + final OtherTestInterface api = Feign.builder().target(OtherTestInterface.class, "http://localhost:" + server.getPort()); assertThat(api.binaryResponseBody()) @@ -768,10 +757,10 @@ public void decodeLogicSupportsByteArray() throws Exception { @Test public void encodeLogicSupportsByteArray() throws Exception { - byte[] expectedRequest = {12, 34, 56}; + final byte[] expectedRequest = {12, 34, 56}; server.enqueue(new MockResponse()); - OtherTestInterface api = + final OtherTestInterface api = Feign.builder().target(OtherTestInterface.class, "http://localhost:" + server.getPort()); api.binaryRequestBody(expectedRequest); @@ -784,7 +773,8 @@ public void encodeLogicSupportsByteArray() throws Exception { public void encodedQueryParam() throws Exception { server.enqueue(new MockResponse()); - TestInterface api = new TestInterfaceBuilder().target("http://localhost:" + server.getPort()); + final TestInterface api = + new TestInterfaceBuilder().target("http://localhost:" + server.getPort()); api.encodedQueryParam("5.2FSi+"); @@ -794,25 +784,22 @@ public void encodedQueryParam() throws Exception { @Test public void responseMapperIsAppliedBeforeDelegate() throws IOException { - ResponseMappingDecoder decoder = + final ResponseMappingDecoder decoder = new ResponseMappingDecoder(upperCaseResponseMapper(), new StringDecoder()); - String output = (String) decoder.decode(responseWithText("response"), String.class); + final String output = (String) decoder.decode(responseWithText("response"), String.class); assertThat(output).isEqualTo("RESPONSE"); } private ResponseMapper upperCaseResponseMapper() { - return new ResponseMapper() { - @Override - public Response map(Response response, Type type) { - try { - return response - .toBuilder() - .body(Util.toString(response.body().asReader()).toUpperCase().getBytes()) - .build(); - } catch (IOException e) { - throw new RuntimeException(e); - } + return (response, type) -> { + try { + return response + .toBuilder() + .body(Util.toString(response.body().asReader()).toUpperCase().getBytes()) + .build(); + } catch (final IOException e) { + throw new RuntimeException(e); } }; } @@ -830,7 +817,7 @@ private Response responseWithText(String text) { public void mapAndDecodeExecutesMapFunction() throws Exception { server.enqueue(new MockResponse().setBody("response!")); - TestInterface api = new Feign.Builder() + final TestInterface api = Feign.builder() .mapAndDecode(upperCaseResponseMapper(), new StringDecoder()) .target(TestInterface.class, "http://localhost:" + server.getPort()); @@ -839,10 +826,10 @@ public void mapAndDecodeExecutesMapFunction() throws Exception { @Test public void beanQueryMapEncoderWithPrivateGetterIgnored() throws Exception { - TestInterface api = new TestInterfaceBuilder().queryMapEndcoder(new BeanQueryMapEncoder()) + final TestInterface api = new TestInterfaceBuilder().queryMapEndcoder(new BeanQueryMapEncoder()) .target("http://localhost:" + server.getPort()); - PropertyPojo.ChildPojoClass propertyPojo = new PropertyPojo.ChildPojoClass(); + final PropertyPojo.ChildPojoClass propertyPojo = new PropertyPojo.ChildPojoClass(); propertyPojo.setPrivateGetterProperty("privateGetterProperty"); propertyPojo.setName("Name"); propertyPojo.setNumber(1); @@ -874,10 +861,10 @@ public void queryMap_with_child_pojo() throws Exception { @Test public void beanQueryMapEncoderWithNullValueIgnored() throws Exception { - TestInterface api = new TestInterfaceBuilder().queryMapEndcoder(new BeanQueryMapEncoder()) + final TestInterface api = new TestInterfaceBuilder().queryMapEndcoder(new BeanQueryMapEncoder()) .target("http://localhost:" + server.getPort()); - PropertyPojo.ChildPojoClass propertyPojo = new PropertyPojo.ChildPojoClass(); + final PropertyPojo.ChildPojoClass propertyPojo = new PropertyPojo.ChildPojoClass(); propertyPojo.setName(null); propertyPojo.setNumber(1); @@ -889,10 +876,10 @@ public void beanQueryMapEncoderWithNullValueIgnored() throws Exception { @Test public void beanQueryMapEncoderWithEmptyParams() throws Exception { - TestInterface api = new TestInterfaceBuilder().queryMapEndcoder(new BeanQueryMapEncoder()) + final TestInterface api = new TestInterfaceBuilder().queryMapEndcoder(new BeanQueryMapEncoder()) .target("http://localhost:" + server.getPort()); - PropertyPojo.ChildPojoClass propertyPojo = new PropertyPojo.ChildPojoClass(); + final PropertyPojo.ChildPojoClass propertyPojo = new PropertyPojo.ChildPojoClass(); server.enqueue(new MockResponse()); api.queryMapPropertyPojo(propertyPojo); @@ -1054,16 +1041,13 @@ public Exception decode(String methodKey, Response response) { static final class TestInterfaceBuilder { - private final Feign.Builder delegate = new Feign.Builder() + private final Feign.Builder delegate = Feign.builder() .decoder(new Decoder.Default()) - .encoder(new Encoder() { - @Override - public void encode(Object object, Type bodyType, RequestTemplate template) { - if (object instanceof Map) { - template.body(new Gson().toJson(object)); - } else { - template.body(object.toString()); - } + .encoder((object, bodyType, template) -> { + if (object instanceof Map) { + template.body(new Gson().toJson(object)); + } else { + template.body(object.toString()); } }); diff --git a/core/src/test/java/feign/codec/RetryAfterDecoderTest.java b/core/src/test/java/feign/codec/RetryAfterDecoderTest.java index fc27a5293a..bcdea58392 100644 --- a/core/src/test/java/feign/codec/RetryAfterDecoderTest.java +++ b/core/src/test/java/feign/codec/RetryAfterDecoderTest.java @@ -16,11 +16,8 @@ import static feign.codec.ErrorDecoder.RetryAfterDecoder.RFC822_FORMAT; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; - import feign.codec.ErrorDecoder.RetryAfterDecoder; - import java.text.ParseException; - import org.junit.Test; public class RetryAfterDecoderTest { diff --git a/example-github/src/main/java/example/github/GitHubExample.java b/example-github/src/main/java/example/github/GitHubExample.java index 4a7d3bc45d..523d6df658 100644 --- a/example-github/src/main/java/example/github/GitHubExample.java +++ b/example-github/src/main/java/example/github/GitHubExample.java @@ -30,17 +30,17 @@ public class GitHubExample { private static final String GITHUB_TOKEN = "GITHUB_TOKEN"; - interface GitHub { + public interface GitHub { - class Repository { + public class Repository { String name; } - class Contributor { + public class Contributor { String login; } - class Issue { + public class Issue { Issue() { @@ -72,7 +72,7 @@ default List contributors(String owner) { } static GitHub connect() { - Decoder decoder = new GsonDecoder(); + final Decoder decoder = new GsonDecoder(); Encoder encoder = new GsonEncoder(); return Feign.builder() .encoder(encoder) @@ -93,7 +93,7 @@ static GitHub connect() { } - static class GitHubClientError extends RuntimeException { + public static class GitHubClientError extends RuntimeException { private String message; // parsed from json @Override @@ -103,18 +103,18 @@ public String getMessage() { } public static void main(String... args) { - GitHub github = GitHub.connect(); + final GitHub github = GitHub.connect(); System.out.println("Let's fetch and print a list of the contributors to this org."); - List contributors = github.contributors("openfeign"); - for (String contributor : contributors) { + final List contributors = github.contributors("openfeign"); + for (final String contributor : contributors) { System.out.println(contributor); } System.out.println("Now, let's cause an error."); try { github.contributors("openfeign", "some-unknown-project"); - } catch (GitHubClientError e) { + } catch (final GitHubClientError e) { System.out.println(e.getMessage()); } @@ -129,12 +129,12 @@ public static void main(String... args) { } } - static class GitHubErrorDecoder implements ErrorDecoder { + public static class GitHubErrorDecoder implements ErrorDecoder { final Decoder decoder; final ErrorDecoder defaultDecoder = new ErrorDecoder.Default(); - GitHubErrorDecoder(Decoder decoder) { + public GitHubErrorDecoder(Decoder decoder) { this.decoder = decoder; } @@ -143,8 +143,10 @@ public Exception decode(String methodKey, Response response) { try { // must replace status by 200 other GSONDecoder returns null response = response.toBuilder().status(200).build(); - return (Exception) decoder.decode(response, GitHubClientError.class); - } catch (IOException fallbackToDefault) { + return (Exception) decoder.decode( + response.toBuilder().status(200).build(), + GitHubClientError.class); + } catch (final IOException fallbackToDefault) { return defaultDecoder.decode(methodKey, response); } } diff --git a/hystrix/src/main/java/feign/hystrix/HystrixFeign.java b/hystrix/src/main/java/feign/hystrix/HystrixFeign.java index cbd13d46f2..8c0ad36281 100644 --- a/hystrix/src/main/java/feign/hystrix/HystrixFeign.java +++ b/hystrix/src/main/java/feign/hystrix/HystrixFeign.java @@ -14,19 +14,8 @@ package feign.hystrix; import com.netflix.hystrix.HystrixCommand; -import java.lang.reflect.InvocationHandler; -import java.lang.reflect.Method; -import java.util.Map; -import feign.Client; -import feign.Contract; -import feign.Feign; -import feign.InvocationHandlerFactory; -import feign.Logger; -import feign.Request; -import feign.RequestInterceptor; -import feign.ResponseMapper; -import feign.Retryer; -import feign.Target; +import feign.*; +import feign.FeignConfig.FeignConfigBuilder; import feign.codec.Decoder; import feign.codec.Encoder; import feign.codec.ErrorDecoder; @@ -39,7 +28,7 @@ public final class HystrixFeign { public static Builder builder() { - return new Builder(); + return new Builder(FeignConfig.builder()); } public static final class Builder extends Feign.Builder { @@ -47,6 +36,10 @@ public static final class Builder extends Feign.Builder { private Contract contract = new Contract.Default(); private SetterFactory setterFactory = new SetterFactory.Default(); + public Builder(FeignConfigBuilder feignConfigBuilder) { + super(feignConfigBuilder); + } + /** * Allows you to override hystrix properties such as thread pools and command keys. */ @@ -80,7 +73,7 @@ public T target(Target target, FallbackFactory fallbackFacto * use this feature, pass a safe implementation of your target interface as the last parameter. * * Here's an example: - * + * *
      * {@code
      *
@@ -139,14 +132,9 @@ public Feign build() {
 
     /** Configures components needed for hystrix integration. */
     Feign build(final FallbackFactory nullableFallbackFactory) {
-      super.invocationHandlerFactory(new InvocationHandlerFactory() {
-        @Override
-        public InvocationHandler create(Target target,
-                                        Map dispatch) {
-          return new HystrixInvocationHandler(target, dispatch, setterFactory,
-              nullableFallbackFactory);
-        }
-      });
+      super.invocationHandlerFactory(
+          (target, dispatch) -> new HystrixInvocationHandler(target, dispatch, setterFactory,
+              nullableFallbackFactory));
       super.contract(new HystrixDelegatingContract(contract));
       return super.build();
     }
diff --git a/pom.xml b/pom.xml
index 408e280076..a50bb2acea 100644
--- a/pom.xml
+++ b/pom.xml
@@ -41,6 +41,7 @@
     slf4j
     soap
     reactive
+    apt-generator
     example-github
     example-wikipedia
     
@@ -60,10 +61,6 @@
     1.8
     java18
 
-    
-    1.8
-    1.8
-
     ${project.basedir}
 
     3.6.0
@@ -321,6 +318,13 @@
         ${slf4j.version}
       
 
+      
+      
+        org.projectlombok
+        lombok
+        1.18.6
+        provided
+      
     
   
 
@@ -388,6 +392,11 @@
       
         maven-compiler-plugin
         true
+        
+          ${main.java.version}
+          ${main.java.version}
+          ${main.java.version}
+        
         
           
           
@@ -396,10 +405,6 @@
             
               compile
             
-            
-              ${main.java.version}
-              ${main.java.version}
-            
           
         
       
diff --git a/reactive/src/main/java/feign/reactive/ReactiveFeign.java b/reactive/src/main/java/feign/reactive/ReactiveFeign.java
index 96535c0fc1..47323f4277 100644
--- a/reactive/src/main/java/feign/reactive/ReactiveFeign.java
+++ b/reactive/src/main/java/feign/reactive/ReactiveFeign.java
@@ -15,7 +15,7 @@
 
 import feign.Contract;
 import feign.Feign;
-import feign.InvocationHandlerFactory;
+import feign.FeignConfig.FeignConfigBuilder;
 
 abstract class ReactiveFeign {
 
@@ -23,6 +23,10 @@ abstract class ReactiveFeign {
 
   public static class Builder extends Feign.Builder {
 
+    protected Builder(FeignConfigBuilder feignConfigBuilder) {
+      super(feignConfigBuilder);
+    }
+
     private Contract contract = new Contract.Default();
 
     /**
diff --git a/reactive/src/main/java/feign/reactive/ReactorFeign.java b/reactive/src/main/java/feign/reactive/ReactorFeign.java
index 33278dfc21..5f52b68e02 100644
--- a/reactive/src/main/java/feign/reactive/ReactorFeign.java
+++ b/reactive/src/main/java/feign/reactive/ReactorFeign.java
@@ -13,22 +13,24 @@
  */
 package feign.reactive;
 
-import feign.Feign;
-import feign.reactive.ReactiveFeign.Builder;
 import java.lang.reflect.InvocationHandler;
 import java.lang.reflect.Method;
 import java.util.Map;
-import feign.InvocationHandlerFactory;
-import feign.Target;
+import feign.*;
+import feign.FeignConfig.FeignConfigBuilder;
 
 public class ReactorFeign extends ReactiveFeign {
 
   public static Builder builder() {
-    return new Builder();
+    return new Builder(FeignConfig.builder());
   }
 
   public static class Builder extends ReactiveFeign.Builder {
 
+    protected Builder(FeignConfigBuilder feignConfigBuilder) {
+      super(feignConfigBuilder);
+    }
+
     @Override
     public Feign build() {
       super.invocationHandlerFactory(new ReactorInvocationHandlerFactory());
diff --git a/reactive/src/main/java/feign/reactive/RxJavaFeign.java b/reactive/src/main/java/feign/reactive/RxJavaFeign.java
index 89ea356400..85f6dd3f6a 100644
--- a/reactive/src/main/java/feign/reactive/RxJavaFeign.java
+++ b/reactive/src/main/java/feign/reactive/RxJavaFeign.java
@@ -16,18 +16,21 @@
 import java.lang.reflect.InvocationHandler;
 import java.lang.reflect.Method;
 import java.util.Map;
-import feign.Feign;
-import feign.InvocationHandlerFactory;
-import feign.Target;
+import feign.*;
+import feign.FeignConfig.FeignConfigBuilder;
 
 public class RxJavaFeign extends ReactiveFeign {
 
   public static Builder builder() {
-    return new Builder();
+    return new Builder(FeignConfig.builder());
   }
 
   public static class Builder extends ReactiveFeign.Builder {
 
+    protected Builder(FeignConfigBuilder feignConfigBuilder) {
+      super(feignConfigBuilder);
+    }
+
     @Override
     public Feign build() {
       super.invocationHandlerFactory(new RxJavaInvocationHandlerFactory());

From 88071e6776b420b80e302a5427872b92e306499a Mon Sep 17 00:00:00 2001
From: Marvin Froeder 
Date: Thu, 20 Jun 2019 20:23:49 +1200
Subject: [PATCH 2/9] Create a test that compile native using graalVM

---
 apt-generator/docker/Dockerfile               |  49 ++++++++
 apt-generator/docker/reflectconfig.json       |  41 +++++++
 apt-generator/pom.xml                         | 106 +++++++++++++++++-
 .../aptgenerator/github/GitHubFactory.java    |  13 ++-
 .../github/GitHubFactoryExample.java          |  10 +-
 .../aptgenerator/github/TypeReference.java    |   0
 .../github/GitHubFactoryExampleIT.java        |  45 ++++++++
 .../main/java/example/github/Contributor.java |  18 +++
 .../example/github/GitHubClientError.java     |  23 ++++
 .../java/example/github/GitHubExample.java    |  46 ++------
 .../src/main/java/example/github/Issue.java   |  24 ++++
 .../main/java/example/github/Repository.java  |  18 +++
 pom.xml                                       |   2 +-
 13 files changed, 347 insertions(+), 48 deletions(-)
 create mode 100644 apt-generator/docker/Dockerfile
 create mode 100644 apt-generator/docker/reflectconfig.json
 rename apt-generator/src/{test => main}/java/feign/aptgenerator/github/GitHubFactory.java (90%)
 rename apt-generator/src/{test => main}/java/feign/aptgenerator/github/GitHubFactoryExample.java (88%)
 rename apt-generator/src/{test => main}/java/feign/aptgenerator/github/TypeReference.java (100%)
 create mode 100644 apt-generator/src/test/java/feign/aptgenerator/github/GitHubFactoryExampleIT.java
 create mode 100644 example-github/src/main/java/example/github/Contributor.java
 create mode 100644 example-github/src/main/java/example/github/GitHubClientError.java
 create mode 100644 example-github/src/main/java/example/github/Issue.java
 create mode 100644 example-github/src/main/java/example/github/Repository.java

diff --git a/apt-generator/docker/Dockerfile b/apt-generator/docker/Dockerfile
new file mode 100644
index 0000000000..e2c491e0fe
--- /dev/null
+++ b/apt-generator/docker/Dockerfile
@@ -0,0 +1,49 @@
+#
+# Copyright 2012-2019 The Feign Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+# in compliance with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software distributed under the License
+# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+# or implied. See the License for the specific language governing permissions and limitations under
+# the License.
+#
+
+# build native image
+FROM oracle/graalvm-ce:19.0.0
+LABEL vendor="The Last Pickle"
+
+RUN gu install native-image
+
+COPY reflectconfig.json .
+COPY ${project.artifactId}-${project.version}.jar .
+
+RUN native-image --verbose \
+    --no-fallback \
+    -H:ReflectionConfigurationFiles=reflectconfig.json \
+    -H:EnableURLProtocols=https \
+    -jar ${project.artifactId}-${project.version}.jar \
+    -cp ${project.artifactId}-${project.version}.jar
+
+
+
+# test the native image
+FROM ubuntu
+
+COPY --from=0 /opt/graalvm-ce-19.0.0/jre/lib/amd64/libsunec.so /
+COPY --from=0 /${project.artifactId}-${project.version} /
+
+RUN /${project.artifactId}-${project.version}
+
+
+
+# create docker image with native
+FROM ubuntu
+
+COPY --from=0 /opt/graalvm-ce-19.0.0/jre/lib/amd64/libsunec.so /
+COPY --from=0 /${project.artifactId}-${project.version} /
+
+CMD /${project.artifactId}-${project.version}
diff --git a/apt-generator/docker/reflectconfig.json b/apt-generator/docker/reflectconfig.json
new file mode 100644
index 0000000000..6e35256716
--- /dev/null
+++ b/apt-generator/docker/reflectconfig.json
@@ -0,0 +1,41 @@
+[
+  {
+    "name" : "example.github.Repository",
+    "allDeclaredConstructors" : true,
+    "allPublicConstructors" : true,
+    "allDeclaredMethods" : true,
+    "allPublicMethods" : true
+  },
+  {
+    "name" : "example.github.Repository",
+    "fields" : [
+      { "name" : "name" }
+    ]
+  },
+  {
+    "name" : "example.github.Contributor",
+    "allDeclaredConstructors" : true,
+    "allPublicConstructors" : true,
+    "allDeclaredMethods" : true,
+    "allPublicMethods" : true
+  },
+  {
+    "name" : "example.github.Contributor",
+    "fields" : [
+      { "name" : "login" }
+    ]
+  },
+  {
+    "name" : "example.github.GitHubClientError",
+    "allDeclaredConstructors" : true,
+    "allPublicConstructors" : true,
+    "allDeclaredMethods" : true,
+    "allPublicMethods" : true
+  },
+  {
+    "name" : "example.github.GitHubClientError",
+    "fields" : [
+      { "name" : "message" }
+    ]
+  }
+]
diff --git a/apt-generator/pom.xml b/apt-generator/pom.xml
index 8ccf6e0c22..ee6c4db4be 100644
--- a/apt-generator/pom.xml
+++ b/apt-generator/pom.xml
@@ -50,14 +50,118 @@
     
       io.github.openfeign
       feign-gson
-      test
     
     
       io.github.openfeign
       feign-example-github
       ${project.version}
+    
+    
+      org.apache.commons
+      commons-exec
+      1.3
       test
     
   
 
+  
+    
+      
+        docker
+        true
+        
+        ${project.basedir}/docker
+      
+      
+      
+        ${basedir}/src/main/resources
+      
+    
+
+    
+      
+        org.apache.maven.plugins
+        maven-shade-plugin
+        2.4.3
+        
+          
+            package
+            
+              shade
+            
+            
+              
+                
+                  feign.aptgenerator.github.GitHubFactoryExample
+                
+              
+              false
+            
+          
+        
+      
+      
+        org.skife.maven
+        really-executable-jar-maven-plugin
+        1.5.0
+        
+          github
+        
+        
+          
+            package
+            
+              really-executable-jar
+            
+          
+        
+      
+      
+        org.apache.maven.plugins
+        maven-failsafe-plugin
+        ${maven-surefire-plugin.version}
+        
+          
+            
+              integration-test
+              verify
+            
+          
+        
+      
+
+      
+        
+        com.spotify
+        docker-maven-plugin
+        
+          
+          ${project.build.directory}/classes/docker/
+
+          
+          true
+
+          docker-hub
+          https://index.docker.io/v1/
+          feign-apt-generator/test
+          
+            
+              /
+              ${project.build.directory}
+              ${project.artifactId}-${project.version}.jar
+            
+          
+        
+        
+          
+          
+            post-integration-test
+            
+              build
+            
+          
+        
+      
+    
+  
 
diff --git a/apt-generator/src/test/java/feign/aptgenerator/github/GitHubFactory.java b/apt-generator/src/main/java/feign/aptgenerator/github/GitHubFactory.java
similarity index 90%
rename from apt-generator/src/test/java/feign/aptgenerator/github/GitHubFactory.java
rename to apt-generator/src/main/java/feign/aptgenerator/github/GitHubFactory.java
index 55690e10c5..e1d30c67e3 100644
--- a/apt-generator/src/test/java/feign/aptgenerator/github/GitHubFactory.java
+++ b/apt-generator/src/main/java/feign/aptgenerator/github/GitHubFactory.java
@@ -15,7 +15,10 @@
 
 import java.util.Arrays;
 import java.util.List;
+import example.github.Contributor;
 import example.github.GitHubExample.*;
+import example.github.Issue;
+import example.github.Repository;
 import feign.*;
 import feign.InvocationHandlerFactory.MethodHandler;
 import feign.Request.HttpMethod;
@@ -27,7 +30,7 @@ public class GitHubFactory implements GitHub {
   static {
     final MethodMetadata md = new MethodMetadata();
     __repos_metadata = md;
-    md.returnType(new TypeReference>() {}.getType());
+    md.returnType(new TypeReference>() {}.getType());
     md.configKey("GitHub#repos(String)");
 
     md.template().method(HttpMethod.GET);
@@ -43,7 +46,7 @@ public class GitHubFactory implements GitHub {
   static {
     final MethodMetadata md = new MethodMetadata();
     __contributors_metadata = md;
-    md.returnType(new TypeReference>() {}.getType());
+    md.returnType(new TypeReference>() {}.getType());
     md.configKey("GitHub#contributors(String,String)");
 
     md.template().method(HttpMethod.GET);
@@ -62,7 +65,7 @@ public class GitHubFactory implements GitHub {
   static {
     final MethodMetadata md = new MethodMetadata();
     __createIssue_metadata = md;
-    md.returnType(new TypeReference>() {}.getType());
+    md.returnType(new TypeReference>() {}.getType());
     md.configKey("GitHub#createIssue(Issue,String,String)");
 
     md.template().method(HttpMethod.POST);
@@ -106,7 +109,7 @@ public GitHubFactory(FeignConfig feignConfig) {
   }
 
   @Override
-  public List repos(String owner) {
+  public List repos(String owner) {
     try {
       return __repos_handler.invoke(owner);
     } catch (final FeignException e) {
@@ -117,7 +120,7 @@ public List repos(String owner) {
   }
 
   @Override
-  public List contributors(String owner, String repo) {
+  public List contributors(String owner, String repo) {
     try {
       return __contributors_handler.invoke(owner, repo);
     } catch (final RuntimeException e) {
diff --git a/apt-generator/src/test/java/feign/aptgenerator/github/GitHubFactoryExample.java b/apt-generator/src/main/java/feign/aptgenerator/github/GitHubFactoryExample.java
similarity index 88%
rename from apt-generator/src/test/java/feign/aptgenerator/github/GitHubFactoryExample.java
rename to apt-generator/src/main/java/feign/aptgenerator/github/GitHubFactoryExample.java
index d85d2139fc..2191a4b3c1 100644
--- a/apt-generator/src/test/java/feign/aptgenerator/github/GitHubFactoryExample.java
+++ b/apt-generator/src/main/java/feign/aptgenerator/github/GitHubFactoryExample.java
@@ -13,9 +13,10 @@
  */
 package feign.aptgenerator.github;
 
+import com.google.gson.GsonBuilder;
 import java.util.List;
+import example.github.GitHubClientError;
 import example.github.GitHubExample.GitHub;
-import example.github.GitHubExample.GitHubClientError;
 import example.github.GitHubExample.GitHubErrorDecoder;
 import feign.FeignConfig;
 import feign.Logger;
@@ -28,17 +29,18 @@
 public class GitHubFactoryExample {
 
   static FeignConfig config() {
-    final Decoder decoder = new GsonDecoder();
+    final Decoder decoder = new GsonDecoder(new GsonBuilder()
+        .create());
     return FeignConfig.builder()
         .decoder(decoder)
         .errorDecoder(new GitHubErrorDecoder(decoder))
         .logger(new Logger.ErrorLogger())
-        .logLevel(Logger.Level.BASIC)
+        .logLevel(Logger.Level.FULL)
         .url("https://api.github.com")
         .build();
   }
 
-  public static void main(String... args) {
+  public static void main(String[] args) {
     final GitHub github = new GitHubFactory(config());
 
     System.out.println("Let's fetch and print a list of the contributors to this org.");
diff --git a/apt-generator/src/test/java/feign/aptgenerator/github/TypeReference.java b/apt-generator/src/main/java/feign/aptgenerator/github/TypeReference.java
similarity index 100%
rename from apt-generator/src/test/java/feign/aptgenerator/github/TypeReference.java
rename to apt-generator/src/main/java/feign/aptgenerator/github/TypeReference.java
diff --git a/apt-generator/src/test/java/feign/aptgenerator/github/GitHubFactoryExampleIT.java b/apt-generator/src/test/java/feign/aptgenerator/github/GitHubFactoryExampleIT.java
new file mode 100644
index 0000000000..336cd6e094
--- /dev/null
+++ b/apt-generator/src/test/java/feign/aptgenerator/github/GitHubFactoryExampleIT.java
@@ -0,0 +1,45 @@
+/**
+ * Copyright 2012-2019 The Feign Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package feign.aptgenerator.github;
+
+import static org.junit.Assert.assertThat;
+import org.apache.commons.exec.CommandLine;
+import org.apache.commons.exec.DefaultExecutor;
+import org.hamcrest.CoreMatchers;
+import org.junit.Test;
+import java.io.File;
+import java.util.Arrays;
+
+/**
+ * Run main for {@link GitHubFactoryExample}
+ */
+public class GitHubFactoryExampleIT {
+
+  @Test
+  public void runMain() throws Exception {
+    final String jar = Arrays.stream(new File("target").listFiles())
+        .filter(file -> file.getName().startsWith("feign-apt-generator")
+            && file.getName().endsWith(".jar"))
+        .findFirst()
+        .map(File::getAbsolutePath)
+        .get();
+
+    final String line = "java -jar " + jar;
+    final CommandLine cmdLine = CommandLine.parse(line);
+    final int exitValue = new DefaultExecutor().execute(cmdLine);
+
+    assertThat(exitValue, CoreMatchers.equalTo(0));
+  }
+
+}
diff --git a/example-github/src/main/java/example/github/Contributor.java b/example-github/src/main/java/example/github/Contributor.java
new file mode 100644
index 0000000000..6f1c39a25b
--- /dev/null
+++ b/example-github/src/main/java/example/github/Contributor.java
@@ -0,0 +1,18 @@
+/**
+ * Copyright 2012-2019 The Feign Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package example.github;
+
+public class Contributor {
+  public String login;
+}
diff --git a/example-github/src/main/java/example/github/GitHubClientError.java b/example-github/src/main/java/example/github/GitHubClientError.java
new file mode 100644
index 0000000000..f0bb4533c7
--- /dev/null
+++ b/example-github/src/main/java/example/github/GitHubClientError.java
@@ -0,0 +1,23 @@
+/**
+ * Copyright 2012-2019 The Feign Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package example.github;
+
+public class GitHubClientError extends RuntimeException {
+  private String message; // parsed from json
+
+  @Override
+  public String getMessage() {
+    return message;
+  }
+}
diff --git a/example-github/src/main/java/example/github/GitHubExample.java b/example-github/src/main/java/example/github/GitHubExample.java
index 523d6df658..9047c75143 100644
--- a/example-github/src/main/java/example/github/GitHubExample.java
+++ b/example-github/src/main/java/example/github/GitHubExample.java
@@ -13,15 +13,15 @@
  */
 package example.github;
 
+import java.io.IOException;
+import java.util.List;
+import java.util.stream.Collectors;
 import feign.*;
 import feign.codec.Decoder;
 import feign.codec.Encoder;
 import feign.codec.ErrorDecoder;
 import feign.gson.GsonDecoder;
 import feign.gson.GsonEncoder;
-import java.io.IOException;
-import java.util.List;
-import java.util.stream.Collectors;
 
 /**
  * Inspired by {@code com.example.retrofit.GitHubClient}
@@ -32,27 +32,6 @@ public class GitHubExample {
 
   public interface GitHub {
 
-    public class Repository {
-      String name;
-    }
-
-    public class Contributor {
-      String login;
-    }
-
-    public class Issue {
-
-      Issue() {
-
-      }
-
-      String title;
-      String body;
-      List assignees;
-      int milestone;
-      List labels;
-    }
-
     @RequestLine("GET /users/{username}/repos?sort=full_name")
     List repos(@Param("username") String owner);
 
@@ -64,7 +43,9 @@ public class Issue {
 
     /** Lists all contributors for all repos owned by a user. */
     default List contributors(String owner) {
-      return repos(owner).stream()
+      return repos(owner)
+          .stream()
+          .peek(System.out::println)
           .flatMap(repo -> contributors(owner, repo.name).stream())
           .map(c -> c.login)
           .distinct()
@@ -73,7 +54,7 @@ default List contributors(String owner) {
 
     static GitHub connect() {
       final Decoder decoder = new GsonDecoder();
-      Encoder encoder = new GsonEncoder();
+      final Encoder encoder = new GsonEncoder();
       return Feign.builder()
           .encoder(encoder)
           .decoder(decoder)
@@ -93,15 +74,6 @@ static GitHub connect() {
   }
 
 
-  public static class GitHubClientError extends RuntimeException {
-    private String message; // parsed from json
-
-    @Override
-    public String getMessage() {
-      return message;
-    }
-  }
-
   public static void main(String... args) {
     final GitHub github = GitHub.connect();
 
@@ -120,11 +92,11 @@ public static void main(String... args) {
 
     System.out.println("Now, try to create an issue - which will also cause an error.");
     try {
-      GitHub.Issue issue = new GitHub.Issue();
+      final Issue issue = new Issue();
       issue.title = "The title";
       issue.body = "Some Text";
       github.createIssue(issue, "OpenFeign", "SomeRepo");
-    } catch (GitHubClientError e) {
+    } catch (final GitHubClientError e) {
       System.out.println(e.getMessage());
     }
   }
diff --git a/example-github/src/main/java/example/github/Issue.java b/example-github/src/main/java/example/github/Issue.java
new file mode 100644
index 0000000000..7b80e23942
--- /dev/null
+++ b/example-github/src/main/java/example/github/Issue.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright 2012-2019 The Feign Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package example.github;
+
+import java.util.List;
+
+public class Issue {
+  public String title;
+  public String body;
+  public List assignees;
+  public int milestone;
+  public List labels;
+}
diff --git a/example-github/src/main/java/example/github/Repository.java b/example-github/src/main/java/example/github/Repository.java
new file mode 100644
index 0000000000..22f66d82a7
--- /dev/null
+++ b/example-github/src/main/java/example/github/Repository.java
@@ -0,0 +1,18 @@
+/**
+ * Copyright 2012-2019 The Feign Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package example.github;
+
+public class Repository {
+  public String name;
+}
diff --git a/pom.xml b/pom.xml
index a50bb2acea..7634acbb29 100644
--- a/pom.xml
+++ b/pom.xml
@@ -19,7 +19,7 @@
 
   io.github.openfeign
   parent
-  10.3.0-SNAPSHOT
+  10.2.1-SNAPSHOT
   pom
 
   Feign (Parent)

From 5c51c2d7cef9ba28f1401edd4ce4b98f155c8297 Mon Sep 17 00:00:00 2001
From: Snyk bot 
Date: Fri, 12 Jul 2019 15:13:29 +0300
Subject: [PATCH 3/9] fix: httpclient/pom.xml to reduce vulnerabilities (#997)

The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-JAVA-ORGAPACHEHTTPCOMPONENTS-31517
---
 httpclient/pom.xml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/httpclient/pom.xml b/httpclient/pom.xml
index 9086ef5429..1731094993 100644
--- a/httpclient/pom.xml
+++ b/httpclient/pom.xml
@@ -40,7 +40,7 @@
     
       org.apache.httpcomponents
       httpclient
-      4.5.2
+      4.5.3
     
 
     

From e48017d18c2933c59d3737915f3d2bd3a858b25b Mon Sep 17 00:00:00 2001
From: Marvin Froeder 
Date: Sat, 31 Aug 2019 07:28:46 +1200
Subject: [PATCH 4/9] Reusable contracts

---
 core/src/main/java/feign/Contract.java        | 269 +++++++++++++-----
 core/src/main/java/feign/Request.java         |   6 +
 .../java/feign/SynchronousMethodHandler.java  |   7 +-
 .../test/java/feign/DefaultContractTest.java  |   2 +-
 4 files changed, 204 insertions(+), 80 deletions(-)

diff --git a/core/src/main/java/feign/Contract.java b/core/src/main/java/feign/Contract.java
index 6363654c02..5c5df901f0 100644
--- a/core/src/main/java/feign/Contract.java
+++ b/core/src/main/java/feign/Contract.java
@@ -14,18 +14,15 @@
 package feign;
 
 import java.lang.annotation.Annotation;
-import java.lang.reflect.Method;
-import java.lang.reflect.Modifier;
-import java.lang.reflect.ParameterizedType;
-import java.lang.reflect.Type;
+import java.lang.reflect.*;
 import java.net.URI;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
+import java.util.function.BiConsumer;
+import java.util.function.BiFunction;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import feign.Contract.VisitorContract.ClassAnnotationProcessor;
 import feign.Request.HttpMethod;
 import static feign.Util.checkState;
 import static feign.Util.emptyToNull;
@@ -98,7 +95,7 @@ protected MethodMetadata parseAndValidateMetadata(Class targetType, Method me
       }
       checkState(data.template().method() != null,
           "Method %s not annotated with HTTP method type (ex. GET, POST)",
-          method.getName());
+          data.configKey());
       Class[] parameterTypes = method.getParameterTypes();
       Type[] genericParameterTypes = method.getGenericParameterTypes();
 
@@ -169,7 +166,6 @@ private static void checkMapKeys(String name, Type genericType) {
       }
     }
 
-
     /**
      * Called by parseAndValidateMetadata twice, first on the declaring class, then on the target
      * type (unless they are the same).
@@ -210,100 +206,220 @@ protected void nameParam(MethodMetadata data, String name, int i) {
       data.indexToName().put(i, names);
     }
   }
+  abstract class VisitorContract extends BaseContract {
 
-  class Default extends BaseContract {
+    Map, ClassAnnotationProcessor> classAnnotationProcessors =
+        new HashMap<>();
+    private Map, MethodAnnotationProcessor> methodAnnotationProcessors =
+        new HashMap<>();
+    Map, ParameterAnnotationProcessor> parameterAnnotationProcessors =
+        new HashMap<>();
 
-    static final Pattern REQUEST_LINE_PATTERN = Pattern.compile("^([A-Z]+)[ ]*(.*)$");
+    /**
+     * Called by parseAndValidateMetadata twice, first on the declaring class, then on the target
+     * type (unless they are the same).
+     *
+     * @param data metadata collected so far relating to the current java method.
+     * @param clz the class to process
+     */
+    @Override
+    protected final void processAnnotationOnClass(MethodMetadata data, Class targetType) {
+      Arrays.stream(targetType.getAnnotations())
+          .forEach(annotation -> classAnnotationProcessors.forEach((type, processor) -> {
+            if (type.isInstance(annotation))
+              processor.process(annotation, data);
+          }));
+    }
 
+    /**
+     * @param data metadata collected so far relating to the current java method.
+     * @param annotation annotations present on the current method annotation.
+     * @param method method currently being processed.
+     */
     @Override
-    protected void processAnnotationOnClass(MethodMetadata data, Class targetType) {
-      if (targetType.isAnnotationPresent(Headers.class)) {
-        String[] headersOnType = targetType.getAnnotation(Headers.class).value();
+    protected final void processAnnotationOnMethod(MethodMetadata data,
+                                                   Annotation annotation,
+                                                   Method method) {
+      methodAnnotationProcessors.forEach((type, processor) -> {
+        if (type.isInstance(annotation))
+          processor.process(annotation, data);
+      });
+    }
+
+
+    /**
+     * @param data metadata collected so far relating to the current java method.
+     * @param annotations annotations present on the current parameter annotation.
+     * @param paramIndex if you find a name in {@code annotations}, call
+     *        {@link #nameParam(MethodMetadata, String, int)} with this as the last parameter.
+     * @return true if you called {@link #nameParam(MethodMetadata, String, int)} after finding an
+     *         http-relevant annotation.
+     */
+    protected final boolean processAnnotationsOnParameter(MethodMetadata data,
+                                                          Annotation[] annotations,
+                                                          int paramIndex) {
+      return Arrays.stream(annotations)
+          .flatMap(annotation -> parameterAnnotationProcessors.entrySet()
+              .stream()
+              .map(entry -> entry.getKey().isInstance(annotation)
+                  ? entry.getValue().process(annotation, data, paramIndex)
+                  : false))
+          .collect(Collectors.reducing(Boolean::logicalOr))
+          .orElse(false);
+    }
+
+    @FunctionalInterface
+    public interface ClassAnnotationProcessor {
+      /**
+       * Called by parseAndValidateMetadata twice, first on the declaring class, then on the target
+       * type (unless they are the same).
+       *
+       * @param annotation present on the current class
+       * @param metadata metadata collected so far relating to the current java method.
+       */
+      void process(E annotation, MethodMetadata metadata);
+    }
+
+    /**
+     * Called while class annotations are being processed
+     *
+     * @param annotation to be processed
+     * @param processor function that defines the annotations modifies {@link MethodMetadata}
+     */
+    protected  void registerClassAnnotation(Class annotation,
+                                                                  ClassAnnotationProcessor processor) {
+      this.classAnnotationProcessors.put((Class) annotation, (ClassAnnotationProcessor) processor);
+    }
+
+    @FunctionalInterface
+    public interface MethodAnnotationProcessor {
+      /**
+       * @param annotation present on the current method.
+       * @param metadata metadata collected so far relating to the current java method.
+       * @param method method currently being processed.
+       */
+      void process(E annotation, MethodMetadata metadata);
+    }
+
+    /**
+     * Called while method annotations are being processed
+     *
+     * @param annotation to be processed
+     * @param processor function that defines the annotations modifies {@link MethodMetadata}
+     */
+    protected  void registerMethodAnnotation(Class annotation,
+                                                                   MethodAnnotationProcessor processor) {
+      this.methodAnnotationProcessors.put((Class) annotation,
+          (MethodAnnotationProcessor) processor);
+    }
+
+    @FunctionalInterface
+    public interface ParameterAnnotationProcessor {
+      /**
+       * @param annotation present on the current parameter annotation.
+       * @param metadata metadata collected so far relating to the current java method.
+       * @param paramIndex if you find a name in {@code annotations}, call
+       *        {@link #nameParam(MethodMetadata, String, int)} with this as the last parameter.
+       * @return true if you called {@link #nameParam(MethodMetadata, String, int)} after finding an
+       *         http-relevant annotation.
+       */
+      boolean process(E annotation, MethodMetadata metadata, int paramIndex);
+    }
+
+    /**
+     * Called while method parameter annotations are being processed
+     *
+     * @param annotation to be processed
+     * @param processor function that defines the annotations modifies {@link MethodMetadata}
+     */
+    protected  void registerParameterAnnotation(Class annotation,
+                                                                      ParameterAnnotationProcessor processor) {
+      this.parameterAnnotationProcessors.put((Class) annotation,
+          (ParameterAnnotationProcessor) processor);
+    }
+
+    public Map, ClassAnnotationProcessor> getClassAnnotationProcessors() {
+      return Collections.unmodifiableMap(classAnnotationProcessors);
+    }
+
+  }
+
+  class Default extends VisitorContract {
+
+    static final Pattern REQUEST_LINE_PATTERN = Pattern.compile("^([A-Z]+)[ ]*(.*)$");
+
+    public Default() {
+      super.registerClassAnnotation(Headers.class, (header, data) -> {
+        String[] headersOnType = header.value();
         checkState(headersOnType.length > 0, "Headers annotation was empty on type %s.",
-            targetType.getName());
+            data.configKey());
         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) {
-      Class annotationType = methodAnnotation.annotationType();
-      if (annotationType == RequestLine.class) {
-        String requestLine = RequestLine.class.cast(methodAnnotation).value();
+      });
+      super.registerMethodAnnotation(RequestLine.class, (ann, data) -> {
+        String requestLine = ann.value();
         checkState(emptyToNull(requestLine) != null,
-            "RequestLine annotation was empty on method %s.", method.getName());
+            "RequestLine annotation was empty on method %s.", data.configKey());
 
         Matcher requestLineMatcher = REQUEST_LINE_PATTERN.matcher(requestLine);
         if (!requestLineMatcher.find()) {
           throw new IllegalStateException(String.format(
               "RequestLine annotation didn't start with an HTTP verb on method %s",
-              method.getName()));
+              data.configKey()));
         } else {
           data.template().method(HttpMethod.valueOf(requestLineMatcher.group(1)));
           data.template().uri(requestLineMatcher.group(2));
         }
-        data.template().decodeSlash(RequestLine.class.cast(methodAnnotation).decodeSlash());
+        data.template().decodeSlash(ann.decodeSlash());
         data.template()
-            .collectionFormat(RequestLine.class.cast(methodAnnotation).collectionFormat());
-
-      } else if (annotationType == Body.class) {
-        String body = Body.class.cast(methodAnnotation).value();
+            .collectionFormat(ann.collectionFormat());
+      });
+      super.registerMethodAnnotation(Body.class, (ann, data) -> {
+        String body = ann.value();
         checkState(emptyToNull(body) != null, "Body annotation was empty on method %s.",
-            method.getName());
+            data.configKey());
         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();
+      });
+      super.registerMethodAnnotation(Headers.class, (header, data) -> {
+        String[] headersOnMethod = header.value();
         checkState(headersOnMethod.length > 0, "Headers annotation was empty on method %s.",
-            method.getName());
+            data.configKey());
         data.template().headers(toMap(headersOnMethod));
-      }
-    }
-
-    @Override
-    protected boolean processAnnotationsOnParameter(MethodMetadata data,
-                                                    Annotation[] annotations,
-                                                    int paramIndex) {
-      boolean isHttpAnnotation = false;
-      for (Annotation annotation : annotations) {
-        Class annotationType = annotation.annotationType();
-        if (annotationType == Param.class) {
-          Param paramAnnotation = (Param) annotation;
-          String name = paramAnnotation.value();
-          checkState(emptyToNull(name) != null, "Param annotation was empty on param %s.",
-              paramIndex);
-          nameParam(data, name, paramIndex);
-          Class expander = paramAnnotation.expander();
-          if (expander != Param.ToStringExpander.class) {
-            data.indexToExpanderClass().put(paramIndex, expander);
-          }
-          data.indexToEncoded().put(paramIndex, paramAnnotation.encoded());
-          isHttpAnnotation = true;
-          if (!data.template().hasRequestVariable(name)) {
-            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;
+      });
+      super.registerParameterAnnotation(Param.class, (paramAnnotation, data, paramIndex) -> {
+        String name = paramAnnotation.value();
+        checkState(emptyToNull(name) != null, "Param annotation was empty on param %s.",
+            paramIndex);
+        nameParam(data, name, paramIndex);
+        Class expander = paramAnnotation.expander();
+        if (expander != Param.ToStringExpander.class) {
+          data.indexToExpanderClass().put(paramIndex, expander);
         }
-      }
-      return isHttpAnnotation;
+        data.indexToEncoded().put(paramIndex, paramAnnotation.encoded());
+        if (!data.template().hasRequestVariable(name)) {
+          data.formParams().add(name);
+        }
+        return true;
+      });
+      super.registerParameterAnnotation(QueryMap.class, (queryMap, data, paramIndex) -> {
+        checkState(data.queryMapIndex() == null,
+            "QueryMap annotation was present on multiple parameters.");
+        data.queryMapIndex(paramIndex);
+        data.queryMapEncoded(queryMap.encoded());
+        return true;
+      });
+      super.registerParameterAnnotation(HeaderMap.class, (queryMap, data, paramIndex) -> {
+        checkState(data.headerMapIndex() == null,
+            "HeaderMap annotation was present on multiple parameters.");
+        data.headerMapIndex(paramIndex);
+        return true;
+      });
     }
 
     private static Map> toMap(String[] input) {
@@ -319,5 +435,6 @@ private static Map> toMap(String[] input) {
       }
       return result;
     }
+
   }
 }
diff --git a/core/src/main/java/feign/Request.java b/core/src/main/java/feign/Request.java
index f5e2657032..609f84617a 100644
--- a/core/src/main/java/feign/Request.java
+++ b/core/src/main/java/feign/Request.java
@@ -230,6 +230,12 @@ public static class Options {
     private final int readTimeoutMillis;
     private final boolean followRedirects;
 
+    @Override
+    public String toString() {
+      return "Options [connectTimeoutMillis=" + connectTimeoutMillis + ", readTimeoutMillis="
+          + readTimeoutMillis + ", followRedirects=" + followRedirects + "]";
+    }
+
     public Options(int connectTimeoutMillis, int readTimeoutMillis, boolean followRedirects) {
       this.connectTimeoutMillis = connectTimeoutMillis;
       this.readTimeoutMillis = readTimeoutMillis;
diff --git a/core/src/main/java/feign/SynchronousMethodHandler.java b/core/src/main/java/feign/SynchronousMethodHandler.java
index 685c7a0f65..84f5de09f1 100644
--- a/core/src/main/java/feign/SynchronousMethodHandler.java
+++ b/core/src/main/java/feign/SynchronousMethodHandler.java
@@ -85,7 +85,7 @@ Object executeAndDecode(RequestTemplate template, Options options) throws Throwa
     Response response;
     long start = System.nanoTime();
     try {
-      response = feignConfig.client.execute(request, feignConfig.options);
+      response = feignConfig.client.execute(request, options);
     } catch (IOException e) {
       if (feignConfig.logLevel != Logger.Level.NONE) {
         feignConfig.logger.logIOException(metadata.configKey(), feignConfig.logLevel, e,
@@ -169,8 +169,9 @@ Options findOptions(Object[] argv) {
     if (argv == null || argv.length == 0) {
       return feignConfig.options;
     }
-    return (Options) Stream.of(argv)
-        .filter(o -> o instanceof Options)
+    return Stream.of(argv)
+        .filter(Options.class::isInstance)
+        .map(Options.class::cast)
         .findFirst()
         .orElse(feignConfig.options);
   }
diff --git a/core/src/test/java/feign/DefaultContractTest.java b/core/src/test/java/feign/DefaultContractTest.java
index de534cbd1a..e144f90f5f 100644
--- a/core/src/test/java/feign/DefaultContractTest.java
+++ b/core/src/test/java/feign/DefaultContractTest.java
@@ -814,7 +814,7 @@ interface MissingMethod {
   public void missingMethod() throws Exception {
     thrown.expect(IllegalStateException.class);
     thrown.expectMessage(
-        "RequestLine annotation didn't start with an HTTP verb on method updateSharing");
+        "RequestLine annotation didn't start with an HTTP verb on method MissingMethod#updateSharing");
 
     contract.parseAndValidatateMetadata(MissingMethod.class);
   }

From 917af49a3151e2008be6aaf4cb976b5bb3920a6d Mon Sep 17 00:00:00 2001
From: Marvin Froeder 
Date: Sun, 1 Sep 2019 22:00:46 +1200
Subject: [PATCH 5/9] APT generator generating MethodMetadata

---
 apt-generator/pom.xml                         | 339 ++++++++++--------
 .../feign/apt/runtime/ParameterizedType.java  |  89 +++++
 .../aptgenerator/APTContractVisitor.java      | 245 +++++++++++++
 .../java/feign/aptgenerator/ContractAPT.java  | 174 +++++++++
 .../MethodMetadataSerializer.java             | 108 ++++++
 .../aptgenerator/github/GitHubFactory.java    |  51 ++-
 .../aptgenerator/github/TypeReference.java    |  70 ----
 .../feign/aptgenerator/ContractAPTTest.java   |  55 +++
 .../MethodMetadataSerializerRepeatTest.java   |  95 +++++
 .../MethodMetadataSerializerTest.java         |  74 ++++
 core/src/main/java/feign/Contract.java        |  14 +-
 core/src/main/java/feign/Request.java         |   4 +
 core/src/main/java/feign/RequestTemplate.java |  12 +
 .../test/java/feign/DefaultContractTest.java  |  66 ++--
 14 files changed, 1132 insertions(+), 264 deletions(-)
 create mode 100644 apt-generator/src/main/java/feign/apt/runtime/ParameterizedType.java
 create mode 100644 apt-generator/src/main/java/feign/aptgenerator/APTContractVisitor.java
 create mode 100644 apt-generator/src/main/java/feign/aptgenerator/ContractAPT.java
 create mode 100644 apt-generator/src/main/java/feign/aptgenerator/MethodMetadataSerializer.java
 delete mode 100644 apt-generator/src/main/java/feign/aptgenerator/github/TypeReference.java
 create mode 100644 apt-generator/src/test/java/feign/aptgenerator/ContractAPTTest.java
 create mode 100644 apt-generator/src/test/java/feign/aptgenerator/MethodMetadataSerializerRepeatTest.java
 create mode 100644 apt-generator/src/test/java/feign/aptgenerator/MethodMetadataSerializerTest.java

diff --git a/apt-generator/pom.xml b/apt-generator/pom.xml
index ee6c4db4be..22d1188ed4 100644
--- a/apt-generator/pom.xml
+++ b/apt-generator/pom.xml
@@ -15,153 +15,210 @@
 
 -->
 
-  4.0.0
+    4.0.0
 
-  
-    io.github.openfeign
-    parent
-    10.3.0-SNAPSHOT
-  
+    
+        io.github.openfeign
+        parent
+        10.3.0-SNAPSHOT
+    
+
+    io.github.openfeign.experimental
+    feign-apt-generator
 
-  io.github.openfeign.experimental
-  feign-apt-generator
+    
+        ${project.basedir}/..
+    
 
-  
-    ${project.basedir}/..
-  
+    
+        
+            
+                io.github.openfeign
+                feign-bom
+                ${project.version}
+                pom
+                import
+            
+        
+    
 
-  
     
-      
-        io.github.openfeign
-        feign-bom
-        ${project.version}
-        pom
-        import
-      
+        
+            io.github.openfeign
+            feign-core
+        
+        
+            io.github.openfeign
+            feign-gson
+        
+        
+            io.github.openfeign
+            feign-example-github
+            ${project.version}
+        
+        
+            org.apache.commons
+            commons-text
+            1.3
+        
+
+        
+            org.apache.commons
+            commons-exec
+            1.3
+            test
+        
+        
+            com.google.testing.compile
+            compile-testing
+            0.18
+            test
+        
+        
+            com.google.guava
+            guava
+            28.0-jre
+        
+        
+            com.google.auto.service
+            auto-service
+            1.0-rc5
+            provided
+        
+
+        
+            org.hamcrest
+            hamcrest
+            2.1
+            test
+        
+
+        
+            org.codehaus.groovy
+            groovy-all
+            2.4.8
+            test
+        
+
+        
+            org.ccci
+            atlassian-hamcrest
+            1.1-SNAPSHOT
+            test
+        
+        
+            io.github.openfeign
+            feign-core
+            ${project.version}
+            test-jar
+            test
+        
     
-  
-
-  
-    
-      io.github.openfeign
-      feign-core
-    
-    
-      io.github.openfeign
-      feign-gson
-    
-    
-      io.github.openfeign
-      feign-example-github
-      ${project.version}
-    
-    
-      org.apache.commons
-      commons-exec
-      1.3
-      test
-    
-  
-
-  
-    
-      
-        docker
-        true
-        
-        ${project.basedir}/docker
-      
-      
-      
-        ${basedir}/src/main/resources
-      
-    
-
-    
-      
-        org.apache.maven.plugins
-        maven-shade-plugin
-        2.4.3
-        
-          
-            package
-            
-              shade
-            
-            
-              
-                
-                  feign.aptgenerator.github.GitHubFactoryExample
-                
-              
-              false
-            
-          
-        
-      
-      
-        org.skife.maven
-        really-executable-jar-maven-plugin
-        1.5.0
-        
-          github
-        
-        
-          
-            package
-            
-              really-executable-jar
-            
-          
-        
-      
-      
-        org.apache.maven.plugins
-        maven-failsafe-plugin
-        ${maven-surefire-plugin.version}
-        
-          
-            
-              integration-test
-              verify
-            
-          
-        
-      
-
-      
-        
-        com.spotify
-        docker-maven-plugin
-        
-          
-          ${project.build.directory}/classes/docker/
-
-          
-          true
-
-          docker-hub
-          https://index.docker.io/v1/
-          feign-apt-generator/test
-          
+
+    
+        
             
-              /
-              ${project.build.directory}
-              ${project.artifactId}-${project.version}.jar
+                docker
+                true
+                
+                ${project.basedir}/docker
             
-          
-        
-        
-          
-          
-            post-integration-test
-            
-              build
-            
-          
-        
-      
-    
-  
+            
+            
+                ${basedir}/src/main/resources
+            
+            
+                src/main/java
+                
+                    **/*.java
+                
+            
+        
+
+        
+            
+                org.apache.maven.plugins
+                maven-shade-plugin
+                2.4.3
+                
+                    
+                        package
+                        
+                            shade
+                        
+                        
+                            
+                                
+                                    feign.aptgenerator.github.GitHubFactoryExample
+                                
+                            
+                            false
+                        
+                    
+                
+            
+            
+                org.skife.maven
+                really-executable-jar-maven-plugin
+                1.5.0
+                
+                    github
+                
+                
+                    
+                        package
+                        
+                            really-executable-jar
+                        
+                    
+                
+            
+            
+                org.apache.maven.plugins
+                maven-failsafe-plugin
+                ${maven-surefire-plugin.version}
+                
+                    
+                        
+                            integration-test
+                            verify
+                        
+                    
+                
+            
+
+            
+                
+                com.spotify
+                docker-maven-plugin
+                
+                    
+                    ${project.build.directory}/classes/docker/
+
+                    
+                    true
+
+                    docker-hub
+                    https://index.docker.io/v1/
+                    feign-apt-generator/test
+                    
+                        
+                            /
+                            ${project.build.directory}
+                            ${project.artifactId}-${project.version}.jar
+                        
+                    
+                
+                
+                    
+                    
+                        post-integration-test
+                        
+                            build
+                        
+                    
+                
+            
+        
+    
 
diff --git a/apt-generator/src/main/java/feign/apt/runtime/ParameterizedType.java b/apt-generator/src/main/java/feign/apt/runtime/ParameterizedType.java
new file mode 100644
index 0000000000..d2eedce592
--- /dev/null
+++ b/apt-generator/src/main/java/feign/apt/runtime/ParameterizedType.java
@@ -0,0 +1,89 @@
+/**
+ * Copyright 2012-2019 The Feign Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package feign.apt.runtime;
+
+import java.lang.reflect.Type;
+import java.util.Arrays;
+
+public class ParameterizedType implements java.lang.reflect.ParameterizedType {
+
+  private final Type ownerType;
+  private final Type rawType;
+  private final Type[] typedArgs;
+
+  public ParameterizedType(Type rawType, Type ownerType, Type... typedArgs) {
+    super();
+    this.ownerType = ownerType;
+    this.rawType = rawType;
+    this.typedArgs = typedArgs;
+  }
+
+  @Override
+  public Type[] getActualTypeArguments() {
+    return this.typedArgs;
+  }
+
+  @Override
+  public Type getRawType() {
+    return this.rawType;
+  }
+
+  @Override
+  public Type getOwnerType() {
+    return this.ownerType;
+  }
+
+  @Override
+  public int hashCode() {
+    final int prime = 31;
+    int result = 1;
+    result = prime * result + ((getOwnerType() == null) ? 0 : getOwnerType().hashCode());
+    result = prime * result + ((getRawType() == null) ? 0 : getRawType().hashCode());
+    result = prime * result + Arrays.hashCode(getActualTypeArguments());
+    return result;
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (this == obj) {
+      return true;
+    }
+    if (obj == null) {
+      return false;
+    }
+    if (!(obj instanceof java.lang.reflect.ParameterizedType)) {
+      return false;
+    }
+    final java.lang.reflect.ParameterizedType other = (java.lang.reflect.ParameterizedType) obj;
+    if (getOwnerType() == null) {
+      if (other.getOwnerType() != null) {
+        return false;
+      }
+    } else if (!getOwnerType().equals(other.getOwnerType())) {
+      return false;
+    }
+    if (getRawType() == null) {
+      if (other.getRawType() != null) {
+        return false;
+      }
+    } else if (!getRawType().equals(other.getRawType())) {
+      return false;
+    }
+    if (!Arrays.equals(getActualTypeArguments(), other.getActualTypeArguments())) {
+      return false;
+    }
+    return true;
+  }
+
+}
diff --git a/apt-generator/src/main/java/feign/aptgenerator/APTContractVisitor.java b/apt-generator/src/main/java/feign/aptgenerator/APTContractVisitor.java
new file mode 100644
index 0000000000..77d391c70c
--- /dev/null
+++ b/apt-generator/src/main/java/feign/aptgenerator/APTContractVisitor.java
@@ -0,0 +1,245 @@
+/**
+ * Copyright 2012-2019 The Feign Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package feign.aptgenerator;
+
+import static feign.Util.checkState;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.net.URI;
+import java.util.*;
+import java.util.stream.Collectors;
+import javax.lang.model.element.*;
+import feign.Contract.VisitorContract;
+import feign.Contract.VisitorContract.ClassAnnotationProcessor;
+import feign.Contract.VisitorContract.MethodAnnotationProcessor;
+import feign.Contract.VisitorContract.ParameterAnnotationProcessor;
+import feign.MethodMetadata;
+
+/**
+ * Defines what annotations and values are valid on interfaces.
+ */
+public class APTContractVisitor {
+
+  public List parseAndValidateMetadata(TypeElement targetType,
+                                                       VisitorContract processorsSource) {
+    checkState(targetType.getTypeParameters().size() == 0, "Parameterized types unsupported: %s",
+        targetType.getSimpleName());
+    checkState(targetType.getInterfaces().size() <= 1, "Only single inheritance supported: %s",
+        targetType.getSimpleName());
+
+    final Map result = new LinkedHashMap();
+    for (final Element element : targetType.getEnclosedElements()) {
+      if (element instanceof ExecutableElement) {
+        final ExecutableElement method = (ExecutableElement) element;
+        if (method.getModifiers().contains(Modifier.STATIC) ||
+            method.isDefault()) {
+          continue;
+        }
+        final MethodMetadata metadata =
+            parseAndValidateMetadata(targetType, method, processorsSource);
+        checkState(!result.containsKey(metadata.configKey()), "Overrides unsupported: %s",
+            metadata.configKey());
+        result.put(metadata.configKey(), metadata);
+      }
+    }
+    return new ArrayList<>(result.values());
+  }
+
+
+  /**
+   * Called indirectly by {@link #parseAndValidateMetadata(Class)}.
+   */
+  protected MethodMetadata parseAndValidateMetadata(TypeElement targetType,
+                                                    ExecutableElement method,
+                                                    VisitorContract processorsSource) {
+    final MethodMetadata data = new MethodMetadata();
+    // TODO create warping type data.returnType(method.getReturnType());
+    data.configKey(configKey(targetType, method));
+
+    // TODO if (targetType.getInterfaces().size() == 1) {
+    // processAnnotationOnClass(data, targetType.getInterfaces().get(0), processorsSource);
+    // }
+    processAnnotationOnClass(data, targetType, processorsSource);
+
+    processAnnotationOnMethod(data, method, processorsSource);
+    checkState(data.template().method() != null,
+        "Method %s not annotated with HTTP method type (ex. GET, POST)",
+        data.configKey());
+    final List parameterTypes = method.getParameters();
+
+    final int count = parameterTypes.size();
+    for (int i = 0; i < count; i++) {
+      boolean isHttpAnnotation = false;
+      if (!parameterTypes.get(i).getAnnotationMirrors().isEmpty()) {
+        isHttpAnnotation =
+            processAnnotationsOnParameter(data, parameterTypes.get(i), i, processorsSource);
+      }
+      if (
+      // TODO this probably do not work as intended
+      parameterTypes.get(i).asType().toString() == URI.class.getName()) {
+        data.urlIndex(i);
+      } else if (!isHttpAnnotation
+      // TODO && parameterTypes[i] != Request.Options.class
+      ) {
+        checkState(data.formParams().isEmpty(),
+            "Body parameters cannot be used with form parameters.");
+        checkState(data.bodyIndex() == null, "Method has too many Body parameters: %s", method);
+        data.bodyIndex(i);
+        // data.bodyType(Types.resolve(targetType, targetType, genericParameterTypes[i]));
+      }
+    }
+
+    if (data.headerMapIndex() != null) {
+      // TODO how to check if one param is a Map on APT?
+      // checkMapString("HeaderMap", parameterTypes[data.headerMapIndex()],
+      // genericParameterTypes[data.headerMapIndex()]);
+    }
+
+    if (data.queryMapIndex() != null) {
+      // TODO how to check if one param is a Map on APT?
+      // if (Map.class.isAssignableFrom(parameterTypes[data.queryMapIndex()])) {
+      // checkMapKeys("QueryMap", genericParameterTypes[data.queryMapIndex()]);
+      // }
+    }
+
+    return data;
+  }
+
+  private String configKey(TypeElement targetType, ExecutableElement method) {
+    final StringBuilder builder = new StringBuilder();
+    builder.append(targetType.getSimpleName());
+    builder.append('#').append(method.getSimpleName()).append('(');
+    for (final TypeParameterElement param : method.getTypeParameters()) {
+      builder.append(param.getSimpleName()).append(',');
+    }
+    if (method.getTypeParameters().size() > 0) {
+      builder.deleteCharAt(builder.length() - 1);
+    }
+    return builder.append(')').toString();
+  }
+
+  private static void checkMapString(String name, Class type, Type genericType) {
+    checkState(Map.class.isAssignableFrom(type),
+        "%s parameter must be a Map: %s", name, type);
+    checkMapKeys(name, genericType);
+  }
+
+  private static void checkMapKeys(String name, Type genericType) {
+    Class keyClass = null;
+
+    // assume our type parameterized
+    if (ParameterizedType.class.isAssignableFrom(genericType.getClass())) {
+      final Type[] parameterTypes = ((ParameterizedType) genericType).getActualTypeArguments();
+      keyClass = (Class) parameterTypes[0];
+    } else if (genericType instanceof Class) {
+      // raw class, type parameters cannot be inferred directly, but we can scan any extended
+      // interfaces looking for any explict types
+      final Type[] interfaces = ((Class) genericType).getGenericInterfaces();
+      if (interfaces != null) {
+        for (final Type extended : interfaces) {
+          if (ParameterizedType.class.isAssignableFrom(extended.getClass())) {
+            // use the first extended interface we find.
+            final Type[] parameterTypes = ((ParameterizedType) extended).getActualTypeArguments();
+            keyClass = (Class) parameterTypes[0];
+            break;
+          }
+        }
+      }
+    }
+
+    if (keyClass != null) {
+      checkState(String.class.equals(keyClass),
+          "%s key must be a String: %s", name, keyClass.getSimpleName());
+    }
+  }
+
+  /**
+   * Called by parseAndValidateMetadata twice, first on the declaring class, then on the target type
+   * (unless they are the same).
+   *
+   * @param data metadata collected so far relating to the current java method.
+   * @param processorsSource
+   * @param clz the class to process
+   */
+  protected void processAnnotationOnClass(MethodMetadata data,
+                                          TypeElement targetType,
+                                          VisitorContract processorsSource) {
+    final Map, ClassAnnotationProcessor> annotations =
+        processorsSource.getClassAnnotationProcessors();
+    annotations.forEach((type, processor) -> {
+      final Annotation annotation = targetType.getAnnotation(type);
+      if (annotation != null) {
+        processor.process(annotation, data);
+      }
+    });
+  }
+
+  /**
+   * @param data metadata collected so far relating to the current java method.
+   * @param method method currently being processed.
+   * @param processorsSource
+   */
+  private void processAnnotationOnMethod(MethodMetadata data,
+                                         ExecutableElement method,
+                                         VisitorContract processorsSource) {
+    final Map, MethodAnnotationProcessor> annotations =
+        processorsSource.getMethodAnnotationProcessors();
+    annotations.forEach((type, processor) -> {
+      final Annotation annotation = method.getAnnotation(type);
+      if (annotation != null) {
+        processor.process(annotation, data);
+      }
+    });
+  }
+
+  /**
+   * @param data metadata collected so far relating to the current java method.
+   * @param annotations annotations present on the current parameter annotation.
+   * @param paramIndex if you find a name in {@code annotations}, call
+   *        {@link #nameParam(MethodMetadata, String, int)} with this as the last parameter.
+   * @return true if you called {@link #nameParam(MethodMetadata, String, int)} after finding an
+   *         http-relevant annotation.
+   */
+  private boolean processAnnotationsOnParameter(MethodMetadata data,
+                                                VariableElement parameter,
+                                                int paramIndex,
+                                                VisitorContract processorsSource) {
+    final Map, ParameterAnnotationProcessor> annotations =
+        processorsSource.getParameterAnnotationProcessors();
+
+    return annotations.entrySet()
+        .stream()
+        .map(entry -> {
+          final Annotation annotation = parameter.getAnnotation(entry.getKey());
+          if (annotation != null) {
+            return entry.getValue().process(annotation, data, paramIndex);
+          }
+          return false;
+        })
+        .collect(Collectors.reducing(Boolean::logicalOr))
+        .orElse(false);
+  }
+
+  /**
+   * links a parameter name to its index in the method signature.
+   */
+  protected void nameParam(MethodMetadata data, String name, int i) {
+    final Collection names =
+        data.indexToName().containsKey(i) ? data.indexToName().get(i) : new ArrayList();
+    names.add(name);
+    data.indexToName().put(i, names);
+  }
+
+}
diff --git a/apt-generator/src/main/java/feign/aptgenerator/ContractAPT.java b/apt-generator/src/main/java/feign/aptgenerator/ContractAPT.java
new file mode 100644
index 0000000000..25016d3154
--- /dev/null
+++ b/apt-generator/src/main/java/feign/aptgenerator/ContractAPT.java
@@ -0,0 +1,174 @@
+/**
+ * Copyright 2012-2019 The Feign Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package feign.aptgenerator;
+
+import static feign.Util.checkState;
+import static feign.Util.emptyToNull;
+import com.google.auto.service.AutoService;
+import com.google.common.collect.ImmutableList;
+import com.google.common.io.ByteStreams;
+import java.lang.reflect.Type;
+import java.util.*;
+import java.util.stream.Collectors;
+import javax.annotation.processing.*;
+import javax.lang.model.SourceVersion;
+import javax.lang.model.element.*;
+import javax.lang.model.type.TypeMirror;
+import javax.lang.model.type.WildcardType;
+import javax.tools.Diagnostic.Kind;
+import javax.tools.JavaFileObject;
+import feign.Contract;
+import feign.MethodMetadata;
+import feign.Param;
+
+@SupportedAnnotationTypes({
+    "feign.RequestLine"
+})
+@SupportedSourceVersion(SourceVersion.RELEASE_8)
+@AutoService(Processor.class)
+public class ContractAPT extends AbstractProcessor {
+
+  @Override
+  public boolean process(Set annotations, RoundEnvironment roundEnv) {
+    System.out.println(annotations);
+    System.out.println(roundEnv);
+
+    final Map> clientsToGenerate = annotations.stream()
+        .map(roundEnv::getElementsAnnotatedWith)
+        .flatMap(Set::stream)
+        .map(ExecutableElement.class::cast)
+        .collect(Collectors.toMap(
+            annotatedMethod -> TypeElement.class.cast(annotatedMethod.getEnclosingElement()),
+            ImmutableList::of,
+            (list1, list2) -> ImmutableList.builder()
+                .addAll(list1)
+                .addAll(list2)
+                .build()));
+
+    System.out.println("Count: " + clientsToGenerate.size());
+    System.out.println("clientsToGenerate: " + clientsToGenerate);
+
+    final Contract.Default contract = new Contract.Default();
+    contract.registerParameterAnnotation(Param.class, (paramAnnotation, data, paramIndex) -> {
+      final String name = paramAnnotation.value();
+      checkState(emptyToNull(name) != null, "Param annotation was empty on param %s.",
+          paramIndex);
+      contract.nameParam(data, name, paramIndex);
+      // FIXME exception "Attempt to access Class object for TypeMirror"
+      // Class expander = paramAnnotation.expander();
+      // if (expander != Param.ToStringExpander.class) {
+      // data.indexToExpanderClass().put(paramIndex, expander);
+      // }
+      data.indexToEncoded().put(paramIndex, paramAnnotation.encoded());
+      if (!data.template().hasRequestVariable(name)) {
+        data.formParams().add(name);
+      }
+      return true;
+    });
+
+    clientsToGenerate.forEach((type, methods) -> {
+      try {
+        final String jPackage = readPackage(type);
+        final JavaFileObject builderFile = processingEnv.getFiler()
+            .createSourceFile(type.getSimpleName() + "Factory");
+        final StringBuilder writer = new StringBuilder();
+        writer.append("package " + jPackage + ";").append("\n");
+        writer.append("import feign.MethodMetadata;").append("\n");
+        writer.append("public class " + type.getSimpleName() + "Factory").append("\n");
+        writer.append("{").append("\n");
+
+        final List methodMetadatas =
+            new APTContractVisitor().parseAndValidateMetadata(type, contract);
+
+        methodMetadatas.stream()
+            .peek(
+                method -> System.out.println("Generating metadata for method" + method.configKey()))
+            .forEach(method -> {
+              final String metadataFieldName =
+                  "__" + method.configKey().replaceAll("\\W+", "_") + "_metadata";
+          writer
+                  .append(
+                      "private static final MethodMetadata " + metadataFieldName + ";")
+                  .append("\n");
+              writer
+                  .append("static { ").append("\n")
+                  .append(MethodMetadataSerializer.toJavaCode(method)).append("\n")
+                  .append(metadataFieldName + " = md;").append("\n")
+                  .append("}").append("\n");
+        });
+
+        writer.append("}").append("\n");
+
+        System.out.println(writer);
+
+        builderFile.openWriter().append(writer).close();
+      } catch (final Exception e) {
+        e.printStackTrace();
+        processingEnv.getMessager().printMessage(Kind.ERROR,
+            "Unable to generate factory for " + type);
+      }
+    });
+
+    if (!clientsToGenerate.isEmpty()) {
+      try {
+        final JavaFileObject builderFile = processingEnv.getFiler()
+            .createSourceFile("feign.apt.runtime.ParameterizedType");
+        ByteStreams.copy(
+            getClass().getResourceAsStream("/feign/apt/runtime/ParameterizedType.java"),
+            builderFile.openOutputStream());
+      } catch (final Exception e) {
+        e.printStackTrace();
+        processingEnv.getMessager().printMessage(Kind.ERROR,
+            "Unable to write ParameterizedType.java");
+      }
+    }
+
+    return true;
+  }
+
+
+
+  private Type toJavaType(TypeMirror type) {
+    outType(type.getClass());
+    if (type instanceof WildcardType) {
+
+    }
+    return Object.class;
+  }
+
+  private void outType(Class class1) {
+    if (Object.class.equals(class1) || class1 == null) {
+      return;
+    }
+    System.out.println(class1);
+    outType(class1.getSuperclass());
+    Arrays.stream(class1.getInterfaces()).forEach(this::outType);
+  }
+
+
+
+  private String readPackage(Element type) {
+    if (type.getKind() == ElementKind.PACKAGE) {
+      return type.getSimpleName().toString();
+    }
+
+    if (type.getKind() == ElementKind.CLASS
+        || type.getKind() == ElementKind.INTERFACE) {
+      return readPackage(type.getEnclosingElement());
+    }
+
+    return null;
+  }
+
+}
diff --git a/apt-generator/src/main/java/feign/aptgenerator/MethodMetadataSerializer.java b/apt-generator/src/main/java/feign/aptgenerator/MethodMetadataSerializer.java
new file mode 100644
index 0000000000..d70c7dd086
--- /dev/null
+++ b/apt-generator/src/main/java/feign/aptgenerator/MethodMetadataSerializer.java
@@ -0,0 +1,108 @@
+/**
+ * Copyright 2012-2019 The Feign Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package feign.aptgenerator;
+
+import org.apache.commons.text.StringEscapeUtils;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.Arrays;
+import java.util.stream.Collectors;
+import feign.MethodMetadata;
+
+public class MethodMetadataSerializer {
+
+  public static StringBuilder toJavaCode(MethodMetadata method) {
+    final StringBuilder sb = new StringBuilder();
+
+    sb.append("    final MethodMetadata md = new MethodMetadata();").append("\n");
+    sb.append("    md.returnType(" + toJavaCode(method.returnType()) + ");").append("\n");
+    sb.append("    md.configKey(\"" + method.configKey() + "\");").append("\n");
+    if (method.bodyIndex() != null) {
+      sb.append("    md.bodyIndex(" + method.bodyIndex() + ");").append("\n");
+    }
+    sb.append("\n");
+    sb.append(
+        "    md.template().method(feign.Request.HttpMethod." + method.template().method() + ");")
+        .append("\n");
+    sb.append("    md.template().uri(\"" + method.template().uri() + "\", "+method.template().uriAppend()+");")
+        .append("\n");
+    sb.append("    md.template().decodeSlash(" + method.template().decodeSlash() + ");")
+        .append("\n");
+    sb.append("    md.template().collectionFormat(CollectionFormat."
+        + method.template().collectionFormat().name() + ");")
+        .append("\n");
+    method.template().headers().forEach((headerName, value) -> sb
+        .append("    md.template().header(\"" + headerName + "\", "
+            + value.stream().collect(Collectors.joining("\",\"", "\"", "\"")) + ");"));
+    if (method.template().requestBody().asBytes() != null) {
+      sb.append("    md.template().body(\""
+          + method.template().requestBody().asString()
+          + "\");")
+          .append("\n");
+    }
+    if (method.template().requestBody().bodyTemplate() != null) {
+      sb.append("    md.template().body(feign.Request.Body.bodyTemplate(\""
+          + StringEscapeUtils.escapeJava(method.template().requestBody().bodyTemplate())
+          + "\", java.nio.charset.Charset.forName(\""
+          + method.template().requestBody().encoding().name() + "\")"
+          + "));")
+          .append("\n");
+    }
+
+
+    sb.append("\n");
+    method.formParams().forEach(formParam -> sb
+        .append("    md.formParams().add(\"" + formParam + "\");"));
+
+    method.indexToName().forEach((index, valueList) -> sb
+        .append("    md.indexToName().put(" + index + ", Arrays.asList("
+            + valueList.stream().collect(Collectors.joining("\",\"", "\"", "\"")) + "));")
+        .append("\n"));
+    if (method.indexToExpander() != null) {
+      method.indexToExpander().forEach((index, expander) -> sb
+          .append("    md.indexToExpander().put(" + index + ", new " + expander.getClass().getName()            + "());")
+          .append("\n"));
+    }
+    method.indexToEncoded().forEach((index, encoded) -> sb
+        .append("    md.indexToEncoded().put(" + index + ", " + encoded + ");")
+        .append("\n"));
+    if (method.bodyType() != null) {
+      sb.append("    md.bodyType(" + toJavaCode(method.bodyType()) + ");").append("\n");
+    }
+
+    return sb;
+  }
+
+  private static String toJavaCode(Type type) {
+    if (type == null) {
+      return null;
+    }
+
+    if (type instanceof Class) {
+      return ((Class) type).getName() + ".class";
+    }
+
+    if (type instanceof ParameterizedType) {
+      return "new feign.apt.runtime.ParameterizedType(" +
+          toJavaCode(((ParameterizedType) type).getRawType()) + ", " +
+          toJavaCode(((ParameterizedType) type).getOwnerType()) + ", " +
+          Arrays.stream(((ParameterizedType) type).getActualTypeArguments())
+              .map(MethodMetadataSerializer::toJavaCode)
+              .collect(Collectors.joining(","))
+          + ")";
+    }
+    throw new RuntimeException("not implemented " + type.getClass());
+  }
+
+}
diff --git a/apt-generator/src/main/java/feign/aptgenerator/github/GitHubFactory.java b/apt-generator/src/main/java/feign/aptgenerator/github/GitHubFactory.java
index e1d30c67e3..af0a6ea2e4 100644
--- a/apt-generator/src/main/java/feign/aptgenerator/github/GitHubFactory.java
+++ b/apt-generator/src/main/java/feign/aptgenerator/github/GitHubFactory.java
@@ -13,24 +13,40 @@
  */
 package feign.aptgenerator.github;
 
+import java.lang.reflect.Type;
 import java.util.Arrays;
 import java.util.List;
 import example.github.Contributor;
-import example.github.GitHubExample.*;
+import example.github.GitHubExample.GitHub;
 import example.github.Issue;
 import example.github.Repository;
 import feign.*;
 import feign.InvocationHandlerFactory.MethodHandler;
 import feign.Request.HttpMethod;
 import feign.Target.HardCodedTarget;
+import feign.apt.runtime.ParameterizedType;
 
 public class GitHubFactory implements GitHub {
 
-  private static final MethodMetadata __repos_metadata;
+  private static abstract class TypeResolver {
+    private final Type type;
+
+    private TypeResolver() {
+      this.type =
+          ((java.lang.reflect.ParameterizedType) getClass().getGenericSuperclass())
+              .getActualTypeArguments()[0];
+    }
+
+    public Type resolve() {
+      return type;
+    }
+
+  }
+
+  private static final MethodMetadata __GitHub_repos__metadata;
   static {
     final MethodMetadata md = new MethodMetadata();
-    __repos_metadata = md;
-    md.returnType(new TypeReference>() {}.getType());
+    md.returnType(new ParameterizedType(List.class, null, Repository.class));
     md.configKey("GitHub#repos(String)");
 
     md.template().method(HttpMethod.GET);
@@ -40,13 +56,14 @@ public class GitHubFactory implements GitHub {
 
     md.indexToName().put(0, Arrays.asList("username"));
     md.indexToEncoded().put(0, false);
+    __GitHub_repos__metadata = md;
   }
 
-  private static final MethodMetadata __contributors_metadata;
+  private static final MethodMetadata __GitHub_contributors__metadata;
   static {
     final MethodMetadata md = new MethodMetadata();
-    __contributors_metadata = md;
-    md.returnType(new TypeReference>() {}.getType());
+    __GitHub_contributors__metadata = md;
+    md.returnType(new ParameterizedType(List.class, null, Contributor.class));
     md.configKey("GitHub#contributors(String,String)");
 
     md.template().method(HttpMethod.GET);
@@ -61,11 +78,11 @@ public class GitHubFactory implements GitHub {
     md.indexToEncoded().put(1, false);
   }
 
-  private static final MethodMetadata __createIssue_metadata;
+  private static final MethodMetadata __GitHub_createIssue__metadata;
   static {
     final MethodMetadata md = new MethodMetadata();
-    __createIssue_metadata = md;
-    md.returnType(new TypeReference>() {}.getType());
+    __GitHub_createIssue__metadata = md;
+    md.returnType(void.class);
     md.configKey("GitHub#createIssue(Issue,String,String)");
 
     md.template().method(HttpMethod.POST);
@@ -82,7 +99,7 @@ public class GitHubFactory implements GitHub {
 
   private final MethodHandler __repos_handler;
   private final MethodHandler __contributors_handler;
-  private SynchronousMethodHandler __createIssue_handler;
+  private final SynchronousMethodHandler __createIssue_handler;
 
   public GitHubFactory(FeignConfig feignConfig) {
     final HardCodedTarget target =
@@ -91,20 +108,20 @@ public GitHubFactory(FeignConfig feignConfig) {
     __repos_handler = new SynchronousMethodHandler(
         target,
         feignConfig,
-        __repos_metadata,
-        ReflectiveFeign.from(__repos_metadata, feignConfig));
+        __GitHub_repos__metadata,
+        ReflectiveFeign.from(__GitHub_repos__metadata, feignConfig));
 
     __contributors_handler = new SynchronousMethodHandler(
         target,
         feignConfig,
-        __contributors_metadata,
-        ReflectiveFeign.from(__contributors_metadata, feignConfig));
+        __GitHub_contributors__metadata,
+        ReflectiveFeign.from(__GitHub_contributors__metadata, feignConfig));
 
     __createIssue_handler = new SynchronousMethodHandler(
         target,
         feignConfig,
-        __contributors_metadata,
-        ReflectiveFeign.from(__contributors_metadata, feignConfig));
+        __GitHub_contributors__metadata,
+        ReflectiveFeign.from(__GitHub_contributors__metadata, feignConfig));
 
   }
 
diff --git a/apt-generator/src/main/java/feign/aptgenerator/github/TypeReference.java b/apt-generator/src/main/java/feign/aptgenerator/github/TypeReference.java
deleted file mode 100644
index de99aefd09..0000000000
--- a/apt-generator/src/main/java/feign/aptgenerator/github/TypeReference.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/**
- * Copyright 2012-2019 The Feign Authors
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
- * in compliance with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under the License
- * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
- * or implied. See the License for the specific language governing permissions and limitations under
- * the License.
- */
-package feign.aptgenerator.github;
-
-import java.lang.reflect.ParameterizedType;
-import java.lang.reflect.Type;
-
-/**
- * This generic abstract class is used for obtaining full generics type information by sub-classing;
- * it must be converted to {@link ResolvedType} implementation (implemented by JavaType
- * from "databind" bundle) to be used. Class is based on ideas from
- * http://gafter.blogspot.com/2006/12/super-type-tokens.html, Additional idea (from a
- * suggestion made in comments of the article) is to require bogus implementation of
- * Comparable (any such generic interface would do, as long as it forces a method with
- * generic type to be implemented). to ensure that a Type argument is indeed given.
- * 

- * Usage is by sub-classing: here is one way to instantiate reference to generic type - * List<Integer>: - * - *

- * TypeReference ref = new TypeReference<List<Integer>>() {};
- * 
- * - * which can be passed to methods that accept TypeReference, or resolved using - * TypeFactory to obtain {@link ResolvedType}. - */ -public abstract class TypeReference implements Comparable> { - protected final Type _type; - - protected TypeReference() { - final Type superClass = getClass().getGenericSuperclass(); - if (superClass instanceof Class) { // sanity check, should never happen - throw new IllegalArgumentException( - "Internal error: TypeReference constructed without actual type information"); - } - /* - * 22-Dec-2008, tatu: Not sure if this case is safe -- I suspect it is possible to make it fail? - * But let's deal with specific case when we know an actual use case, and thereby suitable - * workarounds for valid case(s) and/or error to throw on invalid one(s). - */ - _type = ((ParameterizedType) superClass).getActualTypeArguments()[0]; - } - - public Type getType() { - return _type; - } - - /** - * The only reason we define this method (and require implementation of Comparable) - * is to prevent constructing a reference without type information. - */ - @Override - public int compareTo(TypeReference o) { - return 0; - } - // just need an implementation, not a good one... hence ^^^ -} - diff --git a/apt-generator/src/test/java/feign/aptgenerator/ContractAPTTest.java b/apt-generator/src/test/java/feign/aptgenerator/ContractAPTTest.java new file mode 100644 index 0000000000..fa2c91b571 --- /dev/null +++ b/apt-generator/src/test/java/feign/aptgenerator/ContractAPTTest.java @@ -0,0 +1,55 @@ +/** + * Copyright 2012-2019 The Feign Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package feign.aptgenerator; + +import static com.google.testing.compile.CompilationSubject.assertThat; +import static com.google.testing.compile.Compiler.javac; +import com.google.testing.compile.Compilation; +import com.google.testing.compile.JavaFileObjects; +import org.junit.Test; +import java.io.File; + +/** + * Test for {@link ContractAPT} + */ +public class ContractAPTTest { + + private final File main = new File("../example-github/src/main/java/").getAbsoluteFile(); + + @Test + public void test() throws Exception { + final Compilation compilation = + javac() + .withProcessors(new ContractAPT()) + .compile(JavaFileObjects.forResource( + new File(main, "example/github/GitHubExample.java") + .toURI() + .toURL())); + assertThat(compilation).succeeded(); + assertThat(compilation) + .generatedSourceFile("feign.apt.runtime.ParameterizedType") + .hasSourceEquivalentTo(JavaFileObjects.forResource( + new File(main, "feign/apt/runtime/ParameterizedType.java") + .toURI() + .toURL())); + assertThat(compilation) + .generatedSourceFile("GitHubFactory") + .hasSourceEquivalentTo(JavaFileObjects.forResource( + new File("src/main/java/feign/aptgenerator/github/GitHubFactory.java") + .toURI() + .toURL() + )); + } + +} diff --git a/apt-generator/src/test/java/feign/aptgenerator/MethodMetadataSerializerRepeatTest.java b/apt-generator/src/test/java/feign/aptgenerator/MethodMetadataSerializerRepeatTest.java new file mode 100644 index 0000000000..5f129d020c --- /dev/null +++ b/apt-generator/src/test/java/feign/aptgenerator/MethodMetadataSerializerRepeatTest.java @@ -0,0 +1,95 @@ +/** + * Copyright 2012-2019 The Feign Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package feign.aptgenerator; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.sameInstance; +import com.atlassian.hamcrest.DeepIsEqual; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import example.github.GitHubExample.GitHub; +import feign.Contract; +import feign.DefaultContractTest; +import feign.MethodMetadata; +import groovy.lang.Binding; +import groovy.lang.GroovyShell; + + +/** + * Check if the generated java code generates a new {@link MethodMetadata} that is equal to source + * {@link MethodMetadata} + */ +@RunWith(Parameterized.class) +public class MethodMetadataSerializerRepeatTest { + + private static final GroovyShell shell = new GroovyShell(new Binding()); + + @Parameters(name = "{0}") + public static List methods() { + return Arrays.asList( + GitHub.class, + // TODO ideally we need to test all interfaces from DefaultContractTest + DefaultContractTest.BodyWithoutParameters.class, + // expanders not supported + // DefaultContractTest.CustomExpander.class, + DefaultContractTest.CustomMethod.class, + DefaultContractTest.DefaultMethodOnInterface.class, + DefaultContractTest.FormParams.class, + DefaultContractTest.HeaderParams.class, + DefaultContractTest.HeaderParamsNotAtStart.class, + DefaultContractTest.HeadersContainsWhitespaces.class, + DefaultContractTest.HeadersOnType.class, + DefaultContractTest.Methods.class + ) + .stream() + // create methodmetadata using default reflective code + .map(new Contract.Default()::parseAndValidatateMetadata) + .flatMap(List::stream) + .map(md -> new Object[] {md.configKey(), md}) + .collect(Collectors.toList()); + } + + private final MethodMetadata md; + + public MethodMetadataSerializerRepeatTest(String configKey, MethodMetadata md) { + this.md = md; + } + + @Test + public void serializedSameAsReflective() { + + final StringBuilder result = MethodMetadataSerializer.toJavaCode(md); + + final MethodMetadata resultMetadata = (MethodMetadata) shell.evaluate("" + + "import feign.*;\n" + + result.toString() + + "\nreturn md;"); + + assertThat(resultMetadata, not(sameInstance(md))); + assertThat(resultMetadata, DeepIsEqual.deeplyEqualTo(md)); + // compare transient fields too + assertThat(resultMetadata.indexToExpander(), equalTo(md.indexToExpander())); + assertThat(resultMetadata.returnType(), equalTo(md.returnType())); + assertThat(resultMetadata.bodyType(), equalTo(md.bodyType())); + + } + +} diff --git a/apt-generator/src/test/java/feign/aptgenerator/MethodMetadataSerializerTest.java b/apt-generator/src/test/java/feign/aptgenerator/MethodMetadataSerializerTest.java new file mode 100644 index 0000000000..5b85fe8ed5 --- /dev/null +++ b/apt-generator/src/test/java/feign/aptgenerator/MethodMetadataSerializerTest.java @@ -0,0 +1,74 @@ +/** + * Copyright 2012-2019 The Feign Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package feign.aptgenerator; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import com.atlassian.hamcrest.DeepIsEqual; +import org.junit.Test; +import java.util.Arrays; +import java.util.List; +import example.github.Repository; +import feign.CollectionFormat; +import feign.MethodMetadata; +import feign.Request.HttpMethod; +import feign.apt.runtime.ParameterizedType; +import groovy.lang.Binding; +import groovy.lang.GroovyShell; + + +public class MethodMetadataSerializerTest { + + @Test + public void githubGetRepos() { + final MethodMetadata md = new MethodMetadata(); + md.returnType(new ParameterizedType(List.class, null, Repository.class)); + md.configKey("GitHub#repos(String)"); + + md.template().method(HttpMethod.GET); + md.template().uri("/users/{username}/repos?sort=full_name"); + md.template().decodeSlash(true); + md.template().collectionFormat(CollectionFormat.EXPLODED); + + md.indexToName().put(0, Arrays.asList("username")); + md.indexToEncoded().put(0, false); + + final StringBuilder result = MethodMetadataSerializer.toJavaCode(md); + assertEquals(result.toString(), + " final MethodMetadata md = new MethodMetadata();\n" + + " md.returnType(new feign.apt.runtime.ParameterizedType(java.util.List.class, null, example.github.Repository.class));\n" + + + " md.configKey(\"GitHub#repos(String)\");\n" + + "\n" + + " md.template().method(feign.Request.HttpMethod.GET);\n" + + " md.template().uri(\"/users/{username}/repos?sort=full_name\", false);\n" + + " md.template().decodeSlash(true);\n" + + " md.template().collectionFormat(CollectionFormat.EXPLODED);\n" + + "\n" + + " md.indexToName().put(0, Arrays.asList(\"username\"));\n" + + " md.indexToEncoded().put(0, false);\n"); + + final Binding binding = new Binding(); + binding.setVariable("foo", new Integer(2)); + final GroovyShell shell = new GroovyShell(binding); + + final MethodMetadata resultMetadata = (MethodMetadata) shell.evaluate("" + + "import feign.*;\n" + + result.toString() + + "\nreturn md;"); + + assertThat(resultMetadata, DeepIsEqual.deeplyEqualTo(md)); + } + +} diff --git a/core/src/main/java/feign/Contract.java b/core/src/main/java/feign/Contract.java index 5c5df901f0..f510b80ca8 100644 --- a/core/src/main/java/feign/Contract.java +++ b/core/src/main/java/feign/Contract.java @@ -199,7 +199,7 @@ protected abstract boolean processAnnotationsOnParameter(MethodMetadata data, /** * links a parameter name to its index in the method signature. */ - protected void nameParam(MethodMetadata data, String name, int i) { + public void nameParam(MethodMetadata data, String name, int i) { Collection names = data.indexToName().containsKey(i) ? data.indexToName().get(i) : new ArrayList(); names.add(name); @@ -332,8 +332,8 @@ public interface ParameterAnnotationProcessor { * @param annotation to be processed * @param processor function that defines the annotations modifies {@link MethodMetadata} */ - protected void registerParameterAnnotation(Class annotation, - ParameterAnnotationProcessor processor) { + public void registerParameterAnnotation(Class annotation, + ParameterAnnotationProcessor processor) { this.parameterAnnotationProcessors.put((Class) annotation, (ParameterAnnotationProcessor) processor); } @@ -342,6 +342,14 @@ public Map, ClassAnnotationProcessor> getClassAnno return Collections.unmodifiableMap(classAnnotationProcessors); } + public Map, MethodAnnotationProcessor> getMethodAnnotationProcessors() { + return Collections.unmodifiableMap(methodAnnotationProcessors); + } + + public Map, ParameterAnnotationProcessor> getParameterAnnotationProcessors() { + return Collections.unmodifiableMap(parameterAnnotationProcessors); + } + } class Default extends VisitorContract { diff --git a/core/src/main/java/feign/Request.java b/core/src/main/java/feign/Request.java index 609f84617a..f0a5c09d1c 100644 --- a/core/src/main/java/feign/Request.java +++ b/core/src/main/java/feign/Request.java @@ -82,6 +82,10 @@ public static Body empty() { return new Request.Body(null, null, null); } + public Charset encoding() { + return encoding; + } + } public enum HttpMethod { diff --git a/core/src/main/java/feign/RequestTemplate.java b/core/src/main/java/feign/RequestTemplate.java index fdda85068e..f76de25efe 100644 --- a/core/src/main/java/feign/RequestTemplate.java +++ b/core/src/main/java/feign/RequestTemplate.java @@ -51,6 +51,8 @@ public final class RequestTemplate implements Serializable { private Request.Body body = Request.Body.empty(); private boolean decodeSlash = true; private CollectionFormat collectionFormat = CollectionFormat.EXPLODED; + private String uri; + private boolean uriAppend; /** * Create a new Request Template. @@ -402,6 +404,8 @@ public RequestTemplate uri(String uri, boolean append) { if (UriUtils.isAbsolute(uri)) { throw new IllegalArgumentException("url values must be not be absolute."); } + this.uri = uri; + this.uriAppend = append; if (uri == null) { uri = "/"; @@ -931,4 +935,12 @@ interface Factory { RequestTemplate create(Object[] argv); } + public String uri() { + return uri; + } + + public boolean uriAppend() { + return uriAppend; + } + } diff --git a/core/src/test/java/feign/DefaultContractTest.java b/core/src/test/java/feign/DefaultContractTest.java index e144f90f5f..00e0552e92 100644 --- a/core/src/test/java/feign/DefaultContractTest.java +++ b/core/src/test/java/feign/DefaultContractTest.java @@ -31,7 +31,7 @@ import static org.assertj.core.data.MapEntry.entry; /** - * Tests interfaces defined per {@link Contract.Default} are interpreted into expected + * Tests public interface s defined per {@link Contract.Default} are interpreted into expected * {@link feign .RequestTemplate template} instances. */ public class DefaultContractTest { @@ -387,7 +387,7 @@ public void headerMapSubclass() throws Exception { assertThat(md.headerMapIndex()).isEqualTo(0); } - interface Methods { + public interface Methods { @RequestLine("POST /") void post(); @@ -402,7 +402,7 @@ interface Methods { void delete(); } - interface BodyParams { + public interface BodyParams { @RequestLine("POST") Response post(List body); @@ -414,13 +414,13 @@ interface BodyParams { Response tooMany(List body, List body2); } - interface CustomMethod { + public interface CustomMethod { @RequestLine("PATCH") Response patch(); } - interface WithQueryParamsInPath { + public interface WithQueryParamsInPath { @RequestLine("GET /") Response none(); @@ -444,7 +444,7 @@ interface WithQueryParamsInPath { Response twoEmpty(); } - interface BodyWithoutParameters { + public interface BodyWithoutParameters { @RequestLine("POST /") @Headers("Content-Type: application/xml") @@ -453,7 +453,7 @@ interface BodyWithoutParameters { } @Headers("Content-Type: application/xml") - interface HeadersOnType { + public interface HeadersOnType { @RequestLine("POST /") @Body("") @@ -461,20 +461,20 @@ interface HeadersOnType { } @Headers("Content-Type: application/xml ") - interface HeadersContainsWhitespaces { + public interface HeadersContainsWhitespaces { @RequestLine("POST /") @Body("") Response post(); } - interface WithURIParam { + public interface WithURIParam { @RequestLine("GET /{1}/{2}") Response uriParam(@Param("1") String one, URI endpoint, @Param("2") String two); } - interface WithPathAndQueryParams { + public interface WithPathAndQueryParams { @RequestLine("GET /domains/{domainId}/records?name={name}&type={type}") Response recordsByNameAndType(@Param("domainId") int id, @@ -482,7 +482,7 @@ Response recordsByNameAndType(@Param("domainId") int id, @Param("type") String typeFilter); } - interface FormParams { + public interface FormParams { @RequestLine("POST /") @Body("%7B\"customer_name\": \"{customer_name}\", \"user_name\": \"{user_name}\", \"password\": \"{password}\"%7D") @@ -492,7 +492,7 @@ void login( @Param("password") String password); } - interface HeaderMapInterface { + public interface HeaderMapInterface { @RequestLine("POST /") void multipleHeaderMap(@HeaderMap Map headers, @@ -502,21 +502,21 @@ void multipleHeaderMap(@HeaderMap Map headers, void headerMapSubClass(@HeaderMap SubClassHeaders httpHeaders); } - interface HeaderParams { + public interface HeaderParams { @RequestLine("POST /") @Headers({"Auth-Token: {authToken}", "Auth-Token: Foo"}) void logout(@Param("authToken") String token); } - interface HeaderParamsNotAtStart { + public interface HeaderParamsNotAtStart { @RequestLine("POST /") @Headers({"Authorization: Bearer {authToken}", "Authorization: Foo"}) void logout(@Param("authToken") String token); } - interface CustomExpander { + public interface CustomExpander { @RequestLine("POST /?date={date}") void date(@Param(value = "date", expander = DateToMillis.class) Date date); @@ -530,7 +530,7 @@ public String expand(Object value) { } } - interface QueryMapTestInterface { + public interface QueryMapTestInterface { @RequestLine("POST /") void queryMap(@QueryMap Map queryMap); @@ -563,7 +563,7 @@ void multipleQueryMap(@QueryMap Map mapOne, void nonStringKeyQueryMap(@QueryMap Map queryMap); } - interface SlashNeedToBeEncoded { + public interface SlashNeedToBeEncoded { @RequestLine(value = "GET /api/queues/{vhost}", decodeSlash = false) String getQueues(@Param("vhost") String vhost); @@ -572,13 +572,13 @@ interface SlashNeedToBeEncoded { } @Headers("Foo: Bar") - interface SimpleParameterizedBaseApi { + public interface SimpleParameterizedBaseApi { @RequestLine("GET /api/{zoneId}") M get(@Param("key") String key); } - interface SimpleParameterizedApi extends SimpleParameterizedBaseApi { + public interface SimpleParameterizedApi extends SimpleParameterizedBaseApi { } @@ -603,7 +603,7 @@ public void parameterizedApiUnsupported() throws Exception { contract.parseAndValidatateMetadata(SimpleParameterizedBaseApi.class); } - interface OverrideParameterizedApi extends SimpleParameterizedBaseApi { + public interface OverrideParameterizedApi extends SimpleParameterizedBaseApi { @Override @RequestLine("GET /api/{zoneId}") @@ -617,11 +617,11 @@ public void overrideBaseApiUnsupported() throws Exception { contract.parseAndValidatateMetadata(OverrideParameterizedApi.class); } - interface Child extends SimpleParameterizedBaseApi> { + public interface Child extends SimpleParameterizedBaseApi> { } - interface GrandChild extends Child { + public interface GrandChild extends Child { } @@ -633,7 +633,7 @@ public void onlySingleLevelInheritanceSupported() throws Exception { } @Headers("Foo: Bar") - interface ParameterizedBaseApi { + public interface ParameterizedBaseApi { @RequestLine("GET /api/{key}") Entity get(@Param("key") K key); @@ -659,13 +659,13 @@ static class Entities { } - interface SubClassHeaders extends Map { + public interface SubClassHeaders extends Map { } @Headers("Version: 1") - interface ParameterizedApi extends ParameterizedBaseApi { + public interface ParameterizedApi extends ParameterizedBaseApi { } @@ -697,7 +697,7 @@ public void parameterizedBaseApi() throws Exception { } @Headers("Authorization: {authHdr}") - interface ParameterizedHeaderExpandApi { + public interface ParameterizedHeaderExpandApi { @RequestLine("GET /api/{zoneId}") @Headers("Accept: application/json") String getZone(@Param("zoneId") String vhost, @Param("authHdr") String authHdr); @@ -743,17 +743,17 @@ public void parameterizedHeaderNotStartingWithCurlyBraceExpandApi() throws Excep } @Headers("Authorization: Bearer {authHdr}") - interface ParameterizedHeaderNotStartingWithCurlyBraceExpandApi { + public interface ParameterizedHeaderNotStartingWithCurlyBraceExpandApi { @RequestLine("GET /api/{zoneId}") @Headers("Accept: application/json") String getZone(@Param("zoneId") String vhost, @Param("authHdr") String authHdr); } @Headers("Authorization: {authHdr}") - interface ParameterizedHeaderBase { + public interface ParameterizedHeaderBase { } - interface ParameterizedHeaderExpandInheritedApi extends ParameterizedHeaderBase { + public interface ParameterizedHeaderExpandInheritedApi extends ParameterizedHeaderBase { @RequestLine("GET /api/{zoneId}") @Headers("Accept: application/json") String getZoneAccept(@Param("zoneId") String vhost, @Param("authHdr") String authHdr); @@ -804,7 +804,7 @@ private MethodMetadata parseAndValidateMetadata(Class targetType, targetType.getMethod(method, parameterTypes)); } - interface MissingMethod { + public interface MissingMethod { @RequestLine("/path?queryParam={queryParam}") Response updateSharing(@Param("queryParam") long queryParam, String bodyParam); } @@ -819,7 +819,7 @@ public void missingMethod() throws Exception { contract.parseAndValidatateMetadata(MissingMethod.class); } - interface StaticMethodOnInterface { + public interface StaticMethodOnInterface { @RequestLine("GET /api/{key}") String get(@Param("key") String key); @@ -836,7 +836,7 @@ public void staticMethodsOnInterfaceIgnored() throws Exception { assertThat(md.configKey()).isEqualTo("StaticMethodOnInterface#get(String)"); } - interface DefaultMethodOnInterface { + public interface DefaultMethodOnInterface { @RequestLine("GET /api/{key}") String get(@Param("key") String key); @@ -853,7 +853,7 @@ public void defaultMethodsOnInterfaceIgnored() throws Exception { assertThat(md.configKey()).isEqualTo("DefaultMethodOnInterface#get(String)"); } - interface SubstringQuery { + public interface SubstringQuery { @RequestLine("GET /_search?q=body:{body}") String paramIsASubstringOfAQuery(@Param("body") String body); } From 8024d6f12eca5cc0dc058f020c2562c004ec0641 Mon Sep 17 00:00:00 2001 From: Marvin Froeder Date: Mon, 2 Sep 2019 08:05:59 +1200 Subject: [PATCH 6/9] Remove Arrays import --- .../src/main/java/feign/aptgenerator/ContractAPT.java | 2 +- .../feign/aptgenerator/MethodMetadataSerializer.java | 2 +- .../java/feign/aptgenerator/github/GitHubFactory.java | 11 +++++------ 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/apt-generator/src/main/java/feign/aptgenerator/ContractAPT.java b/apt-generator/src/main/java/feign/aptgenerator/ContractAPT.java index 25016d3154..7a793386d3 100644 --- a/apt-generator/src/main/java/feign/aptgenerator/ContractAPT.java +++ b/apt-generator/src/main/java/feign/aptgenerator/ContractAPT.java @@ -84,7 +84,7 @@ public boolean process(Set annotations, RoundEnvironment .createSourceFile(type.getSimpleName() + "Factory"); final StringBuilder writer = new StringBuilder(); writer.append("package " + jPackage + ";").append("\n"); - writer.append("import feign.MethodMetadata;").append("\n"); + writer.append("import feign.*;").append("\n"); writer.append("public class " + type.getSimpleName() + "Factory").append("\n"); writer.append("{").append("\n"); diff --git a/apt-generator/src/main/java/feign/aptgenerator/MethodMetadataSerializer.java b/apt-generator/src/main/java/feign/aptgenerator/MethodMetadataSerializer.java index d70c7dd086..f5961d6974 100644 --- a/apt-generator/src/main/java/feign/aptgenerator/MethodMetadataSerializer.java +++ b/apt-generator/src/main/java/feign/aptgenerator/MethodMetadataSerializer.java @@ -66,7 +66,7 @@ public static StringBuilder toJavaCode(MethodMetadata method) { .append(" md.formParams().add(\"" + formParam + "\");")); method.indexToName().forEach((index, valueList) -> sb - .append(" md.indexToName().put(" + index + ", Arrays.asList(" + .append(" md.indexToName().put(" + index + ", java.util.Arrays.asList(" + valueList.stream().collect(Collectors.joining("\",\"", "\"", "\"")) + "));") .append("\n")); if (method.indexToExpander() != null) { diff --git a/apt-generator/src/main/java/feign/aptgenerator/github/GitHubFactory.java b/apt-generator/src/main/java/feign/aptgenerator/github/GitHubFactory.java index af0a6ea2e4..eb44b27ecc 100644 --- a/apt-generator/src/main/java/feign/aptgenerator/github/GitHubFactory.java +++ b/apt-generator/src/main/java/feign/aptgenerator/github/GitHubFactory.java @@ -14,7 +14,6 @@ package feign.aptgenerator.github; import java.lang.reflect.Type; -import java.util.Arrays; import java.util.List; import example.github.Contributor; import example.github.GitHubExample.GitHub; @@ -54,7 +53,7 @@ public Type resolve() { md.template().decodeSlash(true); md.template().collectionFormat(CollectionFormat.EXPLODED); - md.indexToName().put(0, Arrays.asList("username")); + md.indexToName().put(0, java.util.Arrays.asList("username")); md.indexToEncoded().put(0, false); __GitHub_repos__metadata = md; } @@ -71,10 +70,10 @@ public Type resolve() { md.template().decodeSlash(true); md.template().collectionFormat(CollectionFormat.EXPLODED); - md.indexToName().put(0, Arrays.asList("owner")); + md.indexToName().put(0, java.util.Arrays.asList("owner")); md.indexToEncoded().put(0, false); - md.indexToName().put(1, Arrays.asList("repo")); + md.indexToName().put(1, java.util.Arrays.asList("repo")); md.indexToEncoded().put(1, false); } @@ -90,10 +89,10 @@ public Type resolve() { md.template().decodeSlash(true); md.template().collectionFormat(CollectionFormat.EXPLODED); - md.indexToName().put(0, Arrays.asList("owner")); + md.indexToName().put(0, java.util.Arrays.asList("owner")); md.indexToEncoded().put(0, false); - md.indexToName().put(1, Arrays.asList("repo")); + md.indexToName().put(1, java.util.Arrays.asList("repo")); md.indexToEncoded().put(1, false); } From f3e9f76e348c7070e0d796483b68ec187803f854 Mon Sep 17 00:00:00 2001 From: Marvin Froeder Date: Sat, 31 Aug 2019 07:28:46 +1200 Subject: [PATCH 7/9] Declarative contracts --- core/src/main/java/feign/Contract.java | 274 +++++++++++++----- core/src/main/java/feign/Request.java | 6 + .../java/feign/SynchronousMethodHandler.java | 5 +- .../main/java/feign/codec/ErrorDecoder.java | 2 - .../test/java/feign/DefaultContractTest.java | 2 +- .../feign/codec/RetryAfterDecoderTest.java | 3 - 6 files changed, 208 insertions(+), 84 deletions(-) diff --git a/core/src/main/java/feign/Contract.java b/core/src/main/java/feign/Contract.java index 6363654c02..3224d9c5c5 100644 --- a/core/src/main/java/feign/Contract.java +++ b/core/src/main/java/feign/Contract.java @@ -14,18 +14,15 @@ package feign; import java.lang.annotation.Annotation; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.lang.reflect.ParameterizedType; -import java.lang.reflect.Type; +import java.lang.reflect.*; import java.net.URI; -import java.util.ArrayList; -import java.util.Collection; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; +import java.util.*; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; +import feign.Contract.DeclarativeContract.ClassAnnotationProcessor; import feign.Request.HttpMethod; import static feign.Util.checkState; import static feign.Util.emptyToNull; @@ -98,7 +95,7 @@ protected MethodMetadata parseAndValidateMetadata(Class targetType, Method me } checkState(data.template().method() != null, "Method %s not annotated with HTTP method type (ex. GET, POST)", - method.getName()); + data.configKey()); Class[] parameterTypes = method.getParameterTypes(); Type[] genericParameterTypes = method.getGenericParameterTypes(); @@ -169,7 +166,6 @@ private static void checkMapKeys(String name, Type genericType) { } } - /** * Called by parseAndValidateMetadata twice, first on the declaring class, then on the target * type (unless they are the same). @@ -211,99 +207,224 @@ protected void nameParam(MethodMetadata data, String name, int i) { } } - class Default extends BaseContract { + /** + * {@link Contract} base implementation that works by declaring witch annotations should be + * processed and how each annotation modifies {@link MethodMetadata} + */ + abstract class DeclarativeContract extends BaseContract { - static final Pattern REQUEST_LINE_PATTERN = Pattern.compile("^([A-Z]+)[ ]*(.*)$"); + Map, ClassAnnotationProcessor> classAnnotationProcessors = + new HashMap<>(); + private Map, MethodAnnotationProcessor> methodAnnotationProcessors = + new HashMap<>(); + Map, ParameterAnnotationProcessor> parameterAnnotationProcessors = + new HashMap<>(); + /** + * Called by parseAndValidateMetadata twice, first on the declaring class, then on the target + * type (unless they are the same). + * + * @param data metadata collected so far relating to the current java method. + * @param clz the class to process + */ + @Override + protected final void processAnnotationOnClass(MethodMetadata data, Class targetType) { + Arrays.stream(targetType.getAnnotations()) + .forEach(annotation -> classAnnotationProcessors.forEach((type, processor) -> { + if (type.isInstance(annotation)) + processor.process(annotation, data); + })); + } + + /** + * @param data metadata collected so far relating to the current java method. + * @param annotation annotations present on the current method annotation. + * @param method method currently being processed. + */ @Override - protected void processAnnotationOnClass(MethodMetadata data, Class targetType) { - if (targetType.isAnnotationPresent(Headers.class)) { - String[] headersOnType = targetType.getAnnotation(Headers.class).value(); + protected final void processAnnotationOnMethod(MethodMetadata data, + Annotation annotation, + Method method) { + methodAnnotationProcessors.forEach((type, processor) -> { + if (type.isInstance(annotation)) + processor.process(annotation, data); + }); + } + + + /** + * @param data metadata collected so far relating to the current java method. + * @param annotations annotations present on the current parameter annotation. + * @param paramIndex if you find a name in {@code annotations}, call + * {@link #nameParam(MethodMetadata, String, int)} with this as the last parameter. + * @return true if you called {@link #nameParam(MethodMetadata, String, int)} after finding an + * http-relevant annotation. + */ + protected final boolean processAnnotationsOnParameter(MethodMetadata data, + Annotation[] annotations, + int paramIndex) { + return Arrays.stream(annotations) + .flatMap(annotation -> parameterAnnotationProcessors.entrySet() + .stream() + .map(entry -> entry.getKey().isInstance(annotation) + ? entry.getValue().process(annotation, data, paramIndex) + : false)) + .collect(Collectors.reducing(Boolean::logicalOr)) + .orElse(false); + } + + @FunctionalInterface + public interface ClassAnnotationProcessor { + /** + * Called by parseAndValidateMetadata twice, first on the declaring class, then on the target + * type (unless they are the same). + * + * @param annotation present on the current class + * @param metadata metadata collected so far relating to the current java method. + */ + void process(E annotation, MethodMetadata metadata); + } + + /** + * Called while class annotations are being processed + * + * @param annotation to be processed + * @param processor function that defines the annotations modifies {@link MethodMetadata} + */ + protected void registerClassAnnotation(Class annotation, + ClassAnnotationProcessor processor) { + this.classAnnotationProcessors.put((Class) annotation, (ClassAnnotationProcessor) processor); + } + + @FunctionalInterface + public interface MethodAnnotationProcessor { + /** + * @param annotation present on the current method. + * @param metadata metadata collected so far relating to the current java method. + * @param method method currently being processed. + */ + void process(E annotation, MethodMetadata metadata); + } + + /** + * Called while method annotations are being processed + * + * @param annotation to be processed + * @param processor function that defines the annotations modifies {@link MethodMetadata} + */ + protected void registerMethodAnnotation(Class annotation, + MethodAnnotationProcessor processor) { + this.methodAnnotationProcessors.put((Class) annotation, + (MethodAnnotationProcessor) processor); + } + + @FunctionalInterface + public interface ParameterAnnotationProcessor { + /** + * @param annotation present on the current parameter annotation. + * @param metadata metadata collected so far relating to the current java method. + * @param paramIndex if you find a name in {@code annotations}, call + * {@link #nameParam(MethodMetadata, String, int)} with this as the last parameter. + * @return true if you called {@link #nameParam(MethodMetadata, String, int)} after finding an + * http-relevant annotation. + */ + boolean process(E annotation, MethodMetadata metadata, int paramIndex); + } + + /** + * Called while method parameter annotations are being processed + * + * @param annotation to be processed + * @param processor function that defines the annotations modifies {@link MethodMetadata} + */ + protected void registerParameterAnnotation(Class annotation, + ParameterAnnotationProcessor processor) { + this.parameterAnnotationProcessors.put((Class) annotation, + (ParameterAnnotationProcessor) processor); + } + + public Map, ClassAnnotationProcessor> getClassAnnotationProcessors() { + return Collections.unmodifiableMap(classAnnotationProcessors); + } + + } + + class Default extends DeclarativeContract { + + static final Pattern REQUEST_LINE_PATTERN = Pattern.compile("^([A-Z]+)[ ]*(.*)$"); + + public Default() { + super.registerClassAnnotation(Headers.class, (header, data) -> { + String[] headersOnType = header.value(); checkState(headersOnType.length > 0, "Headers annotation was empty on type %s.", - targetType.getName()); + data.configKey()); 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) { - Class annotationType = methodAnnotation.annotationType(); - if (annotationType == RequestLine.class) { - String requestLine = RequestLine.class.cast(methodAnnotation).value(); + }); + super.registerMethodAnnotation(RequestLine.class, (ann, data) -> { + String requestLine = ann.value(); checkState(emptyToNull(requestLine) != null, - "RequestLine annotation was empty on method %s.", method.getName()); + "RequestLine annotation was empty on method %s.", data.configKey()); Matcher requestLineMatcher = REQUEST_LINE_PATTERN.matcher(requestLine); if (!requestLineMatcher.find()) { throw new IllegalStateException(String.format( "RequestLine annotation didn't start with an HTTP verb on method %s", - method.getName())); + data.configKey())); } else { data.template().method(HttpMethod.valueOf(requestLineMatcher.group(1))); data.template().uri(requestLineMatcher.group(2)); } - data.template().decodeSlash(RequestLine.class.cast(methodAnnotation).decodeSlash()); + data.template().decodeSlash(ann.decodeSlash()); data.template() - .collectionFormat(RequestLine.class.cast(methodAnnotation).collectionFormat()); - - } else if (annotationType == Body.class) { - String body = Body.class.cast(methodAnnotation).value(); + .collectionFormat(ann.collectionFormat()); + }); + super.registerMethodAnnotation(Body.class, (ann, data) -> { + String body = ann.value(); checkState(emptyToNull(body) != null, "Body annotation was empty on method %s.", - method.getName()); + data.configKey()); 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(); + }); + super.registerMethodAnnotation(Headers.class, (header, data) -> { + String[] headersOnMethod = header.value(); checkState(headersOnMethod.length > 0, "Headers annotation was empty on method %s.", - method.getName()); + data.configKey()); data.template().headers(toMap(headersOnMethod)); - } - } - - @Override - protected boolean processAnnotationsOnParameter(MethodMetadata data, - Annotation[] annotations, - int paramIndex) { - boolean isHttpAnnotation = false; - for (Annotation annotation : annotations) { - Class annotationType = annotation.annotationType(); - if (annotationType == Param.class) { - Param paramAnnotation = (Param) annotation; - String name = paramAnnotation.value(); - checkState(emptyToNull(name) != null, "Param annotation was empty on param %s.", - paramIndex); - nameParam(data, name, paramIndex); - Class expander = paramAnnotation.expander(); - if (expander != Param.ToStringExpander.class) { - data.indexToExpanderClass().put(paramIndex, expander); - } - data.indexToEncoded().put(paramIndex, paramAnnotation.encoded()); - isHttpAnnotation = true; - if (!data.template().hasRequestVariable(name)) { - 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; + }); + super.registerParameterAnnotation(Param.class, (paramAnnotation, data, paramIndex) -> { + String name = paramAnnotation.value(); + checkState(emptyToNull(name) != null, "Param annotation was empty on param %s.", + paramIndex); + nameParam(data, name, paramIndex); + Class expander = paramAnnotation.expander(); + if (expander != Param.ToStringExpander.class) { + data.indexToExpanderClass().put(paramIndex, expander); } - } - return isHttpAnnotation; + data.indexToEncoded().put(paramIndex, paramAnnotation.encoded()); + if (!data.template().hasRequestVariable(name)) { + data.formParams().add(name); + } + return true; + }); + super.registerParameterAnnotation(QueryMap.class, (queryMap, data, paramIndex) -> { + checkState(data.queryMapIndex() == null, + "QueryMap annotation was present on multiple parameters."); + data.queryMapIndex(paramIndex); + data.queryMapEncoded(queryMap.encoded()); + return true; + }); + super.registerParameterAnnotation(HeaderMap.class, (queryMap, data, paramIndex) -> { + checkState(data.headerMapIndex() == null, + "HeaderMap annotation was present on multiple parameters."); + data.headerMapIndex(paramIndex); + return true; + }); } private static Map> toMap(String[] input) { @@ -319,5 +440,6 @@ private static Map> toMap(String[] input) { } return result; } + } } diff --git a/core/src/main/java/feign/Request.java b/core/src/main/java/feign/Request.java index f5e2657032..609f84617a 100644 --- a/core/src/main/java/feign/Request.java +++ b/core/src/main/java/feign/Request.java @@ -230,6 +230,12 @@ public static class Options { private final int readTimeoutMillis; private final boolean followRedirects; + @Override + public String toString() { + return "Options [connectTimeoutMillis=" + connectTimeoutMillis + ", readTimeoutMillis=" + + readTimeoutMillis + ", followRedirects=" + followRedirects + "]"; + } + public Options(int connectTimeoutMillis, int readTimeoutMillis, boolean followRedirects) { this.connectTimeoutMillis = connectTimeoutMillis; this.readTimeoutMillis = readTimeoutMillis; diff --git a/core/src/main/java/feign/SynchronousMethodHandler.java b/core/src/main/java/feign/SynchronousMethodHandler.java index a6371491d3..c22480e7ae 100644 --- a/core/src/main/java/feign/SynchronousMethodHandler.java +++ b/core/src/main/java/feign/SynchronousMethodHandler.java @@ -187,8 +187,9 @@ Options findOptions(Object[] argv) { if (argv == null || argv.length == 0) { return this.options; } - return (Options) Stream.of(argv) - .filter(o -> o instanceof Options) + return Stream.of(argv) + .filter(Options.class::isInstance) + .map(Options.class::cast) .findFirst() .orElse(this.options); } diff --git a/core/src/main/java/feign/codec/ErrorDecoder.java b/core/src/main/java/feign/codec/ErrorDecoder.java index 8f1552502c..ee6ff6fec0 100644 --- a/core/src/main/java/feign/codec/ErrorDecoder.java +++ b/core/src/main/java/feign/codec/ErrorDecoder.java @@ -18,11 +18,9 @@ import static feign.Util.checkNotNull; import static java.util.Locale.US; import static java.util.concurrent.TimeUnit.SECONDS; - import feign.FeignException; import feign.Response; import feign.RetryableException; - import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; diff --git a/core/src/test/java/feign/DefaultContractTest.java b/core/src/test/java/feign/DefaultContractTest.java index de534cbd1a..e144f90f5f 100644 --- a/core/src/test/java/feign/DefaultContractTest.java +++ b/core/src/test/java/feign/DefaultContractTest.java @@ -814,7 +814,7 @@ interface MissingMethod { public void missingMethod() throws Exception { thrown.expect(IllegalStateException.class); thrown.expectMessage( - "RequestLine annotation didn't start with an HTTP verb on method updateSharing"); + "RequestLine annotation didn't start with an HTTP verb on method MissingMethod#updateSharing"); contract.parseAndValidatateMetadata(MissingMethod.class); } diff --git a/core/src/test/java/feign/codec/RetryAfterDecoderTest.java b/core/src/test/java/feign/codec/RetryAfterDecoderTest.java index fc27a5293a..bcdea58392 100644 --- a/core/src/test/java/feign/codec/RetryAfterDecoderTest.java +++ b/core/src/test/java/feign/codec/RetryAfterDecoderTest.java @@ -16,11 +16,8 @@ import static feign.codec.ErrorDecoder.RetryAfterDecoder.RFC822_FORMAT; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; - import feign.codec.ErrorDecoder.RetryAfterDecoder; - import java.text.ParseException; - import org.junit.Test; public class RetryAfterDecoderTest { From 11c4402f4b8038a318c4e7be0ee84ca436d96023 Mon Sep 17 00:00:00 2001 From: Marvin Froeder Date: Wed, 4 Sep 2019 07:45:06 +1200 Subject: [PATCH 8/9] Actually using the data structure to read declaritve contracts --- core/src/main/java/feign/Contract.java | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/core/src/main/java/feign/Contract.java b/core/src/main/java/feign/Contract.java index 3224d9c5c5..d824b831ee 100644 --- a/core/src/main/java/feign/Contract.java +++ b/core/src/main/java/feign/Contract.java @@ -230,10 +230,9 @@ abstract class DeclarativeContract extends BaseContract { @Override protected final void processAnnotationOnClass(MethodMetadata data, Class targetType) { Arrays.stream(targetType.getAnnotations()) - .forEach(annotation -> classAnnotationProcessors.forEach((type, processor) -> { - if (type.isInstance(annotation)) - processor.process(annotation, data); - })); + .forEach(annotation -> classAnnotationProcessors + .get(annotation.annotationType()) + .process(annotation, data)); } /** @@ -245,10 +244,8 @@ protected final void processAnnotationOnClass(MethodMetadata data, Class targ protected final void processAnnotationOnMethod(MethodMetadata data, Annotation annotation, Method method) { - methodAnnotationProcessors.forEach((type, processor) -> { - if (type.isInstance(annotation)) - processor.process(annotation, data); - }); + methodAnnotationProcessors.get(annotation.annotationType()) + .process(annotation, data); } @@ -264,11 +261,10 @@ protected final boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[] annotations, int paramIndex) { return Arrays.stream(annotations) - .flatMap(annotation -> parameterAnnotationProcessors.entrySet() - .stream() - .map(entry -> entry.getKey().isInstance(annotation) - ? entry.getValue().process(annotation, data, paramIndex) - : false)) + .filter( + annotation -> parameterAnnotationProcessors.containsKey(annotation.annotationType())) + .map(annotation -> parameterAnnotationProcessors.get(annotation.annotationType()) + .process(annotation, data, paramIndex)) .collect(Collectors.reducing(Boolean::logicalOr)) .orElse(false); } From 3a8149dc4e0d9a0aa24fa38640cafdae6d197f73 Mon Sep 17 00:00:00 2001 From: Marvin Froeder Date: Fri, 27 Sep 2019 13:10:12 +1200 Subject: [PATCH 9/9] wip --- apt-generator/pom.xml | 2 +- .../aptgenerator/APTContractVisitor.java | 21 +++++++++---------- .../main/java/feign/DeclarativeContract.java | 18 +++++++++------- core/src/main/java/feign/Request.java | 5 +++-- 4 files changed, 24 insertions(+), 22 deletions(-) diff --git a/apt-generator/pom.xml b/apt-generator/pom.xml index 22d1188ed4..29f3e0402b 100644 --- a/apt-generator/pom.xml +++ b/apt-generator/pom.xml @@ -20,7 +20,7 @@ io.github.openfeign parent - 10.3.0-SNAPSHOT + 10.4.1-SNAPSHOT io.github.openfeign.experimental diff --git a/apt-generator/src/main/java/feign/aptgenerator/APTContractVisitor.java b/apt-generator/src/main/java/feign/aptgenerator/APTContractVisitor.java index 77d391c70c..03c44a0843 100644 --- a/apt-generator/src/main/java/feign/aptgenerator/APTContractVisitor.java +++ b/apt-generator/src/main/java/feign/aptgenerator/APTContractVisitor.java @@ -21,10 +21,9 @@ import java.util.*; import java.util.stream.Collectors; import javax.lang.model.element.*; -import feign.Contract.VisitorContract; -import feign.Contract.VisitorContract.ClassAnnotationProcessor; -import feign.Contract.VisitorContract.MethodAnnotationProcessor; -import feign.Contract.VisitorContract.ParameterAnnotationProcessor; +import feign.DeclarativeContract; +import feign.DeclarativeContract.AnnotationProcessor; +import feign.DeclarativeContract.ParameterAnnotationProcessor; import feign.MethodMetadata; /** @@ -33,7 +32,7 @@ public class APTContractVisitor { public List parseAndValidateMetadata(TypeElement targetType, - VisitorContract processorsSource) { + DeclarativeContract processorsSource) { checkState(targetType.getTypeParameters().size() == 0, "Parameterized types unsupported: %s", targetType.getSimpleName()); checkState(targetType.getInterfaces().size() <= 1, "Only single inheritance supported: %s", @@ -63,7 +62,7 @@ public List parseAndValidateMetadata(TypeElement targetType, */ protected MethodMetadata parseAndValidateMetadata(TypeElement targetType, ExecutableElement method, - VisitorContract processorsSource) { + DeclarativeContract processorsSource) { final MethodMetadata data = new MethodMetadata(); // TODO create warping type data.returnType(method.getReturnType()); data.configKey(configKey(targetType, method)); @@ -175,8 +174,8 @@ private static void checkMapKeys(String name, Type genericType) { */ protected void processAnnotationOnClass(MethodMetadata data, TypeElement targetType, - VisitorContract processorsSource) { - final Map, ClassAnnotationProcessor> annotations = + DeclarativeContract processorsSource) { + final List annotations = processorsSource.getClassAnnotationProcessors(); annotations.forEach((type, processor) -> { final Annotation annotation = targetType.getAnnotation(type); @@ -193,8 +192,8 @@ protected void processAnnotationOnClass(MethodMetadata data, */ private void processAnnotationOnMethod(MethodMetadata data, ExecutableElement method, - VisitorContract processorsSource) { - final Map, MethodAnnotationProcessor> annotations = + DeclarativeContract processorsSource) { + final Map, AnnotationProcessor> annotations = processorsSource.getMethodAnnotationProcessors(); annotations.forEach((type, processor) -> { final Annotation annotation = method.getAnnotation(type); @@ -215,7 +214,7 @@ private void processAnnotationOnMethod(MethodMetadata data, private boolean processAnnotationsOnParameter(MethodMetadata data, VariableElement parameter, int paramIndex, - VisitorContract processorsSource) { + DeclarativeContract processorsSource) { final Map, ParameterAnnotationProcessor> annotations = processorsSource.getParameterAnnotationProcessors(); diff --git a/core/src/main/java/feign/DeclarativeContract.java b/core/src/main/java/feign/DeclarativeContract.java index 8fc017597d..dab7f25edf 100644 --- a/core/src/main/java/feign/DeclarativeContract.java +++ b/core/src/main/java/feign/DeclarativeContract.java @@ -25,8 +25,8 @@ */ public abstract class DeclarativeContract extends BaseContract { - private List classAnnotationProcessors = new ArrayList<>(); - private List methodAnnotationProcessors = new ArrayList<>(); + private final List classAnnotationProcessors = new ArrayList<>(); + private final List methodAnnotationProcessors = new ArrayList<>(); Map, DeclarativeContract.ParameterAnnotationProcessor> parameterAnnotationProcessors = new HashMap<>(); @@ -168,8 +168,6 @@ public interface ParameterAnnotationProcessor { * @param metadata metadata collected so far relating to the current java method. * @param paramIndex if you find a name in {@code annotations}, call * {@link #nameParam(MethodMetadata, String, int)} with this as the last parameter. - * @return true if you called {@link #nameParam(MethodMetadata, String, int)} after finding an - * http-relevant annotation. */ void process(E annotation, MethodMetadata metadata, int paramIndex); } @@ -177,8 +175,8 @@ public interface ParameterAnnotationProcessor { private class GuardedAnnotationProcessor implements Predicate, DeclarativeContract.AnnotationProcessor { - private Predicate predicate; - private DeclarativeContract.AnnotationProcessor processor; + private final Predicate predicate; + private final DeclarativeContract.AnnotationProcessor processor; @SuppressWarnings({"rawtypes", "unchecked"}) private GuardedAnnotationProcessor(Predicate predicate, @@ -193,10 +191,14 @@ public void process(Annotation annotation, MethodMetadata metadata) { } @Override - public boolean test(Annotation t) { - return predicate.test(t); + public boolean test(Annotation annotation) { + return predicate.test(annotation); } } + public List getClassAnnotationProcessors() { + return this.classAnnotationProcessors; + } + } diff --git a/core/src/main/java/feign/Request.java b/core/src/main/java/feign/Request.java index 2a0bb1b40e..a1a8e8aa30 100644 --- a/core/src/main/java/feign/Request.java +++ b/core/src/main/java/feign/Request.java @@ -249,8 +249,9 @@ public static class Options { @Override public String toString() { - return "Options [connectTimeoutMillis=" + connectTimeoutMillis + ", readTimeoutMillis=" - + readTimeoutMillis + ", followRedirects=" + followRedirects + "]"; + return "Options [connectTimeout=" + connectTimeout + ", connectTimeoutUnit=" + + connectTimeoutUnit + ", readTimeout=" + readTimeout + ", readTimeoutUnit=" + + readTimeoutUnit + ", followRedirects=" + followRedirects + "]"; } public Options(long connectTimeout, TimeUnit connectTimeoutUnit,