Skip to content

Commit dfd940f

Browse files
feat(aws-api): Support Custom OkHttp Configurations (#1207)
1 parent 0d9ca97 commit dfd940f

File tree

5 files changed

+184
-29
lines changed

5 files changed

+184
-29
lines changed

aws-api/src/androidTest/java/com/amplifyframework/api/aws/TestApiCategory.java

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@
1515

1616
package com.amplifyframework.api.aws;
1717

18-
import android.content.Context;
1918
import androidx.annotation.NonNull;
2019
import androidx.annotation.RawRes;
2120

2221
import com.amplifyframework.AmplifyException;
2322
import com.amplifyframework.api.ApiCategory;
23+
import com.amplifyframework.api.aws.sigv4.CognitoUserPoolsAuthProvider;
2424
import com.amplifyframework.api.aws.sigv4.DefaultCognitoUserPoolsAuthProvider;
2525
import com.amplifyframework.core.Amplify;
2626
import com.amplifyframework.core.AmplifyConfiguration;
@@ -47,24 +47,23 @@ private TestApiCategory() {}
4747
*/
4848
@NonNull
4949
static ApiCategory fromConfiguration(@RawRes int resourceId) throws AmplifyException {
50-
Context context = getApplicationContext();
50+
CognitoUserPoolsAuthProvider cognitoUserPoolsAuthProvider =
51+
new DefaultCognitoUserPoolsAuthProvider(AWSMobileClient.getInstance());
52+
ApiAuthProviders providers = ApiAuthProviders.builder()
53+
.awsCredentialsProvider(AWSMobileClient.getInstance())
54+
.cognitoUserPoolsAuthProvider(cognitoUserPoolsAuthProvider)
55+
.build();
56+
AWSApiPlugin plugin = AWSApiPlugin.builder()
57+
.apiAuthProviders(providers)
58+
.build();
5159
ApiCategory apiCategory = new ApiCategory();
52-
apiCategory.addPlugin(
53-
new AWSApiPlugin(
54-
ApiAuthProviders
55-
.builder()
56-
.awsCredentialsProvider(AWSMobileClient.getInstance())
57-
.cognitoUserPoolsAuthProvider(
58-
new DefaultCognitoUserPoolsAuthProvider(AWSMobileClient.getInstance())
59-
)
60-
.build()
61-
)
62-
);
60+
apiCategory.addPlugin(plugin);
61+
6362
CategoryConfiguration apiConfiguration =
64-
AmplifyConfiguration.fromConfigFile(context, resourceId)
63+
AmplifyConfiguration.fromConfigFile(getApplicationContext(), resourceId)
6564
.forCategoryType(CategoryType.API);
66-
apiCategory.configure(apiConfiguration, context);
67-
// apiCategory.initialize(context); Doesn't currently contain any logic, so, skip it.
65+
apiCategory.configure(apiConfiguration, getApplicationContext());
66+
// apiCategory.initialize(...); Doesn't currently contain any logic, so, skip it.
6867
return apiCategory;
6968
}
7069
}

aws-api/src/main/java/com/amplifyframework/api/aws/AWSApiPlugin.java

Lines changed: 78 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import com.amplifyframework.core.Amplify;
4040
import com.amplifyframework.core.Consumer;
4141
import com.amplifyframework.hub.HubChannel;
42+
import com.amplifyframework.util.Immutable;
4243
import com.amplifyframework.util.UserAgent;
4344

