com.scoreflex.ScoreflexRestClient.java Source code

Java tutorial

Introduction

Here is the source code for com.scoreflex.ScoreflexRestClient.java

Source

/*
 * Licensed to Scoreflex (www.scoreflex.com) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. Scoreflex licenses this
 * file to you 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.scoreflex;

import java.io.UnsupportedEncodingException;
import java.net.SocketException;
import java.net.URLEncoder;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.TreeSet;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

import org.apache.http.Header;
import org.apache.http.NoHttpResponseException;
import org.apache.http.conn.params.ConnManagerParams;
import org.apache.http.message.BasicHeader;
import org.json.JSONException;
import org.json.JSONObject;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Handler;
import android.support.v4.content.LocalBroadcastManager;
import android.util.Base64;
import android.util.Log;

import com.loopj.android.http.AsyncHttpClient;
import com.loopj.android.http.JsonHttpResponseHandler;
import com.scoreflex.Scoreflex.Response;

/**
 * A REST client that lets you hit the Scoreflex REST server.
 *
 */
class ScoreflexRestClient {

    private enum HttpMethod {
        GET, PUT, POST, DELETE
    }

    private static final int RETRY_INTERVAL = 1000; //60000;
    private static final String ACCESS_TOKEN_PREF_NAME = "__scoreflex_access_token";
    private static final String ACCESS_TOKEN_IS_ANONYMOUS_PREF_NAME = "__scoreflex_access_token_is_anonymous";
    private static final String SID_PREF_NAME = "__scoreflex_sid";
    private static final String PLAYER_ID_PREF_NAME = "__player_id";
    private static boolean sIsFetchingAnonymousAccessToken = false;
    private static List<Scoreflex.ResponseHandler> sPendingHandlers = new ArrayList<Scoreflex.ResponseHandler>();

    private static AsyncHttpClient sClient = new AsyncHttpClient();

    /**
     * A GET request
     *
     * @param resource
     *          The resource path, starting with /
     * @param params
     *          AsyncHttpClient request parameters
     * @param responseHandler
     *          An AsyncHttpClient response handler
     */
    protected static void get(String resource, Scoreflex.RequestParams params,
            Scoreflex.ResponseHandler responseHandler) {
        requestAuthenticated(new Request(HttpMethod.GET, resource, params, responseHandler));
    }

    /**
     * A POST request
     *
     * @param resource
     *          The resource path, starting with /
     * @param params
     *          AsyncHttpClient request parameters
     * @param responseHandler
     *          An AsyncHttpClient response handler
     */
    protected static void post(String resource, Scoreflex.RequestParams params,
            Scoreflex.ResponseHandler responseHandler) {
        requestAuthenticated(new Request(HttpMethod.POST, resource, params, responseHandler));
    }

    /**
     * A POST request that is guaranteed to be executed when a network connection
     * is present, surviving application reboot. The responseHandler will be
     * called only if the network is present when the request is first run.
     *
     * @param resource
     * @param params
     * @param responseHandler
     */
    protected static void postEventually(String resource, Scoreflex.RequestParams params,
            final Scoreflex.ResponseHandler responseHandler) {

        // Create a request
        final Request request = new Request(HttpMethod.POST, resource, params, null);

        // Wrap the provided handler with ours
        request.setHandler(new Scoreflex.ResponseHandler() {

            @Override
            public void onFailure(Throwable e, Response errorResponse) {

                // Post to vault on network error
                if (e instanceof NoHttpResponseException || e instanceof UnknownHostException
                        || e instanceof SocketException) {
                    try {
                        ScoreflexRequestVault.getDefaultVault().put(request);
                    } catch (JSONException e1) {
                        Log.e("Scoreflex", "Could not save request to vault", e1);
                    }
                    return;
                }

                // Forward to original handler otherwise
                if (null != responseHandler)
                    responseHandler.onFailure(e, errorResponse);
            }

            @Override
            public void onSuccess(Response response) {
                if (null != responseHandler)
                    responseHandler.onSuccess(response);
            }

            @Override
            public void onSuccess(int statusCode, Response response) {
                if (null != responseHandler)
                    responseHandler.onSuccess(statusCode, response);
            }

        });
        requestAuthenticated(request);

    }

