Skip to content

Commit d9ec312

Browse files
committed
Do zstd optional instead
1 parent bc96567 commit d9ec312

4 files changed

Lines changed: 129 additions & 39 deletions

File tree

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* ====================================================================
3+
* Licensed to the Apache Software Foundation (ASF) under one
4+
* or more contributor license agreements. See the NOTICE file
5+
* distributed with this work for additional information
6+
* regarding copyright ownership. The ASF licenses this file
7+
* to you under the Apache License, Version 2.0 (the
8+
* "License"); you may not use this file except in compliance
9+
* with the License. You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing,
14+
* software distributed under the License is distributed on an
15+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16+
* KIND, either express or implied. See the License for the
17+
* specific language governing permissions and limitations
18+
* under the License.
19+
* ====================================================================
20+
*
21+
* This software consists of voluntary contributions made by many
22+
* individuals on behalf of the Apache Software Foundation. For more
23+
* information on the Apache Software Foundation, please see
24+
* <http://www.apache.org/>.
25+
*
26+
*/
27+
package org.apache.hc.client5.http.impl;
28+
29+
30+
import org.apache.hc.core5.annotation.Contract;
31+
import org.apache.hc.core5.annotation.Internal;
32+
import org.apache.hc.core5.annotation.ThreadingBehavior;
33+
34+
/**
35+
* Utility to detect availability of the Zstandard JNI runtime on the classpath.
36+
* <p>
37+
* Used by the async client implementation to <em>conditionally</em> register
38+
* zstd encoders/decoders without creating a hard dependency on {@code zstd-jni}.
39+
* </p>
40+
*
41+
* <p>This class performs a lightweight reflective check and intentionally avoids
42+
* linking to JNI classes at compile time to prevent {@link LinkageError}s when
43+
* the optional dependency is absent.</p>
44+
*
45+
* @since 5.6
46+
*/
47+
@Internal
48+
@Contract(threading = ThreadingBehavior.STATELESS)
49+
public final class ZstdRuntime {
50+
51+
private static final String ZSTD = "com.github.luben.zstd.Zstd";
52+
53+
private ZstdRuntime() {
54+
}
55+
56+
/**
57+
* @return {@code true} if {@code com.github.luben.zstd.Zstd} can be loaded
58+
* by the current class loader; {@code false} otherwise
59+
*/
60+
public static boolean available() {
61+
try {
62+
Class.forName(ZSTD, false, ZstdRuntime.class.getClassLoader());
63+
return true;
64+
} catch (ClassNotFoundException | LinkageError ex) {
65+
return false;
66+
}
67+
}
68+
}
69+

httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/ContentCompressionAsyncExec.java

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,10 @@
2727
package org.apache.hc.client5.http.impl.async;
2828