4445
import org.json.JSONObject;
@@ -69,6 +70,7 @@
6970
@SuppressWarnings("TypeParameterHidesVisibleType") // <R> shadows >com.amplifyframework.api.aws.R
7071
public final class AWSApiPlugin extends ApiPlugin<Map<String, OkHttpClient>> {
7172
private final Map<String, ClientDetails> apiDetails;
73+
private final Map<String, OkHttpConfigurator> apiConfigurators;
7274
private final GraphQLResponse.Factory gqlResponseFactory;
7375
private final ApiAuthProviders authProvider;
7476
private final ExecutorService executorService;
@@ -78,30 +80,39 @@ public final class AWSApiPlugin extends ApiPlugin<Map<String, OkHttpClient>> {
7880
private final Set<String> gqlApis;
7981

8082
/**
81-
* Default constructor for this plugin without any override.
83+
* Default constructor for this plugin without any overrides.
8284
*/
8385
public AWSApiPlugin() {
84-
this(ApiAuthProviders.noProviderOverrides());
86+
this(builder());
8587
}
8688

8789
/**
88-
* Constructs an instance of AWSApiPlugin with
89-
* configured auth providers to override default modes
90-
* of authorization.
91-
* If no Auth provider implementation is provided, then
92-
* the plugin will assume default behavior for that specific
93-
* mode of authorization.
94-
*
95-
* @param apiAuthProvider configured instance of {@link ApiAuthProviders}
90+
* Deprecated. Use {@link #builder()} instead.
91+
* @param apiAuthProvider Don't use this
92+
* @deprecated Use the fluent {@link #builder()}, instead.
9693
*/
94+
@Deprecated
9795
public AWSApiPlugin(@NonNull ApiAuthProviders apiAuthProvider) {
96+
this(builder().apiAuthProviders(apiAuthProvider));
97+
}
98+
99+
private AWSApiPlugin(@NonNull Builder builder) {
98100
this.apiDetails = new HashMap<>();
99101
this.gqlResponseFactory = new GsonGraphQLResponseFactory();
100-
this.authProvider = Objects.requireNonNull(apiAuthProvider);
102+
this.authProvider = builder.apiAuthProviders;
101103
this.restApis = new HashSet<>();
102104
this.gqlApis = new HashSet<>();
103105
this.executorService = Executors.newCachedThreadPool();
104106
this.requestDecorator = new AuthRuleRequestDecorator(authProvider);
107+
this.apiConfigurators = Immutable.of(builder.apiConfigurators);
108+
}
109+
110+
/**
111+
* Begins construction of a new AWSApiPlugin instance by using a fluent builder.
112+
* @return A builder to help construct an AWSApiPlugin
113+
*/
114+
public static Builder builder() {
115+
return new Builder();
105116
}
106117

107118
@NonNull
@@ -132,7 +143,13 @@ public void configure(
132143
if (apiConfiguration.getAuthorizationType() != AuthorizationType.NONE) {
133144
builder.addInterceptor(interceptorFactory.create(apiConfiguration));
134145
}
146+
147+
OkHttpConfigurator configurator = apiConfigurators.get(apiName);
148+
if (configurator != null) {
149+
configurator.applyConfiguration(builder);
150+
}
135151
final OkHttpClient okHttpClient = builder.build();
152+
136153
final SubscriptionAuthorizer subscriptionAuthorizer =
137154
new SubscriptionAuthorizer(apiConfiguration, authProvider);
138155
final SubscriptionEndpoint subscriptionEndpoint =
@@ -736,4 +753,54 @@ private void transitionTo(ApiEndpointStatus newStatus) {
736753
}
737754
}
738755
}
756+
757+
/**
758+
* Builds an {@link AWSApiPlugin}.
759+
*/
760+
public static final class Builder {
761+
private ApiAuthProviders apiAuthProviders;
762+
private final Map<String, OkHttpConfigurator> apiConfigurators;
763+
764+
private Builder() {
765+
this.apiAuthProviders = ApiAuthProviders.noProviderOverrides();
766+
this.apiConfigurators = new HashMap<>();
767+
}
768+
769+
/**
770+
* Specify authentication providers.
771+
* @param apiAuthProviders A set of authentication providers to use for API calls
772+
* @return Current builder instance, for fluent construction of plugin
773+
*/
774+
@NonNull
775+
public Builder apiAuthProviders(@NonNull ApiAuthProviders apiAuthProviders) {
776+
Objects.requireNonNull(apiAuthProviders);
777+
Builder.this.apiAuthProviders = apiAuthProviders;
778+
return Builder.this;
779+
}
780+
781+
/**
782+
* Apply customizations to an underlying OkHttpClient that will be used
783+
* for a particular API.
784+
* @param forApiName The name of the API for which these customizations should apply.
785+
This can be found in your `amplifyconfiguration.json` file.
786+
* @param byConfigurator A lambda that hands the user an OkHttpClient.Builder,
787+
* and enables the user to set come configurations on it.
788+
* @return A builder instance, to continue chaining configurations
789+
*/
790+
@NonNull
791+
public Builder configureClient(
792+
@NonNull String forApiName, @NonNull OkHttpConfigurator byConfigurator) {
793+
this.apiConfigurators.put(forApiName, byConfigurator);
794+
return this;
795+
}
796+
797+
/**
798+
* Builds an {@link AWSApiPlugin}.
799+
* @return An AWSApiPlugin
800+
*/
801+
@NonNull
802+
public AWSApiPlugin build() {
803+
return new AWSApiPlugin(Builder.this);
804+
}
805+
}
739806
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package com.amplifyframework.api.aws;
17+
18+
import android.content.Context;
19+
import androidx.annotation.NonNull;
20+
21+
import com.amplifyframework.core.Amplify;
22+
23+
import okhttp3.OkHttpClient;
24+
25+
/**
26+
* An OkHttpConfigurator is a hook provided to a customer, enabling them to customize
27+
* the way an API client is setup while the AWS API plugin is being instantiated.
28+
*
29+
* This hook is for advanced use cases, such as where a user may want to append some of
30+
* their own request headers, or otherwise manipulate an outgoing request.
31+
*
32+
* See {@link AWSApiPlugin.Builder#configureClient(String, OkHttpConfigurator)}
33+
* for more details.
34+
*/
35+
@FunctionalInterface
36+
public interface OkHttpConfigurator {
37+
/**
38+
* A customer can implement this hook to apply additional configurations
39+
* for a particular API. The user supplies an implementation of this function
40+
* when the AWSApiPlugin is being constructed:
41+
* <pre>
42+
* AWSApiPlugin plugin = AWSApiPlugin.builder()
43+
* .configureClient("someApi", okHttpBuilder -> {
44+
* okHttpBuilder.connectTimeout(10, TimeUnit.SECONDS);
45+
* })
46+
* .build();
47+
* </pre>
48+
* The hook itself is applied later, when {@link Amplify#configure(Context)} is invoked.
49+
*
50+
* @param okHttpClientBuilder An {@link OkHttpClient.Builder} instance
51+
*/
52+
void applyConfiguration(@NonNull OkHttpClient.Builder okHttpClientBuilder);
53+
}

aws-api/src/test/java/com/amplifyframework/api/aws/AWSApiPluginTest.java

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
import java.io.IOException;
4747
import java.util.Arrays;
4848
import java.util.Map;
49+
import java.util.concurrent.TimeUnit;
4950

5051
import io.reactivex.rxjava3.core.Observable;
5152
import okhttp3.HttpUrl;
@@ -55,6 +56,7 @@
5556
import okhttp3.ResponseBody;
5657
import okhttp3.mockwebserver.MockResponse;
5758
import okhttp3.mockwebserver.MockWebServer;
59+
import okhttp3.mockwebserver.RecordedRequest;
5860

5961
import static org.junit.Assert.assertEquals;
6062
import static org.junit.Assert.assertNotNull;
@@ -89,7 +91,17 @@ public void setup() throws ApiException, IOException, JSONException {
8991
.put("authorizationType", "API_KEY")
9092
.put("apiKey", "FAKE-API-KEY"));
9193

92-
this.plugin = new AWSApiPlugin();
94+
this.plugin = AWSApiPlugin.builder()
95+
.configureClient("graphQlApi", builder -> {
96+
builder.addInterceptor(chain -> {
97+
return chain.proceed(chain.request().newBuilder()
98+
.addHeader("specialKey", "specialValue")
99+
.build()
100+
);
101+
});
102+
builder.connectTimeout(10, TimeUnit.SECONDS);
103+
})
104+
.build();
93105
this.plugin.configure(configuration, ApplicationProvider.getApplicationContext());
94106
}
95107