    /**
     * A PUT request
     *
     * @param resource
     *          The resource path, starting with /
     * @param params
     *          AsyncHttpClient request parameters
     * @param responseHandler
     *          An AsyncHttpClient response handler
     */
    protected static void put(String resource, Scoreflex.RequestParams params,
            Scoreflex.ResponseHandler responseHandler) {
        requestAuthenticated(new Request(HttpMethod.PUT, resource, params, responseHandler));
    }

    /**
     * A DELETE request
     *
     * @param resource
     *          The resource path, starting with /
     * @param params
     *          AsyncHttpClient request parameters
     * @param responseHandler
     *          An AsyncHttpClient response handler
     */
    protected static void delete(String resource, Scoreflex.ResponseHandler responseHandler) {
        requestAuthenticated(new Request(HttpMethod.DELETE, resource, null, responseHandler));
    }

    /**
     * If no access token is found in the user's preferences, fetch an anonymous
     * access token
     *
     * @param onFetchedHandler
     *          a handler called if a request to fetch an access token has been
     *          executed successfully, never called if retreived from cache
     * @return whether or not a request has been executed to fetch an anonymous
     *         access token (true fetching, false retrived from local cache)
     */
    protected static boolean fetchAnonymousAccessTokenIfNeeded(Scoreflex.ResponseHandler onFetchedHandler) {
        if (null == getAccessToken()) {
            fetchAnonymousAccessToken(onFetchedHandler);
            return true;
        }
        return false;
    }

    /**
     * If no access token is found in the user's preferences, fetch an anonymous
     * access token
     */
    protected static void fetchAnonymousAccessTokenIfNeeded() {
        fetchAnonymousAccessTokenIfNeeded(null);
    }

    /**
     * Runs the specified request and ensure a valid access token is fetched if
     * neccessary beforehand or afterwards (and re-run the request) if the request
     * fails for auth reasons
     *
     * @param request
     */
    protected static void requestAuthenticated(final Request request) {
        if (null == request)
            return;

        String accessToken = getAccessToken();

        // User is authenticated
        if (null != accessToken) {

            // Add the access token to the params
            Scoreflex.RequestParams params = request.getParams();
            if (null == params) {
                params = new Scoreflex.RequestParams();
                request.setParams(params);
            }

            params.remove("accessToken");
            params.put("accessToken", accessToken);

            // Wrap the request handler with our own
            Scoreflex.ResponseHandler wrapperHandler = new Scoreflex.ResponseHandler() {

                @Override
                public void onSuccess(int status, Scoreflex.Response response) {
                    if (null != request.getHandler())
                        request.getHandler().onSuccess(status, response);
                }

                @Override
                public void onFailure(Throwable e, Scoreflex.Response errorResponse) {
                    Log.e("Scoreflex", "Request failed", e);
                    if (null != errorResponse
                            && Scoreflex.ERROR_INVALID_ACCESS_TOKEN == errorResponse.getErrorCode()) {

                        // null out the access token
                        setAccessToken(null, true);
                        setSID(null);
                        setPlayerId(null);

                        // retry in 60 secs
                        new Handler().postDelayed(new Runnable() {

                            @Override
                            public void run() {
                                requestAuthenticated(request);

                            }
                        }, RETRY_INTERVAL);

                    } else {
                        if (null == request.getHandler())
                            return;
                        // if (e instanceof ConnectTimeoutException)
                        request.getHandler().onFailure(e, errorResponse);
                    }
                }

                @Override
                public void onSuccess(Response response) {

                }
            };
            Request wrapperRequest = (Request) request.clone();
            wrapperRequest.setHandler(wrapperHandler);

            // Perform request
            request(wrapperRequest);
            return;
        }

        // User is not authenticated
        // request a token
        fetchAnonymousAccessTokenAndRunRequest(request);
    }

