From 8c06dd04d5463c1e10ef17cf44d68beb040ecd0d Mon Sep 17 00:00:00 2001 From: adriancole Date: Sun, 18 Aug 2013 10:13:04 -0700 Subject: [PATCH] closes #35 add RequestInterceptor --- CHANGES.md | 3 + README.md | 19 +++++ core/src/main/java/feign/MethodHandler.java | 35 +++++++--- core/src/main/java/feign/ReflectiveFeign.java | 11 ++- .../main/java/feign/RequestInterceptor.java | 70 +++++++++++++++++++ core/src/main/java/feign/RequestTemplate.java | 15 +--- core/src/main/java/feign/Target.java | 2 +- core/src/test/java/feign/FeignTest.java | 59 ++++++++++++++++ 8 files changed, 183 insertions(+), 31 deletions(-) create mode 100644 core/src/main/java/feign/RequestInterceptor.java diff --git a/CHANGES.md b/CHANGES.md index 6f156f77..176ef6a0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,6 @@ +### Version 4.3 +* Add ability to configure zero or more RequestInterceptors. + ### Version 4.2/3.3 * Document and enforce JAX-RS annotation processing from server POV * Skip query template parameters when corresponding java arg is null diff --git a/README.md b/README.md index c3349f60..e04dedc0 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,25 @@ public static void main(String... args) { Feign includes a fully functional json codec in the `feign-gson` extension. See the `Decoder` section for how to write your own. +### Request Interceptors +When you need to change all requests, regardless of their target, you'll want to configure a `RequestInterceptor`. +For example, if you are acting as an intermediary, you might want to propagate the `X-Forwarded-For` header. + +``` +@Module(library = true) +static class ForwardedForInterceptor implements RequestInterceptor { + @Provides(type = SET) RequestInterceptor provideThis() { + return this; + } + + @Override public void apply(RequestTemplate template) { + template.header("X-Forwarded-For", "origin.host.com"); + } +} +... +GitHub github = Feign.create(GitHub.class, "https://api.github.com", new GsonModule(), new ForwardedForInterceptor()); +``` + ### Observable Methods If specified as the last return type of a method `Observable` will invoke a new http request for each call to `subscribe()`. This is the async equivalent to an `Iterable`. Here's how one looks: diff --git a/core/src/main/java/feign/MethodHandler.java b/core/src/main/java/feign/MethodHandler.java index d7cbffff..173285da 100644 --- a/core/src/main/java/feign/MethodHandler.java +++ b/core/src/main/java/feign/MethodHandler.java @@ -27,6 +27,7 @@ import javax.inject.Named; import javax.inject.Provider; import java.io.IOException; import java.io.Reader; +import java.util.Set; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; @@ -44,29 +45,31 @@ interface MethodHandler { private final Client client; private final Lazy httpExecutor; private final Provider retryer; + private final Set requestInterceptors; private final Logger logger; private final Provider logLevel; - @Inject Factory(Client client, @Named("http") Lazy httpExecutor, Provider retryer, Logger logger, - Provider logLevel) { + @Inject Factory(Client client, @Named("http") Lazy httpExecutor, Provider retryer, + Set requestInterceptors, Logger logger, Provider logLevel) { this.client = checkNotNull(client, "client"); this.httpExecutor = checkNotNull(httpExecutor, "httpExecutor"); this.retryer = checkNotNull(retryer, "retryer"); + this.requestInterceptors = checkNotNull(requestInterceptors, "requestInterceptors"); this.logger = checkNotNull(logger, "logger"); this.logLevel = checkNotNull(logLevel, "logLevel"); } public MethodHandler create(Target target, MethodMetadata md, BuildTemplateFromArgs buildTemplateFromArgs, Options options, Decoder.TextStream decoder, ErrorDecoder errorDecoder) { - return new SynchronousMethodHandler(target, client, retryer, logger, logLevel, md, buildTemplateFromArgs, options, - decoder, errorDecoder); + return new SynchronousMethodHandler(target, client, retryer, requestInterceptors, logger, logLevel, md, + buildTemplateFromArgs, options, decoder, errorDecoder); } public MethodHandler create(Target target, MethodMetadata md, BuildTemplateFromArgs buildTemplateFromArgs, Options options, IncrementalDecoder.TextStream incrementalDecoder, ErrorDecoder errorDecoder) { - ObserverHandler observerHandler = new ObserverHandler(target, client, retryer, logger, logLevel, md, - buildTemplateFromArgs, options, incrementalDecoder, errorDecoder, httpExecutor); + ObserverHandler observerHandler = new ObserverHandler(target, client, retryer, requestInterceptors, logger, + logLevel, md, buildTemplateFromArgs, options, incrementalDecoder, errorDecoder, httpExecutor); return new ObservableMethodHandler(observerHandler); } } @@ -106,12 +109,14 @@ interface MethodHandler { private final Lazy httpExecutor; private final IncrementalDecoder.TextStream incrementalDecoder; - private ObserverHandler(Target target, Client client, Provider retryer, Logger logger, + private ObserverHandler(Target target, Client client, Provider retryer, + Set requestInterceptors, Logger logger, Provider logLevel, MethodMetadata metadata, BuildTemplateFromArgs buildTemplateFromArgs, Options options, IncrementalDecoder.TextStream incrementalDecoder, ErrorDecoder errorDecoder, Lazy httpExecutor) { - super(target, client, retryer, logger, logLevel, metadata, buildTemplateFromArgs, options, errorDecoder); + super(target, client, retryer, requestInterceptors, logger, logLevel, metadata, buildTemplateFromArgs, options, + errorDecoder); this.httpExecutor = checkNotNull(httpExecutor, "httpExecutor for %s", target); this.incrementalDecoder = checkNotNull(incrementalDecoder, "incrementalDecoder for %s", target); } @@ -185,11 +190,13 @@ interface MethodHandler { static class SynchronousMethodHandler extends BaseMethodHandler { private final Decoder.TextStream decoder; - private SynchronousMethodHandler(Target target, Client client, Provider retryer, Logger logger, + private SynchronousMethodHandler(Target target, Client client, Provider retryer, + Set requestInterceptors, Logger logger, Provider logLevel, MethodMetadata metadata, BuildTemplateFromArgs buildTemplateFromArgs, Options options, Decoder.TextStream decoder, ErrorDecoder errorDecoder) { - super(target, client, retryer, logger, logLevel, metadata, buildTemplateFromArgs, options, errorDecoder); + super(target, client, retryer, requestInterceptors, logger, logLevel, metadata, buildTemplateFromArgs, options, + errorDecoder); this.decoder = checkNotNull(decoder, "decoder for %s", target); } @@ -215,18 +222,21 @@ interface MethodHandler { protected final Target target; protected final Client client; protected final Provider retryer; + protected final Set requestInterceptors; protected final Logger logger; protected final Provider logLevel; protected final BuildTemplateFromArgs buildTemplateFromArgs; protected final Options options; protected final ErrorDecoder errorDecoder; - private BaseMethodHandler(Target target, Client client, Provider retryer, Logger logger, + private BaseMethodHandler(Target target, Client client, Provider retryer, + Set requestInterceptors, Logger logger, Provider logLevel, MethodMetadata metadata, BuildTemplateFromArgs buildTemplateFromArgs, Options options, ErrorDecoder errorDecoder) { 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.metadata = checkNotNull(metadata, "metadata for %s", target); @@ -294,6 +304,9 @@ interface MethodHandler { } protected Request targetRequest(RequestTemplate template) { + for (RequestInterceptor interceptor : requestInterceptors) { + interceptor.apply(template); + } return target.apply(new RequestTemplate(template)); } diff --git a/core/src/main/java/feign/ReflectiveFeign.java b/core/src/main/java/feign/ReflectiveFeign.java index 37eebc7d..844f7012 100644 --- a/core/src/main/java/feign/ReflectiveFeign.java +++ b/core/src/main/java/feign/ReflectiveFeign.java @@ -35,6 +35,7 @@ import java.lang.reflect.Type; import java.util.Collection; import java.util.HashMap; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -105,19 +106,17 @@ public class ReflectiveFeign extends Feign { } } - @dagger.Module(complete = false, injects = Feign.class, library = true) + @dagger.Module(complete = false, injects = {Feign.class, MethodHandler.Factory.class}, library = true) public static class Module { + @Provides(type = Provides.Type.SET_VALUES) Set noRequestInterceptors() { + return new LinkedHashSet(); + } @Provides Feign provideFeign(ReflectiveFeign in) { return in; } } - private static IllegalStateException noConfig(String configKey, Class type) { - return new IllegalStateException(format("no configuration for %s present for %s!", configKey, - type.getSimpleName())); - } - static final class ParseHandlersByName { private final Contract contract; private final Options options; diff --git a/core/src/main/java/feign/RequestInterceptor.java b/core/src/main/java/feign/RequestInterceptor.java new file mode 100644 index 00000000..39b79c60 --- /dev/null +++ b/core/src/main/java/feign/RequestInterceptor.java @@ -0,0 +1,70 @@ +/* + * Copyright 2013 Netflix, Inc. + * + * 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; + +/** + * Zero or more {@code RequestInterceptors} may be configured for purposes + * such as adding headers to all requests. No guarantees are give with regards + * to the order that interceptors are applied. Once interceptors are applied, + * {@link Target#apply(RequestTemplate)} is called to create the immutable http + * request sent via {@link Client#execute(Request, feign.Request.Options)}. + *
+ *
+ * For example: + *
+ *
+ * public void apply(RequestTemplate input) {
+ *     input.replaceHeader("X-Auth", currentToken);
+ * }
+ * 
+ *
+ *
Configuration
+ *
+ * {@code RequestInterceptors} are configured via Dagger + * {@link dagger.Provides.Type#SET set} or + * {@link dagger.Provides.Type#SET_VALUES set values} + * {@link dagger.Provides provider} methods. + *
+ *
+ * For example: + *
+ *
+ * {@literal @}Provides(Type = SET) RequestInterceptor addTimestamp(TimestampInterceptor in) {
+ * return in;
+ * }
+ * 
+ *
+ *
Implementation notes
+ *
+ * Do not add parameters, such as {@code /path/{foo}/bar } + * in your implementation of {@link #apply(RequestTemplate)}. + *
+ * Interceptors are applied after the template's parameters are + * {@link RequestTemplate#resolve(java.util.Map) resolved}. This is to ensure + * that you can implement signatures are interceptors. + *
+ *

Relationship to Retrofit 1.x
+ *
+ * This class is similar to {@code RequestInterceptor.intercept()}, + * except that the implementation can read, remove, or otherwise mutate any + * part of the request template. + */ +public interface RequestInterceptor { + /** + * Called for every request. Add data using methods on the supplied {@link RequestTemplate}. + */ + void apply(RequestTemplate template); +} diff --git a/core/src/main/java/feign/RequestTemplate.java b/core/src/main/java/feign/RequestTemplate.java index f3081a19..6fd35bb0 100644 --- a/core/src/main/java/feign/RequestTemplate.java +++ b/core/src/main/java/feign/RequestTemplate.java @@ -73,19 +73,8 @@ public final class RequestTemplate implements Serializable { } /** - * Targets a template to this target, adding the {@link #url() base url} and - * any authentication headers. - *
- *
- * For example: - *
- *
-   * public Request apply(RequestTemplate input) {
-   *     input.insert(0, url());
-   *     input.replaceHeader("X-Auth", currentToken);
-   *     return input.asRequest();
-   * }
-   * 
+ * Resolves any templated variables in the requests path, query, or headers + * against the supplied unencoded arguments. *
*

relationship to JAXRS 2.0
*
diff --git a/core/src/main/java/feign/Target.java b/core/src/main/java/feign/Target.java index d489a10c..ab3588cc 100644 --- a/core/src/main/java/feign/Target.java +++ b/core/src/main/java/feign/Target.java @@ -40,7 +40,7 @@ public interface Target { /** * Targets a template to this target, adding the {@link #url() base url} and - * any authentication headers. + * any target-specific headers or query parameters. *
*
* For example: diff --git a/core/src/test/java/feign/FeignTest.java b/core/src/test/java/feign/FeignTest.java index fba37b91..550cfd23 100644 --- a/core/src/test/java/feign/FeignTest.java +++ b/core/src/test/java/feign/FeignTest.java @@ -18,6 +18,7 @@ package feign; import com.google.common.base.Joiner; import com.google.mockwebserver.MockResponse; import com.google.mockwebserver.MockWebServer; +import com.google.mockwebserver.RecordedRequest; import com.google.mockwebserver.SocketPolicy; import dagger.Lazy; import dagger.Module; @@ -308,6 +309,64 @@ public class FeignTest { } } + @Module(library = true) + static class ForwardedForInterceptor implements RequestInterceptor { + @Provides(type = SET) RequestInterceptor provideThis() { + return this; + } + + @Override public void apply(RequestTemplate template) { + template.header("X-Forwarded-For", "origin.host.com"); + } + } + + @Test + public void singleInterceptor() throws IOException, InterruptedException { + final MockWebServer server = new MockWebServer(); + server.enqueue(new MockResponse().setResponseCode(200).setBody("foo")); + server.play(); + + try { + TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), + new TestInterface.Module(), new ForwardedForInterceptor()); + + api.post(); + assertEquals(server.takeRequest().getHeader("X-Forwarded-For"), "origin.host.com"); + } finally { + server.shutdown(); + } + } + + @Module(library = true) + static class UserAgentInterceptor implements RequestInterceptor { + @Provides(type = SET) RequestInterceptor provideThis() { + return this; + } + + @Override public void apply(RequestTemplate template) { + template.header("User-Agent", "Feign"); + } + } + + @Test + public void multipleInterceptor() throws IOException, InterruptedException { + final MockWebServer server = new MockWebServer(); + server.enqueue(new MockResponse().setResponseCode(200).setBody("foo")); + server.play(); + + try { + TestInterface api = Feign.create(TestInterface.class, "http://localhost:" + server.getPort(), + new TestInterface.Module(), new ForwardedForInterceptor(), new UserAgentInterceptor()); + + api.post(); + RecordedRequest request = server.takeRequest(); + assertEquals(request.getHeader("X-Forwarded-For"), "origin.host.com"); + assertEquals(request.getHeader("User-Agent"), "Feign"); + } finally { + server.shutdown(); + } + } + @Test public void toKeyMethodFormatsAsExpected() throws Exception { assertEquals(Feign.configKey(TestInterface.class.getDeclaredMethod("post")), "TestInterface#post()"); assertEquals(Feign.configKey(TestInterface.class.getDeclaredMethod("uriParam", String.class, URI.class,