@@ -248,4 +260,26 @@ public void singleConfiguredApiIsSelected() throws ApiException {
248260
String selectedApi = plugin.getSelectedApiName(EndpointType.GRAPHQL);
249261
assertEquals("graphQlApi", selectedApi);
250262
}
263+
264+
/**
265+
* Validates that the plugin adds custom headers into the outgoing OkHttp request.
266+
* @throws ApiException Thrown from the query() call.
267+
* @throws InterruptedException Possible thrown from takeRequest()
268+
*/
269+
@Test
270+
public void headerInterceptorsAreConfigured() throws ApiException, InterruptedException {
271+
// Arrange some response. This isn't the point of the test,
272+
// but it keeps the mock web server from freezing up.
273+
webServer.enqueue(new MockResponse()
274+
.setBody(Resources.readAsString("blog-owners-query-results.json")));
275+
276+
// Fire off a request
277+
Await.<GraphQLResponse<PaginatedResult<BlogOwner>>, ApiException>result((onResult, onError) ->
278+
plugin.query(ModelQuery.list(BlogOwner.class), onResult, onError)
279+
);
280+
281+
RecordedRequest recordedRequest = webServer.takeRequest(5, TimeUnit.MILLISECONDS);
282+
assertNotNull(recordedRequest);
283+
assertEquals("specialValue", recordedRequest.getHeader("specialKey"));
284+
}
251285
}

aws-api/src/test/java/com/amplifyframework/api/aws/auth/OwnerBasedAuthTest.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,9 @@ private void configurePlugin() throws ApiException {
127127
// This shouldn't happen...
128128
}
129129

130-
plugin = new AWSApiPlugin(providers);
130+
plugin = AWSApiPlugin.builder()
131+
.apiAuthProviders(providers)
132+
.build();
131133
plugin.configure(configuration, ApplicationProvider.getApplicationContext());
132134
}
133135

0 commit comments

Comments
 (0)