net.fizzl.redditengine.impl.SimpleHttpClient.java Source code

Java tutorial

Introduction

Here is the source code for net.fizzl.redditengine.impl.SimpleHttpClient.java

Source

/**
 * Copyright Maxpower Inc Finland (2014)
 *
 * This file is part of RedditEngine.
 *
 * RedditEngine is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * RedditEngine is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with RedditEngine.  If not, see <http://www.gnu.org/licenses/>.
 **/
package net.fizzl.redditengine.impl;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;

import org.apache.commons.io.IOUtils;
import org.apache.http.Header;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.NameValuePair;
import org.apache.http.StatusLine;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.CookieStore;
import org.apache.http.client.HttpClient;
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.HttpRequestBase;
import org.apache.http.client.params.HttpClientParams;
import org.apache.http.client.protocol.ClientContext;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.message.BasicHeader;
import org.apache.http.params.HttpParams;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.HttpContext;

import com.google.gson.Gson;

import android.net.http.AndroidHttpClient;
import android.util.Log;

/**
* This class provides simple HTTP GET and POST operations using Apache HttpComponents.
* @see AndroidHttpClient
*/
public class SimpleHttpClient {
    // API
    /**
     * Calls HTTP GET with the Request-URI. Returns an InputStream of the response entity.
     * 
     * @param url      HTTP GET URL
     * @param params   GET parameters
     * @return         InputStream
     * @throws          ClientProtocolException
     * @throws          IOException
     * @throws          UnexpectedHttpResponseException
     */
    public InputStream get(String url, List<NameValuePair> params)
            throws ClientProtocolException, IOException, UnexpectedHttpResponseException {
        if (params != null) {
            String strp = URLEncodedUtils.format(params, "UTF-8");
            url += "?" + strp;
        }
        HttpGet get = new HttpGet(url);
        return execute(get);
    }

    /**
     * Calls HTTP PUT with the Request-URI. Returns an InputStream of the response entity.
     * 
     * @param url      HTTP PUT URL
     * @param params   PUT parameters
     * @return         InputStream
     * @throws ClientProtocolException
     * @throws IOException
     * @throws UnexpectedHttpResponseException
     */
    public InputStream put(String url, List<NameValuePair> params)
            throws ClientProtocolException, IOException, UnexpectedHttpResponseException {
        HttpPut put = new HttpPut(url);
        put.setEntity(new UrlEncodedFormEntity(params));
        return execute(put);
    }

    /**
     * Common functionality for all HTTP requests
     */
    private InputStream execute(HttpRequestBase request)
            throws ClientProtocolException, IOException, UnexpectedHttpResponseException {
        addModhash(request);
        HttpResponse response = mClient.execute(request, mHttpContext);

        Header contentType = response.getFirstHeader("Content-Type");
        Log.d("Received(" + request.getMethod() + ") ", contentType.getName() + ": " + contentType.getValue());

        checkHeaders(response);
        checkStatusline(response.getStatusLine());
        InputStream is = checkContent(response);

        return is;
    }

    /**
     * Calls HTTP DELETE with the Request-URI. Returns an InputStream of the response entity.
     * 
     * @param url   HTTP DELETE URL
     * @return      InputStream
     * @throws ClientProtocolException
     * @throws IOException
     * @throws UnexpectedHttpResponseException
     */
    public InputStream delete(String url)
            throws ClientProtocolException, IOException, UnexpectedHttpResponseException {
        HttpDelete delete = new HttpDelete(url);
        return execute(delete);
    }

    /* Response headers to monitor for:
     * X-Ratelimit-Used: Approximate number of requests used in this period
     * X-Ratelimit-Remaining: Approximate number of requests left to use
     * X-Ratelimit-Reset: Approximate number of seconds to end of period
     */

    /**
     * Calls HTTP POST with the Request-URI. Returns an InputStream of the response entity.
     * 
     * @param url      HTTP POST URL
     * @param params   POST parameters
     * @return         InputStream
     * @throws ClientProtocolException
     * @throws IOException
     * @throws UnexpectedHttpResponseException
     */
    public InputStream post(String url, List<NameValuePair> params)
            throws ClientProtocolException, IOException, UnexpectedHttpResponseException {
        HttpPost post = new HttpPost(url);
        post.setEntity(new UrlEncodedFormEntity(params));
        return execute(post);
    }

    /**
     * If response is "application/json", try find a modhash from the contents and return a clone of the InputStream.
     * Otherwise return the InputStream.
     * 
     * @param response   HTTP response
     * @return InputStream of HTTP response
     * @throws IllegalStateException
     * @throws IOException
     */
    private InputStream checkContent(HttpResponse response) throws IllegalStateException, IOException {
        InputStream content = response.getEntity().getContent();
        InputStream ret = null;
        Header contentType = response.getFirstHeader("Content-Type");
        if (contentType != null && contentType.getValue().contains(APPLICATION_JSON)) {
            ret = clone(content);
        } else {
            ret = content;
        }

        return ret;
    }

    /**
     * Add modhash to outgoing headers if it is available
     * @param http   HTTP request
     */
    void addModhash(HttpRequestBase http) {
        if (lastModhash != null && !lastModhash.isEmpty()) {
            http.addHeader(new BasicHeader(UrlUtils.X_MODHASH, lastModhash));
            Log.d(getClass().getName(), "adding modhash " + lastModhash + " to outgoing headers");
        }
        Log.d(http.getMethod(), http.getURI().toString());
    }

