com.dropbox.client2.RESTUtility.java Source code

Java tutorial

Introduction

Here is the source code for com.dropbox.client2.RESTUtility.java

Source

/*
 * Copyright (c) 2009-2011 Dropbox, Inc.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

package com.dropbox.client2;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Scanner;

import javax.net.ssl.SSLException;

import org.apache.http.HttpEntity;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.conn.params.ConnRoutePNames;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
import org.apache.http.protocol.HTTP;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;

import com.dropbox.client2.DropboxAPI.RequestAndResponse;
import com.dropbox.client2.exception.DropboxException;
import com.dropbox.client2.exception.DropboxIOException;
import com.dropbox.client2.exception.DropboxParseException;
import com.dropbox.client2.exception.DropboxProxyChangeException;
import com.dropbox.client2.exception.DropboxSSLException;
import com.dropbox.client2.exception.DropboxServerException;
import com.dropbox.client2.exception.DropboxUnlinkedException;
import com.dropbox.client2.session.Session;
import com.dropbox.client2.session.Session.ProxyInfo;

/**
 * This class is mostly used internally by {@link DropboxAPI} for creating and
 * executing REST requests to the Dropbox API, and parsing responses. You
 * probably won't have a use for it other than {@link #parseDate(String)} for
 * parsing modified times returned in metadata, or (in very rare circumstances)
 * writing your own API calls.
 */
public class RESTUtility {

    private RESTUtility() {
    }

    private static final DateFormat dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy kk:mm:ss ZZZZZ", Locale.US);

    public enum RequestMethod {
        GET, POST;
    }

    /**
     * Creates and sends a request to the Dropbox API, parses the response as
     * JSON, and returns the result.
     *
     * @param method GET or POST.
     * @param host the hostname to use. Should be either api server,
     *         content server, or web server.
     * @param path the URL path, starting with a '/'.
     * @param apiVersion the API version to use. This should almost always be
     *         set to {@code DropboxAPI.VERSION}.
     * @param params the URL params in an array, with the even numbered
     *         elements the parameter names and odd numbered elements the
     *         values, e.g. <code>new String[] {"path", "/Public", "locale",
     *         "en"}</code>.
     * @param session the {@link Session} to use for this request.
     *
     * @return a parsed JSON object, typically a Map or a JSONArray.
     *
     * @throws DropboxServerException if the server responds with an error
     *         code. See the constants in {@link DropboxServerException} for
     *         the meaning of each error code.
     * @throws DropboxIOException if any network-related error occurs.
     * @throws DropboxUnlinkedException if the user has revoked access.
     * @throws DropboxParseException if a malformed or unknown response was
     *         received from the server.
     * @throws DropboxException for any other unknown errors. This is also a
     *         superclass of all other Dropbox exceptions, so you may want to
     *         only catch this exception which signals that some kind of error
     *         occurred.
     */
    static public Object request(RequestMethod method, String host, String path, int apiVersion, String[] params,
            Session session) throws DropboxException {
        HttpResponse resp = streamRequest(method, host, path, apiVersion, params, session).response;
        return parseAsJSON(resp);
    }