    /**
     * Thin wrapper to the {@link AsyncHttpClient} library
     *
     * @param request
     */
    private static void request(final Request request) {

        if (null == request) {
            Log.e("Scoreflex", "Request with null request.");
            return;
        }

        // Decorate parameters
        ScoreflexRequestParamsDecorator.decorate(request.getResource(), request.getParams());

        // Generate signature
        Header authorizationHeader = request.getAuthorizationHeader();

        // Headers
        Header[] headers = null;
        if (null != authorizationHeader) {
            headers = new Header[1];
            headers[0] = authorizationHeader;
        }

        // Handler
        JsonHttpResponseHandler jsonHandler = null;
        if (null != request.getHandler()) {
            jsonHandler = new JsonHttpResponseHandler() {

                @Override
                public void onFailure(Throwable arg0, JSONObject arg1) {
                    if (arg1 != null) {
                        if (Scoreflex.showDebug) {
                            Log.d("Scoreflex", "Requesting Error: " + arg1);
                        }
                        Scoreflex.setNetworkAvailable(true);
                        request.getHandler().onFailure(arg0, new Scoreflex.Response(arg1));
                    } else {
                        Scoreflex.setNetworkAvailable(false);
                        request.getHandler().onFailure(arg0, null);
                    }
                }

                @Override
                public void onFailure(Throwable arg0, String arg1) {
                    Scoreflex.setNetworkAvailable(false);
                    request.getHandler().onFailure(arg0, null);
                }

                @Override
                public void onSuccess(int arg0, JSONObject arg1) {
                    Scoreflex.setNetworkAvailable(true);
                    request.getHandler().onSuccess(arg0, new Scoreflex.Response(arg1));
                }
            };
        }

        String url = ScoreflexUriHelper.getAbsoluteUrl(request.getResource());
        if (Scoreflex.showDebug) {
            Log.d("Scoreflex", "requesting url[" + request.getMethod() + "]: " + url + "?"
                    + request.getParams().getURLEncodedString());
        }
        // TODO: support other contentTypes such as "application/json"
        String contentType = "application/x-www-form-urlencoded";
        switch (request.getMethod()) {
        case GET:
            sClient.get(null, url, headers, request.getParams(), jsonHandler);
            break;
        case PUT:
            sClient.put(null, url, headers, request.getParams() != null ? request.getParams().getEntity() : null,
                    contentType, jsonHandler);
            break;
        case POST:
            sClient.post(null, url, headers, request.getParams(), contentType, jsonHandler);
            break;
        case DELETE:
            sClient.delete(null, url, headers, jsonHandler);
            break;
        }
    }

    protected static void fetchAnonymousAccessToken(final Scoreflex.ResponseHandler handler) {
        fetchAnonymousAccessToken(handler, 0);
    }

