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      /**
79       * An empty immutable {@code String} array.
80       */
81      private static final String[] EMPTY_STRING_ARRAY = new String[0];
82  
83      public ContentCompressionExec(
84              final List<String> acceptEncoding,
85              final Lookup<InputStreamFactory> decoderRegistry,
86              final boolean ignoreUnknown) {
87          this.acceptEncoding = MessageSupport.format(HttpHeaders.ACCEPT_ENCODING,
88              acceptEncoding != null ? acceptEncoding.toArray(
89                      EMPTY_STRING_ARRAY) : new String[] {"gzip", "x-gzip", "deflate"});
90  
91          this.decoderRegistry = decoderRegistry != null ? decoderRegistry :
92                  RegistryBuilder.<InputStreamFactory>create()
93                          .register("gzip", GZIPInputStreamFactory.getInstance())
94                          .register("x-gzip", GZIPInputStreamFactory.getInstance())
95                          .register("deflate", DeflateInputStreamFactory.getInstance())
96                          .build();
97          this.ignoreUnknown = ignoreUnknown;
98      }
99  
100     public ContentCompressionExec(final boolean ignoreUnknown) {
101         this(null, null, ignoreUnknown);
102     }
103 
104     /**
105      * Handles {@code gzip} and {@code deflate} compressed entities by using the following
106      * decoders:
107      * <ul>
108      * <li>gzip - see {@link java.util.zip.GZIPInputStream}</li>
109      * <li>deflate - see {@link org.apache.hc.client5.http.entity.DeflateInputStream}</li>
110      * </ul>
111      */
112     public ContentCompressionExec() {
113         this(null, null, true);
114     }
115 
116 
117     @Override
118     public ClassicHttpResponse execute(
119             final ClassicHttpRequest request,
120             final ExecChain.Scope scope,
121             final ExecChain chain) throws IOException, HttpException {
122         Args.notNull(request, "HTTP request");
123         Args.notNull(scope, "Scope");
124 
125         final HttpClientContext clientContext = scope.clientContext;
126         final RequestConfig requestConfig = clientContext.getRequestConfig();
127 
128         /* Signal support for Accept-Encoding transfer encodings. */
129         if (!request.containsHeader(HttpHeaders.ACCEPT_ENCODING) && requestConfig.isContentCompressionEnabled()) {
130             request.addHeader(acceptEncoding);
131         }
132 
133         final ClassicHttpResponse response = chain.proceed(request, scope);
134 
135         final HttpEntity entity = response.getEntity();
136         // entity can be null in case of 304 Not Modified, 204 No Content or similar
137         // check for zero length entity.
138         if (requestConfig.isContentCompressionEnabled() && entity != null && entity.getContentLength() != 0) {
139             final String contentEncoding = entity.getContentEncoding();
140             if (contentEncoding != null) {
141                 final ParserCursor cursor = new ParserCursor(0, contentEncoding.length());
142                 final HeaderElement[] codecs = BasicHeaderValueParser.INSTANCE.parseElements(contentEncoding, cursor);
143                 for (final HeaderElement codec : codecs) {
144                     final String codecname = codec.getName().toLowerCase(Locale.ROOT);
145                     final InputStreamFactory decoderFactory = decoderRegistry.lookup(codecname);
146                     if (decoderFactory != null) {
147                         response.setEntity(new DecompressingEntity(response.getEntity(), decoderFactory));
148                         response.removeHeaders(HttpHeaders.CONTENT_LENGTH);
149                         response.removeHeaders(HttpHeaders.CONTENT_ENCODING);
150                         response.removeHeaders(HttpHeaders.CONTENT_MD5);
151                     } else {
152                         if (!"identity".equals(codecname) && !ignoreUnknown) {
153                             throw new HttpException("Unsupported Content-Encoding: " + codec.getName());
154                         }
155                     }
156                 }
157             }
158         }
159         return response;
160     }
161 
162 }