Skip to content

Commit 24ac6ca

Browse files
committed
Unify content-coding support: replace InputStreamFactory with symmetric Encoder/Decoder API, introduce IOFunction and ContentCodecRegistry, update DecompressingEntity and all tests accordingly.
1 parent d0ed410 commit 24ac6ca

9 files changed

Lines changed: 139 additions & 111 deletions

File tree

httpclient5/src/main/java/org/apache/hc/client5/http/entity/compress/CommonsCompressDecoderFactory.java

Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555
*/
5656
@Internal
5757
@Contract(threading = ThreadingBehavior.STATELESS)
58-
final class CommonsCompressDecoderFactory implements Decoder {
58+
final class CommonsCompressDecoderFactory {
5959

6060
/**
6161
* Map of codings that need extra JARs → the fully‐qualified class we test for
@@ -71,27 +71,19 @@ final class CommonsCompressDecoderFactory implements Decoder {
7171
REQUIRED_CLASS_NAME = Collections.unmodifiableMap(m);
7272
}
7373

74-
private final String encoding; // lower-case IANA token
75-
private final CompressorStreamFactory factory = new CompressorStreamFactory();
76-
77-
CommonsCompressDecoderFactory(final String encoding) {
78-
this.encoding = encoding.toLowerCase(Locale.ROOT);
79-
}
80-
81-
public String getContentEncoding() {
82-
return encoding;
83-
}
84-
8574
/**
86-
* Lazily wraps the source stream in a Commons-Compress decoder.
75+
* @return lazy decoder for the given IANA token (lower-case).
8776
*/
88-
@Override
89-
public InputStream wrap(final InputStream source) throws IOException {
90-
try {
91-
return factory.createCompressorInputStream(encoding, source);
92-
} catch (final CompressorException | LinkageError ex) {
93-
throw new IOException("Unable to decode Content-Encoding '" + encoding + '\'', ex);
94-
}
77+
static IOFunction<InputStream, InputStream> decoder(final String token) {
78+
final String enc = token.toLowerCase(Locale.ROOT);
79+
final CompressorStreamFactory factory = new CompressorStreamFactory();
80+
return in -> {
81+
try {
82+
return factory.createCompressorInputStream(enc, in);
83+
} catch (final CompressorException | LinkageError ex) {
84+
throw new IOException("Unable to decode Content-Encoding '" + enc + '\'', ex);
85+
}
86+
};
9587
}
9688

9789
/**

httpclient5/src/main/java/org/apache/hc/client5/http/entity/compress/ContentCodecRegistry.java

Lines changed: 13 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@
2828
package org.apache.hc.client5.http.entity.compress;
2929

3030
import java.io.IOException;
31-
import java.io.InputStream;
3231
import java.util.Arrays;
3332
import java.util.Collections;
3433
import java.util.EnumMap;
@@ -59,20 +58,17 @@ public final class ContentCodecRegistry {
5958
private static final Map<ContentCoding, Codec> REGISTRY = build();
6059

6160
private static Map<ContentCoding, Codec> build() {
62-
final Map<ContentCoding, Codec> m =
63-
new EnumMap<>(ContentCoding.class);
61+
final Map<ContentCoding, Codec> m = new EnumMap<>(ContentCoding.class);
6462

65-
/* 1. Built-ins ------------------------------------------------- */
6663
m.put(ContentCoding.GZIP,
6764
new Codec(
6865
// encoder
6966
org.apache.hc.client5.http.entity.GzipCompressingEntity::new,
70-
// decoder
71-
GZIPInputStream::new));
67+
ent -> new DecompressingEntity(ent, GZIPInputStream::new)));
7268
m.put(ContentCoding.DEFLATE,
7369
new Codec(
7470
org.apache.hc.client5.http.entity.DeflateCompressingEntity::new,
75-
DeflateInputStream::new));
71+
ent -> new DecompressingEntity(ent, DeflateInputStream::new)));
7672

