Skip to content

Commit

Permalink
feat: Support zstd compression via zstd-jni.
Browse files Browse the repository at this point in the history
  • Loading branch information
nstdio committed Jan 26, 2025
1 parent b414bd8 commit c7e8775
Show file tree
Hide file tree
Showing 21 changed files with 539 additions and 53 deletions.
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ implementation 'io.github.nstdio:http-client-ext:2.3.2'
### Features

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

### Caching
Expand Down Expand Up @@ -127,11 +127,12 @@ HttpRequest request = HttpRequest.newBuilder(uri)

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

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

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)
by providing [CompressionFactory](https://github.com/nstdio/http-client-ext/blob/main/src/main/java/io/github/nstdio/http/ext/spi/CompressionFactory.java)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2022 Edgar Asatryan
* Copyright (C) 2022, 2025 Edgar Asatryan
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -70,6 +70,7 @@ val slf4jVersion = "2.0.16"
val jacksonVersion = "2.18.2"
val brotli4JVersion = "1.18.0"
val brotliOrgVersion = "0.1.2"
val zstdJniVersion = "1.5.6-9"
val gsonVersion = "2.11.0"
val equalsverifierVersion = "3.18.1"
val coroutinesVersion = "1.10.1"
Expand All @@ -82,6 +83,7 @@ val jsonLibs = mapOf(
val spiDeps = listOf(
"org.brotli:dec:$brotliOrgVersion",
"com.aayushatharva.brotli4j:brotli4j:$brotli4JVersion",
"com.github.luben:zstd-jni:$zstdJniVersion",
"com.google.code.gson:gson:$gsonVersion",
"com.fasterxml.jackson.core:jackson-databind:$jacksonVersion"
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2022 Edgar Asatryan
* Copyright (C) 2022, 2025 Edgar Asatryan
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -40,7 +40,7 @@ class DecompressingBodyHandler<T> implements BodyHandler<T> {
private static final String UNSUPPORTED_DIRECTIVE = "Compression directive '%s' is not supported";
private static final String UNKNOWN_DIRECTIVE = "Unknown compression directive '%s'";
private static final UnaryOperator<InputStream> IDENTITY = UnaryOperator.identity();
private static final Set<String> WELL_KNOWN_DIRECTIVES = Set.of("gzip", "x-gzip", "br", "compress", "deflate", "identity");
private static final Set<String> WELL_KNOWN_DIRECTIVES = Set.of("gzip", "x-gzip", "br", "compress", "deflate", "identity", "zstd");

private final BodyHandler<T> original;
private final Options options;
Expand Down
14 changes: 8 additions & 6 deletions src/main/java/io/github/nstdio/http/ext/spi/Classpath.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2022 Edgar Asatryan
* Copyright (C) 2022, 2025 Edgar Asatryan
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -19,8 +19,6 @@
public class Classpath {
private static final boolean JACKSON = isPresent("com.fasterxml.jackson.databind.ObjectMapper");
private static final boolean GSON = isPresent("com.google.gson.Gson");
private static final boolean ORG_BROTLI = isPresent("org.brotli.dec.BrotliInputStream");
private static final boolean BROTLI_4J = isPresent("com.aayushatharva.brotli4j.Brotli4jLoader");

private Classpath() {
}
Expand All @@ -34,16 +32,20 @@ public static boolean isGsonPresent() {
}

public static boolean isOrgBrotliPresent() {
return ORG_BROTLI;
return isPresent("org.brotli.dec.BrotliInputStream");
}

public static boolean isBrotli4jPresent() {
return BROTLI_4J;
return isPresent("com.aayushatharva.brotli4j.Brotli4jLoader");
}

public static boolean isZstdJniPresent() {
return isPresent("com.github.luben.zstd.ZstdInputStream");
}

public static boolean isPresent(String cls) {
try {
Class.forName(cls);
Class.forName(cls, false, Thread.currentThread().getContextClassLoader());
return true;
} catch (ClassNotFoundException e) {
return false;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright (C) 2025 Edgar Asatryan
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.github.nstdio.http.ext.spi;

import java.io.IOException;
import java.io.InputStream;
import java.util.List;

class DelegatingCompressionFactory extends CompressionFactoryBase {

private final CompressionFactory delegate;

DelegatingCompressionFactory(CompressionFactory delegate) {
this.delegate = delegate;
}

@Override
InputStream doDecompressing(InputStream in, String type) throws IOException {
if (delegate == null) {
throw new IllegalStateException("No delegate available");
}

return delegate.decompressing(in, type);
}

@Override
public List<String> supported() {
return delegate != null ? delegate.supported() : List.of();
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2022 Edgar Asatryan
* Copyright (C) 2022, 2025 Edgar Asatryan
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -16,34 +16,22 @@

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

import java.io.IOException;
import java.io.InputStream;
import java.util.List;

import static io.github.nstdio.http.ext.spi.Classpath.isBrotli4jPresent;
import static io.github.nstdio.http.ext.spi.Classpath.isOrgBrotliPresent;

public class OptionalBrotliCompressionFactory extends CompressionFactoryBase {

private final CompressionFactory delegate;
public class OptionalBrotliCompressionFactory extends DelegatingCompressionFactory {

public OptionalBrotliCompressionFactory() {
super(getDelegate());
}

private static CompressionFactory getDelegate() {
if (isOrgBrotliPresent()) {
delegate = new BrotliOrgCompressionFactory();
return new BrotliOrgCompressionFactory();
} else if (isBrotli4jPresent()) {
delegate = new Brotli4JCompressionFactory();
} else {
delegate = null;
return new Brotli4JCompressionFactory();
}
}

@Override
public List<String> supported() {
return delegate != null ? delegate.supported() : List.of();
}

@Override
InputStream doDecompressing(InputStream in, String type) throws IOException {
return delegate.decompressing(in, type);
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright (C) 2025 Edgar Asatryan
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

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

import static io.github.nstdio.http.ext.spi.Classpath.isZstdJniPresent;

public class OptionalZstdCompressionFactory extends DelegatingCompressionFactory {

public OptionalZstdCompressionFactory() {
super(isZstdJniPresent() ? new ZstdJniCompressionFactory() : null);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright (C) 2025 Edgar Asatryan
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.github.nstdio.http.ext.spi;

import com.github.luben.zstd.ZstdInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;

final class ZstdJniCompressionFactory implements CompressionFactory {

private final List<String> supported = List.of("zstd");

@Override
public List<String> supported() {
return supported;
}

@Override
public InputStream decompressing(InputStream in, String type) throws IOException {
return new ZstdInputStream(in);
}
}
7 changes: 5 additions & 2 deletions src/main/java/module-info.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2022 Edgar Asatryan
* Copyright (C) 2022, 2025 Edgar Asatryan
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -19,6 +19,7 @@
import io.github.nstdio.http.ext.spi.JdkCompressionFactory;
import io.github.nstdio.http.ext.spi.JsonMappingProvider;
import io.github.nstdio.http.ext.spi.OptionalBrotliCompressionFactory;
import io.github.nstdio.http.ext.spi.OptionalZstdCompressionFactory;

module http.client.ext {
uses CompressionFactory;
Expand All @@ -28,6 +29,7 @@

requires static com.aayushatharva.brotli4j;
requires static org.brotli.dec;
requires static com.github.luben.zstd_jni;

requires static transitive com.fasterxml.jackson.core;
requires static transitive com.fasterxml.jackson.databind;
Expand All @@ -38,5 +40,6 @@

provides CompressionFactory with JdkCompressionFactory,
IdentityCompressionFactory,
OptionalBrotliCompressionFactory;
OptionalBrotliCompressionFactory,
OptionalZstdCompressionFactory;
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#
# Copyright (C) 2022 Edgar Asatryan
# Copyright (C) 2022, 2025 Edgar Asatryan
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand All @@ -16,4 +16,5 @@

io.github.nstdio.http.ext.spi.JdkCompressionFactory
io.github.nstdio.http.ext.spi.IdentityCompressionFactory
io.github.nstdio.http.ext.spi.OptionalBrotliCompressionFactory
io.github.nstdio.http.ext.spi.OptionalBrotliCompressionFactory
io.github.nstdio.http.ext.spi.OptionalZstdCompressionFactory
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright (C) 2025 Edgar Asatryan
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.github.nstdio.http.ext.jupiter;

import java.net.URL;
import java.net.URLClassLoader;
import java.util.Arrays;
import java.util.List;

public final class FilteredClassLoader extends URLClassLoader {

private final List<Class<?>> classes;

public FilteredClassLoader(Class<?>... classes) {
this(FilteredClassLoader.class.getClassLoader(), classes);
}

public FilteredClassLoader(ClassLoader parent, Class<?>... classes) {
super(new URL[0], parent);
this.classes = Arrays.asList(classes);
}

@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
for (var cls : classes) {
if (cls.getName().equals(name)) {
throw new ClassNotFoundException();
}
}

return super.loadClass(name, resolve);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright (C) 2025 Edgar Asatryan
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.github.nstdio.http.ext.jupiter;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

/**
* {@code @FilteredClassLoaderTest} is used to filter certain classes from {@link Thread#getContextClassLoader()}. Note
* that this annotation would not have effect on any other classloader that is used for loading classes, resources etc.
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Test
@ExtendWith(FilteredClassLoaderTestExtension.class)
public @interface FilteredClassLoaderTest {

/**
* The list of classes that needs to hidden when executing annotated element.
*
* @return The list of classes.
*/
Class<?>[] value() default {};
}
Loading

0 comments on commit c7e8775

Please sign in to comment.