    /**
    * Creates and sends a request to the Dropbox API, and returns a
    * {@link RequestAndResponse} containing the {@link HttpUriRequest} and
    * {@link HttpResponse}.
    *
    * @param method GET or POST.
    * @param host the hostname to use. Should be either api server,
    *         content server, or web server.
    * @param path the URL path, starting with a '/'.
    * @param apiVersion the API version to use. This should almost always be
    *         set to {@code DropboxAPI.VERSION}.
    * @param params the URL params in an array, with the even numbered
    *         elements the parameter names and odd numbered elements the
    *         values, e.g. <code>new String[] {"path", "/Public", "locale",
    *         "en"}</code>.
    * @param session the {@link Session} to use for this request.
    *
    * @return a parsed JSON object, typically a Map or a JSONArray.
    *
    * @throws DropboxServerException if the server responds with an error
    *         code. See the constants in {@link DropboxServerException} for
    *         the meaning of each error code.
    * @throws DropboxIOException if any network-related error occurs.
    * @throws DropboxUnlinkedException if the user has revoked access.
    * @throws DropboxException for any other unknown errors. This is also a
    *         superclass of all other Dropbox exceptions, so you may want to
    *         only catch this exception which signals that some kind of error
    *         occurred.
    */
    static public RequestAndResponse streamRequest(RequestMethod method, String host, String path, int apiVersion,
            String params[], Session session) throws DropboxException {
        HttpUriRequest req;
        String target;

        if (method == RequestMethod.GET) {
            target = buildURL(host, apiVersion, path, params);
            req = new HttpGet(target);
        } else {
            target = buildURL(host, apiVersion, path, null);
            HttpPost post = new HttpPost(target);

            if (params != null && params.length >= 2) {
                if (params.length % 2 != 0) {
                    throw new IllegalArgumentException("Params must have an even number of elements.");
                }
                List<NameValuePair> nvps = new ArrayList<NameValuePair>();

                for (int i = 0; i < params.length; i += 2) {
                    if (params[i + 1] != null) {
                        nvps.add(new BasicNameValuePair(params[i], params[i + 1]));
                    }
                }

                try {
                    post.setEntity(new UrlEncodedFormEntity(nvps, HTTP.UTF_8));
                } catch (UnsupportedEncodingException e) {
                    throw new DropboxException(e);
                }
            }

            req = post;
        }

        session.sign(req);
        HttpResponse resp = execute(session, req);

        return new RequestAndResponse(req, resp);
    }

    /**
     * Reads in content from an {@link HttpResponse} and parses it as JSON.
     *
     * @param response the {@link HttpResponse}.
     *
     * @return a parsed JSON object, typically a Map or a JSONArray.
     *
     * @throws DropboxServerException if the server responds with an error
     *         code. See the constants in {@link DropboxServerException} for
     *         the meaning of each error code.
     * @throws DropboxIOException if any network-related error occurs while
     *         reading in content from the {@link HttpResponse}.
     * @throws DropboxUnlinkedException if the user has revoked access.
     * @throws DropboxParseException if a malformed or unknown response was
     *         received from the server.
     * @throws DropboxException for any other unknown errors. This is also a
     *         superclass of all other Dropbox exceptions, so you may want to
     *         only catch this exception which signals that some kind of error
     *         occurred.
     */
    public static Object parseAsJSON(HttpResponse response) throws DropboxException {
        Object result = null;

        BufferedReader bin = null;
        try {
            HttpEntity ent = response.getEntity();
            if (ent != null) {
                InputStreamReader in = new InputStreamReader(ent.getContent());
                // Wrap this with a Buffer, so we can re-parse it if it's
                // not JSON
                // Has to be at least 16384, because this is defined as the buffer size in
                //     org.json.simple.parser.Yylex.java
                // and otherwise the reset() call won't work
                bin = new BufferedReader(in, 16384);
                bin.mark(16384);

                JSONParser parser = new JSONParser();
                result = parser.parse(bin);
            }
        } catch (IOException e) {
            throw new DropboxIOException(e);
        } catch (ParseException e) {
            if (DropboxServerException.isValidWithNullBody(response)) {
                // We have something from Dropbox, but it's an error with no reason
                throw new DropboxServerException(response);
            } else {
                // This is from Dropbox, and we shouldn't be getting it
                throw new DropboxParseException(bin);
            }
        } catch (OutOfMemoryError e) {
            throw new DropboxException(e);
        } finally {
            if (bin != null) {
                try {
                    bin.close();
                } catch (IOException e) {
                }
            }
        }

        int statusCode = response.getStatusLine().getStatusCode();
        if (statusCode != DropboxServerException._200_OK) {
            if (statusCode == DropboxServerException._401_UNAUTHORIZED) {
                throw new DropboxUnlinkedException();
            } else {
                throw new DropboxServerException(response, result);
            }
        }

        return result;
    }