7773
/* 2. Commons-Compress extras ---------------------------------- */
7874
if (CommonsCompressSupport.isPresent()) {
@@ -88,8 +84,10 @@ private static Map<ContentCoding, Codec> build() {
8884
ContentCoding.DEFLATE64)) {
8985

9086
if (CommonsCompressDecoderFactory.runtimeAvailable(c.token())) {
91-
m.put(c, new Codec(e -> new CommonsCompressingEntity(e, c.token()), new CommonsCompressDecoderFactory(c.token())
92-
));
87+
m.put(c, new Codec(
88+
e -> new CommonsCompressingEntity(e, c.token()),
89+
ent -> new DecompressingEntity(ent,
90+
CommonsCompressDecoderFactory.decoder(c.token()))));
9391
}
9492
}
9593
}
@@ -98,35 +96,20 @@ private static Map<ContentCoding, Codec> build() {
9896
if (!m.containsKey(ContentCoding.BROTLI)
9997
&& CommonsCompressDecoderFactory.runtimeAvailable(ContentCoding.BROTLI.token())) {
10098
m.put(ContentCoding.BROTLI,
101-
Codec.decodeOnly(BrotliInputStream::new));
99+
Codec.decodeOnly(ent ->
100+
new DecompressingEntity(ent, BrotliInputStream::new)));
102101
}
103102

104103
return Collections.unmodifiableMap(m);
105104
}
106105

