Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support custom requestbuilder interceptors #3476

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions retrofit/src/main/java/retrofit2/RequestBuilderInterceptor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package retrofit2;

import okhttp3.Request;

/**
* Intercepts a {@link Request.Builder} just before it is built into an actual {@link Request}. Interceptors
* can be {@linkplain Retrofit.Builder#addRequestBuilderInterceptor(RequestBuilderInterceptor) added}
* to the {@link Retrofit} instance.
*/
public interface RequestBuilderInterceptor {

/**
* Intercepts the {@code requestBuilder} with the provided {@code invocation}. The {@code invocation}
* is used to access all method annotations, parameter annotations and method arguments. Use this
* method to provide your own modifications on the {@code requestBuilder} just before it's built into
* a {@link Request}.
*/
void intercept(Request.Builder requestBuilder, Invocation invocation);

}
19 changes: 16 additions & 3 deletions retrofit/src/main/java/retrofit2/RequestFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import okhttp3.HttpUrl;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.Request;
import okhttp3.RequestBody;
import retrofit2.http.Body;
import retrofit2.http.DELETE;
Expand Down Expand Up @@ -77,6 +78,7 @@ static RequestFactory parseAnnotations(Retrofit retrofit, Method method) {
private final boolean isFormEncoded;
private final boolean isMultipart;
private final ParameterHandler<?>[] parameterHandlers;
private final List<RequestBuilderInterceptor> requestBuilderInterceptors;
final boolean isKotlinSuspendFunction;

RequestFactory(Builder builder) {
Expand All @@ -90,6 +92,7 @@ static RequestFactory parseAnnotations(Retrofit retrofit, Method method) {
isFormEncoded = builder.isFormEncoded;
isMultipart = builder.isMultipart;
parameterHandlers = builder.parameterHandlers;
requestBuilderInterceptors = builder.retrofit.requestBuilderInterceptors;
isKotlinSuspendFunction = builder.isKotlinSuspendFunction;
}

Expand Down Expand Up @@ -126,10 +129,18 @@ okhttp3.Request create(Object[] args) throws IOException {
List<Object> argumentList = new ArrayList<>(argumentCount);
for (int p = 0; p < argumentCount; p++) {
argumentList.add(args[p]);
handlers[p].apply(requestBuilder, args[p]);
ParameterHandler<Object> handler = handlers[p];
if (handler != null) {
handler.apply(requestBuilder, args[p]);
}
}

return requestBuilder.get().tag(Invocation.class, new Invocation(method, argumentList)).build();
Invocation invocation = new Invocation(method, argumentList);
Request.Builder rawRequestBuilder = requestBuilder.get().tag(Invocation.class, invocation);
for (int i = 0; i < requestBuilderInterceptors.size(); i++) {
requestBuilderInterceptors.get(i).intercept(rawRequestBuilder, invocation);
}
return rawRequestBuilder.build();
}

/**
Expand Down Expand Up @@ -347,7 +358,9 @@ private Headers parseHeaders(String[] headers) {
} catch (NoClassDefFoundError ignored) {
}
}
throw parameterError(method, p, "No Retrofit annotation found.");
if (annotations == null || annotations.length == 0) {
throw parameterError(method, p, "No annotation found.");
}
}

return result;
Expand Down
29 changes: 29 additions & 0 deletions retrofit/src/main/java/retrofit2/Retrofit.java
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ public final class Retrofit {
final HttpUrl baseUrl;
final List<Converter.Factory> converterFactories;
final List<CallAdapter.Factory> callAdapterFactories;
final List<RequestBuilderInterceptor> requestBuilderInterceptors;
final @Nullable Executor callbackExecutor;
final boolean validateEagerly;

Expand All @@ -78,12 +79,14 @@ public final class Retrofit {
HttpUrl baseUrl,
List<Converter.Factory> converterFactories,
List<CallAdapter.Factory> callAdapterFactories,
List<RequestBuilderInterceptor> requestBuilderInterceptors,
@Nullable Executor callbackExecutor,
boolean validateEagerly) {
this.callFactory = callFactory;
this.baseUrl = baseUrl;
this.converterFactories = converterFactories; // Copy+unmodifiable at call site.
this.callAdapterFactories = callAdapterFactories; // Copy+unmodifiable at call site.
this.requestBuilderInterceptors = requestBuilderInterceptors; // Copy+unmodifiable at call site.
this.callbackExecutor = callbackExecutor;
this.validateEagerly = validateEagerly;
}
Expand Down Expand Up @@ -406,6 +409,14 @@ public <T> Converter<T, String> stringConverter(Type type, Annotation[] annotati
return (Converter<T, String>) BuiltInConverters.ToStringConverter.INSTANCE;
}

/**
* Returns an unmodifiable list of the request builder interceptors used to intercept
* and possibly modify the {@link okhttp3.Request.Builder} using the provided {@link Invocation}.
*/
public List<RequestBuilderInterceptor> requestBuilderInterceptors() {
return requestBuilderInterceptors;
}

/**
* The executor used for {@link Callback} methods on a {@link Call}. This may be {@code null}, in
* which case callbacks should be made synchronously on the background thread.
Expand All @@ -430,6 +441,7 @@ public static final class Builder {
private @Nullable HttpUrl baseUrl;
private final List<Converter.Factory> converterFactories = new ArrayList<>();
private final List<CallAdapter.Factory> callAdapterFactories = new ArrayList<>();
private final List<RequestBuilderInterceptor> requestBuilderInterceptors = new ArrayList<>();
private @Nullable Executor callbackExecutor;
private boolean validateEagerly;

Expand Down Expand Up @@ -463,6 +475,7 @@ public Builder() {
callAdapterFactories.add(retrofit.callAdapterFactories.get(i));
}

requestBuilderInterceptors.addAll(retrofit.requestBuilderInterceptors);
callbackExecutor = retrofit.callbackExecutor;
validateEagerly = retrofit.validateEagerly;
}
Expand Down Expand Up @@ -581,6 +594,17 @@ public Builder addCallAdapterFactory(CallAdapter.Factory factory) {
return this;
}

/**
* Add a request builder interceptor to implement custom request logic based on the {@link
* Invocation}.
*/
public Builder addRequestBuilderInterceptor(RequestBuilderInterceptor interceptor) {
requestBuilderInterceptors.add(
Objects.requireNonNull(interceptor, "interceptor == null")
);
return this;
}

/**
* The executor on which {@link Callback} methods are invoked when returning {@link Call} from
* your service method.
Expand Down Expand Up @@ -648,11 +672,16 @@ public Retrofit build() {
converterFactories.addAll(this.converterFactories);
converterFactories.addAll(platform.defaultConverterFactories());

// Make a defensive copy of the request builder interceptors.
List<RequestBuilderInterceptor> requestBuilderInterceptors =
new ArrayList<>(this.requestBuilderInterceptors);

return new Retrofit(
callFactory,
baseUrl,
unmodifiableList(converterFactories),
unmodifiableList(callAdapterFactories),
unmodifiableList(requestBuilderInterceptors),
callbackExecutor,
validateEagerly);
}
Expand Down
66 changes: 65 additions & 1 deletion retrofit/src/test/java/retrofit2/RequestFactoryTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@
*/
package retrofit2;

import static java.lang.annotation.RetentionPolicy.RUNTIME;
import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.fail;

import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.reflect.Method;
import java.math.BigInteger;
import java.net.URI;
Expand Down Expand Up @@ -465,7 +467,7 @@ Call<ResponseBody> method(String a) {
} catch (IllegalArgumentException e) {
assertThat(e)
.hasMessage(
"No Retrofit annotation found. (parameter #1)\n for method Example.method");
"No annotation found. (parameter #1)\n for method Example.method");
}
}

