diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/ContentCodingSupport.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/ContentCodingSupport.java new file mode 100644 index 0000000000..441c0d3295 --- /dev/null +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/ContentCodingSupport.java @@ -0,0 +1,65 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.client5.http.impl; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; + +import org.apache.hc.core5.annotation.Internal; +import org.apache.hc.core5.http.EntityDetails; +import org.apache.hc.core5.http.message.MessageSupport; +import org.apache.hc.core5.http.message.ParserCursor; + +/** + * @since 5.6 + */ +@Internal +public final class ContentCodingSupport { + + private ContentCodingSupport() { + } + + public static List parseContentCodecs(final EntityDetails entityDetails) { + if (entityDetails == null || entityDetails.getContentEncoding() == null) { + return Collections.emptyList(); + } + final String contentEncoding = entityDetails.getContentEncoding(); + final ParserCursor cursor = new ParserCursor(0, contentEncoding.length()); + final List codecs = new ArrayList<>(); + MessageSupport.parseTokens(contentEncoding, cursor, token -> { + final String codec = token.toLowerCase(Locale.ROOT); + if (!codec.isEmpty() && !"identity".equals(codec)) { + codecs.add(codec); + } + }); + return codecs; + } + +} diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/ContentCompressionAsyncExec.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/ContentCompressionAsyncExec.java index 7f1f2153e2..dea6d7d7e3 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/ContentCompressionAsyncExec.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/ContentCompressionAsyncExec.java @@ -29,7 +29,7 @@ import java.io.IOException; import java.util.Arrays; import java.util.LinkedHashMap; -import java.util.Locale; +import java.util.List; import java.util.Set; import java.util.function.UnaryOperator; @@ -39,21 +39,19 @@ import org.apache.hc.client5.http.async.methods.InflatingAsyncDataConsumer; import org.apache.hc.client5.http.async.methods.InflatingGzipDataConsumer; import org.apache.hc.client5.http.entity.compress.ContentCoding; +import org.apache.hc.client5.http.impl.ContentCodingSupport; import org.apache.hc.client5.http.protocol.HttpClientContext; import org.apache.hc.core5.annotation.Contract; import org.apache.hc.core5.annotation.Internal; import org.apache.hc.core5.annotation.ThreadingBehavior; import org.apache.hc.core5.http.EntityDetails; -import org.apache.hc.core5.http.HeaderElement; import org.apache.hc.core5.http.HttpException; import org.apache.hc.core5.http.HttpHeaders; import org.apache.hc.core5.http.HttpRequest; import org.apache.hc.core5.http.HttpResponse; import org.apache.hc.core5.http.config.Lookup; import org.apache.hc.core5.http.config.RegistryBuilder; -import org.apache.hc.core5.http.message.BasicHeaderValueParser; import org.apache.hc.core5.http.message.MessageSupport; -import org.apache.hc.core5.http.message.ParserCursor; import org.apache.hc.core5.http.nio.AsyncDataConsumer; import org.apache.hc.core5.http.nio.AsyncEntityProducer; import org.apache.hc.core5.util.Args; @@ -119,23 +117,16 @@ public AsyncDataConsumer handleResponse(final HttpResponse rsp, return cb.handleResponse(rsp, details); } - final String coding = details != null ? details.getContentEncoding() : null; - - if (coding != null) { + final List codecs = ContentCodingSupport.parseContentCodecs(details); + if (!codecs.isEmpty()) { AsyncDataConsumer downstream = cb.handleResponse(rsp, wrapEntityDetails(details)); - - final HeaderElement[] el = BasicHeaderValueParser.INSTANCE - .parseElements(coding, new ParserCursor(0, coding.length())); - for (int i = el.length - 1; i >= 0; i--) { - final String token = el[i].getName().toLowerCase(Locale.ROOT); - if ("identity".equals(token) || token.isEmpty()) { - continue; - } - final UnaryOperator op = decoders.lookup(token); + for (int i = codecs.size() - 1; i >= 0; i--) { + final String codec = codecs.get(i); + final UnaryOperator op = decoders.lookup(codec); if (op != null) { downstream = op.apply(downstream); } else { - throw new HttpException("Unsupported Content-Encoding: " + token); + throw new HttpException("Unsupported Content-Encoding: " + codec); } } rsp.removeHeaders(HttpHeaders.CONTENT_ENCODING); @@ -143,6 +134,7 @@ public AsyncDataConsumer handleResponse(final HttpResponse rsp, rsp.removeHeaders(HttpHeaders.CONTENT_MD5); return downstream; } + return cb.handleResponse(rsp, details); } diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ContentCompressionExec.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ContentCompressionExec.java index c6491ec3e5..e3dca2d090 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ContentCompressionExec.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ContentCompressionExec.java @@ -31,7 +31,6 @@ import java.util.ArrayList; import java.util.EnumMap; import java.util.List; -import java.util.Locale; import java.util.Map; import java.util.function.UnaryOperator; @@ -40,6 +39,7 @@ import org.apache.hc.client5.http.config.RequestConfig; import org.apache.hc.client5.http.entity.compress.ContentCodecRegistry; import org.apache.hc.client5.http.entity.compress.ContentCoding; +import org.apache.hc.client5.http.impl.ContentCodingSupport; import org.apache.hc.client5.http.protocol.HttpClientContext; import org.apache.hc.core5.annotation.Contract; import org.apache.hc.core5.annotation.Internal; @@ -47,15 +47,12 @@ import org.apache.hc.core5.http.ClassicHttpRequest; import org.apache.hc.core5.http.ClassicHttpResponse; import org.apache.hc.core5.http.Header; -import org.apache.hc.core5.http.HeaderElement; import org.apache.hc.core5.http.HttpEntity; import org.apache.hc.core5.http.HttpException; import org.apache.hc.core5.http.HttpHeaders; import org.apache.hc.core5.http.config.Lookup; import org.apache.hc.core5.http.config.RegistryBuilder; -import org.apache.hc.core5.http.message.BasicHeaderValueParser; import org.apache.hc.core5.http.message.MessageSupport; -import org.apache.hc.core5.http.message.ParserCursor; import org.apache.hc.core5.util.Args; /** @@ -130,22 +127,20 @@ public ClassicHttpResponse execute( // entity can be null in case of 304 Not Modified, 204 No Content or similar // check for zero length entity. if (requestConfig.isContentCompressionEnabled() && entity != null && entity.getContentLength() != 0) { - final String contentEncoding = entity.getContentEncoding(); - if (contentEncoding != null) { - final ParserCursor cursor = new ParserCursor(0, contentEncoding.length()); - final HeaderElement[] codecs = BasicHeaderValueParser.INSTANCE.parseElements(contentEncoding, cursor); - for (final HeaderElement codec : codecs) { - final String codecname = codec.getName().toLowerCase(Locale.ROOT); - final UnaryOperator decoder = decoderRegistry.lookup(codecname); + final List codecs = ContentCodingSupport.parseContentCodecs(entity); + if (!codecs.isEmpty()) { + for (int i = codecs.size() - 1; i >= 0; i--) { + final String codec = codecs.get(i); + final UnaryOperator decoder = decoderRegistry.lookup(codec); if (decoder != null) { response.setEntity(decoder.apply(response.getEntity())); - response.removeHeaders(HttpHeaders.CONTENT_LENGTH); - response.removeHeaders(HttpHeaders.CONTENT_ENCODING); - response.removeHeaders(HttpHeaders.CONTENT_MD5); - } else if (!"identity".equals(codecname)) { - throw new HttpException("Unsupported Content-Encoding: " + codec.getName()); + } else { + throw new HttpException("Unsupported Content-Encoding: " + codec); } } + response.removeHeaders(HttpHeaders.CONTENT_LENGTH); + response.removeHeaders(HttpHeaders.CONTENT_ENCODING); + response.removeHeaders(HttpHeaders.CONTENT_MD5); } } return response; diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/ContentCodingSupportTest.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/ContentCodingSupportTest.java new file mode 100644 index 0000000000..24a5e72648 --- /dev/null +++ b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/ContentCodingSupportTest.java @@ -0,0 +1,109 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ +package org.apache.hc.client5.http.impl; + +import java.util.Set; + +import org.apache.hc.core5.http.EntityDetails; +import org.apache.hc.core5.http.impl.BasicEntityDetails; +import org.hamcrest.MatcherAssert; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Test; + +class ContentCodingSupportTest { + + static class MockEntityDetails implements EntityDetails { + + private final String contentEncoding; + + MockEntityDetails(final String contentEncoding) { + this.contentEncoding = contentEncoding; + } + + @Override + public long getContentLength() { + return -1; + } + + @Override + public String getContentType() { + return null; + } + + @Override + public String getContentEncoding() { + return contentEncoding; + } + + @Override + public boolean isChunked() { + return false; + } + + @Override + public Set getTrailerNames() { + return null; + } + + } + + @Test + void testNoEntity() { + MatcherAssert.assertThat( + ContentCodingSupport.parseContentCodecs(null), + Matchers.empty()); + } + + @Test + void testNotEncoded() { + MatcherAssert.assertThat(ContentCodingSupport.parseContentCodecs( + new BasicEntityDetails(-1, null)), + Matchers.empty()); + } + + @Test + void testNotEncodedNoise() { + MatcherAssert.assertThat(ContentCodingSupport.parseContentCodecs( + new MockEntityDetails(", ,, , ")), + Matchers.empty()); + } + + @Test + void testIdentityEncoded() { + MatcherAssert.assertThat(ContentCodingSupport.parseContentCodecs( + new MockEntityDetails("identity,,,identity")), + Matchers.empty()); + } + + @Test + void testEncodedMultipleCodes() { + MatcherAssert.assertThat(ContentCodingSupport.parseContentCodecs( + new MockEntityDetails("This,,that, \"This and That\"")), + Matchers.contains("this", "that", "\"this and that\"")); + } + +}