com.etalio.android.EtalioBase.java Source code

Java tutorial

Introduction

Here is the source code for com.etalio.android.EtalioBase.java

Source

/*
 * Copyright 2014 Ericsson AB
 *
 * 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.etalio.android;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import com.etalio.android.util.Log;

import com.etalio.android.client.DefaultEtalioHttpClient;
import com.etalio.android.client.GsonHttpBodyConverter;
import com.etalio.android.client.HttpBodyConverter;
import com.etalio.android.client.HttpClient;
import com.etalio.android.client.HttpRequest;
import com.etalio.android.client.HttpResponse;
import com.etalio.android.client.exception.EtalioAuthorizationCodeException;
import com.etalio.android.client.exception.EtalioHttpException;
import com.etalio.android.client.exception.EtalioTokenException;
import com.etalio.android.client.models.EtalioToken;
import com.etalio.android.client.models.TokenResponse;
import com.etalio.android.util.MimeTypeUtil;

import org.json.JSONException;
import org.json.JSONObject;

import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.SecureRandom;
import java.text.ParseException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import java.util.Random;

/**
 * The base class wraps the Etalio authentication flow and contain methods to handle API calls.
 */
public abstract class EtalioBase implements EtalioPersistentStore {

    private static final String TAG = EtalioBase.class.getCanonicalName();

    //Constants
    private static final String AB = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
    private static final String UTF_8 = "UTF-8";

    //Api base url
    public static final String API_URL = "https://api.etalio.com/";
    protected static final String SDK_TAG = "etalio-android-sdk-";

    //Request parameters
    protected static final String PARAM_CLIENT_ID = "client_id";
    protected static final String PARAM_CLIENT_SECRET = "client_secret";
    protected static final String PARAM_RESPONSE_TYPE = "response_type";
    protected static final String PARAM_REDIRECT_URI = "redirect_uri";
    protected static final String PARAM_GRANT_TYPE = "grant_type";
    protected static final String PARAM_REFRESH_TOKEN = "refresh_token";
    protected static final String PARAM_STATE = "state";
    protected static final String PARAM_CODE = "code";
    protected static final String PARAM_SCOPE = "scope";
    protected static final String PARAM_SDK = "sdk";
    protected static final String PARAM_ERROR = "error";

    //Content types
    public static final String CONTENT_TYPE_APPLICATION_X_WWW_FORM_URLENCODED = "application/x-www-form-urlencoded";
    public static final String CONTENT_TYPE_APPLICATION_JSON = "application/json";

    //Redirect uri
    private static final String REDIRECT_URI_PROTOCOL_BASE = "etalio";
    private static final String REDIRECT_URI_PATH = "authentication";

    //Extras
    public static final String EXTRA_CLIENT_ID = "com.etalio.CLIENT_ID";
    public static final String EXTRA_PACKAGE_NAME = "com.etalio.PACKAGE_NAME";
    public static final String EXTRA_ACCESS_TOKEN = "com.etalio.ACCESS_TOKEN";
    public static final String EXTRA_REFRESH_TOKEN = "com.etalio.REFRESH_TOKEN";
    public static final String EXTRA_EXPIRES_IN = "com.etalio.EXPIRES_IN";
    private static final int REQUEST_CODE_SSO = 10001;

    //Map for storing api endpoints
    protected final Map<String, String> domainMap = new HashMap<String, String>();

    private final Context mContext;

    //Storage of Oauth2 values
    protected String mClientId, mClientSecret, mRedirectUri;
    private HttpClient mHttpClient;
    private HttpBodyConverter httpBodyConverter;

    /**
     * Constructor
     *
     * @param clientID     the client identificator
     * @param clientSecret the client secret
     */
    public EtalioBase(String clientID, String clientSecret, Context context) {
        this(clientID, clientSecret, context, API_URL);
    }