107-
108-
/**
109-
* Returns the {@link Codec} (or {@code null}).
110-
*/
111-
public static Codec codec(final ContentCoding coding) {
112-
return REGISTRY.get(coding);
113-
}
114-
115-
/**
116-
* Convenient encoder helper – returns {@code null} if unsupported.
117-
*/
118-
public static HttpEntity wrap(final ContentCoding coding,
119-
final HttpEntity src) {
120-
final Codec c = codec(coding);
106+
public static HttpEntity wrap(final ContentCoding coding, final HttpEntity src) {
107+
final Codec c = REGISTRY.get(coding);
121108
return c != null && c.encoder != null ? c.encoder.wrap(src) : null;
122109
}
123110

124-
/**
125-
* Convenient decoder helper – returns {@code null} if unsupported.
126-
*/
127-
public static InputStream unwrap(final ContentCoding coding,
128-
final InputStream src) throws IOException {
129-
final Codec c = codec(coding);
111+
public static HttpEntity unwrap(final ContentCoding coding, final HttpEntity src) throws IOException {
112+
final Codec c = REGISTRY.get(coding);
130113
return c != null && c.decoder != null ? c.decoder.wrap(src) : null;
131114
}
132115

httpclient5/src/main/java/org/apache/hc/client5/http/entity/compress/Decoder.java

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,33 +28,33 @@
2828
package org.apache.hc.client5.http.entity.compress;
2929

3030
import java.io.IOException;
31-
import java.io.InputStream;
31+
32+
import org.apache.hc.core5.http.HttpEntity;
3233

3334
/**
34-
* Pull-side transformer that takes a compressed {@link InputStream} and
35+
* Pull-side transformer that takes a compressed {@link HttpEntity} and
3536
* returns a lazily-decoded view of the same byte sequence.
3637
*
37-
* <p>Implementations <strong>must</strong> return a stream that honours the
38-
* usual {@code InputStream} contract and propagates {@link IOException}s
39-
* raised by the underlying transport.</p>
38+
* <p>Implementations <strong>must</strong> return an entity whose
39+
* {@code InputStream} honours the usual stream contract and propagates
40+
* {@link IOException}s raised by the underlying transport.</p>
4041
*
4142
* @since 5.6
4243
*/
4344
@FunctionalInterface
4445
public interface Decoder {
4546

4647
/**
47-
* Wraps the supplied source stream in a decoding stream that transparently
48+
* Wraps the supplied source entity in a decoding entity that transparently
4849
* produces the uncompressed data.
4950
*
50-
* @param src the source {@code InputStream} positioned at the start of the
51+
* @param src the source {@code HttpEntity} positioned at the start of the
5152
* encoded payload (never {@code null})
52-
* @return a new, undecoded {@code InputStream}; the caller is responsible
53-
* for closing it
54-
* @throws IOException if the decoding stream cannot be created or an
53+
* @return a new, undecoded {@code HttpEntity}; the caller is responsible
54+
* for consuming / closing its content stream
55+
* @throws IOException if the decoding entity cannot be created or an
5556
* underlying I/O error occurs
5657
*/
57-
InputStream wrap(InputStream src) throws IOException;
58+
HttpEntity wrap(HttpEntity src) throws IOException;
5859
}
5960

60-

httpclient5/src/main/java/org/apache/hc/client5/http/entity/compress/DecompressingEntity.java

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,15 @@
3939
public class DecompressingEntity extends HttpEntityWrapper {
4040

4141
private static final int BUF_SIZE = 8 * 1024; // 8 KiB buffer
42-
43-
private final Decoder decoder;
42+
private final IOFunction<InputStream, InputStream> decoder;
4443
private final ReentrantLock lock = new ReentrantLock();
4544
private volatile InputStream cached;
4645

47-
public DecompressingEntity(final HttpEntity src, final Decoder decoder) {
46+
public DecompressingEntity(
47+
final HttpEntity src,
48+
final IOFunction<InputStream, InputStream> decoder) {
4849
super(src);
49-
this.decoder = Args.notNull(decoder, "Decoder");
50+
this.decoder = Args.notNull(decoder, "Stream decoder");
5051
}
5152

5253
/**
@@ -55,15 +56,15 @@ public DecompressingEntity(final HttpEntity src, final Decoder decoder) {
5556
@Override
5657
public InputStream getContent() throws IOException {
5758
if (!isStreaming()) {
58-
return decoder.wrap(super.getContent());
59+
return decoder.apply(super.getContent());
5960
}
6061

6162
InputStream local = cached;
6263
if (local == null) {
6364
lock.lock();
6465
try {
6566
if (cached == null) {
66-
cached = decoder.wrap(super.getContent());
67+
cached = decoder.apply(super.getContent());
6768
}
6869
local = cached;
6970
} finally {
@@ -81,6 +82,16 @@ public long getContentLength() {
8182
return -1;
8283
}
8384

85+
@Override
86+
public boolean isRepeatable() {
87+
return super.isRepeatable();
88+
}
89+
90+
@Override
91+
public boolean isStreaming() {
92+
return super.isStreaming();
93+
}
94+
8495
/**
8596
* Streams the decoded bytes directly to {@code out}.
8697
*/
@@ -95,14 +106,4 @@ public void writeTo(final OutputStream out) throws IOException {
95106
}
96107
}
97108
}
98-
99-
@Override
100-
public boolean isRepeatable() {
101-
return super.isRepeatable();
102-
}
103-
104-
@Override
105-
public boolean isStreaming() {
106-
return super.isStreaming();
107-
}
108-
}
109+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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+
28+
package org.apache.hc.client5.http.entity.compress;
29+
30+
import java.io.IOException;
31+
32+
import org.conscrypt.Internal;
33+
34+
/**
35+
* Minimal equivalent of {@link java.util.function.Function} whose
36+
* {@link #apply(Object)} method is allowed to throw {@link IOException}.
37+
* <p>
38+
* Used internally by the content-coding layer to pass lambdas that wrap /
39+
* unwrap streams without forcing boiler-plate try/catch blocks.
40+
* </p>
41+
*
42+
* @param <T> input type
43+
* @param <R> result type
44+
* @since 5.6
45+
*/
46+
@Internal
47+
@FunctionalInterface
48+
public interface IOFunction<T, R> {
49+
50+
/**
51+
* Applies the transformation.
52+
*
53+
* @param value source value (never {@code null})
54+
* @return transformed value
55+
* @throws IOException if the transformation cannot be performed
56+
*/
57+
R apply(T value) throws IOException;
58+
}

httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ContentCompressionExec.java

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@
4040
import org.apache.hc.client5.http.entity.compress.ContentCodecRegistry;
4141
import org.apache.hc.client5.http.entity.compress.ContentCoding;
4242
import org.apache.hc.client5.http.entity.compress.Decoder;
43-
import org.apache.hc.client5.http.entity.compress.DecompressingEntity;
4443
import org.apache.hc.client5.http.protocol.HttpClientContext;
4544
import org.apache.hc.core5.annotation.Contract;
4645
import org.apache.hc.core5.annotation.Internal;
@@ -149,14 +148,12 @@ public ClassicHttpResponse execute(
149148
final String codecname = codec.getName().toLowerCase(Locale.ROOT);
150149
final Decoder decoder = decoderRegistry.lookup(codecname);
151150
if (decoder != null) {
152-
response.setEntity(new DecompressingEntity(response.getEntity(), decoder));
151+
response.setEntity(decoder.wrap(response.getEntity()));
153152
response.removeHeaders(HttpHeaders.CONTENT_LENGTH);
154153
response.removeHeaders(HttpHeaders.CONTENT_ENCODING);
155154
response.removeHeaders(HttpHeaders.CONTENT_MD5);
156-
} else {
157-
if (!"identity".equals(codecname) && !ignoreUnknown) {
158-
throw new HttpException("Unsupported Content-Encoding: " + codec.getName());
159-
}
155+
} else if (!"identity".equals(codecname) && !ignoreUnknown) {
156+
throw new HttpException("Unsupported Content-Encoding: " + codec.getName());
160157
}
161158
}
162159
}