    /**
     * Reads in content from an {@link HttpResponse} and parses it as a query
     * string.
     *
     * @param response the {@link HttpResponse}.
     *
     * @return a map of parameter names to values from the query string.
     *
     * @throws DropboxIOException if any network-related error occurs while
     *         reading in content from the {@link HttpResponse}.
     * @throws DropboxParseException if a malformed or unknown response was
     *         received from the server.
     * @throws DropboxException for any other unknown errors. This is also a
     *         superclass of all other Dropbox exceptions, so you may want to
     *         only catch this exception which signals that some kind of error
     *         occurred.
     */
    public static Map<String, String> parseAsQueryString(HttpResponse response) throws DropboxException {
        HttpEntity entity = response.getEntity();

        if (entity == null) {
            throw new DropboxParseException("Bad response from Dropbox.");
        }

        Scanner scanner;
        try {
            scanner = new Scanner(entity.getContent()).useDelimiter("&");
        } catch (IOException e) {
            throw new DropboxIOException(e);
        }

        Map<String, String> result = new HashMap<String, String>();

        while (scanner.hasNext()) {
            String nameValue = scanner.next();
            String[] parts = nameValue.split("=");
            if (parts.length != 2) {
                throw new DropboxParseException("Bad query string from Dropbox.");
            }
            result.put(parts[0], parts[1]);
        }

        return result;
    }

    /**
     * Executes an {@link HttpUriRequest} with the given {@link Session} and
     * returns an {@link HttpResponse}.
     *
     * @param session the session to use.
     * @param req the request to execute.
     *
     * @return an {@link HttpResponse}.
     *
     * @throws DropboxServerException if the server responds with an error
     *         code. See the constants in {@link DropboxServerException} for
     *         the meaning of each error code.
     * @throws DropboxIOException if any network-related error occurs.
     * @throws DropboxUnlinkedException if the user has revoked access.
     * @throws DropboxException for any other unknown errors. This is also a
     *         superclass of all other Dropbox exceptions, so you may want to
     *         only catch this exception which signals that some kind of error
     *         occurred.
     */
    public static HttpResponse execute(Session session, HttpUriRequest req) throws DropboxException {
        return execute(session, req, -1);
    }

    /**
     * Executes an {@link HttpUriRequest} with the given {@link Session} and
     * returns an {@link HttpResponse}.
     *
     * @param session the session to use.
     * @param req the request to execute.
     * @param socketTimeoutOverrideMs if >= 0, the socket timeout to set on
     *         this request. Does nothing if set to a negative number.
     *
     * @return an {@link HttpResponse}.
     *
     * @throws DropboxServerException if the server responds with an error
     *         code. See the constants in {@link DropboxServerException} for
     *         the meaning of each error code.
     * @throws DropboxIOException if any network-related error occurs.
     * @throws DropboxUnlinkedException if the user has revoked access.
     * @throws DropboxException for any other unknown errors. This is also a
     *         superclass of all other Dropbox exceptions, so you may want to
     *         only catch this exception which signals that some kind of error
     *         occurred.
     */
    public static HttpResponse execute(Session session, HttpUriRequest req, int socketTimeoutOverrideMs)
            throws DropboxException {
        HttpClient client = updatedHttpClient(session);

        // Set request timeouts.
        session.setRequestTimeout(req);
        if (socketTimeoutOverrideMs >= 0) {
            HttpParams reqParams = req.getParams();
            HttpConnectionParams.setSoTimeout(reqParams, socketTimeoutOverrideMs);
        }

        boolean repeatable = isRequestRepeatable(req);

        try {
            HttpResponse response = null;
            for (int retries = 0; retries < 5; retries++) {
                /*
                 * The try/catch is a workaround for a bug in the HttpClient
                 * libraries. It should be returning null instead when an
                 * error occurs. Fixed in HttpClient 4.1, but we're stuck with
                 * this for now. See:
                 * http://code.google.com/p/android/issues/detail?id=5255
                 */
                try {
                    response = client.execute(req);
                } catch (NullPointerException e) {
                    // Leave 'response' as null.  This is handled below.
                }

                if (response != null)
                    break;

                /*
                 * We've potentially connected to a different network, but are
                 * still using the old proxy settings. Refresh proxy settings
                 * so that we can retry this request.
                 */
                updateClientProxy(client, session);

                if (!repeatable) {
                    throw new DropboxProxyChangeException();
                }
            }

            if (response == null) {
                // This is from that bug, and retrying hasn't fixed it.
                throw new DropboxIOException("Apache HTTPClient encountered an error. No response, try again.");
            }

            int statusCode = response.getStatusLine().getStatusCode();
            if (statusCode != DropboxServerException._200_OK
                    && statusCode != DropboxServerException._206_PARTIAL_CONTENT) {
                // This will throw the right thing: either a DropboxServerException or a DropboxProxyException
                parseAsJSON(response);
            }

            return response;
        } catch (SSLException e) {
            throw new DropboxSSLException(e);
        } catch (IOException e) {
            // Quite common for network going up & down or the request being
            // cancelled, so don't worry about logging this
            throw new DropboxIOException(e);
        } catch (OutOfMemoryError e) {
            throw new DropboxException(e);
        }
    }

