hc-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From ol...@apache.org
Subject svn commit: r755629 - in /httpcomponents/httpclient/branches/branch_4_1: ./ httpclient/src/main/java/org/apache/http/impl/client/ httpclient/src/test/java/org/apache/http/impl/client/
Date Wed, 18 Mar 2009 16:30:49 GMT
Author: olegk
Date: Wed Mar 18 16:30:49 2009
New Revision: 755629

URL: http://svn.apache.org/viewvc?rev=755629&view=rev
Log:
HTTPCLIENT-834: Transparent content encoding support (initial patch).

* Support for gzip and deflate content codings

Contributed by James Abley <james.abley at gmail.com>

Added:
    httpcomponents/httpclient/branches/branch_4_1/httpclient/src/main/java/org/apache/http/impl/client/ContentEncodingProcessor.java
    httpcomponents/httpclient/branches/branch_4_1/httpclient/src/main/java/org/apache/http/impl/client/DeflateDecompressingEntity.java
    httpcomponents/httpclient/branches/branch_4_1/httpclient/src/main/java/org/apache/http/impl/client/GzipDecompressingEntity.java
    httpcomponents/httpclient/branches/branch_4_1/httpclient/src/test/java/org/apache/http/impl/client/TestContentCodings.java
Modified:
    httpcomponents/httpclient/branches/branch_4_1/RELEASE_NOTES.txt
    httpcomponents/httpclient/branches/branch_4_1/httpclient/src/main/java/org/apache/http/impl/client/AbstractHttpClient.java

Modified: httpcomponents/httpclient/branches/branch_4_1/RELEASE_NOTES.txt
URL: http://svn.apache.org/viewvc/httpcomponents/httpclient/branches/branch_4_1/RELEASE_NOTES.txt?rev=755629&r1=755628&r2=755629&view=diff
==============================================================================
--- httpcomponents/httpclient/branches/branch_4_1/RELEASE_NOTES.txt (original)
+++ httpcomponents/httpclient/branches/branch_4_1/RELEASE_NOTES.txt Wed Mar 18 16:30:49 2009
@@ -1,3 +1,9 @@
+Changes in 4.1 branch
+-------------------
+
+* [HTTPCLIENT-834] Transparent content encoding support.
+  Contributed by James Abley <james.abley at gmail.com>
+
 Changes since 4.0 beta 2
 -------------------
 

Modified: httpcomponents/httpclient/branches/branch_4_1/httpclient/src/main/java/org/apache/http/impl/client/AbstractHttpClient.java
URL: http://svn.apache.org/viewvc/httpcomponents/httpclient/branches/branch_4_1/httpclient/src/main/java/org/apache/http/impl/client/AbstractHttpClient.java?rev=755629&r1=755628&r2=755629&view=diff
==============================================================================
--- httpcomponents/httpclient/branches/branch_4_1/httpclient/src/main/java/org/apache/http/impl/client/AbstractHttpClient.java
(original)
+++ httpcomponents/httpclient/branches/branch_4_1/httpclient/src/main/java/org/apache/http/impl/client/AbstractHttpClient.java
Wed Mar 18 16:30:49 2009
@@ -533,6 +533,16 @@
             } else {
                 execContext = new DefaultedHttpContext(context, defaultContext);
             }
+            
+            // Copy the HttpProcessor (along with interceptors) and add the default 
+            // handling for Content Codings
+            // This is the last interceptor added, so that client code is unaffected 
+            // by this addition.
+            BasicHttpProcessor httpProcessorCopy = getHttpProcessor().copy();
+            ContentEncodingProcessor ceProcessor = new ContentEncodingProcessor();
+            httpProcessorCopy.addRequestInterceptor(ceProcessor);
+            httpProcessorCopy.addResponseInterceptor(ceProcessor);
+            
             // Create a director for this request
             director = createClientRequestDirector(
                     getRequestExecutor(),
@@ -540,7 +550,7 @@
                     getConnectionReuseStrategy(),
                     getConnectionKeepAliveStrategy(),
                     getRoutePlanner(),
-                    getHttpProcessor().copy(),
+                    httpProcessorCopy,
                     getHttpRequestRetryHandler(),
                     getRedirectHandler(),
                     getTargetAuthenticationHandler(),
@@ -691,5 +701,4 @@
         return result;
     }
 
