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.http.impl.client.integration;
29  
30  import java.io.ByteArrayInputStream;
31  import java.io.ByteArrayOutputStream;
32  import java.io.IOException;
33  import java.io.OutputStream;
34  import java.util.ArrayList;
35  import java.util.List;
36  import java.util.concurrent.CountDownLatch;
37  import java.util.concurrent.ExecutorService;
38  import java.util.concurrent.Executors;
39  import java.util.zip.Deflater;
40  import java.util.zip.GZIPOutputStream;
41  
42  import org.apache.http.Consts;
43  import org.apache.http.Header;
44  import org.apache.http.HeaderElement;
45  import org.apache.http.HttpException;
46  import org.apache.http.HttpHost;
47  import org.apache.http.HttpRequest;
48  import org.apache.http.HttpResponse;
49  import org.apache.http.HttpStatus;
50  import org.apache.http.client.HttpClient;
51  import org.apache.http.client.methods.HttpGet;
52  import org.apache.http.entity.InputStreamEntity;
53  import org.apache.http.entity.StringEntity;
54  import org.apache.http.impl.client.BasicResponseHandler;
55  import org.apache.http.localserver.LocalServerTestBase;
56  import org.apache.http.protocol.HttpContext;
57  import org.apache.http.protocol.HttpRequestHandler;
58  import org.apache.http.util.EntityUtils;
59  import org.junit.Assert;
60  import org.junit.Test;
61  
62  /**
63   * Test case for how Content Codings are processed. By default, we want to do the right thing and
64   * require no intervention from the user of HttpClient, but we still want to let clients do their
65   * own thing if they so wish.
66   */
67  public class TestContentCodings extends LocalServerTestBase {
68  
69      /**
70       * Test for when we don't get an entity back; e.g. for a 204 or 304 response; nothing blows
71       * up with the new behaviour.
72       *
73       * @throws Exception
74       *             if there was a problem
75       */
76      @Test
77      public void testResponseWithNoContent() throws Exception {
78          this.serverBootstrap.registerHandler("*", new HttpRequestHandler() {
79  
80              /**
81               * {@inheritDoc}
82               */
83              @Override
84              public void handle(
85                      final HttpRequest request,
86                      final HttpResponse response,
87                      final HttpContext context) throws HttpException, IOException {
88                  response.setStatusCode(HttpStatus.SC_NO_CONTENT);
89              }
90          });
91  
92          final HttpHost target = start();
93  
94          final HttpGet request = new HttpGet("/some-resource");
95          final HttpResponse response = this.httpclient.execute(target, request);
96          Assert.assertEquals(HttpStatus.SC_NO_CONTENT, response.getStatusLine().getStatusCode());
97          Assert.assertNull(response.getEntity());
98      }
99  
100     /**
101      * Test for when we are handling content from a server that has correctly interpreted RFC2616
102      * to return RFC1950 streams for {@code deflate} content coding.
103      *
104      * @throws Exception
105      */
106     @Test
107     public void testDeflateSupportForServerReturningRfc1950Stream() throws Exception {
108         final String entityText = "Hello, this is some plain text coming back.";
109 
110         this.serverBootstrap.registerHandler("*", createDeflateEncodingRequestHandler(entityText, false));
111 
112         final HttpHost target = start();
113 
114         final HttpGet request = new HttpGet("/some-resource");
115         final HttpResponse response = this.httpclient.execute(target, request);
116         Assert.assertEquals("The entity text is correctly transported", entityText,
117                 EntityUtils.toString(response.getEntity()));
118     }
119 
120     /**
121      * Test for when we are handling content from a server that has incorrectly interpreted RFC2616
122      * to return RFC1951 streams for {@code deflate} content coding.
123      *
124      * @throws Exception
125      */
126     @Test
127     public void testDeflateSupportForServerReturningRfc1951Stream() throws Exception {
128         final String entityText = "Hello, this is some plain text coming back.";
129 
130         this.serverBootstrap.registerHandler("*", createDeflateEncodingRequestHandler(entityText, true));
131 
132         final HttpHost target = start();
133 
134         final HttpGet request = new HttpGet("/some-resource");
135         final HttpResponse response = this.httpclient.execute(target, request);
136         Assert.assertEquals("The entity text is correctly transported", entityText,
137                 EntityUtils.toString(response.getEntity()));
138     }
139 
140     /**
141      * Test for a server returning gzipped content.
142      *
143      * @throws Exception
144      */
145     @Test
146     public void testGzipSupport() throws Exception {
147         final String entityText = "Hello, this is some plain text coming back.";
148 
149         this.serverBootstrap.registerHandler("*", createGzipEncodingRequestHandler(entityText));
150 
151         final HttpHost target = start();
152 
153         final HttpGet request = new HttpGet("/some-resource");
154         final HttpResponse response = this.httpclient.execute(target, request);
155         Assert.assertEquals("The entity text is correctly transported", entityText,
156                 EntityUtils.toString(response.getEntity()));
157     }
158 
159     /**
160      * Try with a bunch of client threads, to check that it's thread-safe.
161      *
162      * @throws Exception
163      *             if there was a problem
164      */
165     @Test
166     public void testThreadSafetyOfContentCodings() throws Exception {
167         final String entityText = "Hello, this is some plain text coming back.";
168 
169         this.serverBootstrap.registerHandler("*", createGzipEncodingRequestHandler(entityText));
170 
171         /*
172          * Create a load of workers which will access the resource. Half will use the default
173          * gzip behaviour; half will require identity entity.
174          */
175         final int clients = 100;
176 
177         this.connManager.setMaxTotal(clients);
178 
179         final HttpHost target = start();
180 
181         final ExecutorService executor = Executors.newFixedThreadPool(clients);
182 
183         final CountDownLatch startGate = new CountDownLatch(1);
184         final CountDownLatch endGate = new CountDownLatch(clients);
185 
186         final List<WorkerTask> workers = new ArrayList<WorkerTask>();
187 
188         for (int i = 0; i < clients; ++i) {
189             workers.add(new WorkerTask(this.httpclient, target, i % 2 == 0, startGate, endGate));
190         }
191 
192         for (final WorkerTask workerTask : workers) {
193 
194             /* Set them all in motion, but they will block until we call startGate.countDown(). */
195             executor.execute(workerTask);
196         }
197 
198         startGate.countDown();
199 
200         /* Wait for the workers to complete. */
201         endGate.await();
202 
203         for (final WorkerTask workerTask : workers) {
204             if (workerTask.isFailed()) {
205                 Assert.fail("A worker failed");
206             }
207             Assert.assertEquals(entityText, workerTask.getText());
208         }
209     }
210 
211     @Test
212     public void testHttpEntityWriteToForGzip() throws Exception {
213         final String entityText = "Hello, this is some plain text coming back.";
214 
215         this.serverBootstrap.registerHandler("*", createGzipEncodingRequestHandler(entityText));
216 
217         final HttpHost target = start();
218 
219         final HttpGet request = new HttpGet("/some-resource");
220         final HttpResponse response = this.httpclient.execute(target, request);
221         final ByteArrayOutputStream out = new ByteArrayOutputStream();
222 
223         response.getEntity().writeTo(out);
224 
225         Assert.assertEquals(entityText, out.toString("utf-8"));
226     }
227 
228     @Test
229     public void testHttpEntityWriteToForDeflate() throws Exception {
230         final String entityText = "Hello, this is some plain text coming back.";
231 
232         this.serverBootstrap.registerHandler("*", createDeflateEncodingRequestHandler(entityText, true));
233 
234         final HttpHost target = start();
235 
236         final HttpGet request = new HttpGet("/some-resource");
237         final HttpResponse response = this.httpclient.execute(target, request);
238         final ByteArrayOutputStream out = new ByteArrayOutputStream();
239 
240         response.getEntity().writeTo(out);
241 
242         Assert.assertEquals(entityText, out.toString("utf-8"));
243     }
244 
245     @Test
246     public void gzipResponsesWorkWithBasicResponseHandler() throws Exception {
247         final String entityText = "Hello, this is some plain text coming back.";
248 
249         this.serverBootstrap.registerHandler("*", createGzipEncodingRequestHandler(entityText));
250 
251         final HttpHost target = start();
252 
253         final HttpGet request = new HttpGet("/some-resource");
254         final String response = this.httpclient.execute(target, request, new BasicResponseHandler());
255         Assert.assertEquals("The entity text is correctly transported", entityText, response);
256     }
257 
258     @Test
259     public void deflateResponsesWorkWithBasicResponseHandler() throws Exception {
260         final String entityText = "Hello, this is some plain text coming back.";
261 
262         this.serverBootstrap.registerHandler("*", createDeflateEncodingRequestHandler(entityText, false));
263 
264         final HttpHost target = start();
265 
266         final HttpGet request = new HttpGet("/some-resource");
267         final String response = this.httpclient.execute(target, request, new BasicResponseHandler());
268         Assert.assertEquals("The entity text is correctly transported", entityText, response);
269     }
270 
271     /**
272      * Creates a new {@link HttpRequestHandler} that will attempt to provide a deflate stream
273      * Content-Coding.
274      *
275      * @param entityText
276      *            the non-null String entity text to be returned by the server
277      * @param rfc1951
278      *            if true, then the stream returned will be a raw RFC1951 deflate stream, which
279      *            some servers return as a result of misinterpreting the HTTP 1.1 RFC. If false,
280      *            then it will return an RFC2616 compliant deflate encoded zlib stream.
281      * @return a non-null {@link HttpRequestHandler}
282      */
283     private HttpRequestHandler createDeflateEncodingRequestHandler(
284             final String entityText, final boolean rfc1951) {
285         return new HttpRequestHandler() {
286 
287             /**
288              * {@inheritDoc}
289              */
290             @Override
291             public void handle(
292                     final HttpRequest request,
293                     final HttpResponse response,
294                     final HttpContext context) throws HttpException, IOException {
295                 response.setEntity(new StringEntity(entityText));
296                 response.addHeader("Content-Type", "text/plain");
297                 final Header[] acceptEncodings = request.getHeaders("Accept-Encoding");
298 
299                 for (final Header header : acceptEncodings) {
300                     for (final HeaderElement element : header.getElements()) {
301                         if ("deflate".equalsIgnoreCase(element.getName())) {
302                             response.addHeader("Content-Encoding", "deflate");
303 
304                             /* Gack. DeflaterInputStream is Java 6. */
305                             // response.setEntity(new InputStreamEntity(new DeflaterInputStream(new
306                             // ByteArrayInputStream(
307                             // entityText.getBytes("utf-8"))), -1));
308                             final byte[] uncompressed = entityText.getBytes(Consts.UTF_8);
309                             final Deflater compressor = new Deflater(Deflater.DEFAULT_COMPRESSION, rfc1951);
310                             compressor.setInput(uncompressed);
311                             compressor.finish();
312                             final byte[] output = new byte[100];
313                             final int compressedLength = compressor.deflate(output);
314                             final byte[] compressed = new byte[compressedLength];
315                             System.arraycopy(output, 0, compressed, 0, compressedLength);
316                             response.setEntity(new InputStreamEntity(
317                                     new ByteArrayInputStream(compressed), compressedLength));
318                             return;
319                         }
320                     }
321                 }
322             }
323         };
324     }
325 
326     /**
327      * Returns an {@link HttpRequestHandler} implementation that will attempt to provide a gzip
328      * Content-Encoding.
329      *
330      * @param entityText
331      *            the non-null String entity to be returned by the server
332      * @return a non-null {@link HttpRequestHandler}
333      */
334     private HttpRequestHandler createGzipEncodingRequestHandler(final String entityText) {
335         return new HttpRequestHandler() {
336 
337             /**
338              * {@inheritDoc}
339              */
340             @Override
341             public void handle(
342                     final HttpRequest request,
343                     final HttpResponse response,
344                     final HttpContext context) throws HttpException, IOException {
345                 response.setEntity(new StringEntity(entityText));
346                 response.addHeader("Content-Type", "text/plain");
347                 final Header[] acceptEncodings = request.getHeaders("Accept-Encoding");
348 
349                 for (final Header header : acceptEncodings) {
350                     for (final HeaderElement element : header.getElements()) {
351                         if ("gzip".equalsIgnoreCase(element.getName())) {
352                             response.addHeader("Content-Encoding", "gzip");
353 
354                             /*
355                              * We have to do a bit more work with gzip versus deflate, since
356                              * Gzip doesn't appear to have an equivalent to DeflaterInputStream in
357                              * the JDK.
358                              *
359                              * UPDATE: DeflaterInputStream is Java 6 anyway, so we have to do a bit
360                              * of work there too!
361                              */
362                             final ByteArrayOutputStream bytes = new ByteArrayOutputStream();
363                             final OutputStream out = new GZIPOutputStream(bytes);
364 
365                             final ByteArrayInputStream uncompressed = new ByteArrayInputStream(
366                                     entityText.getBytes(Consts.UTF_8));
367 
368                             final byte[] buf = new byte[60];
369 
370                             int n;
371                             while ((n = uncompressed.read(buf)) != -1) {
372                                 out.write(buf, 0, n);
373                             }
374 
375                             out.close();
376 
377                             final byte[] arr = bytes.toByteArray();
378                             response.setEntity(new InputStreamEntity(new ByteArrayInputStream(arr),
379                                     arr.length));
380 
381                             return;
382                         }
383                     }
384                 }
385             }
386         };
387     }
388 
389     /**
390      * Sub-ordinate task passed off to a different thread to be executed.
391      *
392      * @author jabley
393      *
394      */
395     class WorkerTask implements Runnable {
396 
397         private final HttpClient client;
398         private final HttpHost target;
399         private final HttpGet request;
400         private final CountDownLatch startGate;
401         private final CountDownLatch endGate;
402 
403         private boolean failed = false;
404         private String text;
405 
406         WorkerTask(final HttpClient client, final HttpHost target, final boolean identity, final CountDownLatch startGate, final CountDownLatch endGate) {
407             this.client = client;
408             this.target = target;
409             this.request = new HttpGet("/some-resource");
410             if (identity) {
411                 request.addHeader("Accept-Encoding", "identity");
412             }
413             this.startGate = startGate;
414             this.endGate = endGate;
415         }
416 
417         /**
418          * Returns the text of the HTTP entity.
419          *
420          * @return a String - may be null.
421          */
422         public String getText() {
423             return this.text;
424         }
425 
426         /**
427          * {@inheritDoc}
428          */
429         @Override
430         public void run() {
431             try {
432                 startGate.await();
433                 try {
434                     final HttpResponse response = client.execute(target, request);
435                     text = EntityUtils.toString(response.getEntity());
436                 } catch (final Exception e) {
437                     failed = true;
438                 } finally {
439                     endGate.countDown();
440                 }
441             } catch (final InterruptedException ignore) {
442             }
443         }
444 
445         /**
446          * Returns true if this task failed, otherwise false.
447          *
448          * @return a flag
449          */
450         boolean isFailed() {
451             return this.failed;
452         }
453     }
454 }