View Javadoc
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.impl.classic;
29  
30  import java.io.IOException;
31  import java.util.ArrayList;
32  import java.util.List;
33  import java.util.Locale;
34  
35  import org.apache.hc.client5.http.classic.ExecChain;
36  import org.apache.hc.client5.http.classic.ExecChainHandler;
37  import org.apache.hc.client5.http.config.RequestConfig;
38  import org.apache.hc.client5.http.entity.BrotliDecompressingEntity;
39  import org.apache.hc.client5.http.entity.BrotliInputStreamFactory;
40  import org.apache.hc.client5.http.entity.DecompressingEntity;
41  import org.apache.hc.client5.http.entity.DeflateInputStreamFactory;
42  import org.apache.hc.client5.http.entity.GZIPInputStreamFactory;
43  import org.apache.hc.client5.http.entity.InputStreamFactory;
44  import org.apache.hc.client5.http.protocol.HttpClientContext;
45  import org.apache.hc.core5.annotation.Contract;
46  import org.apache.hc.core5.annotation.Internal;
47  import org.apache.hc.core5.annotation.ThreadingBehavior;
48  import org.apache.hc.core5.http.ClassicHttpRequest;
49  import org.apache.hc.core5.http.ClassicHttpResponse;
50  import org.apache.hc.core5.http.Header;
51  import org.apache.hc.core5.http.HeaderElement;
52  import org.apache.hc.core5.http.HttpEntity;
53  import org.apache.hc.core5.http.HttpException;
54  import org.apache.hc.core5.http.HttpHeaders;
55  import org.apache.hc.core5.http.config.Lookup;
56  import org.apache.hc.core5.http.config.RegistryBuilder;
57  import org.apache.hc.core5.http.message.BasicHeaderValueParser;
58  import org.apache.hc.core5.http.message.MessageSupport;
59  import org.apache.hc.core5.http.message.ParserCursor;
60  import org.apache.hc.core5.util.Args;
61  
62  /**
63   * Request execution handler in the classic request execution chain
64   * that is responsible for automatic response content decompression.
65   * <p>
66   * Further responsibilities such as communication with the opposite
67   * endpoint is delegated to the next executor in the request execution
68   * chain.
69   * </p>
70   *
71   * @since 5.0
72   */
73  @Contract(threading = ThreadingBehavior.STATELESS)
74  @Internal
75  public final class ContentCompressionExec implements ExecChainHandler {
76  
77      private final Header acceptEncoding;
78      private final Lookup<InputStreamFactory> decoderRegistry;
79      private final boolean ignoreUnknown;
80  
81      public ContentCompressionExec(
82              final List<String> acceptEncoding,
83              final Lookup<InputStreamFactory> decoderRegistry,
84              final boolean ignoreUnknown) {
85  
86          final boolean brotliSupported = BrotliDecompressingEntity.isAvailable();
87          final List<String> encodings = new ArrayList<>(4);
88          encodings.add("gzip");
89          encodings.add("x-gzip");
90          encodings.add("deflate");
91          if (brotliSupported) {
92              encodings.add("br");
93          }
94          this.acceptEncoding = MessageSupport.headerOfTokens(HttpHeaders.ACCEPT_ENCODING, encodings);
95  
96          if (decoderRegistry != null) {
97              this.decoderRegistry = decoderRegistry;
98          } else {
99              final RegistryBuilder<InputStreamFactory> builder = RegistryBuilder.<InputStreamFactory>create()
100                 .register("gzip", GZIPInputStreamFactory.getInstance())
101                 .register("x-gzip", GZIPInputStreamFactory.getInstance())
102                 .register("deflate", DeflateInputStreamFactory.getInstance());
103             if (brotliSupported) {
104                 builder.register("br", BrotliInputStreamFactory.getInstance());
105             }
106             this.decoderRegistry = builder.build();
107         }
108 
109 
110         this.ignoreUnknown = ignoreUnknown;
111     }
112 
113     public ContentCompressionExec(final boolean ignoreUnknown) {
114         this(null, null, ignoreUnknown);
115     }
116 
117     /**
118      * Handles {@code gzip} and {@code deflate} compressed entities by using the following
119      * decoders:
120      * <ul>
121      * <li>gzip - see {@link java.util.zip.GZIPInputStream}</li>
122      * <li>deflate - see {@link org.apache.hc.client5.http.entity.DeflateInputStream}</li>
123      * <li>brotli - see {@link org.brotli.dec.BrotliInputStream}</li>
124      * </ul>
125      */
126     public ContentCompressionExec() {
127         this(null, null, true);
128     }
129 
130 
131     @Override
132     public ClassicHttpResponse execute(
133             final ClassicHttpRequest request,
134             final ExecChain.Scope scope,
135             final ExecChain chain) throws IOException, HttpException {
136         Args.notNull(request, "HTTP request");
137         Args.notNull(scope, "Scope");
138 
139         final HttpClientContext clientContext = scope.clientContext;
140         final RequestConfig requestConfig = clientContext.getRequestConfigOrDefault();
141 
142         /* Signal support for Accept-Encoding transfer encodings. */
143         if (!request.containsHeader(HttpHeaders.ACCEPT_ENCODING) && requestConfig.isContentCompressionEnabled()) {
144             request.addHeader(acceptEncoding);
145         }
146 
147         final ClassicHttpResponse response = chain.proceed(request, scope);
148 
149         final HttpEntity entity = response.getEntity();
150         // entity can be null in case of 304 Not Modified, 204 No Content or similar
151         // check for zero length entity.
152         if (requestConfig.isContentCompressionEnabled() && entity != null && entity.getContentLength() != 0) {
153             final String contentEncoding = entity.getContentEncoding();
154             if (contentEncoding != null) {
155                 final ParserCursor cursor = new ParserCursor(0, contentEncoding.length());
156                 final HeaderElement[] codecs = BasicHeaderValueParser.INSTANCE.parseElements(contentEncoding, cursor);
157                 for (final HeaderElement codec : codecs) {
158                     final String codecname = codec.getName().toLowerCase(Locale.ROOT);
159                     final InputStreamFactory decoderFactory = decoderRegistry.lookup(codecname);
160                     if (decoderFactory != null) {
161                         response.setEntity(new DecompressingEntity(response.getEntity(), decoderFactory));
162                         response.removeHeaders(HttpHeaders.CONTENT_LENGTH);
163                         response.removeHeaders(HttpHeaders.CONTENT_ENCODING);
164                         response.removeHeaders(HttpHeaders.CONTENT_MD5);
165                     } else {
166                         if (!"identity".equals(codecname) && !ignoreUnknown) {
167                             throw new HttpException("Unsupported Content-Encoding: " + codec.getName());
168                         }
169                     }
170                 }
171             }
172         }
173         return response;
174     }
175 
176 }