    public EtalioBase(String clientID, String clientSecret, Context context, String apiUrl) {
        if (Utils.isNullOrEmpty(clientID)) {
            throw new IllegalArgumentException("Client ID must not be null");
        }
        if (Utils.isNullOrEmpty(clientSecret)) {
            throw new IllegalArgumentException("Client Secret must not be null");
        }
        this.mClientId = clientID;
        this.mClientSecret = clientSecret;
        this.mRedirectUri = REDIRECT_URI_PROTOCOL_BASE + clientID + "://" + REDIRECT_URI_PATH;
        this.mContext = context;
        domainMap.put("oauth2", apiUrl + "oauth2");
        domainMap.put("token", apiUrl + "oauth2/token");
        domainMap.put("revoke", apiUrl + "oauth2/revoke");
    }

    /**
     * Redirects the end user to Etalio sign in. Sign in will happen through the
     * Etalio app if installed, else through the web browser.
     *
     * <code>mEtalio.initiateEtalioSignIn(this, "profile.basic.r profile.email.r");</code>
     *
     * @param activity the activity used to call Etalio sign in.
     * @param scope    scopes for the sign in. The scopes should be separated by spaces, like "profile.basic.r profile.email.r"
     */
    public void initiateEtalioSignIn(Activity activity, String scope) {
        resetState();
        if (!isEtalioInstalled(activity)) {
            activity.startActivity(new Intent(Intent.ACTION_VIEW, getSignInUrl(scope)));
        } else {
            Intent etalio = new Intent();
            etalio.setClassName("com.etalio.android", "com.etalio.android.app.ui.activity.SingleSignOnActivity");
            etalio.putExtra(EXTRA_CLIENT_ID, mClientId);
            etalio.putExtra(EXTRA_PACKAGE_NAME, activity.getPackageName());
            etalio.setFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
            activity.startActivityForResult(etalio, REQUEST_CODE_SSO);
        }
    }

    private boolean isEtalioInstalled(Context context) {
        PackageManager pm = context.getPackageManager();
        try {
            pm.getPackageInfo("com.etalio.android", PackageManager.GET_ACTIVITIES);
            return true;
        } catch (PackageManager.NameNotFoundException e) {
            return false;
        }
    }

    /**
     * Redirects the end user to Etalio sign in with default "profile.r" scope. Sign in will happen through the Etalio app if installed, else through the web browser.
     * @param activity the activity used to call Etalio sign in.
     */
    public void initiateEtalioSignIn(Activity activity) {
        initiateEtalioSignIn(activity, null);
    }

    /**
     * Request the access token given for this session to be revoked and not valid anymore.
     * @return true if the token was successfully revoked. If not an exception should be thrown.
     * @throws IOException If request fails due to a problem with IO.
     * @throws EtalioHttpException If the http request fails when revoking the token.
     */
    public boolean requestRevokeAccess() throws IOException, EtalioHttpException {
        Map<String, String> params = new HashMap<String, String>();
        params.put("token", getEtalioToken().getAccessToken());
        params.put("token_type_hint", "access_token");

        byte[] body = generateFormEncodedRequestBody(params);

        Map<String, String> headers = buildSignedApiCallHeaders();
        addEtalioUserAgent(headers);
        HttpRequest request = new HttpRequest(HttpRequest.HttpMethod.POST, getUrl("revoke", null), headers, body,
                CONTENT_TYPE_APPLICATION_X_WWW_FORM_URLENCODED, body != null ? body.length : 0);
        HttpResponse response = getHttpClient().executeRequest(request, getHttpBodyConverter(), String.class);

        return true;
    }

    /**
     * Checks the intent for data belonging to an Etalio Callback
     *
     * @param intent the intent
     * @return if the intent contains an Etalio Callback
     * @throws com.etalio.android.client.exception.EtalioAuthorizationCodeException if callback contains error parameter.
     */
    protected boolean isEtalioSignInCallback(Intent intent) throws EtalioAuthorizationCodeException {
        if (intent != null && intent.getData() != null
                && Utils.matchUris(intent.getData(), Uri.parse(mRedirectUri))) {
            //Check state
            if (!getState().equals(intent.getData().getQueryParameter(PARAM_STATE))) {
                throw new EtalioAuthorizationCodeException(
                        EtalioAuthorizationCodeException.AuthorizationCodeGrantErrorResponse.INVALID_STATE
                                .getError());
            }
            String code = getAuthorizationCodeFromUri(intent.getData());
            if (Utils.isNullOrEmpty(code)) {
                //Code is missing look for fail callback
                String error = intent.getData().getQueryParameter(PARAM_ERROR);
                throw new EtalioAuthorizationCodeException(error);
            }
            return true;
        }
        return false;
    }

