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