    protected static void fetchAnonymousAccessToken(final Scoreflex.ResponseHandler handler, final int nbRetries) {
        if (sIsFetchingAnonymousAccessToken) {
            queueHandler(handler);
            return;
        }
        sIsFetchingAnonymousAccessToken = true;
        Scoreflex.RequestParams authParams = new Scoreflex.RequestParams();
        authParams.put("clientId", Scoreflex.getClientId());
        authParams.put("devicePlatform", "Android");
        authParams.put("deviceModel", Scoreflex.getDeviceModel());
        String udid = Scoreflex.getUDID();
        if (null != udid)
            authParams.put("deviceId", udid);

        String resource = "/oauth/anonymousAccessToken";

        request(new Request(HttpMethod.POST, resource, authParams, new Scoreflex.ResponseHandler() {

            @Override
            public void onFailure(Throwable e, Response errorResponse) {

                if (nbRetries <= 0) {
                    Log.e("Scoreflex",
                            "Error request anonymous access token (aborting):"
                                    + (errorResponse != null ? errorResponse.getJSONObject().toString()
                                            : " null error response, aborting"),
                            e);

                    sIsFetchingAnonymousAccessToken = false;
                    if (null != handler) {
                        handler.onFailure(e, errorResponse);
                    }
                    Scoreflex.ResponseHandler chainedHandler = null;
                    while ((chainedHandler = dequeueHandler()) != null) {
                        chainedHandler.onFailure(e, errorResponse);
                    }
                    return;
                }
                Log.e("Scoreflex",
                        "Error request anonymous access token (retrying : " + nbRetries + "):"
                                + (errorResponse != null ? errorResponse.getJSONObject().toString()
                                        : " null error response, retrying"),
                        e);

                new Handler().postDelayed(new Runnable() {

                    @Override
                    public void run() {
                        sIsFetchingAnonymousAccessToken = false;
                        fetchAnonymousAccessToken(handler, nbRetries - 1);
                    }
                }, RETRY_INTERVAL);
            }

            @Override
            public void onSuccess(int statusCode, Response response) {
                // Parse response
                JSONObject json = response.getJSONObject();
                if (json.has("accessToken") && json.has("sid")) {
                    JSONObject accessToken = json.optJSONObject("accessToken");
                    String sid = json.optString("sid");
                    JSONObject meObject = json.optJSONObject("me");
                    String playerId = meObject.optString("id");
                    if (null != accessToken && accessToken.has("token")) {
                        String token = accessToken.optString("token");

                        // Store access token
                        setAccessToken(token, true);
                        setSID(sid);
                        setPlayerId(playerId);
                        sIsFetchingAnonymousAccessToken = false;

                        Intent intent = new Intent(Scoreflex.INTENT_USER_LOGED_IN);
                        intent.putExtra(Scoreflex.INTENT_USER_LOGED_IN_EXTRA_SID, sid);
                        intent.putExtra(Scoreflex.INTENT_USER_LOGED_IN_EXTRA_ACCESS_TOKEN, token);
                        LocalBroadcastManager.getInstance(Scoreflex.getApplicationContext()).sendBroadcast(intent);

                        // call handlers
                        if (null != handler) {
                            handler.onSuccess(statusCode, response);
                        }
                        Scoreflex.ResponseHandler chainedHandler = null;
                        while ((chainedHandler = dequeueHandler()) != null) {
                            chainedHandler.onSuccess(statusCode, response);
                        }
                        return;
                    }
                }
                Log.e("Scoreflex", "Could not obtain anonymous access token from server");

            }

            @Override
            public void onSuccess(Response response) {

            }

        }));

    }

    /**
     * Fetches an anonymous access token and run the given request with that
     * token. Retries when access token cannot be fetched.
     *
     * @param request
     *          The request to be run
     */
    public static void fetchAnonymousAccessTokenAndRunRequest(final Request request) {
        fetchAnonymousAccessToken(new Scoreflex.ResponseHandler() {

            @Override
            public void onSuccess(Response response) {
                requestAuthenticated(request);
            }

            @Override
            public void onFailure(Throwable e, Response errorResponse) {

            }
        });
    }

    private static void queueHandler(Scoreflex.ResponseHandler handler) {
        if (null == handler) {
            return;
        }

        synchronized (sPendingHandlers) {
            sPendingHandlers.add(handler);
        }
    }

    private static Scoreflex.ResponseHandler dequeueHandler() {
        Scoreflex.ResponseHandler handler = null;
        synchronized (sPendingHandlers) {
            if (sPendingHandlers.size() > 0) {
                handler = sPendingHandlers.get(0);
                if (null != handler) {
                    sPendingHandlers.remove(0);
                }
            }
        }
        return handler;
    }

    /**
     * Get the access token stored in the user's shared preferences.
     *
     * @return
     */

    protected static String getAccessToken() {
        SharedPreferences prefs = Scoreflex.getSharedPreferences();
        if (null == prefs) {
            return null;
        }
        String token = prefs.getString(ACCESS_TOKEN_PREF_NAME, null);
        return token;
    }

    /**
     * Is the access token stored in the user's shared preferences anonymous ?
     *
     * @return
     */

    protected static boolean getAccessTokenIsAnonymous() {
        SharedPreferences prefs = Scoreflex.getSharedPreferences();
        if (null == prefs) {
            return true;
        }
        return prefs.getBoolean(ACCESS_TOKEN_IS_ANONYMOUS_PREF_NAME, true);
    }