    private static boolean isRequestRepeatable(HttpRequest req) {
        // If the request contains an HttpEntity that can't be "reset" (like an InputStream),
        // then it isn't repeatable.
        if (req instanceof HttpEntityEnclosingRequest) {
            HttpEntityEnclosingRequest ereq = (HttpEntityEnclosingRequest) req;
            HttpEntity entity = ereq.getEntity();
            if (entity != null && !entity.isRepeatable()) {
                return false;
            }
        }
        return true;
    }

    /**
     * Creates a URL for a request to the Dropbox API.
     *
     * @param host the Dropbox host (i.e., api server, content server, or web
     *         server).
     * @param apiVersion the API version to use. You should almost always use
     *         {@code DropboxAPI.VERSION} for this.
     * @param target the target path, staring with a '/'.
     * @param params any URL params in an array, with the even numbered
     *         elements the parameter names and odd numbered elements the
     *         values, e.g. <code>new String[] {"path", "/Public", "locale",
     *         "en"}</code>.
     *
     * @return a full URL for making a request.
     */
    public static String buildURL(String host, int apiVersion, String target, String[] params) {
        if (!target.startsWith("/")) {
            target = "/" + target;
        }

        try {
            // We have to encode the whole line, then remove + and / encoding
            // to get a good OAuth URL.
            target = URLEncoder.encode("/" + apiVersion + target, "UTF-8");
            target = target.replace("%2F", "/");

            if (params != null && params.length > 0) {
                target += "?" + urlencode(params);
            }

            // These substitutions must be made to keep OAuth happy.
            target = target.replace("+", "%20").replace("*", "%2A");
        } catch (UnsupportedEncodingException uce) {
            return null;
        }

        return "https://" + host + ":443" + target;
    }

    /**
     * Parses a date/time returned by the Dropbox API. Returns null if it
     * cannot be parsed.
     *
     * @param date a date returned by the API.
     *
     * @return a {@link Date}.
     */
    public static Date parseDate(String date) {
        try {
            return dateFormat.parse(date);
        } catch (java.text.ParseException e) {
            return null;
        }
    }

    /**
     * Gets the session's client and updates its proxy.
     */
    private static synchronized HttpClient updatedHttpClient(Session session) {
        HttpClient client = session.getHttpClient();
        updateClientProxy(client, session);
        return client;
    }

    /**
     * Updates the given client's proxy from the session.
     */
    private static void updateClientProxy(HttpClient client, Session session) {
        ProxyInfo proxyInfo = session.getProxyInfo();
        if (proxyInfo != null && proxyInfo.host != null && !proxyInfo.host.equals("")) {
            HttpHost proxy;
            if (proxyInfo.port < 0) {
                proxy = new HttpHost(proxyInfo.host);
            } else {
                proxy = new HttpHost(proxyInfo.host, proxyInfo.port);
            }
            client.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, proxy);
        } else {
            client.getParams().removeParameter(ConnRoutePNames.DEFAULT_PROXY);
        }
    }

    /**
     * URL encodes an array of parameters into a query string.
     */
    private static String urlencode(String[] params) {
        if (params.length % 2 != 0) {
            throw new IllegalArgumentException("Params must have an even number of elements.");
        }

        String result = "";
        try {
            boolean firstTime = true;
            for (int i = 0; i < params.length; i += 2) {
                if (params[i + 1] != null) {
                    if (firstTime) {
                        firstTime = false;
                    } else {
                        result += "&";
                    }
                    result += URLEncoder.encode(params[i], "UTF-8") + "="
                            + URLEncoder.encode(params[i + 1], "UTF-8");
                }
            }
        } catch (UnsupportedEncodingException e) {
            return null;
        }
        return result;
    }
}