    /**
     * Get the authorization code parameter from a uri.
     * @param data The Uri to get the code from.
     * @return The code
     */
    private String getAuthorizationCodeFromUri(Uri data) {
        return data.getQueryParameter(PARAM_CODE);
    }

    /**
     * 1. Handles the redirect/callback when user presses in sign in in browser or Etalio app.
     * 2. Initiates a request for a session and return the result of that request to the AuthenticationCallback.
     * @param intent The intent that might contain a Etalio sign in callback.
     * @param callback The AuthenticationCallback to handle resulting authentication.
     * @return boolean true if the intent contains an authorization callback that will be handled. false if there was no Etalio authentication callback within the intent.
     * @throws com.etalio.android.client.exception.EtalioAuthorizationCodeException
     */
    public boolean handleSignInCallback(final Intent intent, final AuthenticationCallback callback)
            throws EtalioAuthorizationCodeException {
        if (isEtalioSignInCallback(intent)) {
            authorizeAndAcquireTokensFromCallback(intent, callback);
            return true;
        }
        return false;
    }

    /**
     * Call this method in authenticating activity's onActivityResult to handle Etalio single sign on results.
     * @param requestCode
     * @param resultCode
     * @param data
     */
    public void onActivityResult(int requestCode, int resultCode, Intent data, AuthenticationCallback callback) {
        if (requestCode == REQUEST_CODE_SSO && resultCode == Activity.RESULT_OK) {
            String accessToken = data.getStringExtra(EtalioBase.EXTRA_ACCESS_TOKEN);
            String refreshToken = data.getStringExtra(EtalioBase.EXTRA_REFRESH_TOKEN);
            int expiresIn = data.getIntExtra(EtalioBase.EXTRA_EXPIRES_IN, 0);
            setEtalioToken(new EtalioToken(accessToken, refreshToken, expiresIn));
            callback.onAuthenticationSuccess();
        } else {
            callback.onAuthenticationFailure(
                    new EtalioTokenException(EtalioTokenException.TokensErrorResponse.INVALID_RESPONSE));
        }
    }

    /**
     * Requests an access token and a refresh token from Etalio
     */
    protected void authorizeAndAcquireTokensFromCallback(final Intent intent, final AuthenticationCallback callback)
            throws EtalioAuthorizationCodeException {
        if (!isEtalioSignInCallback(intent)) {
            throw new IllegalArgumentException("The intent does not contain a valid authentication callback.");
        }
        //set the code for later
        final String code = getAuthorizationCodeFromUri(intent.getData());
        //Reset intent data to not trigger this call again.
        if (intent != null) {
            intent.setData(null);
        }

        new AsyncTask<String, Void, Boolean>() {

            public EtalioTokenException error;

            @Override
            protected Boolean doInBackground(String... params) {
                //get and set the access/refresh tokens
                try {
                    requestNewToken(params[0]);
                } catch (EtalioTokenException e) {
                    this.error = e;
                    return false;
                } catch (IOException e) {
                    return false;
                }
                return true;
            }

            @Override
            protected void onPostExecute(Boolean result) {
                if (callback != null) {
                    if (result) {
                        callback.onAuthenticationSuccess();
                    } else {
                        callback.onAuthenticationFailure(this.error);
                    }
                }
            }
        }.execute(code);
    }

    /**
     * Returns the Etalio token object, or null if it's not stored yet.
     * @return EtalioToken containing access token, refresh token and expiration time.
     */
    public EtalioToken getEtalioToken() {
        String serializedToken = getPersistentData(SupportedKey.ETALIO_TOKEN);
        if (Utils.isNullOrEmpty(serializedToken)) {
            return null;
        }
        return new EtalioToken(serializedToken);
    }

