Skip to content

Commit 2e5fda6

Browse files
committed
DEVEXP-562 Can now prevent Accept-Encoding=gzip header being used
DatabaseClientFactory also supports multiple configurators now. This was essential for testing this new story, but it also aligns with what a user would expect a method named "add" to do (as opposed to "set"). `clearConfigurators` was also added both for testing purposes and so a user could remove all the configurators in case one or more should no longer be used.
1 parent a712d7c commit 2e5fda6

File tree

8 files changed

+165
-16
lines changed

8 files changed

+165
-16
lines changed

examples/src/main/java/com/marklogic/client/example/cookbook/ConfigureOkHttp.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@ public class ConfigureOkHttp {
2222
* header should not result in losing any other values besides "gzip" for the header. You are free to
2323
* customize this as you wish though; this is primarily intended as an example for how to customize OkHttp
2424
* when using the MarkLogic Java Client.
25-
*/
25+
*
26+
* As of Java Client 6.3.0, this can now be accomplished via the {@code DatabaseClientFactory} class and
27+
* {@code RemoveAcceptEncodingConfigurator}.
28+
*/
2629
public static void removeAcceptEncodingGzipHeader() {
2730
DatabaseClientFactory.addConfigurator(new OkHttpClientConfigurator() {
2831
@Override

marklogic-client-api/src/main/java/com/marklogic/client/DatabaseClientBuilder.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,6 @@ public DatabaseClientBuilder withCloudAuth(String apiKey, String basePath) {
148148
}
149149

150150
/**
151-
*
152151
* @param apiKey
153152
* @param basePath
154153
* @param tokenDuration length in minutes until the generated access token expires
@@ -174,7 +173,6 @@ public DatabaseClientBuilder withCertificateAuth(String file, String password) {
174173
}
175174

176175
/**
177-
*
178176
* @param sslContext
179177
* @param trustManager
180178
* @return
@@ -250,6 +248,17 @@ public DatabaseClientBuilder withSSLHostnameVerifier(DatabaseClientFactory.SSLHo
250248
props.put(PREFIX + "sslHostnameVerifier", sslHostnameVerifier);
251249
return this;
252250
}
251+
252+
/**
253+
* Prevents the underlying OkHttp library from sending an "Accept-Encoding-gzip" request header on each request.
254+
*
255+
* @return
256+
* @since 6.3.0
257+
*/
258+
public DatabaseClientBuilder withGzippedResponsesDisabled() {
259+
props.put(PREFIX + "disableGzippedResponses", true);
260+
return this;
261+
}
253262
}
254263

255264

marklogic-client-api/src/main/java/com/marklogic/client/DatabaseClientFactory.java

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@
6363
*/
6464
public class DatabaseClientFactory {
6565

66-
static private ClientConfigurator<?> clientConfigurator;
66+
static private List<ClientConfigurator<?>> clientConfigurators = new ArrayList<>();
6767
static private HandleFactoryRegistry handleRegistry =
6868
HandleFactoryRegistryImpl.newDefault();
6969

@@ -1273,6 +1273,9 @@ public String getCertificatePassword() {
12731273
* <li>marklogic.client.basePath = must be a String</li>
12741274
* <li>marklogic.client.database = must be a String</li>
12751275
* <li>marklogic.client.connectionType = must be a String or instance of {@code ConnectionType}</li>
1276+
* <li>marklogic.client.disableGzippedResponses = can be a String or Boolean; if "true" or true, the client
1277+
* will not send an "Accept-Encoding" request header with a value of "gzip" on each request; supported
1278+
* since 6.3.0.</li>
12761279
* <li>marklogic.client.securityContext = an instance of {@code SecurityContext}; if set, then all other
12771280
* authentication properties pertaining to the construction of a {@code SecurityContext} will be ignored,
12781281
* including the properties pertaining to SSL; this is effectively an escape hatch for providing a
@@ -1285,6 +1288,8 @@ public String getCertificatePassword() {
12851288
* <li>marklogic.client.certificate.file = must be a String; optional for certificate authentication</li>
12861289
* <li>marklogic.client.certificate.password = must be a String; optional for certificate authentication</li>
12871290
* <li>marklogic.client.cloud.apiKey = must be a String; required for cloud authentication</li>
1291+
* <li>marklogic.client.cloud.tokenDuration = must be a number; optional for configuring the duration in
1292+
* minutes for which an access token lasts; supported since 6.3.0.</li>
12881293
* <li>marklogic.client.kerberos.principal = must be a String; required for Kerberos authentication</li>
12891294
* <li>marklogic.client.saml.token = must be a String; required for SAML authentication</li>
12901295
* <li>marklogic.client.sslContext = must be an instance of {@code javax.net.ssl.SSLContext}</li>
@@ -1408,17 +1413,19 @@ static public DatabaseClient newClient(String host, int port, String basePath, S
14081413
}
14091414
services.connect(host, port, basePath, database, securityContext);
14101415

1411-
if (clientConfigurator != null) {
1412-
if (clientConfigurator instanceof OkHttpClientConfigurator) {
1413-
OkHttpClient okHttpClient = (OkHttpClient) services.getClientImplementation();
1414-
OkHttpClient.Builder clientBuilder = okHttpClient.newBuilder();
1415-
((OkHttpClientConfigurator) clientConfigurator).configure(clientBuilder);
1416-
((OkHttpServices) services).setClientImplementation(clientBuilder.build());
1417-
} else if (clientConfigurator instanceof HttpClientConfigurator) {
1418-
// do nothing as we no longer use HttpClient so there's nothing this can configure
1419-
} else {
1420-
throw new IllegalArgumentException("A ClientConfigurator must implement OkHttpClientConfigurator");
1421-
}
1416+
if (clientConfigurators != null) {
1417+
clientConfigurators.forEach(configurator -> {
1418+
if (configurator instanceof OkHttpClientConfigurator) {
1419+
OkHttpClient okHttpClient = (OkHttpClient) services.getClientImplementation();
1420+
OkHttpClient.Builder clientBuilder = okHttpClient.newBuilder();
1421+
((OkHttpClientConfigurator) configurator).configure(clientBuilder);
1422+
((OkHttpServices) services).setClientImplementation(clientBuilder.build());
1423+
} else if (configurator instanceof HttpClientConfigurator) {
1424+
// do nothing as we no longer use HttpClient so there's nothing this can configure
1425+
} else {
1426+
throw new IllegalArgumentException("A ClientConfigurator must implement OkHttpClientConfigurator");
1427+
}
1428+
});
14221429
}
14231430

14241431
DatabaseClientImpl client = new DatabaseClientImpl(
@@ -1584,6 +1591,10 @@ static public void registerDefaultHandles() {
15841591
/**
15851592
* Adds a listener that provides custom configuration when a communication library
15861593
* is created.
1594+
*
1595+
* As of 6.3.0, this method can now be called multiple times. When a {@code DatabaseClient} is constructed,
1596+
* configurators will be invoked in the order they were passed in.
1597+
*
15871598
* @see com.marklogic.client.extra.okhttpclient.OkHttpClientConfigurator
15881599
* @param configurator the listener for configuring the communication library
15891600
*/
@@ -1594,7 +1605,16 @@ static public void addConfigurator(ClientConfigurator<?> configurator) {
15941605
);
15951606
}
15961607

1597-
clientConfigurator = configurator;
1608+
clientConfigurators.add(configurator);
1609+
}
1610+
1611+
/**
1612+
* Removes any instances of {@code ClientConfigurator} that were passed in via {@code addConfigurator}.
1613+
*
1614+
* @since 6.3.0
1615+
*/
1616+
static public void removeConfigurators() {
1617+
clientConfigurators.clear();
15981618
}
15991619

16001620
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.marklogic.client.extra.okhttpclient;
2+
3+
import okhttp3.OkHttpClient;
4+
import okhttp3.Request;
5+
6+
/**
7+
* Can be used with {@code DatabaseClientFactory.addConfigurator} to remove the "Accept-Encoding=gzip" request header
8+
* that the underlying OkHttp library adds by default. This is useful in a scenario where many small HTTP responses
9+
* are expected to be returned by MarkLogic, and thus the costs of gzipping the responses may outweigh the benefits.
10+
*
11+
* @since 6.3.0
12+
*/
13+
public class RemoveAcceptEncodingConfigurator implements OkHttpClientConfigurator {
14+
15+
@Override
16+
public void configure(OkHttpClient.Builder builder) {
17+
builder.addNetworkInterceptor(chain -> {
18+
Request newRequest = chain.request().newBuilder().removeHeader("Accept-Encoding").build();
19+
return chain.proceed(newRequest);
20+
});
21+
}
22+
}

marklogic-client-api/src/main/java/com/marklogic/client/impl/DatabaseClientPropertySource.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import com.marklogic.client.DatabaseClient;
1919
import com.marklogic.client.DatabaseClientBuilder;
2020
import com.marklogic.client.DatabaseClientFactory;
21+
import com.marklogic.client.extra.okhttpclient.RemoveAcceptEncodingConfigurator;
2122
import org.slf4j.Logger;
2223
import org.slf4j.LoggerFactory;
2324

@@ -91,6 +92,17 @@ public class DatabaseClientPropertySource {
9192
throw new IllegalArgumentException("Connection type must either be a String or an instance of ConnectionType");
9293
}
9394
});
95+
connectionPropertyHandlers.put(PREFIX + "disableGzippedResponses", (bean, value) -> {
96+
boolean disableGzippedResponses = false;
97+
if (value instanceof Boolean && Boolean.TRUE.equals(value)) {
98+
disableGzippedResponses = true;
99+
} else if (value instanceof String) {
100+
disableGzippedResponses = Boolean.parseBoolean((String)value);
101+
}
102+
if (disableGzippedResponses) {
103+
DatabaseClientFactory.addConfigurator(new RemoveAcceptEncodingConfigurator());
104+
}
105+
});
94106
}
95107

96108
public DatabaseClientPropertySource(Function<String, Object> propertySource) {

marklogic-client-api/src/test/java/com/marklogic/client/impl/DatabaseClientPropertySourceTest.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,21 @@ void cloudWithNonNumericDuration() {
108108
assertEquals("Cloud token duration must be numeric", ex.getMessage());
109109
}
110110

111+
@Test
112+
void disableGzippedResponses() {
113+
final String prop = PREFIX + "disableGzippedResponses";
114+
115+
props.put(PREFIX + prop, "true");
116+
// Won't throw an error, but we can't verify the results because the list of configurators in
117+
// DatabaseClientFactory is private.
118+
buildBean();
119+
120+
// Verifying this doesn't throw an error either; the impl should be using Boolean.parseBoolean which only cares
121+
// if the value equals 'true'.
122+
props.put(prop, "123");
123+
buildBean();
124+
}
125+
111126
private DatabaseClientFactory.Bean buildBean() {
112127
DatabaseClientPropertySource source = new DatabaseClientPropertySource(propertyName -> props.get(propertyName));
113128
return source.newClientBean();

marklogic-client-api/src/test/java/com/marklogic/client/test/DatabaseClientFactoryTest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ public void testConfigurator() {
168168
assertEquals(testConnectTimeoutMillis, okClient.connectTimeoutMillis());
169169
} finally {
170170
client.release();
171+
DatabaseClientFactory.removeConfigurators();
171172
}
172173
}
173174

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package com.marklogic.client.test.extra;
2+
3+
import com.marklogic.client.DatabaseClientFactory;
4+
import com.marklogic.client.extra.okhttpclient.OkHttpClientConfigurator;
5+
import com.marklogic.client.test.Common;
6+
import com.marklogic.client.test.junit5.RequiresML11;
7+
import okhttp3.Interceptor;
8+
import okhttp3.Response;
9+
import org.jetbrains.annotations.NotNull;
10+
import org.junit.jupiter.api.AfterEach;
11+
import org.junit.jupiter.api.Test;
12+
import org.junit.jupiter.api.extension.ExtendWith;
13+
14+
import java.io.IOException;
15+
16+
import static org.junit.jupiter.api.Assertions.assertEquals;
17+
import static org.junit.jupiter.api.Assertions.assertNull;
18+
19+
@ExtendWith(RequiresML11.class)
20+
public class DisableGzippedResponsesTest {
21+
22+
@AfterEach
23+
void afterEach() {
24+
DatabaseClientFactory.removeConfigurators();
25+
}
26+
27+
@Test
28+
void test() {
29+
// Add a DatabaseClientFactory configurator that uses an OkHttp interceptor to capture the value of the
30+
// Content-Encoding response header. That is the easiest way to verify whether the Accept-Encoding:gzip request
31+
// header is being sent or not.
32+
TestInterceptor testInterceptor = new TestInterceptor();
33+
DatabaseClientFactory.addConfigurator((OkHttpClientConfigurator) builder -> builder.addNetworkInterceptor(testInterceptor));
34+
35+
36+
final String testUri = "/optic/test/musician1.json";
37+
38+
Common.newClient().newJSONDocumentManager().read(testUri);
39+
assertEquals("gzip", testInterceptor.contentEncoding, "MarkLogic 11 now supports the Accept-Encoding:gzip " +
40+
"header. The OkHttp library used by the Java Client includes this request header by default. So we " +
41+
"expect for responses from the REST API to be gzipped, which is indicated by the Content-Encoding " +
42+
"response header having a value of 'gzip'.");
43+
44+
Common.newClientBuilder().withGzippedResponsesDisabled().build()
45+
.newJSONDocumentManager().read(testUri);
46+
assertNull(testInterceptor.contentEncoding, "When a DatabaseClient is constructed with gzipped responses " +
47+
"disabled, the Accept-Encoding header added automatically by OkHttp should be removed before the request " +
48+
"is sent to MarkLogic. This prevents MarkLogic from gzipping the response, which is indicated by the " +
49+
"HTTP response not having a 'Content-Encoding' header.");
50+
}
51+
52+
/**
53+
* Used to capture the value of the Content-Encoding response header.
54+
*/
55+
private static class TestInterceptor implements Interceptor {
56+
57+
String contentEncoding;
58+
59+
@NotNull
60+
@Override
61+
public Response intercept(@NotNull Chain chain) throws IOException {
62+
Response response = chain.proceed(chain.request());
63+
contentEncoding = response.header("Content-Encoding");
64+
return response;
65+
}
66+
}
67+
}

0 commit comments

Comments
 (0)