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