    /**
     * Sets the Etalio token object
     * @param t The etalio token to use for the session from now on.
     */
    public void setEtalioToken(EtalioToken t) {
        setPersistentData(SupportedKey.ETALIO_TOKEN, t.toSerializedString());
    }

    /**
     *
     * @return If there is a valid access token. If token expired it can be renewed with @see EtalioBase#requestRefreshToken().
     *
     * NOTE: There is no guarantee that the session is active or can be renewed.
     * The access might be revoked from another client, this needs to be verified with an API request.
     */
    public boolean hasEtalioSession() {
        EtalioToken token = getEtalioToken();
        return token != null && token.isTokenExpired();
    }

    /**
     * Do an authorized call against the API
     *
     * @param apiName    the name of the api in the domainMap
     * @param method     the http method, GET, POST, PUT or DELETE
     * @param outputBody an object to be sent as JSON if method is PUT, POST or DELETE.
     * @return the result as a String
     * @throws java.io.IOException if request fails
     */
    protected <T> T apiCall(String apiName, HttpRequest.HttpMethod method, Object outputBody, Class<T> returnType,
            boolean signedCall) throws EtalioTokenException, EtalioHttpException, IOException {
        Map<String, String> headers = new HashMap<String, String>();
        if (signedCall) {
            EtalioToken token = getEtalioToken();
            if (token == null) {
                throw new EtalioTokenException(EtalioTokenException.TokensErrorResponse.INVALID_TOKEN);
            }
            if (token.willTokenSoonExpire()) {
                requestRefreshToken();
            }

            headers = buildSignedApiCallHeaders();
        }
        addEtalioUserAgent(headers);

        byte[] body;
        if (outputBody == null) {
            body = new byte[0];
        } else {
            body = getHttpBodyConverter().toBody(outputBody, "UTF-8");
        }

        HttpRequest request = new HttpRequest(method, getUrl(apiName, null), headers, body,
                CONTENT_TYPE_APPLICATION_JSON, body != null ? body.length : 0);
        Log.v(TAG, method + " " + getUrl(apiName, null) + " " + new String(body, "UTF8") + " ");
        HttpResponse<T> response;
        response = getHttpClient().executeRequest(request, getHttpBodyConverter(), returnType);
        if (response.getBody() != null) {
            Log.v(TAG, response.getStatus() + " : "
                    + new String(getHttpBodyConverter().toBody(response.getBody(), "UTF-8"), "UTF-8"));
        } else {
            Log.v(TAG, "" + response.getStatus());
        }
        return response.getBody();
    }

    /**
     * Do an authorized call against the API
     *
     * @param apiName    the name of the api in the domainMap
     * @param method     the http method, GET, POST, PUT or DELETE
     * @param outputBody an object to be sent as JSON if method is PUT, POST or DELETE.
     * @return the result as a String
     * @throws java.io.IOException if request fails
     */
    protected <T> T apiCall(String apiName, HttpRequest.HttpMethod method, Object outputBody, Class<T> returnType)
            throws EtalioTokenException, IOException, EtalioHttpException {
        return apiCall(apiName, method, outputBody, returnType, true);
    }

    /**
     * Returns the url for the given endpoint in the domainmap
     *
     * @param key    the name of the endpoint in the domainmap
     * @param params optional map with key value parameters
     * @return the url to the given endpoint including request parameters
     */
    protected String getUrl(String key, Map<String, String> params) {
        String url = domainMap.get(key);
        if (params != null && !params.isEmpty()) {
            boolean first = true;
            for (Map.Entry<String, String> param : params.entrySet()) {
                url += ((first) ? "?" : "&") + param.getKey() + "=" + param.getValue();
                first = false;
            }
        }
        Log.d(TAG, url);
        return url;
    }

    /**
     * Build the URL for the first step in the oauth hand shake
     *
     * @param scope (Optional) If another scope than the default should be used
     * @return the Url as a String with Get parameters
     */
    private Uri getSignInUrl(String scope) {
        Map<String, String> params = new HashMap<String, String>();
        params.put(PARAM_CLIENT_ID, mClientId);
        params.put(PARAM_STATE, getState());
        params.put(PARAM_REDIRECT_URI, mRedirectUri);
        params.put(PARAM_SDK, SDK_TAG + getVersion());
        params.put(PARAM_RESPONSE_TYPE, "code");

        if (!Utils.isNullOrEmpty(scope)) {
            params.put(PARAM_SCOPE, scope);
        }

        return Uri.parse(getUrl("oauth2", params));
    }