Expand Down Expand Up @@ -3252,6 +3254,68 @@ Call<ResponseBody> method(@Tag List<String> one, @Tag List<Long> two) {
}
}

@Retention(RUNTIME)
@interface CustomAnnotation {}

@Test
public void parameterWithCustomAnnotation() {
class Example {
@GET("/")
Call<ResponseBody> method(@CustomAnnotation String a) {
return null;
}
}
Request request = buildRequest(Example.class, "CustomArgument");
assertThat(request.url().toString()).isEqualTo("http://example.com/");
Invocation invocation = request.tag(Invocation.class);
assertThat(invocation.arguments().get(0)).isEqualTo("CustomArgument");
assertThat(invocation.method().getParameterAnnotations()[0][0]).isInstanceOf(CustomAnnotation.class);
}

@Test
public void requestAlteredWithRequestBuilderInterceptor() {
class Example {
@GET("/")
Call<ResponseBody> method() {
return null;
}
}

Retrofit.Builder retrofitBuilder =
new Retrofit.Builder()
.baseUrl("http://example.com")
.addRequestBuilderInterceptor((requestBuilder, invocation) -> {
requestBuilder.addHeader("CustomHeader", "CustomHeaderValue");
});

Request request = buildRequest(Example.class, retrofitBuilder);
assertThat(request.header("CustomHeader")).isEqualTo("CustomHeaderValue");
}

