Project: lightbox-android-webservices
/**
 * Copyright (c) 2012 Lightbox 
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); 
 * you may not use this file except in compliance with the License. 
 * You may obtain a copy of the License at 
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0 
 * 
 * Unless required by applicable law or agreed to in writing, software 
 * distributed under the License is distributed on an "AS IS" BASIS, 
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 * See the License for the specific language governing permissions and 
 * limitations under the License. 
 */
package com.lightbox.android.network; 
 
import java.io.IOException; 
import java.io.InputStream; 
import java.io.UnsupportedEncodingException; 
import java.net.URI; 
import java.util.ArrayList; 
import java.util.List; 
import java.util.Locale; 
import java.util.Map; 
import java.util.zip.GZIPInputStream; 
 
import org.apache.http.Header; 
import org.apache.http.HeaderElement; 
import org.apache.http.HttpEntity; 
import org.apache.http.HttpEntityEnclosingRequest; 
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.NameValuePair; 
import org.apache.http.auth.AuthScope; 
import org.apache.http.auth.UsernamePasswordCredentials; 
import org.apache.http.client.entity.UrlEncodedFormEntity; 
import org.apache.http.client.methods.HttpDelete; 
import org.apache.http.client.methods.HttpGet; 
import org.apache.http.client.methods.HttpPost; 
import org.apache.http.client.methods.HttpPut; 
import org.apache.http.client.methods.HttpUriRequest; 
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.conn.ssl.SSLSocketFactory; 
import org.apache.http.entity.HttpEntityWrapper; 
import org.apache.http.entity.StringEntity; 
import org.apache.http.impl.client.DefaultHttpClient; 
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; 
import org.apache.http.message.BasicNameValuePair; 
import org.apache.http.params.BasicHttpParams; 
import org.apache.http.params.HttpConnectionParams; 
import org.apache.http.params.HttpParams; 
import org.apache.http.params.HttpProtocolParams; 
import org.apache.http.protocol.HTTP; 
import org.apache.http.protocol.HttpContext; 
 
import android.net.Uri; 
 
import com.lightbox.android.utils.AndroidUtils; 
import com.lightbox.android.utils.debug.DebugLog; 
 
/** 
 * HttpHelper  
 * @author Fabien Devos 
 */
 
public class HttpHelper { 
 /** Used to tag logs */ 
 //@SuppressWarnings("unused") 
 private static final String TAG = "HttpHelper"
  
 private static final int HTTP_TIMEOUT = 30 * 1000// 30 sec 
 private static final int SOCKET_BUFFER_SIZE = 8 * 1024// 8 Ko 
 private static final String STRING_HTTP = "http"
 private static final String STRING_HTTPS = "https"
 private static final String USER_AGENT_FORMAT_STRING = "%s/%s (Android %s; %s)"
  
 private DefaultHttpClient mHttpClient = null
  