    private String getVersion() {
        PackageInfo pInfo = null;
        try {
            pInfo = mContext.getPackageManager().getPackageInfo(mContext.getPackageName(), 0);
        } catch (PackageManager.NameNotFoundException e) {
            return "unknown-version";
        }
        return pInfo.versionName;
    }

    /**
     * Build the URL for exchanging the authorization code for an access token
     *
     * @param code The Code as a string
     * @return the POST body as a String.
     */
    private byte[] buildTokenBody(String code) {
        if (Utils.isNullOrEmpty(code)) {
            throw new IllegalArgumentException("Code can not be empty");
        }

        Map<String, String> params = new HashMap<String, String>();
        params.put(PARAM_GRANT_TYPE, "authorization_code");
        params.put(PARAM_REDIRECT_URI, mRedirectUri);
        params.put(PARAM_CODE, code);
        params.put(PARAM_CLIENT_ID, mClientId);
        params.put(PARAM_CLIENT_SECRET, mClientSecret);

        return generateFormEncodedRequestBody(params);
    }

    /**
     * Generates a form request body for application/x-www-form-urlencoded requests.
     *
     * Example:
     * client_id=7y7gp0495bt7acqbqdaw7y7gp0495bt7
     * &client_secret=ckm6ssv30cwz1zg7xu2pckm6ssv30cwz1zg7xu2p
     * &redirect_uri=http%253A%252F%252Flocalhost%253A3000%252Fauth%252Fetalio%252Fcallback
     * &code=2260bbf6918ce3c1715366580cc568acf58afe45
     * &grant_type=authorization_code
     *
     * @param params The key value pairs to generate the body for.
     * @return The request body as a byte array.
     */
    protected byte[] generateFormEncodedRequestBody(Map<String, String> params) {
        String body = "";
        Iterator<Map.Entry<String, String>> iterator = params.entrySet().iterator();
        while (iterator.hasNext()) {
            Map.Entry<String, String> entry = iterator.next();
            body += entry.getKey() + "=" + entry.getValue();
            if (iterator.hasNext()) {
                body += "&";
            }
        }
        try {
            return body.getBytes(UTF_8);
        } catch (UnsupportedEncodingException e) {
            Log.e(TAG, "Missing UTF-8 encoding", e);
            return null;
        }
    }

    /**
     * Builds the URL that exchanges the refresh token to for a new access token
     *
     * @return the Url as a String with Get parameters
     */
    private byte[] buildRefreshTokenBody() {
        Map<String, String> params = new HashMap<String, String>();
        params.put(PARAM_GRANT_TYPE, "refresh_token");
        params.put(PARAM_REFRESH_TOKEN, getEtalioToken().getRefreshToken());
        params.put(PARAM_CLIENT_ID, mClientId);
        params.put(PARAM_CLIENT_SECRET, mClientSecret);

        return generateFormEncodedRequestBody(params);
    }

    public Map<String, String> buildSignedApiCallHeaders() {
        Map<String, String> headers = new HashMap<String, String>();
        headers.put("Authorization", " Bearer " + getEtalioToken().getAccessToken());
        Log.d("Access Token", getEtalioToken().getAccessToken());
        return headers;
    }

    protected void addEtalioUserAgent(Map<String, String> headers) {
        headers.put("User-Agent", getEtalioUserAgent());
    }

    protected String getEtalioUserAgent() {
        Locale locale = Locale.getDefault();
        Resources resources;
        resources = mContext.getApplicationContext().getResources();
        return "Etalio/" + getVersion() + " (Linux; " + " Android " + Build.VERSION.RELEASE + "; "
                + locale.toString() + "; " + Build.MANUFACTURER + " " + Build.MODEL + " Build/" + Build.DISPLAY
                + "; " + " Density/" + (resources != null ? resources.getDisplayMetrics().density : "unknown")
                + ")";
    }