@Test
public void requestAlteredWithMultipleRequestBuilderInterceptors() {
class Example {
@GET("/")
Call<ResponseBody> method() {
return null;
}
}

Retrofit.Builder retrofitBuilder =
new Retrofit.Builder()
.baseUrl("http://example.com")
.addRequestBuilderInterceptor((requestBuilder, invocation) -> {
requestBuilder.addHeader("CustomHeader", "CustomHeaderValue");
})
.addRequestBuilderInterceptor((requestBuilder, invocation) -> {
requestBuilder.tag(String.class, "CustomTag");
});

Request request = buildRequest(Example.class, retrofitBuilder);
assertThat(request.header("CustomHeader")).isEqualTo("CustomHeaderValue");
assertThat(request.tag(String.class)).isEqualTo("CustomTag");
}

private static void assertBody(RequestBody body, String expected) {
assertThat(body).isNotNull();
Buffer buffer = new Buffer();
Expand Down
33 changes: 33 additions & 0 deletions retrofit/src/test/java/retrofit2/RetrofitTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -245,11 +245,13 @@ public void cloneSharesStatefulInstances() {
request -> {
throw new AssertionError();
};
RequestBuilderInterceptor interceptor = (requestBuilder, invocation) -> {};

Retrofit one =
new Retrofit.Builder()
.addCallAdapterFactory(callAdapter)
.addConverterFactory(converter)
.addRequestBuilderInterceptor(interceptor)
.baseUrl(baseUrl)
.callbackExecutor(executor)
.callFactory(callFactory)
Expand All @@ -265,15 +267,19 @@ public void cloneSharesStatefulInstances() {
}
};
Converter.Factory converter2 = new Converter.Factory() {};
RequestBuilderInterceptor interceptor2 = (requestBuilder, invocation) -> {};
Retrofit two =
one.newBuilder()
.addCallAdapterFactory(callAdapter2)
.addConverterFactory(converter2)
.addRequestBuilderInterceptor(interceptor2)
.build();
assertEquals(one.callAdapterFactories().size() + 1, two.callAdapterFactories().size());
assertThat(two.callAdapterFactories()).contains(callAdapter, callAdapter2);
assertEquals(one.converterFactories().size() + 1, two.converterFactories().size());
assertThat(two.converterFactories()).contains(converter, converter2);
assertEquals(one.requestBuilderInterceptors().size() + 1, two.requestBuilderInterceptors().size());
assertThat(two.requestBuilderInterceptors()).contains(interceptor, interceptor2);
assertSame(baseUrl, two.baseUrl());
assertSame(executor, two.callbackExecutor());
assertSame(callFactory, two.callFactory());
Expand Down Expand Up @@ -1500,6 +1506,33 @@ public void callAdapterFactoryDelegateNoMatchThrows() {
assertThat(nonMatchingFactory.called).isTrue();
}

@Test
public void requestBuilderInterceptorNullThrows() {
try {
new Retrofit.Builder().addRequestBuilderInterceptor(null);
fail();
} catch (NullPointerException e) {
assertThat(e).hasMessage("interceptor == null");
}
}

@Test
public void requestBuilderInterceptorDefault() {
Retrofit retrofit = new Retrofit.Builder().baseUrl("http://example.com/").build();
assertThat(retrofit.requestBuilderInterceptors()).isEmpty();
}

@Test
public void requestBuilderInterceptorPropagated() {
RequestBuilderInterceptor interceptor = (requestBuilder, invocation) -> {};
Retrofit retrofit =
new Retrofit.Builder()
.baseUrl("http://example.com/")
.addRequestBuilderInterceptor(interceptor)
.build();
assertThat(retrofit.requestBuilderInterceptors()).contains(interceptor);
}

@Test
public void platformAwareAdapterAbsentInCloneBuilder() {
Retrofit retrofit = new Retrofit.Builder().baseUrl(server.url("/")).build();
Expand Down