    /**
     * Check HTTP status code
     * @param line
     * @throws UnexpectedHttpResponseException
     */
    void checkStatusline(StatusLine line) throws UnexpectedHttpResponseException {
        int statusCode = line.getStatusCode();
        // accept all 2xx Success codes
        if (statusCode >= HttpStatus.SC_OK && statusCode < HttpStatus.SC_MULTIPLE_CHOICES) {
            return;
        } else {
            String msg = String.format("Unexpected return code %d %s", statusCode, line.getReasonPhrase());
            UnexpectedHttpResponseException ex = new UnexpectedHttpResponseException(msg);
            throw ex;
        }
    }

    /**
     * modhash probably isn't in the HTTP response headers, but check anyway
     * TODO delete this if modhash doesn't exist in headers
     * @throws UnexpectedHttpResponseException 
     */
    private void checkHeaders(HttpResponse response) throws UnexpectedHttpResponseException {
        Header[] headers = response.getAllHeaders();
        StringBuilder builder = new StringBuilder();
        for (Header header : headers) {
            builder.append(header.toString());
            builder.append(" ");
        }
        String string = builder.toString();
        if (string.contains("modhash")) {
            Log.e(getClass().getCanonicalName(), "modhash found in " + string);
            throw new UnexpectedHttpResponseException(string); // to notice this among debug logging
        }
    }

    /**
     * clone inputstream to be able to look for a modhash
     * TODO use tee and pipes and not a byte array
     */
    private InputStream clone(InputStream in) {
        InputStream retval = in; // as a default if an exception is thrown, use the original inputstream
        try {
            byte[] bytes = IOUtils.toByteArray(in); // consume the original inputstream
            retval = new ByteArrayInputStream(bytes); // return an inputstream based on copied bytes
            in.close(); // original inputstream is consumed (can't reset, unmarkable) so close it
            InputStream is = new ByteArrayInputStream(bytes); // create a temporary inputstream from saved bytes
            StringWriter writer = new StringWriter();
            IOUtils.copy(is, writer, "UTF-8");
            String json = writer.toString();
            is.close(); // got a copy of the inputstream as string, close it
            is = null;
            //Log.d("clone IS:", json);
            Object data = new Gson().fromJson(json, Object.class); // create a POJO data hierarchy from JSON
            String modhash = (String) findKeyRecursion(data, "modhash"); // look for modhash key, find its value
            if (modhash != null) {
                lastModhash = modhash;
                Log.d(getClass().getName(), "cloned inputstream found a modhash: " + modhash);
            } else {
                Log.d(getClass().getName(), "cloned inputstream didn't find a modhash");
            }
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return retval;
    }

    private String lastModhash; // last known modhash

    /**
     * Look for key that matches <tt>find</tt> in data structure recursively and returns its value if found
     * 
     * @param object   data hierarchy to go through
     * @param find      look for this key
     * @return         value of find (if found, otherwise null)
     */
    private Object findKeyRecursion(Object object, Object find) {
        // TODO what if there are two modhashes
        if (object == null || find == null) {
            return null;
        }
        Object clazz = object.getClass();
        //Log.d(clazz.toString(), object.toString().substring(0, 255));      
        Collection<?> values = new ArrayList<Object>();

        if (object instanceof Map) {
            Map<?, ?> map = (Map<?, ?>) object;
            values = map.values();
            //Set<?> entry = map.entrySet();
            //Set<String> keys = map.keySet();
            boolean found = map.containsKey(find);
            if (found) {
                Object get = map.get(find);
                return get;
            }
        } else if (object instanceof List) {
            values = (List<?>) object;
        } else {
            Log.d(getClass().getName(), "findKeyRecursion got class " + clazz);
        }

        for (Object value : values) {
            if (value instanceof Map || value instanceof List) {
                Object retval = findKeyRecursion(value, find);
                // skip paths that did not find the key
                if (retval != null) {
                    return retval;
                }
            }
        }

        return null;
    }

    // Singleton
    /**
     * Private constructor. To get an instance use {@link #getInstance()}
     * @see AndroidHttpClient
     */
    private SimpleHttpClient() {
        mClient = AndroidHttpClient.newInstance(USER_AGENT);
        HttpParams params = mClient.getParams();
        HttpClientParams.setRedirecting(params, true);
        mCookieStore = new PersistentCookieStore();
        mHttpContext = new BasicHttpContext();
        mHttpContext.setAttribute(ClientContext.COOKIE_STORE, mCookieStore);
    }

    public BasicCookieStore getCookieStore() {
        return (BasicCookieStore) this.mCookieStore;
    }

    /**
     * Returns a (singleton) instance of this class
     * 
     * @return   SimpleHttpClient
     */
    public static SimpleHttpClient getInstance() {
        if (me == null) {
            me = new SimpleHttpClient();
        }
        return me;
    }

    private static SimpleHttpClient me;

    // Local variables
    private HttpClient mClient;
    private HttpContext mHttpContext;
    private CookieStore mCookieStore;

    // Constants
    private static final String USER_AGENT = "fizzl.net/RedditEngine 1.0/SimpleHttpClient 1.1/by fizzl";
    private static final String APPLICATION_JSON = "application/json";
}