    /**
     * Set the SID stored in the user's shared preferences.
     *
     * @param accessToken
     *          The access token to be stored
     */
    protected static void setSID(String sid) {

        SharedPreferences preferences = Scoreflex.getSharedPreferences();
        SharedPreferences.Editor editor = preferences.edit();
        if (null == sid)
            editor.remove(SID_PREF_NAME);
        else
            editor.putString(SID_PREF_NAME, sid);
        editor.commit();

    }

    protected static void setPlayerId(String playerId) {
        SharedPreferences preferences = Scoreflex.getSharedPreferences();
        SharedPreferences.Editor editor = preferences.edit();
        if (null == playerId)
            editor.remove(PLAYER_ID_PREF_NAME);
        editor.putString(PLAYER_ID_PREF_NAME, playerId);
        editor.commit();

    }

    protected static String getPlayerId(Context applicationContext) {
        SharedPreferences prefs = Scoreflex.getSharedPreferences(applicationContext);
        if (null == prefs) {
            return null;
        }
        String playerId = prefs.getString(PLAYER_ID_PREF_NAME, null);
        return playerId;

    }

    protected static String getPlayerId() {
        SharedPreferences prefs = Scoreflex.getSharedPreferences();
        if (null == prefs) {
            return null;
        }
        String playerId = prefs.getString(PLAYER_ID_PREF_NAME, null);
        return playerId;

    }

    /**
     * Get the access token stored in the user's shared preferences.
     *
     * @return
     */

    protected static String getSID() {
        SharedPreferences prefs = Scoreflex.getSharedPreferences();
        if (null == prefs) {
            return null;
        }
        String sid = prefs.getString(SID_PREF_NAME, null);
        return sid;
    }

    /**
     * Set the access token stored in the user's shared preferences.
     *
     * @param accessToken
     *          The access token to be stored
     * @param isAnonymous
     *          Is this access token anonymous
     */
    protected static void setAccessToken(String accessToken, boolean isAnonymous) {
        SharedPreferences preferences = Scoreflex.getSharedPreferences();
        SharedPreferences.Editor editor = preferences.edit();
        if (null == accessToken) {
            editor.remove(ACCESS_TOKEN_PREF_NAME);
            editor.remove(ACCESS_TOKEN_IS_ANONYMOUS_PREF_NAME);
        } else {
            editor.putString(ACCESS_TOKEN_PREF_NAME, accessToken);
            editor.putBoolean(ACCESS_TOKEN_IS_ANONYMOUS_PREF_NAME, isAnonymous);
        }
        editor.commit();

    }

    /**
     * A serializable object that represents a request to the Scoreflex API.
     *
     *
     */

    protected static class Request implements Cloneable {
        HttpMethod mMethod;
        Scoreflex.RequestParams mParams;
        Scoreflex.ResponseHandler mHandler;
        String mResource;

        public Request(HttpMethod method, String resource, Scoreflex.RequestParams params,
                Scoreflex.ResponseHandler handler) {
            mMethod = method;
            mParams = params;
            mHandler = handler;
            mResource = resource;
        }

        public Request(JSONObject data) throws JSONException {
            mMethod = HttpMethod.values()[data.getInt("method")];
            mResource = data.getString("resource");
            JSONObject paramsJson = data.getJSONObject("params");
            mParams = new Scoreflex.RequestParams();
            @SuppressWarnings("unchecked")
            Iterator<String> keys = paramsJson.keys();
            while (keys.hasNext()) {
                String key = keys.next();
                mParams.put(key, paramsJson.getString(key));
            }
        }

        public JSONObject toJSON() throws JSONException {
            JSONObject result = new JSONObject();
            result.put("method", mMethod.ordinal());
            result.put("resource", mResource);
            JSONObject params = new JSONObject();
            if (null != mParams)
                for (String key : mParams.getParamNames())
                    params.put(key, mParams.getParamValue(key));
            result.put("params", params);
            return result;
        }

