it.restrung.rest.client.DefaultRestClientImpl.java Source code

Java tutorial

Introduction

Here is the source code for it.restrung.rest.client.DefaultRestClientImpl.java

Source

/*
 * Copyright (C) 2012 47 Degrees, LLC
 * http://47deg.com
 * hello@47deg.com
 *
 * 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 it.restrung.rest.client;

import android.util.Log;
import it.restrung.rest.async.runnables.DeleteRunnable;
import it.restrung.rest.async.runnables.GetRunnable;
import it.restrung.rest.async.runnables.PostRunnable;
import it.restrung.rest.async.runnables.PutRunnable;
import it.restrung.rest.cache.RequestCache;
import it.restrung.rest.exceptions.APIException;
import it.restrung.rest.exceptions.InvalidCredentialsException;
import it.restrung.rest.marshalling.request.HttpMessageRequestOperationImpl;
import it.restrung.rest.marshalling.request.JSONSerializable;
import it.restrung.rest.marshalling.response.HttpMessageResponseOperationImpl;
import it.restrung.rest.marshalling.response.JSONResponse;
import it.restrung.rest.misc.CountingMultipartEntity;
import it.restrung.rest.misc.HttpClientFactory;
import it.restrung.rest.misc.HttpDeleteWithBody;
import it.restrung.rest.utils.Base64;
import it.restrung.rest.utils.IOUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.*;
import org.apache.http.entity.StringEntity;
import org.apache.http.entity.mime.HttpMultipartMode;
import org.apache.http.entity.mime.content.FileBody;
import org.apache.http.entity.mime.content.StringBody;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.params.CoreProtocolPNames;
import org.apache.http.params.HttpConnectionParams;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.util.concurrent.Callable;

/**
 * Default thread safe impl of the RestClient that delegates calls in AsyncTask and Loader for its async operations.
 */
public class DefaultRestClientImpl implements RestClient {