    /**
     * Requests a new access token after authorization has already occurred.
     * @param authorizationCode The access code retrieved in the authentication request.
     */
    void requestNewToken(String authorizationCode) throws EtalioTokenException, IOException {
        Log.d(TAG, "Requesting new token.");

        byte[] body = buildTokenBody(authorizationCode);
        String data = new String(body, "UTF-8");
        Log.d(TAG, "Body: " + data);
        Map<String, String> headers = new HashMap<String, String>();
        addEtalioUserAgent(headers);

        HttpRequest request = new HttpRequest(HttpRequest.HttpMethod.POST, getUrl("token", null), null, body,
                CONTENT_TYPE_APPLICATION_X_WWW_FORM_URLENCODED, body.length);
        HttpResponse<TokenResponse> response = null;
        try {
            response = getHttpClient().executeRequest(request, new GsonHttpBodyConverter<TokenResponse>(),
                    TokenResponse.class);
        } catch (EtalioHttpException e) {
            Log.e(TAG, "Error when fetching new token", e);
            parseEtalioTokenError(e);
        }

        updateEtalioToken(response);
    }

    /**
     * Requests a refresh of the access token to extend the session.
     */
    public void requestRefreshToken() throws EtalioTokenException, IOException {
        Log.d(TAG, "Refreshing token.");
        if (getEtalioToken() == null) {
            throw new EtalioTokenException(EtalioTokenException.TokensErrorResponse.INVALID_TOKEN);
        }

        byte[] body = buildRefreshTokenBody();
        Map<String, String> headers = new HashMap<String, String>();
        addEtalioUserAgent(headers);

        HttpRequest request = new HttpRequest(HttpRequest.HttpMethod.POST, getUrl("token", null), headers, body,
                CONTENT_TYPE_APPLICATION_X_WWW_FORM_URLENCODED, body.length);
        HttpResponse<TokenResponse> response = null;
        try {
            response = getHttpClient().executeRequest(request, new GsonHttpBodyConverter<TokenResponse>(),
                    TokenResponse.class);
        } catch (EtalioHttpException e) {
            parseEtalioTokenError(e);
        }

        updateEtalioToken(response);
    }

    private void parseEtalioTokenError(EtalioHttpException e) throws EtalioTokenException {
        try {
            String message = e.getMessage();
            JSONObject json = new JSONObject(message);
            throw new EtalioTokenException(json.getString("error"));
        } catch (JSONException ej) {
            throw new EtalioTokenException(EtalioTokenException.TokensErrorResponse.INVALID_REQUEST);
        }
    }

    /**
     * Validates the Etalio token from a HTTP response.
     * @param response The response to read token from.
     * @throws EtalioTokenException If the request is invalid or if the token contains an error message.
     */
    protected void updateEtalioToken(HttpResponse<TokenResponse> response) throws EtalioTokenException {
        if (response == null) {
            throw new EtalioTokenException(EtalioTokenException.TokensErrorResponse.INVALID_RESPONSE);
        }

        TokenResponse token = response.getBody();

        if (!Utils.isNullOrEmpty(token.getError())) {
            throw new EtalioTokenException(token.getError());
        }

        setEtalioToken(new EtalioToken(token.getAccessToken(), token.getRefreshToken(), token.getExpiresIn()));
    }

    /**
     * Get the http client wrapper to be used for http request during authentication and API requests.  Override this method in a subclass to use your own implementation.
     * @return The @see HttpClient implementation to use.
     */
    protected HttpClient getHttpClient() {
        if (mHttpClient == null) {
            mHttpClient = new DefaultEtalioHttpClient(mContext);
        }
        return mHttpClient;
    }

    /**
     * Get the converter used converting from and to the http body in requests. Override this method in a subclass to use your own implementation.
     * @return The @see HttpBodyConverter implementation to use.
     */
    protected HttpBodyConverter getHttpBodyConverter() {
        if (httpBodyConverter == null) {
            httpBodyConverter = new GsonHttpBodyConverter();
        }
        return httpBodyConverter;
    }