 /** Used to select an HTTP method. */  
 public enum HttpMethod { 
  GET, 
  PUT, 
  POST, 
  DELETE; 
  public HttpUriRequest createHttpRequest(URI uri) { 
   switch(this) { 
    case GET: 
     return new HttpGet(uri); 
    case PUT: 
     return new HttpPut(uri); 
    case POST: 
     return new HttpPost(uri); 
    case DELETE: 
     return new HttpDelete(uri); 
   
   throw new IllegalArgumentException(String.format("Unknown Http Method: %s."this.toString())); 
  
 
  
 //---------------------------------------------------------------------------- 
 // Singleton pattern 
 private HttpHelper() { 
        mHttpClient = createHttpClient(); 
 
 
 /** HttpHelperHolder is loaded on the first execution of HttpHelper.getInstance()   * or the first access to HttpHelperHolder.INSTANCE, not before. */ 
 private static class HttpHelperHolder { 
  private static final HttpHelper INSTANCE = new HttpHelper(); 
 
 
 /** @return a unique instance of the class */ 
 public static HttpHelper getInstance() { 
  return HttpHelperHolder.INSTANCE; 
 
 
 /** Not supported  * @throws CloneNotSupportedException (every time) */ 
 public Object clone() throws CloneNotSupportedException { 
  // to prevent any kind of cheating 
  throw new CloneNotSupportedException(); 
 
 //---------------------------------------------------------------------------- 
  
 /**
  * Create an HttpClient. 
  * @return a properly set HttpClient 
  */
 
 private static DefaultHttpClient createHttpClient() { 
  HttpParams params = new BasicHttpParams(); 
  HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1); 
  SchemeRegistry schemeRegistry = new SchemeRegistry(); 
  schemeRegistry.register(new Scheme(STRING_HTTP, PlainSocketFactory.getSocketFactory(), 80)); 
  schemeRegistry.register(new Scheme(STRING_HTTPS, SSLSocketFactory.getSocketFactory(), 443)); 
  HttpConnectionParams.setConnectionTimeout(params, HTTP_TIMEOUT); 
  HttpConnectionParams.setSoTimeout(params, HTTP_TIMEOUT); 
  ConnManagerParams.setTimeout(params, HTTP_TIMEOUT); 
  HttpConnectionParams.setSocketBufferSize(params, SOCKET_BUFFER_SIZE); 
        HttpProtocolParams.setContentCharset(params, HTTP.UTF_8); 
        HttpProtocolParams.setHttpElementCharset(params, HTTP.UTF_8); 
  HttpProtocolParams.setUserAgent(params, String.format(USER_AGENT_FORMAT_STRING, 
    AndroidUtils.getApplicationLabel(), 
    AndroidUtils.getVersionCode(), 
    android.os.Build.VERSION.RELEASE, 
    android.os.Build.MODEL)); 
         
  DefaultHttpClient client = new DefaultHttpClient(new ThreadSafeClientConnManager(params, schemeRegistry), params); 
  enableGzipCompression(client); 
  return client; 
 
  
 /**
  * Set Http Basic Authentication.  
  * @param hostname 
  * @param username 
  * @param password 
  */
 
 public void setBasicAuth(String hostname, String username, String password) { 
  if (hostname != null && username != null && password != null) { 
   mHttpClient.getCredentialsProvider().setCredentials( 
     new AuthScope(hostname, 443, AuthScope.ANY_REALM), 
     new UsernamePasswordCredentials(username, password)); 
   mHttpClient.getCredentialsProvider().setCredentials( 
    new AuthScope(hostname, 80, AuthScope.ANY_REALM), 
    new UsernamePasswordCredentials(username, password)); 
  
 
 
 //------------------------------------------------------------------------------------------- 
 // HTTP Methods call 
  
 /**
  * Call the specified {@link HttpMethod}. 
  * Convenience method for {@link #call(HttpMethod, URI, Map, Object, Map)}. 
  * @see #call(HttpMethod, URI, Map, Object, Map) 
  */
 
    public HttpResponse call(HttpMethod method, URI uri, Map<String, Object> urlParameters) throws IOException { 
  return call(method, uri, urlParameters, null); 
    } 
     
 /**
  * Call the specified {@link HttpMethod}. 
  * Convenience method for {@link #call(HttpMethod, URI, Map, Object, Map)}. 
  * @see #call(HttpMethod, URI, Map, Object, Map) 
  */
 
    public HttpResponse call(HttpMethod method, URI uri, Map<String, Object> urlParameters, Object body) throws IOException { 
  return call(method, uri, urlParameters, body, null); 
    } 
  
 /**
  * Call the specified {@link HttpMethod}. 
  * @param method 
  * @param uri 
  * @param urlParameters Optional. 
  * @param body Optional. If you pass a Map, it will be passed as Form-URLEncoded, if you pass an {@link HttpEntity} 
  * it will be used directly, otherwise the toString will be used. 
  * @param headers Optional. 
  * @return the {@link HttpResponse} 
  * @throws IOException if the request cannot be sent. 
  */
 
    public HttpResponse call(HttpMethod method, URI uri, Map<String, Object> urlParameters, Object body, Map<String, String> headers) throws IOException { 
  
     // Add query parameters 
     uri = addQueryParametersToUri(uri, urlParameters); 
           
        // Create the http request 
     HttpUriRequest httpRequest = method.createHttpRequest(uri); 
 
     // Add body 
     httpRequest = addBodyToHttpRequest(httpRequest, body); 
      
     // Set language 
     httpRequest.setHeader("Accept-Language", Locale.getDefault().toString().replace('_', '-')); 
      
        // Set headers 
        if(headers != null) { 
            for(Map.Entry<String, String> header : headers.entrySet()) { 
                httpRequest.setHeader(header.getKey(), header.getValue()); 
            } 
        }  
         
        // Send the request 
        DebugLog.d(TAG, "Calling %s", uri.toString()); 
        HttpResponse httpResponse = mHttpClient.execute(httpRequest); 
 
        // Check for redirect 
        final int statusCode = httpResponse.getStatusLine().getStatusCode(); 
        if (statusCode == HttpStatus.SC_MOVED_TEMPORARILY || statusCode == HttpStatus.SC_MOVED_PERMANENTLY) { 
            Header locationHeader = httpResponse.getFirstHeader("Location"); 
            if (locationHeader != null) { 
                String newUrl = locationHeader.getValue(); 
                // call again with new URL 
                return call(method, URI.create(newUrl), urlParameters, body, headers); 
            }  
        } 
         
        // return a httpResponse 
        return httpResponse;  
    } 
      
    private static URI addQueryParametersToUri(URI uri, Map<String, Object> urlParameters) { 
     if (urlParameters != null && urlParameters.size() > 0) { 
      Uri androidUri = Uri.parse(uri.toString()); 
      Uri.Builder uriBuilder = androidUri.buildUpon(); 
      for (Map.Entry<String, Object> param : urlParameters.entrySet()) { 
          uriBuilder.appendQueryParameter(param.getKey(), param.getValue().toString()); 
   
      uri = URI.create(uriBuilder.build().toString()); 
     
     return uri; 
    } 
     
    private static HttpUriRequest addBodyToHttpRequest(HttpUriRequest request, Object body) throws IOException { 
     if (body != null && request instanceof HttpEntityEnclosingRequest) { 
      HttpEntityEnclosingRequest entityEnclosingRequest = (HttpEntityEnclosingRequest) request; 
       
      // Form case 
      if (body instanceof Map) { 
       Map<?, ?> bodyMap = (Map<?,?>) body; 
       entityEnclosingRequest.setEntity(new UrlEncodedFormEntity(mapToNameValueList(bodyMap), HTTP.UTF_8)); 
         // Already an entity case 
      else if (body instanceof HttpEntity) { 
       HttpEntity entity = (HttpEntity) body; 
       entityEnclosingRequest.setEntity(entity); 
      // Fall-back: string case 
      else { 
       entityEnclosingRequest.setEntity(new StringEntity(body.toString(), HTTP.UTF_8)); 
      
     
  return request; 
    } 
     
    public static List<NameValuePair> mapToNameValueList(Map<?, ?> map) throws UnsupportedEncodingException { 
  List<NameValuePair> nameValueList = new ArrayList<NameValuePair>(map.size()); 
  for(Map.Entry<?, ?> param : map.entrySet()) { 
   NameValuePair nameValuePair = new BasicNameValuePair(param.getKey().toString(), param.getValue().toString()); 
   nameValueList.add(nameValuePair); 
  
  return nameValueList; 
    } 
     
 //------------------------------------------------------------------------------------------- 
 // GZip compression 
  
 private static void enableGzipCompression(DefaultHttpClient client) { 
  // Gzip support 
        client.addRequestInterceptor(new HttpRequestInterceptor() { 
   @Override 
   public void process(HttpRequest request, HttpContext context) 
     throws HttpException, IOException { 
                if (!request.containsHeader("Accept-Encoding")) { 
                    request.addHeader("Accept-Encoding""gzip"); 
                } 
   
  }); 
 
        client.addResponseInterceptor(new HttpResponseInterceptor() { 
   @Override 
   public void process(HttpResponse response, HttpContext context) 
     throws HttpException, IOException { 
                HttpEntity entity = response.getEntity(); 
                if(entity != null) { 
                    Header ceheader = entity.getContentEncoding(); 
                    if (ceheader != null) { 
                        HeaderElement[] codecs = ceheader.getElements(); 
                        for (int i = 0; i < codecs.length; i++) { 
                            if (codecs[i].getName().equalsIgnoreCase("gzip")) { 
                                response.setEntity(new GzipDecompressingEntity(response.getEntity())); 
                                return
                            } 
                        } 
                    } 
                } 
   
  }); 
 
  
 /**
  * GzipDecompressingEntity  
  */
 
 private static class GzipDecompressingEntity extends HttpEntityWrapper { 
 
        public GzipDecompressingEntity(final HttpEntity entity) { 
            super(entity); 
        } 
 
        @Override 
        public InputStream getContent() 
            throws IOException, IllegalStateException { 
 
            // the wrapped entity's getContent() decides about repeatability 
            InputStream wrappedin = wrappedEntity.getContent(); 
            return new GZIPInputStream(wrappedin); 
        } 
 
        @Override 
        public long getContentLength() { 
            // length of ungzipped content is not known 
            return -1
        } 
 
  
}