2929
import java.io.IOException;
30+
import java.util.ArrayList;
3031
import java.util.Arrays;
3132
import java.util.LinkedHashMap;
33+
import java.util.List;
3234
import java.util.Locale;
3335
import java.util.Set;
3436
import java.util.function.UnaryOperator;
@@ -38,8 +40,8 @@
3840
import org.apache.hc.client5.http.async.AsyncExecChainHandler;
3941
import org.apache.hc.client5.http.async.methods.InflatingAsyncDataConsumer;
4042
import org.apache.hc.client5.http.async.methods.InflatingGzipDataConsumer;
41-
import org.apache.hc.client5.http.async.methods.InflatingZstdDataConsumer;
4243
import org.apache.hc.client5.http.entity.compress.ContentCoding;
44+
import org.apache.hc.client5.http.impl.ZstdRuntime;
4345
import org.apache.hc.client5.http.protocol.HttpClientContext;
4446
import org.apache.hc.core5.annotation.Contract;
4547
import org.apache.hc.core5.annotation.Internal;
@@ -81,18 +83,32 @@ public ContentCompressionAsyncExec(
8183
*/
8284
public ContentCompressionAsyncExec() {
8385
final LinkedHashMap<String, UnaryOperator<AsyncDataConsumer>> map = new LinkedHashMap<>();
84-
map.put(ContentCoding.DEFLATE.token(),
85-
d -> new InflatingAsyncDataConsumer(d, null));
86+
map.put(ContentCoding.DEFLATE.token(), d -> new InflatingAsyncDataConsumer(d, null));
8687
map.put(ContentCoding.GZIP.token(), InflatingGzipDataConsumer::new);
8788
map.put(ContentCoding.X_GZIP.token(), InflatingGzipDataConsumer::new);
88-
map.put(ContentCoding.ZSTD.token(), InflatingZstdDataConsumer::new);
89-
90-
this.decoders = RegistryBuilder.<UnaryOperator<AsyncDataConsumer>>create()
91-
.register(ContentCoding.GZIP.token(), map.get(ContentCoding.GZIP.token()))
92-
.register(ContentCoding.X_GZIP.token(), map.get(ContentCoding.X_GZIP.token()))
93-
.register(ContentCoding.DEFLATE.token(), map.get(ContentCoding.DEFLATE.token()))
94-
.register(ContentCoding.ZSTD.token(), map.get(ContentCoding.ZSTD.token()))
95-
.build();
89+
90+
final RegistryBuilder<UnaryOperator<AsyncDataConsumer>> rb =
91+
RegistryBuilder.<UnaryOperator<AsyncDataConsumer>>create()
92+
.register(ContentCoding.GZIP.token(), InflatingGzipDataConsumer::new)
93+
.register(ContentCoding.X_GZIP.token(), InflatingGzipDataConsumer::new)
94+
.register(ContentCoding.DEFLATE.token(), d -> new InflatingAsyncDataConsumer(d, null));
95+
96+
// Add zstd only when zstd-jni is present
97+
if (ZstdRuntime.available()) {
98+
// Use reflection to avoid hard-linking InflatingZstdDataConsumer when absent
99+
rb.register(ContentCoding.ZSTD.token(), downstream -> {
100+
try {
101+
final Class<?> c = Class.forName(
102+
"org.apache.hc.client5.http.async.methods.InflatingZstdDataConsumer");
103+
return (AsyncDataConsumer) c.getConstructor(AsyncDataConsumer.class)
104+
.newInstance(downstream);
105+
} catch (ReflectiveOperationException | LinkageError e) {
106+
throw new IllegalStateException("Zstd support not available at runtime", e);
107+
}
108+
});
109+
}
110+
111+
this.decoders = rb.build();
96112
}
97113

98114

@@ -108,8 +124,11 @@ public void execute(
108124
final boolean enabled = ctx.getRequestConfigOrDefault().isContentCompressionEnabled();
109125

110126
if (enabled && !request.containsHeader(HttpHeaders.ACCEPT_ENCODING)) {
111-
request.addHeader(MessageSupport.headerOfTokens(
112-
HttpHeaders.ACCEPT_ENCODING, Arrays.asList("gzip", "x-gzip", "deflate", "zstd")));
127+
final List<String> tokens = new ArrayList<>(Arrays.asList("gzip", "x-gzip", "deflate"));
128+
if (ZstdRuntime.available()) {
129+
tokens.add("zstd");
130+
}
131+
request.addHeader(MessageSupport.headerOfTokens(HttpHeaders.ACCEPT_ENCODING, tokens));
113132
}
114133

115134
chain.proceed(request, producer, scope, new AsyncExecCallback() {

httpclient5/src/test/java/org/apache/hc/client5/http/examples/AsyncClientZstdCompressionExample.java

Lines changed: 27 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,13 @@
2727
package org.apache.hc.client5.http.examples;
2828

2929
import java.io.ByteArrayOutputStream;
30+
import java.io.IOException;
3031
import java.io.InputStream;
32+
import java.io.OutputStream;
3133
import java.nio.charset.StandardCharsets;
3234
import java.util.concurrent.Future;
3335

34-
import org.apache.commons.compress.compressors.CompressorInputStream;
36+
import org.apache.commons.compress.compressors.CompressorException;
3537
import org.apache.commons.compress.compressors.CompressorStreamFactory;
3638
import org.apache.hc.client5.http.async.methods.DeflatingZstdEntityProducer;
3739
import org.apache.hc.client5.http.async.methods.SimpleHttpRequest;
@@ -42,14 +44,14 @@
4244
import org.apache.hc.core5.http.ClassicHttpRequest;
4345
import org.apache.hc.core5.http.ClassicHttpResponse;
4446
import org.apache.hc.core5.http.ContentType;
45-
import org.apache.hc.core5.http.HttpEntity;
4647
import org.apache.hc.core5.http.HttpHeaders;
4748
import org.apache.hc.core5.http.HttpResponse;
4849
import org.apache.hc.core5.http.HttpStatus;
4950
import org.apache.hc.core5.http.Message;
5051
import org.apache.hc.core5.http.impl.bootstrap.HttpServer;
5152
import org.apache.hc.core5.http.impl.bootstrap.ServerBootstrap;
5253
import org.apache.hc.core5.http.io.HttpRequestHandler;
54+
import org.apache.hc.core5.http.io.entity.ByteArrayEntity;
5355
import org.apache.hc.core5.http.io.entity.StringEntity;
5456
import org.apache.hc.core5.http.nio.entity.StringAsyncEntityConsumer;
5557
import org.apache.hc.core5.http.nio.entity.StringAsyncEntityProducer;
@@ -151,36 +153,35 @@ public static void main(final String[] args) throws Exception {
151153
* Classic echo handler that decodes request Content-Encoding: zstd and returns plain text.
152154
*/
153155
private static final class EchoHandler implements HttpRequestHandler {
156+
154157
@Override
155-
public void handle(final ClassicHttpRequest request,
156-
final ClassicHttpResponse response,
157-
final HttpContext context) {
158-
try {
159-
final HttpEntity ent = request.getEntity();
160-
final byte[] body;
161-
if (ent != null && hasZstd(ent)) {
162-
try (InputStream in = ent.getContent();
163-
CompressorInputStream zin = new CompressorStreamFactory()
164-
.createCompressorInputStream("zstd", in)) {
165-
body = readAll(zin);
166-
}
167-
} else {
168-
body = ent != null ? readAll(ent.getContent()) : new byte[0];
169-
}
170-
final String text = new String(body, StandardCharsets.UTF_8);
158+
public void handle(
159+
final ClassicHttpRequest request,
160+
final ClassicHttpResponse response,
161+
final HttpContext context) throws IOException {
162+
163+
try (InputStream in = new CompressorStreamFactory()
164+
.createCompressorInputStream(ContentCoding.ZSTD.token(), request.getEntity().getContent())) {
165+
166+
final byte[] data = readAll(in);
167+
final String text = new String(data, StandardCharsets.UTF_8);
168+
171169
response.setCode(HttpStatus.SC_OK);
172-
response.setEntity(new StringEntity("echo: " + text, ContentType.TEXT_PLAIN));
173-
} catch (final Exception ex) {
170+
response.addHeader("Content-Encoding", ContentCoding.ZSTD.token());
171+
172+
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
173+
try (final OutputStream zstdOut = new CompressorStreamFactory()
174+
.createCompressorOutputStream("zstd", baos)) {
175+
zstdOut.write(text.getBytes(StandardCharsets.UTF_8));
176+
}
177+
response.setEntity(new ByteArrayEntity(baos.toByteArray(), ContentType.TEXT_PLAIN));
178+
} catch (final CompressorException ex) {
174179
response.setCode(HttpStatus.SC_BAD_REQUEST);
175-
response.setEntity(new StringEntity("Bad request", ContentType.TEXT_PLAIN));
180+
response.setEntity(new StringEntity("Unable to process compressed payload", StandardCharsets.UTF_8));
176181
}
177182
}
178183

179-
private static boolean hasZstd(final HttpEntity e) {
180-
return "zstd".equalsIgnoreCase(e.getContentEncoding());
181-
}
182-
183-
private static byte[] readAll(final InputStream in) throws Exception {
184+
private static byte[] readAll(final InputStream in) throws IOException {
184185
final ByteArrayOutputStream bos = new ByteArrayOutputStream();
185186
final byte[] buf = new byte[8192];
186187
int n;

pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,7 @@
215215
<groupId>com.github.luben</groupId>
216216
<artifactId>zstd-jni</artifactId>
217217
<version>${zstd.jni.version}</version>
218+
<optional>true</optional>
218219
</dependency>
219220
</dependencies>
220221
</dependencyManagement>

0 commit comments

Comments
 (0)