Add more unit tests for client.result, and more small code tweaks.
[zxing.git] / android / src / com / google / zxing / client / android / AndroidHttpClient.java
1 /*
2  * Copyright (C) 2008 ZXing authors
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 package com.google.zxing.client.android;
18
19 import android.util.Log;
20 import org.apache.http.Header;
21 import org.apache.http.HttpEntity;
22 import org.apache.http.HttpEntityEnclosingRequest;
23 import org.apache.http.HttpHost;
24 import org.apache.http.HttpMessage;
25 import org.apache.http.HttpRequest;
26 import org.apache.http.HttpRequestInterceptor;
27 import org.apache.http.HttpResponse;
28 import org.apache.http.client.HttpClient;
29 import org.apache.http.client.ResponseHandler;
30 import org.apache.http.client.methods.HttpUriRequest;
31 import org.apache.http.client.params.HttpClientParams;
32 import org.apache.http.client.protocol.ClientContext;
33 import org.apache.http.conn.ClientConnectionManager;
34 import org.apache.http.conn.scheme.PlainSocketFactory;
35 import org.apache.http.conn.scheme.Scheme;
36 import org.apache.http.conn.scheme.SchemeRegistry;
37 import org.apache.http.conn.ssl.SSLSocketFactory;
38 import org.apache.http.entity.AbstractHttpEntity;
39 import org.apache.http.entity.ByteArrayEntity;
40 import org.apache.http.impl.client.DefaultHttpClient;
41 import org.apache.http.impl.client.RequestWrapper;
42 import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
43 import org.apache.http.params.BasicHttpParams;
44 import org.apache.http.params.HttpConnectionParams;
45 import org.apache.http.params.HttpParams;
46 import org.apache.http.params.HttpProtocolParams;
47 import org.apache.http.protocol.BasicHttpContext;
48 import org.apache.http.protocol.BasicHttpProcessor;
49 import org.apache.http.protocol.HttpContext;
50
51 import java.io.ByteArrayOutputStream;
52 import java.io.IOException;
53 import java.io.InputStream;
54 import java.io.OutputStream;
55 import java.net.URI;
56 import java.util.zip.GZIPInputStream;
57 import java.util.zip.GZIPOutputStream;
58
59 /**
60  * <p>Subclass of the Apache {@link DefaultHttpClient} that is configured with
61  * reasonable default settings and registered schemes for Android, and
62  * also lets the user add {@link HttpRequestInterceptor} classes.
63  * Don't create this directly, use the {@link #newInstance} factory method.</p>
64  * <p/>
65  * <p>This client processes cookies but does not retain them by default.
66  * To retain cookies, simply add a cookie store to the HttpContext:
67  * <pre>context.setAttribute(ClientContext.COOKIE_STORE, cookieStore);</pre>
68  * </p>
69  */
70 public final class AndroidHttpClient implements HttpClient {
71
72   // Gzip of data shorter than this probably won't be worthwhile
73   private static final long DEFAULT_SYNC_MIN_GZIP_BYTES = 256;
74
75   private static final String TAG = "AndroidHttpClient";
76
77
78   /**
79    * Set if HTTP requests are blocked from being executed on this thread
80    */
81   private static final ThreadLocal<Boolean> sThreadBlocked =
82       new ThreadLocal<Boolean>();
83
84   /**
85    * Interceptor throws an exception if the executing thread is blocked
86    */
87   private static final HttpRequestInterceptor sThreadCheckInterceptor =
88       new HttpRequestInterceptor() {
89         public void process(HttpRequest request, HttpContext context) {
90           if (sThreadBlocked.get() != null && sThreadBlocked.get()) {
91             throw new RuntimeException("This thread forbids HTTP requests");
92           }
93         }
94       };
95
96   /**
97    * Create a new HttpClient with reasonable defaults (which you can update).
98    *
99    * @param userAgent to report in your HTTP requests.
100    * @return AndroidHttpClient for you to use for all your requests.
101    */
102   public static AndroidHttpClient newInstance(String userAgent) {
103     HttpParams params = new BasicHttpParams();
104
105     // Turn off stale checking.  Our connections break all the time anyway,
106     // and it's not worth it to pay the penalty of checking every time.
107     HttpConnectionParams.setStaleCheckingEnabled(params, false);
108
109     // Default connection and socket timeout of 20 seconds.  Tweak to taste.
110     HttpConnectionParams.setConnectionTimeout(params, 20 * 1000);
111     HttpConnectionParams.setSoTimeout(params, 20 * 1000);
112     HttpConnectionParams.setSocketBufferSize(params, 8192);
113
114     // Don't handle redirects -- return them to the caller.  Our code
115     // often wants to re-POST after a redirect, which we must do ourselves.
116     HttpClientParams.setRedirecting(params, false);
117
118     // Set the specified user agent and register standard protocols.
119     HttpProtocolParams.setUserAgent(params, userAgent);
120     SchemeRegistry schemeRegistry = new SchemeRegistry();
121     schemeRegistry.register(new Scheme("http",
122         PlainSocketFactory.getSocketFactory(), 80));
123     schemeRegistry.register(new Scheme("https",
124         SSLSocketFactory.getSocketFactory(), 443));
125     ClientConnectionManager manager =
126         new ThreadSafeClientConnManager(params, schemeRegistry);
127
128     // We use a factory method to modify superclass initialization
129     // parameters without the funny call-a-static-method dance.
130     return new AndroidHttpClient(manager, params);
131   }
132
133   private final HttpClient delegate;
134
135   private RuntimeException mLeakedException = new IllegalStateException(
136       "AndroidHttpClient created and never closed");
137
138   private AndroidHttpClient(ClientConnectionManager ccm, HttpParams params) {
139     this.delegate = new DefaultHttpClient(ccm, params) {
140       @Override
141       protected BasicHttpProcessor createHttpProcessor() {
142         // Add interceptor to prevent making requests from main thread.
143         BasicHttpProcessor processor = super.createHttpProcessor();
144         processor.addRequestInterceptor(sThreadCheckInterceptor);
145         processor.addRequestInterceptor(new CurlLogger());
146
147         return processor;
148       }
149
150       @Override
151       protected HttpContext createHttpContext() {
152         // Same as DefaultHttpClient.createHttpContext() minus the
153         // cookie store.
154         HttpContext context = new BasicHttpContext();
155         context.setAttribute(ClientContext.AUTHSCHEME_REGISTRY, getAuthSchemes());
156         context.setAttribute(ClientContext.COOKIESPEC_REGISTRY, getCookieSpecs());
157         context.setAttribute(ClientContext.CREDS_PROVIDER, getCredentialsProvider());
158         return context;
159       }
160     };
161   }
162
163   @Override
164   protected void finalize() throws Throwable {
165     super.finalize();
166     if (mLeakedException != null) {
167       Log.e(TAG, "Leak found", mLeakedException);
168       mLeakedException = null;
169     }
170   }
171
172   /**
173    * Block this thread from executing HTTP requests.
174    * Used to guard against HTTP requests blocking the main application thread.
175    *
176    * @param blocked if HTTP requests run on this thread should be denied
177    */
178   public static void setThreadBlocked(boolean blocked) {
179     sThreadBlocked.set(blocked);
180   }
181
182   /**
183    * Modifies a request to indicate to the server that we would like a
184    * gzipped response.  (Uses the "Accept-Encoding" HTTP header.)
185    *
186    * @param request the request to modify
187    * @see #getUngzippedContent
188    */
189   public static void modifyRequestToAcceptGzipResponse(HttpMessage request) {
190     request.addHeader("Accept-Encoding", "gzip");
191   }
192
193   /**
194    * Gets the input stream from a response entity.  If the entity is gzipped
195    * then this will get a stream over the uncompressed data.
196    *
197    * @param entity the entity whose content should be read
198    * @return the input stream to read from
199    * @throws IOException
200    */
201   public static InputStream getUngzippedContent(HttpEntity entity) throws IOException {
202     InputStream responseStream = entity.getContent();
203     if (responseStream == null) {
204       return responseStream;
205     }
206     Header header = entity.getContentEncoding();
207     if (header == null) {
208       return responseStream;
209     }
210     String contentEncoding = header.getValue();
211     if (contentEncoding == null) {
212       return responseStream;
213     }
214     if (contentEncoding.contains("gzip")) {
215       responseStream = new GZIPInputStream(responseStream);
216     }
217     return responseStream;
218   }
219
220   /**
221    * Release resources associated with this client.  You must call this,
222    * or significant resources (sockets and memory) may be leaked.
223    */
224   public void close() {
225     if (mLeakedException != null) {
226       getConnectionManager().shutdown();
227       mLeakedException = null;
228     }
229   }
230
231   public HttpParams getParams() {
232     return delegate.getParams();
233   }
234
235   public ClientConnectionManager getConnectionManager() {
236     return delegate.getConnectionManager();
237   }
238
239   public HttpResponse execute(HttpUriRequest request) throws IOException {
240     return delegate.execute(request);
241   }
242
243   public HttpResponse execute(HttpUriRequest request, HttpContext context) throws IOException {
244     return delegate.execute(request, context);
245   }
246
247   public HttpResponse execute(HttpHost target, HttpRequest request) throws IOException {
248     return delegate.execute(target, request);
249   }
250
251   public HttpResponse execute(HttpHost target, HttpRequest request,
252                               HttpContext context) throws IOException {
253     return delegate.execute(target, request, context);
254   }
255
256   public <T> T execute(HttpUriRequest request, ResponseHandler<? extends T> responseHandler) throws IOException {
257     return delegate.execute(request, responseHandler);
258   }
259
260   public <T> T execute(HttpUriRequest request, ResponseHandler<? extends T> responseHandler, HttpContext context)
261       throws IOException {
262     return delegate.execute(request, responseHandler, context);
263   }
264
265   public <T> T execute(HttpHost target, HttpRequest request, ResponseHandler<? extends T> responseHandler)
266       throws IOException {
267     return delegate.execute(target, request, responseHandler);
268   }
269
270   public <T> T execute(HttpHost target, HttpRequest request,
271                        ResponseHandler<? extends T> responseHandler,
272                        HttpContext context)
273       throws IOException {
274     return delegate.execute(target, request, responseHandler, context);
275   }
276
277   /**
278    * Compress data to send to server.
279    * Creates a Http Entity holding the gzipped data.
280    * The data will not be compressed if it is too short.
281    *
282    * @param data The bytes to compress
283    * @return Entity holding the data
284    */
285   public static AbstractHttpEntity getCompressedEntity(byte[] data) throws IOException {
286     AbstractHttpEntity entity;
287     if (data.length < getMinGzipSize()) {
288       entity = new ByteArrayEntity(data);
289     } else {
290       ByteArrayOutputStream arr = new ByteArrayOutputStream();
291       OutputStream zipper = new GZIPOutputStream(arr);
292       try {
293         zipper.write(data);
294       } finally {
295         zipper.close();
296       }
297       entity = new ByteArrayEntity(arr.toByteArray());
298       entity.setContentEncoding("gzip");
299     }
300     return entity;
301   }
302
303   /**
304    * Retrieves the minimum size for compressing data.
305    * Shorter data will not be compressed.
306    */
307   private static long getMinGzipSize() {
308     return DEFAULT_SYNC_MIN_GZIP_BYTES;
309   }
310
311   /* cURL logging support. */
312
313   /**
314    * Logging tag and level.
315    */
316   private static final class LoggingConfiguration {
317
318     private final String tag;
319     private final int level;
320
321     private LoggingConfiguration(String tag, int level) {
322       this.tag = tag;
323       this.level = level;
324     }
325
326     /**
327      * Returns true if logging is turned on for this configuration.
328      */
329     private boolean isLoggable() {
330       return Log.isLoggable(tag, level);
331     }
332
333     /**
334      * Prints a message using this configuration.
335      */
336     private void println(String message) {
337       Log.println(level, tag, message);
338     }
339   }
340
341   /**
342    * cURL logging configuration.
343    */
344   private volatile LoggingConfiguration curlConfiguration;
345
346   /**
347    * Enables cURL request logging for this client.
348    *
349    * @param name  to log messages with
350    * @param level at which to log messages (see {@link android.util.Log})
351    */
352   public void enableCurlLogging(String name, int level) {
353     if (name == null) {
354       throw new NullPointerException("name");
355     }
356     if (level < Log.VERBOSE || level > Log.ASSERT) {
357       throw new IllegalArgumentException("Level is out of range ["
358           + Log.VERBOSE + ".." + Log.ASSERT + ']');
359     }
360
361     curlConfiguration = new LoggingConfiguration(name, level);
362   }
363
364   /**
365    * Disables cURL logging for this client.
366    */
367   public void disableCurlLogging() {
368     curlConfiguration = null;
369   }
370
371   /**
372    * Logs cURL commands equivalent to requests.
373    */
374   private final class CurlLogger implements HttpRequestInterceptor {
375     public void process(HttpRequest request, HttpContext context)
376         throws IOException {
377       LoggingConfiguration configuration = curlConfiguration;
378       if (configuration != null
379           && configuration.isLoggable()
380           && request instanceof HttpUriRequest) {
381         configuration.println(toCurl((HttpUriRequest) request));
382       }
383     }
384
385     /**
386      * Generates a cURL command equivalent to the given request.
387      */
388     private String toCurl(HttpUriRequest request) throws IOException {
389       StringBuilder builder = new StringBuilder();
390
391       builder.append("curl ");
392
393       for (Header header : request.getAllHeaders()) {
394         builder.append("--header \"");
395         builder.append(header.toString().trim());
396         builder.append("\" ");
397       }
398
399       URI uri = request.getURI();
400
401       // If this is a wrapped request, use the URI from the original
402       // request instead. getURI() on the wrapper seems to return a
403       // relative URI. We want an absolute URI.
404       if (request instanceof RequestWrapper) {
405         HttpRequest original = ((RequestWrapper) request).getOriginal();
406         if (original instanceof HttpUriRequest) {
407           uri = ((HttpUriRequest) original).getURI();
408         }
409       }
410
411       builder.append('"');
412       builder.append(uri);
413       builder.append('"');
414
415       if (request instanceof HttpEntityEnclosingRequest) {
416         HttpEntityEnclosingRequest entityRequest =
417             (HttpEntityEnclosingRequest) request;
418         HttpEntity entity = entityRequest.getEntity();
419         if (entity != null && entity.isRepeatable()) {
420           if (entity.getContentLength() < 1024) {
421             OutputStream stream = new ByteArrayOutputStream();
422             entity.writeTo(stream);
423             String entityString = stream.toString();
424             // TODO: Check the content type, too.
425             builder.append(" --data-ascii \"").append(entityString).append('"');
426           } else {
427             builder.append(" [TOO MUCH DATA TO INCLUDE]");
428           }
429         }
430       }
431
432       return builder.toString();
433     }
434   }
435
436 }