    protected String multipartRequest(String urlTo, InputStream fileInputStream, String filefield, String filename)
            throws ParseException, IOException, EtalioHttpException {
        HttpURLConnection connection = null;
        DataOutputStream outputStream = null;
        InputStream inputStream = null;

        String twoHyphens = "--";
        String boundary = "*****" + Long.toString(System.currentTimeMillis()) + "*****";
        String lineEnd = "\r\n";

        String result = "";

        int bytesRead, bytesAvailable, bufferSize;
        byte[] buffer;
        int maxBufferSize = 1 * 1024 * 1024;

        try {
            URL url = new URL(urlTo);
            connection = (HttpURLConnection) url.openConnection();

            connection.setDoInput(true);
            connection.setDoOutput(true);
            connection.setUseCaches(false);
            connection.setRequestMethod("POST");
            connection.setRequestProperty("Connection", "Keep-Alive");

            connection.setRequestProperty("Authorization", " Bearer " + getEtalioToken().getAccessToken());
            connection.setRequestProperty("User-Agent", getEtalioUserAgent());
            connection.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);

            outputStream = new DataOutputStream(connection.getOutputStream());
            outputStream.writeBytes(twoHyphens + boundary + lineEnd);
            outputStream.writeBytes("Content-Disposition: form-data; name=\"" + filefield + "\"; filename=\""
                    + filename + "\"" + lineEnd);
            outputStream.writeBytes("Content-Type: " + MimeTypeUtil.getMimeType(filename) + lineEnd);
            outputStream.writeBytes("Content-Transfer-Encoding: binary" + lineEnd);
            outputStream.writeBytes(lineEnd);

            bytesAvailable = fileInputStream.available();
            bufferSize = Math.min(bytesAvailable, maxBufferSize);
            buffer = new byte[bufferSize];

            bytesRead = fileInputStream.read(buffer, 0, bufferSize);
            while (bytesRead > 0) {
                outputStream.write(buffer, 0, bufferSize);
                bytesAvailable = fileInputStream.available();
                bufferSize = Math.min(bytesAvailable, maxBufferSize);
                bytesRead = fileInputStream.read(buffer, 0, bufferSize);
            }

            outputStream.writeBytes(lineEnd);

            outputStream.writeBytes(twoHyphens + boundary + twoHyphens + lineEnd);

            inputStream = connection.getInputStream();

            if (connection.getResponseCode() > 300) {
                result = convertStreamToString(connection.getErrorStream());
            } else {
                result = this.convertStreamToString(inputStream);
            }

            fileInputStream.close();
            inputStream.close();
            outputStream.flush();
            outputStream.close();

            return result;
        } catch (Exception e) {
            throw new EtalioHttpException(connection.getResponseCode(), connection.getResponseMessage(),
                    e.getMessage());
        }
    }

    protected String convertStreamToString(InputStream is) {
        BufferedReader reader = new BufferedReader(new InputStreamReader(is));
        StringBuilder sb = new StringBuilder();

        String line = null;
        try {
            while ((line = reader.readLine()) != null) {
                sb.append(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                is.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return sb.toString();
    }

    private void resetState() {
        clearPersistentData(SupportedKey.STATE);
    }

    protected String getState() {
        String state = getPersistentData(SupportedKey.STATE);
        if (Utils.isNullOrEmpty(state)) {
            state = setState(randomString());
        }
        return state;
    }

    private String setState(String state) {
        setPersistentData(SupportedKey.STATE, state);
        return state;
    }

    private synchronized String randomString() {
        Random generator = new SecureRandom();
        StringBuilder sb = new StringBuilder();
        //Random length but at least half max length
        int randomLength = (48 / 2) + generator.nextInt(48 / 2);
        for (int i = 0; i < randomLength; i++) {
            sb.append(AB.charAt(generator.nextInt(AB.length())));
        }
        return sb.toString();
    }

    public abstract void setPersistentData(SupportedKey key, String val);

    public abstract String getPersistentData(SupportedKey key);

    public abstract void clearPersistentData(SupportedKey key);

    public abstract void clearAllPersistentData();
}