Skip to content

Commit c7e8775

Browse files
committed
feat: Support zstd compression via zstd-jni.
1 parent b414bd8 commit c7e8775

21 files changed

+539
-53
lines changed

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ implementation 'io.github.nstdio:http-client-ext:2.3.2'
3535
### Features
3636

3737
- [Caching](#Caching), both in memory and disk.
38-
- [Decompression](#Decompression): `br, gzip, deflate`
38+
- [Decompression](#Decompression): `br, zstd, gzip, deflate`
3939
- [JSON](#JSON) mappings
4040

4141
### Caching
@@ -127,11 +127,12 @@ HttpRequest request = HttpRequest.newBuilder(uri)
127127

128128
HttpResponse<String> response = client.send(request, BodyHandlers.ofDecompressing(ofString()));
129129
```
130-
Out of the box support for `gzip` and `deflate` is provided by JDK itself. For `br` (brotli) compression please add
131-
one of following dependencies to your project:
130+
Out of the box support for `gzip` and `deflate` is provided by JDK itself. For `br` (brotli) or `zstd` compression
131+
please add one of following dependencies to your project:
132132

133133
- [org.brotli:dec](https://mvnrepository.com/artifact/org.brotli/dec/0.1.2)
134134
- [Brotli4j](https://github.com/hyperxpro/Brotli4j)
135+
- [zstd-jni](https://github.com/luben/zstd-jni)
135136

136137
service loader will pick up correct dependency. If none of these preferred there is always an options to extend via [SPI](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/ServiceLoader.html)
137138
by providing [CompressionFactory](https://github.com/nstdio/http-client-ext/blob/main/src/main/java/io/github/nstdio/http/ext/spi/CompressionFactory.java)

buildSrc/src/main/kotlin/io.github.nstdio.http.ext.test-conventions.gradle.kts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (C) 2022 Edgar Asatryan
2+
* Copyright (C) 2022, 2025 Edgar Asatryan
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -70,6 +70,7 @@ val slf4jVersion = "2.0.16"
7070
val jacksonVersion = "2.18.2"
7171
val brotli4JVersion = "1.18.0"
7272
val brotliOrgVersion = "0.1.2"
73+
val zstdJniVersion = "1.5.6-9"
7374
val gsonVersion = "2.11.0"
7475
val equalsverifierVersion = "3.18.1"
7576
val coroutinesVersion = "1.10.1"
@@ -82,6 +83,7 @@ val jsonLibs = mapOf(
8283
val spiDeps = listOf(
8384
"org.brotli:dec:$brotliOrgVersion",
8485
"com.aayushatharva.brotli4j:brotli4j:$brotli4JVersion",
86+
"com.github.luben:zstd-jni:$zstdJniVersion",
8587
"com.google.code.gson:gson:$gsonVersion",
8688
"com.fasterxml.jackson.core:jackson-databind:$jacksonVersion"
8789
)

src/main/java/io/github/nstdio/http/ext/DecompressingBodyHandler.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (C) 2022 Edgar Asatryan
2+
* Copyright (C) 2022, 2025 Edgar Asatryan
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -40,7 +40,7 @@ class DecompressingBodyHandler<T> implements BodyHandler<T> {
4040
private static final String UNSUPPORTED_DIRECTIVE = "Compression directive '%s' is not supported";
4141
private static final String UNKNOWN_DIRECTIVE = "Unknown compression directive '%s'";
4242
private static final UnaryOperator<InputStream> IDENTITY = UnaryOperator.identity();
43-
private static final Set<String> WELL_KNOWN_DIRECTIVES = Set.of("gzip", "x-gzip", "br", "compress", "deflate", "identity");
43+
private static final Set<String> WELL_KNOWN_DIRECTIVES = Set.of("gzip", "x-gzip", "br", "compress", "deflate", "identity", "zstd");
4444

4545
private final BodyHandler<T> original;
4646
private final Options options;

src/main/java/io/github/nstdio/http/ext/spi/Classpath.java

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (C) 2022 Edgar Asatryan
2+
* Copyright (C) 2022, 2025 Edgar Asatryan
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -19,8 +19,6 @@
1919
public class Classpath {
2020
private static final boolean JACKSON = isPresent("com.fasterxml.jackson.databind.ObjectMapper");
2121
private static final boolean GSON = isPresent("com.google.gson.Gson");
22-
private static final boolean ORG_BROTLI = isPresent("org.brotli.dec.BrotliInputStream");
23-
private static final boolean BROTLI_4J = isPresent("com.aayushatharva.brotli4j.Brotli4jLoader");
2422

2523
private Classpath() {
2624
}
@@ -34,16 +32,20 @@ public static boolean isGsonPresent() {
3432
}
3533

3634
public static boolean isOrgBrotliPresent() {
37-
return ORG_BROTLI;
35+
return isPresent("org.brotli.dec.BrotliInputStream");
3836
}
3937

4038
public static boolean isBrotli4jPresent() {
41-
return BROTLI_4J;
39+
return isPresent("com.aayushatharva.brotli4j.Brotli4jLoader");
40+
}
41+
42+
public static boolean isZstdJniPresent() {
43+
return isPresent("com.github.luben.zstd.ZstdInputStream");
4244
}
4345

4446
public static boolean isPresent(String cls) {
4547
try {
46-
Class.forName(cls);
48+
Class.forName(cls, false, Thread.currentThread().getContextClassLoader());
4749
return true;
4850
} catch (ClassNotFoundException e) {
4951
return false;
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright (C) 2025 Edgar Asatryan
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+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.github.nstdio.http.ext.spi;
17+
18+
import java.io.IOException;
19+
import java.io.InputStream;
20+
import java.util.List;
21+
22+
class DelegatingCompressionFactory extends CompressionFactoryBase {
23+
24+
private final CompressionFactory delegate;
25+
26+
DelegatingCompressionFactory(CompressionFactory delegate) {
27+
this.delegate = delegate;
28+
}
29+
30+
@Override
31+
InputStream doDecompressing(InputStream in, String type) throws IOException {
32+
if (delegate == null) {
33+
throw new IllegalStateException("No delegate available");
34+
}
35+
36+
return delegate.decompressing(in, type);
37+
}
38+
39+
@Override
40+
public List<String> supported() {
41+
return delegate != null ? delegate.supported() : List.of();
42+
}
43+
}
Lines changed: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (C) 2022 Edgar Asatryan
2+
* Copyright (C) 2022, 2025 Edgar Asatryan
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,34 +16,22 @@
1616

1717
package io.github.nstdio.http.ext.spi;
1818

19-
import java.io.IOException;
20-
import java.io.InputStream;
21-
import java.util.List;
22-
2319
import static io.github.nstdio.http.ext.spi.Classpath.isBrotli4jPresent;
2420
import static io.github.nstdio.http.ext.spi.Classpath.isOrgBrotliPresent;
2521

26-
public class OptionalBrotliCompressionFactory extends CompressionFactoryBase {
27-
28-
private final CompressionFactory delegate;
22+
public class OptionalBrotliCompressionFactory extends DelegatingCompressionFactory {
2923

3024
public OptionalBrotliCompressionFactory() {
25+
super(getDelegate());
26+
}
27+
28+
private static CompressionFactory getDelegate() {
3129
if (isOrgBrotliPresent()) {
32-
delegate = new BrotliOrgCompressionFactory();
30+
return new BrotliOrgCompressionFactory();
3331
} else if (isBrotli4jPresent()) {
34-
delegate = new Brotli4JCompressionFactory();
35-
} else {
36-
delegate = null;
32+
return new Brotli4JCompressionFactory();
3733
}
38-
}
39-
40-
@Override
41-
public List<String> supported() {
42-
return delegate != null ? delegate.supported() : List.of();
43-
}
4434

45-
@Override
46-
InputStream doDecompressing(InputStream in, String type) throws IOException {
47-
return delegate.decompressing(in, type);
35+
return null;
4836
}
4937
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Copyright (C) 2025 Edgar Asatryan
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+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.github.nstdio.http.ext.spi;
18+
19+
import static io.github.nstdio.http.ext.spi.Classpath.isZstdJniPresent;
20+
21+
public class OptionalZstdCompressionFactory extends DelegatingCompressionFactory {
22+
23+
public OptionalZstdCompressionFactory() {
24+
super(isZstdJniPresent() ? new ZstdJniCompressionFactory() : null);
25+
}
26+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright (C) 2025 Edgar Asatryan
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+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.github.nstdio.http.ext.spi;
17+
18+
import com.github.luben.zstd.ZstdInputStream;
19+
import java.io.IOException;
20+
import java.io.InputStream;
21+
import java.util.List;
22+
23+
final class ZstdJniCompressionFactory implements CompressionFactory {
24+
25+
private final List<String> supported = List.of("zstd");
26+
27+
@Override
28+
public List<String> supported() {
29+
return supported;
30+
}
31+
32+
@Override
33+
public InputStream decompressing(InputStream in, String type) throws IOException {
34+
return new ZstdInputStream(in);
35+
}
36+
}

src/main/java/module-info.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (C) 2022 Edgar Asatryan
2+
* Copyright (C) 2022, 2025 Edgar Asatryan
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -19,6 +19,7 @@
1919
import io.github.nstdio.http.ext.spi.JdkCompressionFactory;
2020
import io.github.nstdio.http.ext.spi.JsonMappingProvider;
2121
import io.github.nstdio.http.ext.spi.OptionalBrotliCompressionFactory;
22+
import io.github.nstdio.http.ext.spi.OptionalZstdCompressionFactory;
2223

2324
module http.client.ext {
2425
uses CompressionFactory;
@@ -28,6 +29,7 @@
2829

2930
requires static com.aayushatharva.brotli4j;
3031
requires static org.brotli.dec;
32+
requires static com.github.luben.zstd_jni;
3133

3234
requires static transitive com.fasterxml.jackson.core;
3335
requires static transitive com.fasterxml.jackson.databind;
@@ -38,5 +40,6 @@
3840

3941
provides CompressionFactory with JdkCompressionFactory,
4042
IdentityCompressionFactory,
41-
OptionalBrotliCompressionFactory;
43+
OptionalBrotliCompressionFactory,
44+
OptionalZstdCompressionFactory;
4245
}

src/main/resources/META-INF/services/io.github.nstdio.http.ext.spi.CompressionFactory

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#
2-
# Copyright (C) 2022 Edgar Asatryan
2+
# Copyright (C) 2022, 2025 Edgar Asatryan
33
#
44
# Licensed under the Apache License, Version 2.0 (the "License");
55
# you may not use this file except in compliance with the License.
@@ -16,4 +16,5 @@
1616

1717
io.github.nstdio.http.ext.spi.JdkCompressionFactory
1818
io.github.nstdio.http.ext.spi.IdentityCompressionFactory
19-
io.github.nstdio.http.ext.spi.OptionalBrotliCompressionFactory
19+
io.github.nstdio.http.ext.spi.OptionalBrotliCompressionFactory
20+
io.github.nstdio.http.ext.spi.OptionalZstdCompressionFactory

0 commit comments

Comments
 (0)