httpclient5/src/test/java/org/apache/hc/client5/http/entity/TestBrotli.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,9 @@ void testDecompressionWithBrotli() throws Exception {
4747

4848
final byte[] bytes = new byte[] {33, 44, 0, 4, 116, 101, 115, 116, 32, 98, 114, 111, 116, 108, 105, 10, 3};
4949

50-
final HttpEntity entity = new org.apache.hc.client5.http.entity.compress.DecompressingEntity(
51-
new ByteArrayEntity(bytes, null),
52-
ContentCodecRegistry.decoder(ContentCoding.BROTLI));
50+
final HttpEntity entity = ContentCodecRegistry.unwrap(
51+
ContentCoding.BROTLI,
52+
new ByteArrayEntity(bytes, null));
5353

5454
Assertions.assertEquals("test brotli\n", EntityUtils.toString(entity));
5555
}

httpclient5/src/test/java/org/apache/hc/client5/http/entity/TestDeflate.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,9 @@ void testCompressDecompress() throws Exception {
5757
compresser.finish();
5858
final int len = compresser.deflate(compressed);
5959

60-
final HttpEntity entity = new org.apache.hc.client5.http.entity.compress.DecompressingEntity(
61-
new ByteArrayEntity(compressed, 0, len, ContentType.APPLICATION_OCTET_STREAM),
62-
ContentCodecRegistry.decoder(ContentCoding.DEFLATE));
60+
final HttpEntity entity = ContentCodecRegistry
61+
.decoder(ContentCoding.DEFLATE)
62+
.wrap(new ByteArrayEntity(compressed, 0, len, ContentType.APPLICATION_OCTET_STREAM));
6363

6464
Assertions.assertEquals(s, EntityUtils.toString(entity));
6565
}
@@ -78,9 +78,9 @@ void testEncodeThenDecode() throws Exception {
7878
final ByteArrayOutputStream buf = new ByteArrayOutputStream();
7979
deflated.writeTo(buf);
8080

81-
final HttpEntity decoded = new org.apache.hc.client5.http.entity.compress.DecompressingEntity(
82-
new ByteArrayEntity(buf.toByteArray(), ContentType.APPLICATION_OCTET_STREAM),
83-
ContentCodecRegistry.decoder(ContentCoding.DEFLATE));
81+
final HttpEntity decoded = ContentCodecRegistry
82+
.decoder(ContentCoding.DEFLATE)
83+
.wrap(new ByteArrayEntity(buf.toByteArray(), ContentType.APPLICATION_OCTET_STREAM));
8484

8585
Assertions.assertEquals(text, EntityUtils.toString(decoded, StandardCharsets.US_ASCII));
8686
}

0 commit comments

Comments
 (0)