    /**
     * Hack to avoid initialization exception in certain versions of Android
     */
    static {
        try {
            Class.forName("android.os.AsyncTask");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

    /**
     * @see RestClient#getAsync
     */
    @Override
    public <T extends JSONResponse> void getAsync(final APIDelegate<T> delegate, final String path,
            final Object... args) {
        new GetRunnable<T>(delegate, path, args).run();
    }

    /**
     * @see RestClient#deleteAsync
     */
    @Override
    public <T extends JSONResponse> void deleteAsync(final APIDelegate<T> delegate, final String path,
            final JSONSerializable body, final Object... args) {
        new DeleteRunnable<T>(delegate, path, body, args).run();
    }

    /**
     * @see RestClient#postAsync
     */
    @Override
    public <T extends JSONResponse> void postAsync(final APIDelegate<T> delegate, final String path,
            final JSONSerializable body, final Object... args) {
        new PostRunnable<T>(delegate, path, body, null, null, args).run();
    }

    /**
     * @see RestClient#postAsync
     */
    @Override
    public <T extends JSONResponse> void postAsync(final APIDelegate<T> delegate, final String path,
            final File file, final JSONSerializable body, final APIPostParams postParams, final Object... args) {
        new PostRunnable<T>(delegate, path, body, file, postParams, args).run();
    }

    /**
     * @see RestClient#putAsync
     */
    @Override
    public <T extends JSONResponse> void putAsync(final APIDelegate<T> delegate, final String path,
            final JSONSerializable body, final Object... args) {
        new PutRunnable<T>(delegate, path, body, args).run();
    }

    /**
     * @see RestClient#post(APIDelegate, String, it.restrung.rest.marshalling.request.JSONSerializable, int)
     */
    @Override
    public <T extends JSONResponse> T post(final APIDelegate<T> apiDelegate, final String url,
            final JSONSerializable body, final int timeout) throws APIException {
        return executeCacheableOperation(new Callable<T>() {
            @Override
            public T call() throws Exception {
                String result = performPost(url, null, null, body != null ? body.toJSON() : null, null, timeout,
                        null, apiDelegate);
                return serializeResultForDelegate(result, apiDelegate);
            }
        }, apiDelegate, url, null, null, timeout);
    }

    /**
     * @see RestClient#post(APIDelegate, String, it.restrung.rest.marshalling.request.JSONSerializable, java.io.File, int, APIPostParams)
     */
    @Override
    public <T extends JSONResponse> T post(final APIDelegate<T> apiDelegate, final String url,
            final JSONSerializable body, final File file, final int timeout, final APIPostParams apiPostParams)
            throws APIException {
        return executeCacheableOperation(new Callable<T>() {
            @Override
            public T call() throws Exception {
                String result = performPost(url, null, null, body != null ? body.toJSON() : null, file, timeout,
                        apiPostParams, apiDelegate);
                return serializeResultForDelegate(result, apiDelegate);
            }
        }, apiDelegate, url, null, null, timeout);
    }

    /**
     * @see RestClient#put(APIDelegate, String, it.restrung.rest.marshalling.request.JSONSerializable, int)
     */
    @Override
    public <T extends JSONResponse> T put(final APIDelegate<T> apiDelegate, final String url,
            final JSONSerializable body, final int timeout) throws APIException {
        return executeCacheableOperation(new Callable<T>() {
            @Override
            public T call() throws Exception {
                String result = performPut(url, null, null, body != null ? body.toJSON() : null, timeout,
                        apiDelegate);
                return serializeResultForDelegate(result, apiDelegate);
            }
        }, apiDelegate, url, null, null, timeout);
    }

    /**
     * @see RestClient#get(APIDelegate, String, int)
     */
    @Override
    public <T extends JSONResponse> T get(final APIDelegate<T> apiDelegate, final String url, final int timeout)
            throws APIException {
        return executeCacheableOperation(new Callable<T>() {
            @Override
            public T call() throws Exception {
                String result = performGet(url, null, null, timeout, apiDelegate);
                return serializeResultForDelegate(result, apiDelegate);
            }
        }, apiDelegate, url, null, null, timeout);
    }

    /**
     * @see RestClient#delete(APIDelegate, String, it.restrung.rest.marshalling.request.JSONSerializable, int)
     */
    @Override
    public <T extends JSONResponse> T delete(final APIDelegate<T> apiDelegate, final String url,
            final JSONSerializable body, final int timeout) throws APIException {
        return executeCacheableOperation(new Callable<T>() {
            @Override
            public T call() throws Exception {
                String result = performDelete(url, null, null, body, timeout, apiDelegate);
                return serializeResultForDelegate(result, apiDelegate);
            }
        }, apiDelegate, url, null, null, timeout);
    }

    /**
     * Private helper that executes an operation that may be cacheable
     *
     * @param callable    the operation callable
     * @param apiDelegate the api delegate
     * @param url         the url
     * @param user        the user
     * @param password    the password
     * @param timeout     a request timeout
     * @param <T>         something that extends @see JSONResponse
     * @return the result from executing the remote operation as a @see JSONResponse
     * @throws APIException
     */
    private static <T extends JSONResponse> T executeCacheableOperation(Callable<T> callable,
            final APIDelegate<T> apiDelegate, final String url, final String user, final String password,
            final int timeout) throws APIException {
        RequestCache.CacheAwareOperation<T> operation = new RequestCache.CacheAwareOperation<T>(apiDelegate,
                callable, url, user, password, timeout);
        T result;
        try {
            result = operation.execute();
        } catch (Exception e) {
            if (APIException.class.isAssignableFrom(e.getClass())) {
                throw (APIException) e;
            } else {
                throw new APIException(e, APIException.UNTRACKED_ERROR);
            }
        }
        return result;
    }

    /**
     * Private helper for result serialization //todo consider moving to serialization stack
     *
     * @param result   the result as a string
     * @param delegate the api delegate
     * @return the serialized object of type <T>
     */
    private static <T extends JSONResponse> T serializeResultForDelegate(String result, APIDelegate<T> delegate)
            throws InstantiationException, IllegalAccessException, JSONException {
        T serializedResult = delegate.getExpectedResponseType().newInstance();
        serializedResult.fromJSON(IOUtils.stringToJSON(result));
        return serializedResult;
    }

    /**
     * Performs a GET request enforcing basic auth
     *
     * @param url      the url to be requested
     * @param user     the user
     * @param password the password
     * @param timeout  a request timeout
     * @return the response body as a String
     * @throws APIException
     */
    public static String performGet(String url, String user, String password, int timeout, APIDelegate<?> delegate)
            throws APIException {
        Log.d(RestClient.class.getSimpleName(), "GET: " + url);
        String responseBody;
        try {
            DefaultHttpClient httpclient = HttpClientFactory.getClient();
            HttpGet httpget = new HttpGet(url);
            setupTimeout(timeout, httpclient);
            setupCommonHeaders(httpget);
            setupBasicAuth(user, password, httpget);
            handleRequest(httpget, delegate);
            HttpResponse response = httpclient.execute(httpget);
            responseBody = handleResponse(response, delegate);
        } catch (ClientProtocolException e) {
            throw new APIException(e, APIException.UNTRACKED_ERROR);
        } catch (IOException e) {
            throw new APIException(e, APIException.UNTRACKED_ERROR);
        }
        return responseBody;
    }

    /**
     * Performs a POST request to the given url
     *
     * @param url           the url
     * @param user          an optional user for basic auth
     * @param password      an optional password for basic auth
     * @param body          an optional body to send with the request
     * @param file          an optional file that would result in a multipart request
     * @param timeout       the request timeout
     * @param apiPostParams an optional callback to get progress
     * @param delegate      the api delegate that will receive callbacks regarding progress and results
     * @return the response body as a string
     * @throws APIException
     */
    public static String performPost(String url, String user, String password, String body, final File file,
            int timeout, APIPostParams apiPostParams, APIDelegate<?> delegate) throws APIException {
        Log.d(RestClient.class.getSimpleName(), "POST: " + url);
        String responseBody;
        try {
            DefaultHttpClient httpclient = HttpClientFactory.getClient();
            HttpPost httppost = new HttpPost(url);
            setupTimeout(timeout, httpclient);
            setupCommonHeaders(httppost);
            setupBasicAuth(user, password, httppost);
            setupRequestBody(httppost, body, apiPostParams, file);
            handleRequest(httppost, delegate);
            HttpResponse response = httpclient.execute(httppost);
            responseBody = handleResponse(response, delegate);
        } catch (ClientProtocolException e) {
            throw new APIException(e, APIException.UNTRACKED_ERROR);
        } catch (IOException e) {
            throw new APIException(e, APIException.UNTRACKED_ERROR);
        }
        return responseBody;
    }

    /**
     * Performs a PUT request to the given url
     *
     * @param url      the url
     * @param user     an optional user for basic auth
     * @param password an optional password for basic auth
     * @param body     an optional body to send with the request
     * @param timeout  the request timeout
     * @param delegate the api delegate that will receive callbacks regarding progress and results
     * @return the response body as a string
     * @throws APIException
     */
    public static String performPut(String url, String user, String password, String body, int timeout,
            APIDelegate<?> delegate) throws APIException {
        Log.d(RestClient.class.getSimpleName(), "PUT: " + url);
        String responseBody;
        try {
            DefaultHttpClient httpclient = HttpClientFactory.getClient();
            HttpPut httpPut = new HttpPut(url);
            setupTimeout(timeout, httpclient);
            setupCommonHeaders(httpPut);
            setupBasicAuth(user, password, httpPut);
            setupRequestBody(httpPut, body, null, null);
            handleRequest(httpPut, delegate);
            HttpResponse response = httpclient.execute(httpPut);
            responseBody = handleResponse(response, delegate);
        } catch (ClientProtocolException e) {
            throw new APIException(e, APIException.UNTRACKED_ERROR);
        } catch (IOException e) {
            throw new APIException(e, APIException.UNTRACKED_ERROR);
        }
        return responseBody;
    }

    /**
     * Performs a DELETE request to the given url
     *
     * @param url      the url
     * @param user     an optional user for basic auth
     * @param password an optional password for basic auth
     * @param body     the requests body if any
     * @param timeout  the request timeout
     * @param delegate the api delegate that will receive callbacks regarding progress and results   @return the response body as a string
     * @throws APIException
     */
    public static String performDelete(String url, String user, String password, JSONSerializable body, int timeout,
            APIDelegate<?> delegate) throws APIException {
        Log.d(RestClient.class.getSimpleName(), "DELETE: " + url);
        String responseBody;
        try {
            DefaultHttpClient httpclient = HttpClientFactory.getClient();
            HttpRequestBase httpDelete = body == null ? new HttpDelete(url) : new HttpDeleteWithBody(url);
            setupTimeout(timeout, httpclient);
            setupCommonHeaders(httpDelete);
            setupBasicAuth(user, password, httpDelete);
            if (body != null && HttpEntityEnclosingRequestBase.class.isAssignableFrom(httpDelete.getClass())) {
                setupRequestBody((HttpEntityEnclosingRequestBase) httpDelete, body.toJSON(), null, null);
            }
            handleRequest(httpDelete, delegate);
            HttpResponse response = httpclient.execute(httpDelete);
            responseBody = handleResponse(response, delegate);
        } catch (ClientProtocolException e) {
            throw new APIException(e, APIException.UNTRACKED_ERROR);
        } catch (IOException e) {
            throw new APIException(e, APIException.UNTRACKED_ERROR);
        }
        return responseBody;
    }

    /**
     * Private helper to setup teh request body
     */
    protected static void setupRequestBody(HttpEntityEnclosingRequestBase httpEntityEnclosingRequestBase,
            String body, APIPostParams apiPostParams, File file) throws UnsupportedEncodingException {
        if (body != null && apiPostParams != null && apiPostParams.isMultipart()) {
            setupMultipartBodyWithFile(httpEntityEnclosingRequestBase, apiPostParams, body, file);
        } else {
            setupSimpleRequestBody(httpEntityEnclosingRequestBase, body);
        }
    }

    /**
     * Private helper to setup a simple request body
     */
    private static void setupSimpleRequestBody(HttpEntityEnclosingRequestBase httpEntityEnclosingRequestBase,
            String body) throws UnsupportedEncodingException {
        if (body != null) {
            httpEntityEnclosingRequestBase.setHeader("Content-Type", "application/json; charset=utf-8");
            httpEntityEnclosingRequestBase.setEntity(new StringEntity(body, "UTF-8"));
        }
    }

    /**
     * Private helper to setup a multipart request body
     */
    private static void setupMultipartBodyWithFile(HttpEntityEnclosingRequestBase httpEntityEnclosingRequestBase,
            APIPostParams apiPostParams, String body, File file) throws UnsupportedEncodingException {
        //MultipartEntity mpEntity = new MultipartEntity(HttpMultipartMode.BROWSER_COMPATIBLE);
        CountingMultipartEntity mpEntity = new CountingMultipartEntity(HttpMultipartMode.BROWSER_COMPATIBLE, null,
                Charset.forName("UTF-8"), file != null ? body.length() + file.length() : body.length(),
                apiPostParams);
        mpEntity.addPart("body", new StringBody(body, Charset.forName("UTF-8")));
        if (file != null) {
            FileBody uploadFilePart = new FileBody(file, "application/octet-stream");
            mpEntity.addPart("file", uploadFilePart);
        }
        httpEntityEnclosingRequestBase.setHeader(mpEntity.getContentType());
        httpEntityEnclosingRequestBase.setEntity(mpEntity);
    }

    /**
     * Private helper to setup the request timeout
     */
    private static void setupTimeout(int timeout, HttpClient client) {
        if (timeout > 0) {
            HttpConnectionParams.setConnectionTimeout(client.getParams(), timeout);
        }
    }

    /**
     * Private helper to notify the request is about to be sent
     */
    private static void handleRequest(HttpRequestBase request, APIDelegate<?> delegate)
            throws IOException, APIException {
        if (delegate != null) {
            delegate.onRequest(new HttpMessageRequestOperationImpl(request));
        }
    }

    /**
     * Private helper to notify the response has been received
     */
    private static String handleResponse(HttpResponse response, APIDelegate<?> delegate)
            throws IOException, APIException {
        if (delegate != null) {
            delegate.onResponse(new HttpMessageResponseOperationImpl(response));
        }
        String responseBody = null;
        int statusCode = response.getStatusLine().getStatusCode();
        // Examine the response status
        //Log.i(RestClient.class.getName(), response.getStatusLine().toString());

        // Get hold of the response entity
        HttpEntity entity = response.getEntity();
        // If the response does not enclose an entity, there is no need
        // to worry about connection release

        if (entity != null) {
            // A Simple JSON Response Read
            InputStream instream = entity.getContent();
            responseBody = IOUtils.convertStreamToString(instream);
            //Log.i(RestClient.class.getName(), result);

            // Closing the input stream will trigger connection release
            instream.close();
        }
        handleStatusCode(statusCode, responseBody);
        return responseBody;
    }

    /**
     * Private helper that handles status codes and translates 401 status codes into invalid credentials exceptions
     */
    private static void handleStatusCode(int statusCode, String responseBody) throws APIException {
        if (statusCode == 401) {
            JSONObject json = IOUtils.stringToJSON(responseBody);
            throw new InvalidCredentialsException(json != null ? json.optString("message") : "", statusCode);
        }
        if (statusCode != 200) {
            JSONObject json = IOUtils.stringToJSON(responseBody);
            throw new APIException(json != null ? json.optString("message") : "", statusCode);
        }
    }

    /**
     * Private helper to setup some common headers such as the user agent and the platform headers
     *
     * @param request the user agent
     */
    private static void setupCommonHeaders(HttpRequestBase request) {
        request.addHeader("X-47deg-Platform", "android");
        request.addHeader(CoreProtocolPNames.USER_AGENT, "47deg.com/restrung");
        request.addHeader("Accept", "application/json");
    }

    /**
     * Convenience method to set basic auth on a request that is about to be sent
     *
     * @param user     the user name
     * @param password the user password
     * @param request  the request
     */
    public static void setupBasicAuth(String user, String password, HttpRequestBase request)
            throws UnsupportedEncodingException {
        if (user != null && password != null) {
            String base64EncodedCredentials = Base64.encodeWebSafe((user + ":" + password).getBytes("UTF-8"), true);
            request.addHeader("Authorization", "Basic " + base64EncodedCredentials);
        }
    }

}