-
 } // class AbstractHttpClient

Added: httpcomponents/httpclient/branches/branch_4_1/httpclient/src/main/java/org/apache/http/impl/client/ContentEncodingProcessor.java
URL: http://svn.apache.org/viewvc/httpcomponents/httpclient/branches/branch_4_1/httpclient/src/main/java/org/apache/http/impl/client/ContentEncodingProcessor.java?rev=755629&view=auto
==============================================================================
--- httpcomponents/httpclient/branches/branch_4_1/httpclient/src/main/java/org/apache/http/impl/client/ContentEncodingProcessor.java
(added)
+++ httpcomponents/httpclient/branches/branch_4_1/httpclient/src/main/java/org/apache/http/impl/client/ContentEncodingProcessor.java
Wed Mar 18 16:30:49 2009
@@ -0,0 +1,78 @@
+package org.apache.http.impl.client;
+
+import java.io.IOException;
+
+import org.apache.http.Header;
+import org.apache.http.HeaderElement;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpException;
+import org.apache.http.HttpRequest;
+import org.apache.http.HttpRequestInterceptor;
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpResponseInterceptor;
+import org.apache.http.protocol.HttpContext;
+
+/**
+ * Class responsible for handling Content Encoding in HTTP. This takes the form of 
+ * an {@link HttpRequestInterceptor} that will modify the {@link HttpRequest} if the client
hasn't 
+ * already specified an <code>Accept-Encoding</code> header. There is an accompanying

+ * {@link HttpResponseInterceptor} implementation that will only examine the {@link HttpResponse}

+ * if the {@link HttpRequestInterceptor} implementation did any modifications.
+ * <p>
+ * Instances of this class are stateless, therefore they're thread-safe and immutable.
+ * 
+ * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.5
+ */
+class ContentEncodingProcessor implements HttpResponseInterceptor, HttpRequestInterceptor
{
+
+    /**
+     * {@inheritDoc}
+     */
+    public void process(
+            HttpRequest request, HttpContext context) throws HttpException, IOException {
+
+        /*
+         * If a client of this library has already set this header, presume that they did
so for 
+         * a reason and so this instance shouldn't handle the response at all.
+         */
+        if (!request.containsHeader("Accept-Encoding")) {
+
+            /* Signal support for Accept-Encoding transfer encodings. */
+            // TODO add compress support.
+            request.addHeader("Accept-Encoding", "gzip,deflate");
+
+            /* Store the fact that the request was modified, so that we can potentially handle
+             *  the response. */
+            context.setAttribute(ContentEncodingProcessor.class.getName(), Boolean.TRUE);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void process(
+            HttpResponse response, HttpContext context) throws HttpException, IOException
{
+        
+        if (context.getAttribute(ContentEncodingProcessor.class.getName()) != null) {
+            HttpEntity entity = response.getEntity();
+
+            if (entity != null) { // It wasn't a 304 Not Modified response, 204 No Content
or similar
+                Header ceheader = entity.getContentEncoding();
+                if (ceheader != null) {
+                    HeaderElement[] codecs = ceheader.getElements();
+                    for (int i = 0, n = codecs.length; i < n; ++i) {
+                        if ("gzip".equalsIgnoreCase(codecs[i].getName())) {
+                            response.setEntity(new GzipDecompressingEntity(response.getEntity()));
+                            return;
+                        } else if ("deflate".equalsIgnoreCase(codecs[i].getName())) {
+                            response.setEntity(new DeflateDecompressingEntity(response.getEntity()));
+                            return;
+                        }
+                        // TODO add compress. identity is a no-op.
+                    }
+                }
+            }
+        }
+    }
+
+}

Added: httpcomponents/httpclient/branches/branch_4_1/httpclient/src/main/java/org/apache/http/impl/client/DeflateDecompressingEntity.java
URL: http://svn.apache.org/viewvc/httpcomponents/httpclient/branches/branch_4_1/httpclient/src/main/java/org/apache/http/impl/client/DeflateDecompressingEntity.java?rev=755629&view=auto
==============================================================================
--- httpcomponents/httpclient/branches/branch_4_1/httpclient/src/main/java/org/apache/http/impl/client/DeflateDecompressingEntity.java
(added)
+++ httpcomponents/httpclient/branches/branch_4_1/httpclient/src/main/java/org/apache/http/impl/client/DeflateDecompressingEntity.java
Wed Mar 18 16:30:49 2009
@@ -0,0 +1,143 @@
+package org.apache.http.impl.client;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PushbackInputStream;
+import java.util.zip.DataFormatException;
+import java.util.zip.Inflater;
+import java.util.zip.InflaterInputStream;
+
+import org.apache.http.Header;
+import org.apache.http.HttpEntity;
+import org.apache.http.entity.HttpEntityWrapper;
+
+/**
+ * {@link HttpEntityWrapper} responsible for handling deflate Content Coded responses. In
RFC2616 
+ * terms, <code>deflate</code> means a <code>zlib</code> stream as
defined in RFC1950. Some server 
+ * implementations have misinterpreted RFC2616 to mean that a <code>deflate</code>
stream as 
+ * defined in RFC1951 should be used (or maybe they did that since that's how IE behaves?).
It's 
+ * confusing that <code>deflate</code> in HTTP 1.1 means <code>zlib</code>
streams rather than 
+ * <code>deflate</code> streams. We handle both types in here, since that's what
is seen on the 
+ * internet. Moral - prefer <code>gzip</code>!
+ */
+class DeflateDecompressingEntity extends HttpEntityWrapper {
+
+    /**
+     * Creates a new {@link DeflateDecompressingEntity} which will wrap the specified 
+     * {@link HttpEntity}.
+     *
+     * @param entity
+     *            a non-null {@link HttpEntity} to be wrapped
+     */
+    public DeflateDecompressingEntity(final HttpEntity entity) {
+        super(entity);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public InputStream getContent() throws IOException {
+        InputStream wrapped = this.wrappedEntity.getContent();
+
+        /*
+         * A zlib stream will have a header.
+         *
+         * CMF | FLG [| DICTID ] | ...compressed data | ADLER32 |
+         *
+         * * CMF is one byte.
+         *
+         * * FLG is one byte.
+         *
+         * * DICTID is four bytes, and only present if FLG.FDICT is set.
+         *
+         * Sniff the content. Does it look like a zlib stream, with a CMF, etc? c.f. RFC1950,

+         * section 2.2. http://tools.ietf.org/html/rfc1950#page-4
+         *
+         * We need to see if it looks like a proper zlib stream, or whether it is just a
deflate 
+         * stream. RFC2616 calls zlib streams deflate. Confusing, isn't it? That's why some
servers 
+         * implement deflate Content-Encoding using deflate streams, rather than zlib streams.
+         *
+         * We could start looking at the bytes, but to be honest, someone else has already
read 
+         * the RFCs and implemented that for us. So we'll just use the JDK libraries and
exception 
+         * handling to do this. If that proves slow, then we could potentially change this
to check 
+         * the first byte - does it look like a CMF? What about the second byte - does it
look like 
+         * a FLG, etc.
+         */
+
+        /* We read a small buffer to sniff the content. */
+        byte[] peeked = new byte[6];
+
+        PushbackInputStream pushback = new PushbackInputStream(wrapped, peeked.length);
+
+        int headerLength = pushback.read(peeked);
+
+        if (headerLength == -1) {
+            throw new IOException("Unable to read the response");
+        }
+
+        /* We try to read the first uncompressed byte. */
+        byte[] dummy = new byte[1];
+
+        Inflater inf = new Inflater();
+
+        try {
+            int n;
+            while ((n = inf.inflate(dummy)) == 0) {
+                if (inf.finished()) {
+
+                    /* Not expecting this, so fail loudly. */
+                    throw new IOException("Unable to read the response");
+                }
+
+                if (inf.needsDictionary()) {
+
+                    /* Need dictionary - then it must be zlib stream with DICTID part? */
+                    break;
+                }
+
+                if (inf.needsInput()) {
+                    inf.setInput(peeked);
+                }
+            }
+
+            if (n == -1) {
+                throw new IOException("Unable to read the response");
+            }
+
+            /*
+             * We read something without a problem, so it's a valid zlib stream. Just need
to reset 
+             * and return an unused InputStream now.
+             */
+            pushback.unread(peeked, 0, headerLength);
+            return new InflaterInputStream(pushback);
+        } catch (DataFormatException e) {
+
+            /* Presume that it's an RFC1951 deflate stream rather than RFC1950 zlib stream
and try 
+             * again. */
+            pushback.unread(peeked, 0, headerLength);
+            return new InflaterInputStream(pushback, new Inflater(true));
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public Header getContentEncoding() {
+
+        /* This HttpEntityWrapper has dealt with the Content-Encoding. */
+        return null;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public long getContentLength() {
+
+        /* Length of inflated content is unknown. */
+        return -1;
+    }
+
+}

Added: httpcomponents/httpclient/branches/branch_4_1/httpclient/src/main/java/org/apache/http/impl/client/GzipDecompressingEntity.java
URL: http://svn.apache.org/viewvc/httpcomponents/httpclient/branches/branch_4_1/httpclient/src/main/java/org/apache/http/impl/client/GzipDecompressingEntity.java?rev=755629&view=auto
==============================================================================
--- httpcomponents/httpclient/branches/branch_4_1/httpclient/src/main/java/org/apache/http/impl/client/GzipDecompressingEntity.java
(added)
+++ httpcomponents/httpclient/branches/branch_4_1/httpclient/src/main/java/org/apache/http/impl/client/GzipDecompressingEntity.java
Wed Mar 18 16:30:49 2009
@@ -0,0 +1,59 @@
+package org.apache.http.impl.client;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.zip.GZIPInputStream;
+
+import org.apache.http.Header;
+import org.apache.http.HttpEntity;
+import org.apache.http.entity.HttpEntityWrapper;
+
+/**
+ * {@link HttpEntityWrapper} for handling gzip Content Coded responses.
+ */
+class GzipDecompressingEntity extends HttpEntityWrapper {
+
+    /**
+     * Creates a new {@link GzipDecompressingEntity} which will wrap the specified 
+     * {@link HttpEntity}.
+     *
+     * @param entity
+     *            the non-null {@link HttpEntity} to be wrapped
+     */
+    public GzipDecompressingEntity(final HttpEntity entity) {
+        super(entity);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public InputStream getContent() throws IOException, IllegalStateException {
+
+        // the wrapped entity's getContent() decides about repeatability
+        InputStream wrappedin = wrappedEntity.getContent();
+
+        return new GZIPInputStream(wrappedin);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public Header getContentEncoding() {
+
+        /* This HttpEntityWrapper has dealt with the Content-Encoding. */
+        return null;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public long getContentLength() {
+
+        /* length of ungzipped content is not known */
+        return -1;
+    }
+
+}

Added: httpcomponents/httpclient/branches/branch_4_1/httpclient/src/test/java/org/apache/http/impl/client/TestContentCodings.java
URL: http://svn.apache.org/viewvc/httpcomponents/httpclient/branches/branch_4_1/httpclient/src/test/java/org/apache/http/impl/client/TestContentCodings.java?rev=755629&view=auto
==============================================================================
--- httpcomponents/httpclient/branches/branch_4_1/httpclient/src/test/java/org/apache/http/impl/client/TestContentCodings.java
(added)
+++ httpcomponents/httpclient/branches/branch_4_1/httpclient/src/test/java/org/apache/http/impl/client/TestContentCodings.java
Wed Mar 18 16:30:49 2009
@@ -0,0 +1,463 @@
+package org.apache.http.impl.client;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.zip.Deflater;
+
+/* Don't use Java 6 functionality, even in tests. */
+//import java.util.zip.DeflaterInputStream;
+import java.util.zip.GZIPOutputStream;
+
+import org.apache.http.Header;
+import org.apache.http.HeaderElement;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpException;
+import org.apache.http.HttpRequest;
+import org.apache.http.HttpRequestInterceptor;
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpResponseInterceptor;
+import org.apache.http.HttpStatus;
+import org.apache.http.HttpVersion;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.conn.ClientConnectionManager;
+import org.apache.http.conn.params.ConnManagerParams;
+import org.apache.http.conn.scheme.PlainSocketFactory;
+import org.apache.http.conn.scheme.Scheme;
+import org.apache.http.conn.scheme.SchemeRegistry;
+import org.apache.http.entity.InputStreamEntity;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
+import org.apache.http.localserver.ServerTestBase;
+import org.apache.http.params.BasicHttpParams;
+import org.apache.http.params.HttpParams;
+import org.apache.http.params.HttpProtocolParams;
+import org.apache.http.protocol.HttpContext;
+import org.apache.http.protocol.HttpRequestHandler;
+import org.apache.http.util.EntityUtils;
+
+/**
+ * Test case for how Content Codings are processed. By default, we want to do the right thing
and 
+ * require no intervention from the user of HttpClient, but we still want to let clients
do their 
+ * own thing if they so wish.
+ */
+public class TestContentCodings extends ServerTestBase {
+
+    public TestContentCodings(String testName) {
+        super(testName);
+    }
+
+    /**
+     * Test for when we don't get an entity back; e.g. for a 204 or 304 response; nothing
blows 
+     * up with the new behaviour.
+     * 
+     * @throws Exception
+     *             if there was a problem
+     */
+    public void testResponseWithNoContent() throws Exception {
+        this.localServer.register("*", new HttpRequestHandler() {
+
+            /**
+             * {@inheritDoc}
+             */
+            public void handle(
+                    HttpRequest request, 
+                    HttpResponse response, 
+                    HttpContext context) throws HttpException, IOException {
+                response.setStatusCode(HttpStatus.SC_NO_CONTENT);
+            }
+        });
+
+        DefaultHttpClient client = new DefaultHttpClient();
+
+        HttpGet request = new HttpGet("/some-resource");
+        HttpResponse response = client.execute(getServerHttp(), request);
+        assertEquals(HttpStatus.SC_NO_CONTENT, response.getStatusLine().getStatusCode());
+        assertNull(response.getEntity());
+
+        client.getConnectionManager().shutdown();
+    }
+
+    /**
+     * Test for when we are handling content from a server that has correctly interpreted
RFC2616 
+     * to return RFC1950 streams for <code>deflate</code> content coding.
+     * 
+     * @throws Exception
+     * @see {@link DeflateDecompressingEntity}
+     */
+    public void testDeflateSupportForServerReturningRfc1950Stream() throws Exception {
+        final String entityText = "Hello, this is some plain text coming back.";
+
+        this.localServer.register("*", createDeflateEncodingRequestHandler(entityText, false));
+
+        DefaultHttpClient client = new DefaultHttpClient();
+
+        HttpGet request = new HttpGet("/some-resource");
+        HttpResponse response = client.execute(getServerHttp(), request);
+        assertEquals("The entity text is correctly transported", entityText, 
+                EntityUtils.toString(response.getEntity()));
+
+        client.getConnectionManager().shutdown();
+    }
+
+    /**
+     * Test for when we are handling content from a server that has incorrectly interpreted
RFC2616 
+     * to return RFC1951 streams for <code>deflate</code> content coding.
+     * 
+     * @throws Exception
+     * @see {@link DeflateDecompressingEntity}
+     */
+    public void testDeflateSupportForServerReturningRfc1951Stream() throws Exception {
+        final String entityText = "Hello, this is some plain text coming back.";
+
+        this.localServer.register("*", createDeflateEncodingRequestHandler(entityText, true));
+
+        DefaultHttpClient client = new DefaultHttpClient();
+        HttpGet request = new HttpGet("/some-resource");
+        HttpResponse response = client.execute(getServerHttp(), request);
+        assertEquals("The entity text is correctly transported", entityText, 
+                EntityUtils.toString(response.getEntity()));
+
+        client.getConnectionManager().shutdown();
+    }
+
+    /**
+     * Test for a server returning gzipped content.
+     * 
+     * @throws Exception
+     */
+    public void testGzipSupport() throws Exception {
+        final String entityText = "Hello, this is some plain text coming back.";
+
+        this.localServer.register("*", createGzipEncodingRequestHandler(entityText));
+
+        DefaultHttpClient client = new DefaultHttpClient();
+        HttpGet request = new HttpGet("/some-resource");
+        HttpResponse response = client.execute(getServerHttp(), request);
+        assertEquals("The entity text is correctly transported", entityText, 
+                EntityUtils.toString(response.getEntity()));
+
+        client.getConnectionManager().shutdown();
+    }
+
+    /**
+     * Try with a bunch of client threads, to check that it's thread-safe.
+     * 
+     * @throws Exception
+     *             if there was a problem
+     */
+    public void testThreadSafetyOfContentCodings() throws Exception {
+        final String entityText = "Hello, this is some plain text coming back.";
+
+        this.localServer.register("*", createGzipEncodingRequestHandler(entityText));
+
+        /*
+         * Create a load of workers which will access the resource. Half will use the default

+         * gzip behaviour; half will require identity entity.
+         */
+        int clients = 100;
+
+        HttpParams params = new BasicHttpParams();
+        ConnManagerParams.setMaxTotalConnections(params, clients);
+        HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);
+
+        SchemeRegistry schemeRegistry = new SchemeRegistry();
+        schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(),
80));
+
+        ClientConnectionManager cm = new ThreadSafeClientConnManager(params, schemeRegistry);
+        final HttpClient httpClient = new DefaultHttpClient(cm, params);
+
+        ExecutorService executor = Executors.newFixedThreadPool(clients);
+
+        CountDownLatch startGate = new CountDownLatch(1);
+        CountDownLatch endGate = new CountDownLatch(clients);
+
+        List<WorkerTask> workers = new ArrayList<WorkerTask>();
+
+        for (int i = 0; i < clients; ++i) {
+            workers.add(new WorkerTask(httpClient, i % 2 == 0, startGate, endGate));
+        }
+
+        for (WorkerTask workerTask : workers) {
+
+            /* Set them all in motion, but they will block until we call startGate.countDown().
*/
+            executor.execute(workerTask);
+        }
+
+        startGate.countDown();
+
+        /* Wait for the workers to complete. */
+        endGate.await();
+
+        for (WorkerTask workerTask : workers) {
+            if (workerTask.isFailed()) {
+                fail("A worker failed");
+            }
+            assertEquals(entityText, workerTask.getText());
+        }
+    }
+
+    public void testExistingProtocolInterceptorsAreNotAffected() throws Exception {
+        final String entityText = "Hello, this is some plain text coming back.";
+
+        this.localServer.register("*", createGzipEncodingRequestHandler(entityText));
+
+        DefaultHttpClient client = new DefaultHttpClient();
+        HttpGet request = new HttpGet("/some-resource");
+
+        client.addRequestInterceptor(new HttpRequestInterceptor() {
+
+            /**
+             * {@inheritDoc}
+             */
+            public void process(
+                    HttpRequest request, HttpContext context) throws HttpException, IOException
{
+                request.addHeader("Accept-Encoding", "gzip");
+            }
+        });
+
+        /* Get around Java The Language's lack of mutable closures */
+        final boolean clientSawGzip[] = new boolean[1];
+
+        client.addResponseInterceptor(new HttpResponseInterceptor() {
+
+            /**
+             * {@inheritDoc}
+             */
+            public void process(
+                    HttpResponse response, HttpContext context) throws HttpException, IOException
{
+                HttpEntity entity = response.getEntity();
+                if (entity != null) {
+                    Header ce = entity.getContentEncoding();
+
+                    if (ce != null) {
+                        HeaderElement[] codecs = ce.getElements();
+                        for (int i = 0, n = codecs.length; i < n; ++i) {
+                            if ("gzip".equalsIgnoreCase(codecs[i].getName())) {
+                                clientSawGzip[0] = true;
+                                return;
+                            }
+                        }
+                    }
+                }
+            }
+        });
+
+        client.execute(getServerHttp(), request);
+
+        assertTrue("Client which added the new custom protocol interceptor to handle gzip
responses " +
+        		"was unaffected.",
+                clientSawGzip[0]);
+
+        client.getConnectionManager().shutdown();
+    }
+
+    /**
+     * Creates a new {@link HttpRequestHandler} that will attempt to provide a deflate stream

+     * Content-Coding.
+     * 
+     * @param entityText
+     *            the non-null String entity text to be returned by the server
+     * @param rfc1951
+     *            if true, then the stream returned will be a raw RFC1951 deflate stream,
which 
+     *            some servers return as a result of misinterpreting the HTTP 1.1 RFC. If
false, 
+     *            then it will return an RFC2616 compliant deflate encoded zlib stream.
+     * @return a non-null {@link HttpRequestHandler}
+     */
+    private HttpRequestHandler createDeflateEncodingRequestHandler(
+            final String entityText, final boolean rfc1951) {
+        return new HttpRequestHandler() {
+
+            /**
+             * {@inheritDoc}
+             */
+            public void handle(
+                    HttpRequest request, 
+                    HttpResponse response, 
+                    HttpContext context) throws HttpException, IOException {
+                response.setEntity(new StringEntity(entityText));
+                response.addHeader("Content-Type", "text/plain");
+                Header[] acceptEncodings = request.getHeaders("Accept-Encoding");
+
+                for (Header header : acceptEncodings) {
+                    for (HeaderElement element : header.getElements()) {
+                        if ("deflate".equalsIgnoreCase(element.getName())) {
+                            response.addHeader("Content-Encoding", "deflate");
+
+                            /* Gack. DeflaterInputStream is Java 6. */
+                            // response.setEntity(new InputStreamEntity(new DeflaterInputStream(new
+                            // ByteArrayInputStream(
+                            // entityText.getBytes("utf-8"))), -1));
+                            byte[] uncompressed = entityText.getBytes("utf-8");
+                            Deflater compressor = new Deflater(Deflater.DEFAULT_COMPRESSION,
rfc1951);
+                            compressor.setInput(uncompressed);
+                            compressor.finish();
+                            byte[] output = new byte[100];
+                            int compressedLength = compressor.deflate(output);
+                            byte[] compressed = new byte[compressedLength];
+                            System.arraycopy(output, 0, compressed, 0, compressedLength);
+                            response.setEntity(new InputStreamEntity(
+                                    new ByteArrayInputStream(compressed), compressedLength));
+                            return;
+                        }
+                    }
+                }
+            }
+        };
+    }
+
+    /**
+     * Returns an {@link HttpRequestHandler} implementation that will attempt to provide
a gzip 
+     * Content-Encoding.
+     * 
+     * @param entityText
+     *            the non-null String entity to be returned by the server
+     * @return a non-null {@link HttpRequestHandler}
+     */
+    private HttpRequestHandler createGzipEncodingRequestHandler(final String entityText)
{
+        return new HttpRequestHandler() {
+
+            /**
+             * {@inheritDoc}
+             */
+            public void handle(
+                    HttpRequest request, 
+                    HttpResponse response, 
+                    HttpContext context) throws HttpException, IOException {
+                response.setEntity(new StringEntity(entityText));
+                response.addHeader("Content-Type", "text/plain");
+                Header[] acceptEncodings = request.getHeaders("Accept-Encoding");
+
+                for (Header header : acceptEncodings) {
+                    for (HeaderElement element : header.getElements()) {
+                        if ("gzip".equalsIgnoreCase(element.getName())) {
+                            response.addHeader("Content-Encoding", "gzip");
+
+                            /*
+                             * We have to do a bit more work with gzip versus deflate, since

+                             * Gzip doesn't appear to have an equivalent to DeflaterInputStream
in 
+                             * the JDK.
+                             * 
+                             * UPDATE: DeflaterInputStream is Java 6 anyway, so we have to
do a bit 
+                             * of work there too!
+                             */
+                            ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+                            OutputStream out = new GZIPOutputStream(bytes);
+
+                            ByteArrayInputStream uncompressed = new ByteArrayInputStream(
+                                    entityText.getBytes("utf-8"));
+
+                            byte[] buf = new byte[60];
+
+                            int n;
+                            while ((n = uncompressed.read(buf)) != -1) {
+                                out.write(buf, 0, n);
+                            }
+
+                            out.close();
+
+                            byte[] arr = bytes.toByteArray();
+                            response.setEntity(new InputStreamEntity(new ByteArrayInputStream(arr),

+                                    arr.length));
+
+                            return;
+                        }
+                    }
+                }
+            }
+        };
+    }
+
+    /**
+     * Sub-ordinate task passed off to a different thread to be executed.
+     * 
+     * @author jabley
+     * 
+     */
+    class WorkerTask implements Runnable {
+
+        /**
+         * The {@link HttpClient} used to make requests.
+         */
+        private final HttpClient client;
+
+        /**
+         * The {@link HttpRequest} to be executed.
+         */
+        private final HttpGet request;
+
+        /**
+         * Flag indicating if there were failures.
+         */
+        private boolean failed = false;
+
+        /**
+         * The latch that this runnable instance should wait on.
+         */
+        private final CountDownLatch startGate;
+
+        /**
+         * The latch that this runnable instance should countdown on when the runnable is
finished.
+         */
+        private final CountDownLatch endGate;
+
+        /**
+         * The text returned from the HTTP server.
+         */
+        private String text;
+
+        WorkerTask(HttpClient client, boolean identity, CountDownLatch startGate, CountDownLatch
endGate) {
+            this.client = client;
+            this.request = new HttpGet("/some-resource");
+            if (identity) {
+                request.addHeader("Accept-Encoding", "identity");
+            }
+            this.startGate = startGate;
+            this.endGate = endGate;
+        }
+
+        /**
+         * Returns the text of the HTTP entity.
+         * 
+         * @return a String - may be null.
+         */
+        public String getText() {
+            return this.text;
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        public void run() {
+            try {
+                startGate.await();
+                try {
+                    HttpResponse response = client.execute(TestContentCodings.this.getServerHttp(),
request);
+                    text = EntityUtils.toString(response.getEntity());
+                } catch (Exception e) {
+                    failed = true;
+                } finally {
+                    endGate.countDown();
+                }
+            } catch (InterruptedException e) {
+
+            }
+        }
+
+        /**
+         * Returns true if this task failed, otherwise false.
+         * 
+         * @return a flag
+         */
+        boolean isFailed() {
+            return this.failed;
+        }
+    }
+}



Mime
View raw message