        public HttpMethod getMethod() {
            return mMethod;
        }

        public Scoreflex.RequestParams getParams() {
            return mParams;
        }

        public Scoreflex.ResponseHandler getHandler() {
            return mHandler;
        }

        public String getResource() {
            return mResource;
        }

        public void setMethod(HttpMethod mMethod) {
            this.mMethod = mMethod;
        }

        public void setParams(Scoreflex.RequestParams mParams) {
            this.mParams = mParams;
        }

        public void setHandler(Scoreflex.ResponseHandler mHandler) {
            this.mHandler = mHandler;
        }

        public void setResource(String resource) {
            this.mResource = resource;
        }

        @Override
        protected Object clone() {
            return new Request(mMethod, mResource, mParams, mHandler);
        }

        /**
         * Generates X-Scoreflex-Authorization header with request signature
         *
         * @return The authorization header or null for GET requests
         */
        @SuppressLint("DefaultLocale")
        protected BasicHeader getAuthorizationHeader() {
            try {
                StringBuilder sb = new StringBuilder();

                // Step 1: add HTTP method uppercase
                switch (mMethod) {
                case POST:
                    sb.append("POST");
                    break;
                case PUT:
                    sb.append("PUT");
                    break;
                case GET:
                    // No authorization header for GET requests
                    return null;
                case DELETE:
                    sb.append("DELETE");
                    break;
                }

                sb.append('&');

                // Step 2: add the URI
                Uri uri = Uri.parse(mResource);

                // Query string is stripped from resource
                sb.append(encode(String.format("%s%s", Scoreflex.getBaseURL(), uri.getEncodedPath())));

                // Step 3: add URL encoded parameters
                sb.append('&');
                TreeSet<String> paramNames = new TreeSet<String>();

                // Params from the URL
                Scoreflex.RequestParams queryStringParams = QueryStringParser.getRequestParams(uri.getQuery());
                if (null != queryStringParams)
                    paramNames.addAll(queryStringParams.getParamNames());

                // Params from the request
                if (null != mParams)
                    paramNames.addAll(mParams.getParamNames());

                if (paramNames.size() > 0) {

                    String last = paramNames.last();
                    for (String paramName : paramNames) {
                        String paramValue = null;

                        if (null != mParams)
                            paramValue = mParams.getParamValue(paramName);

                        if (null == paramValue && null != queryStringParams)
                            paramValue = queryStringParams.getParamValue(paramName);

                        sb.append(encode(String.format("%s=%s", encode(paramName), encode(paramValue))));
                        if (!last.equals(paramName))
                            sb.append("%26");

                    }
                }

                // Step 4: add body
                sb.append('&');
                // TODO: add the body here when we support other content types
                // than application/x-www-form-urlencoded
                Mac mac = Mac.getInstance("HmacSHA1");
                SecretKeySpec secret = new SecretKeySpec(Scoreflex.getClientSecret().getBytes("UTF-8"),
                        mac.getAlgorithm());
                mac.init(secret);
                byte[] digest = mac.doFinal(sb.toString().getBytes());
                String sig = Base64.encodeToString(digest, Base64.DEFAULT).trim();
                String encodedSig = encode(sig.trim());
                BasicHeader result = new BasicHeader("X-Scoreflex-Authorization",
                        String.format("Scoreflex sig=\"%s\", meth=\"0\"", encodedSig));
                return result;
            } catch (Exception e) {
                Log.e("Scoreflex", "Could not generate signature", e);
                return null;
            }
        }

        private static String encode(String s) throws UnsupportedEncodingException {
            return URLEncoder.encode(s, "UTF-8").replace("+", "%20").replace("*", "%2A").replace("%7E", "~");
        }

        @Override
        public String toString() {
            String method = null;
            switch (mMethod) {
            case POST:
                method = "POST";
                break;
            case PUT:
                method = "PUT";
                break;
            case GET:
                method = "GET";
                break;
            case DELETE:
                method = "DELETE";
            }
            return String.format("%s %s", method, mResource);
        }

    }

}