public class Request extends BaseRequest {

    private static Logger logger = Logger.getLogger(Logger.INTERNAL_PREFIX + Request.class.getSimpleName());

    private int oauthFailCounter = 0; // Number of times the request failed authentication
    private RequestBody savedRequestBody; // Used to resend the original request after successful authentication
    private Context context;

    /**
     * Constructs a new resource request with the specified URL, using the specified HTTP method.
     *
     * @param url    The resource URL, may be either relative or absolute.
     * @param method The HTTP method to use
     */
    public Request(String url, String method) {
        super(url, method, DEFAULT_TIMEOUT);
    }

    /**
     * Constructs a new resource request with the specified URL, using the specified HTTP method.
     * Additionally this constructor sets a custom timeout.
     *
     * @param url     The resource URL
     * @param method  The HTTP method to use.
     * @param timeout The timeout in milliseconds for this request.
     */
    public Request(String url, String method, int timeout) {
        super(url, method, timeout);
    }

    /**
     * Constructs a new resource request with the specified URL, using the specified HTTP method.
     * Additionally this constructor sets a custom timeout and number of times to automatically
     * retry failed requests.
     *
     * @param url           The resource URL
     * @param method        The HTTP method to use.
     * @param timeout       The timeout in milliseconds for this request.
     * @param autoRetries   The number of times to retry each request if it fails due to timeout or loss of network connection.
     */
    public Request(String url, String method, int timeout, int autoRetries) {
        super(url, method, timeout, autoRetries);
    }

    /**
     * Returns the URL for this resource request.
     *
     * @return String The URL representing the path for this resource request.
     */
    public String getUrl(){
        return super.getUrl();
    }

    /**
     * Returns the HTTP method for this resource request.
     *
     * @return A string containing the name of the HTTP method.
     */
    public String getMethod() {
        return super.getMethod();
    }

    /**
     * Returns the timeout for this resource request.
     *
     * @return the timeout for this resource request
     */
    public int getTimeout() {
        return super.getTimeout();
    }

    /**
     * Returns the query parameters set for this resource request.
     *
     * @return A Map containing the query parameters
     */
    public Map<String, String> getQueryParameters() {
        return super.getQueryParameters();
    }

    /**
     * Sets the query parameters for this resource request.
     *
     * @param parameters A HashMap containing the query parameters
     */
    public void setQueryParameters(Map<String, String> parameters) {
        super.setQueryParameters(parameters);
    }

    /**
     * Returns all the headers that were set for this resource request.
     *
     * @return An array of Headers
     */
    public Map<String, List<String>> getHeaders() {
        return super.getAllHeaders();
    }

    /**
     * Sets headers for this resource request. Overrides all headers previously added.
     *
     * @param headers A multimap containing the header names and corresponding values
     */
    public void setHeaders(Map<String, List<String>> headers) {
        super.setHeaders(headers);
    }


    //region Send

    /**
     * Send this resource request asynchronously, without a request body.
     *
     * @param context   The context that will be passed to authentication listener.
     * @param listener  The listener whose onSuccess or onFailure methods will be called when this request finishes
     */
    public void send(Context context, ResponseListener listener) {
        setContext(context);
        super.send(listener);
    }

    /**
     * Send this resource request asynchronously, with the given string as the request body.
     * If no Content-Type header was set, this method will set it to "text/plain".
     *
     * @param context   The context that will be passed to authentication listener.
     * @param text      The text to put in the request body
     * @param listener  The listener whose onSuccess or onFailure methods will be called when this request finishes
     */
    public void send(Context context, String text, ResponseListener listener) {
        setContext(context);
        super.send(text, listener);
    }

    /**
     * Send this resource request asynchronously, with the given form parameters as the request body.
     * If no Content-Type header was set, this method will set it to "application/x-www-form-urlencoded".
     *
     * @param context           The context that will be passed to authentication listener.
     * @param formParameters    The parameters to put in the request body
     * @param listener          The listener whose onSuccess or onFailure methods will be called when this request finishes
     */
    protected void send(Context context, Map<String, String> formParameters, ResponseListener listener) {
        setContext(context);
        super.send(formParameters, listener);
    }

    /**
     * Send this resource request asynchronously, with the given JSON object as the request body.
     * If no Content-Type header was set, this method will set it to "application/json".
     *
     * @param context   The context that will be passed to authentication listener.
     * @param json      The JSON object to put in the request body
     * @param listener  The listener whose onSuccess or onFailure methods will be called when this request finishes
     */
    protected void send(Context context, JSONObject json, ResponseListener listener) {
        setContext(context);
        super.send(json, listener);
    }

    /**
     * Send this resource request asynchronously, with the given byte array as the request body.
     * This method does not set any Content-Type header; if such a header is required, it must be set before calling this method.
     *
     * @param context   The context that will be passed to authentication listener.
     * @param data      The byte array to put in the request body
     * @param listener  The listener whose onSuccess or onFailure methods will be called when this request finishes
     */
    public void send(Context context, byte[] data, ResponseListener listener) {
        setContext(context);
        super.send(data, listener);
    }

    //endregion


    // region Download

    /**
     * <p>
     * Download this resource asynchronously, without a request body.
     * The download progress will be monitored with a {@link ProgressListener}.
     * </p>
     *
     * <p>
     * <b>Note: </b>This method consumes the <code>InputStream</code> from the response and closes it,
     * so the {@link Response#getResponseByteStream()} method will always return null for this request.
     * </p>
     *
     * @param context           The context that will be passed to authentication listener.
     * @param progressListener  The listener that monitors the download progress
     * @param responseListener  The listener whose onSuccess or onFailure methods will be called when this request finishes
     */
    public void download(Context context, ProgressListener progressListener, ResponseListener responseListener) {
        setContext(context);
        super.download(progressListener, responseListener);
    }

    /**
     * <p>
     * Download this resource asynchronously, with the given string as the request body.
     * The download progress will be monitored with a {@link ProgressListener}.
     * If no Content-Type header was set, this method will set it to "text/plain".
     * </p>
     *
     * <p>
     * <b>Note: </b>This method consumes the <code>InputStream</code> from the response and closes it,
     * so the {@link Response#getResponseByteStream()} method will always return null for this request.
     * </p>
     *
     * @param context           The context that will be passed to authentication listener.
     * @param requestBody       The text to put in the request body
     * @param progressListener  The listener that monitors the download progress
     * @param responseListener  The listener whose onSuccess or onFailure methods will be called when this request finishes
     */
    public void download(Context context, final String requestBody, ProgressListener progressListener, final ResponseListener responseListener) {
        setContext(context);
        super.download(requestBody, progressListener, responseListener);
    }

    /**
     * <p>
     * Download this resource asynchronously, with the given form parameters as the request body.
     * The download progress will be monitored with a {@link ProgressListener}.
     * If no Content-Type header was set, this method will set it to "application/x-www-form-urlencoded".
     * </p>
     *
     * <p>
     * <b>Note: </b>This method consumes the <code>InputStream</code> from the response and closes it,
     * so the {@link Response#getResponseByteStream()} method will always return null for this request.
     * </p>
     *
     * @param context           The context that will be passed to authentication listener.
     * @param formParameters    The parameters to put in the request body
     * @param progressListener  The listener that monitors the download progress
     * @param responseListener  The listener whose onSuccess or onFailure methods will be called when this request finishes
     */
    public void download(Context context, Map<String, String> formParameters, ProgressListener progressListener, ResponseListener responseListener) {
        setContext(context);
        super.download(formParameters, progressListener, responseListener);
    }

    /**
     * <p>
     * Download this resource asynchronously, with the given JSON object as the request body.
     * The download progress will be monitored with a {@link ProgressListener}.
     * If no Content-Type header was set, this method will set it to "application/json".
     * </p>
     *
     * <p>
     * <b>Note: </b>This method consumes the <code>InputStream</code> from the response and closes it,
     * so the {@link Response#getResponseByteStream()} method will always return null for this request.
     * </p>
     *
     * @param context           The context that will be passed to authentication listener.
     * @param json              The JSON object to put in the request body
     * @param progressListener  The listener that monitors the download progress
     * @param responseListener  The listener whose onSuccess or onFailure methods will be called when this request finishes
     */
    public void download(Context context, JSONObject json, ProgressListener progressListener, ResponseListener responseListener) {
        setContext(context);
        super.download(json, progressListener, responseListener);
    }

    /**
     * <p>
     * Download this resource asynchronously, with the given byte array as the request body.
     * The download progress will be monitored with a {@link ProgressListener}.
     * This method does not set any Content-Type header; if such a header is required, it must be set before calling this method.
     * </p>
     *
     * <p>
     * <b>Note: </b>This method consumes the <code>InputStream</code> from the response and closes it,
     * so the {@link Response#getResponseByteStream()} method will always return null for this request.
     * </p>
     *
     * @param context           The context that will be passed to authentication listener.
     * @param data              The byte array to put in the request body
     * @param progressListener  The listener that monitors the download progress
     * @param responseListener  The listener whose onSuccess or onFailure methods will be called when this request finishes
     */
    public void download(Context context, byte[] data, ProgressListener progressListener, ResponseListener responseListener) {
        setContext(context);
        super.download(data, progressListener, responseListener);
    }

    // endregion


    // region Upload

    /**
     * Upload text asynchronously.
     * If no Content-Type header was set, this method will set it to "text/plain".
     *
     * @param context           The context that will be passed to authentication listener.
     * @param text              The text to upload
     * @param progressListener  The listener that monitors the upload progress
     * @param responseListener  The listener whose onSuccess or onFailure methods will be called when this request finishes
     */
    public void upload(Context context, final String text, final ProgressListener progressListener, ResponseListener responseListener) {
        setContext(context);
        super.upload(text, progressListener, responseListener);
    }

    /**
     * Upload a byte array asynchronously.
     * This method does not set any Content-Type header; if such a header is required, it must be set before calling this method.
     *
     * @param context           The context that will be passed to authentication listener.
     * @param data              The byte array to upload
     * @param progressListener  The listener that monitors the upload progress
     * @param responseListener  The listener whose onSuccess or onFailure methods will be called when this request finishes
     */
    public void upload(Context context, final byte[] data, final ProgressListener progressListener, ResponseListener responseListener) {
        setContext(context);
        super.upload(data, progressListener, responseListener);
    }

    /**
     * Upload a file asynchronously.
     * This method does not set any Content-Type header; if such a header is required, it must be set before calling this method.
     *
     * @param context           The context that will be passed to authentication listener.
     * @param file              The file to upload
     * @param progressListener  The listener that monitors the upload progress
     * @param responseListener  The listener whose onSuccess or onFailure methods will be called when this request finishes
     */
    public void upload(Context context, final File file, final ProgressListener progressListener, ResponseListener responseListener) {
        setContext(context);
        super.upload(file, progressListener, responseListener);
    }


    void setContext(Context context) {
        this.context = context;
    }

    // endregion


    @Override
    protected void sendRequest(final ProgressListener progressListener, final ResponseListener listener, final RequestBody requestBody) {
        // Add authorization header if this request is being made to a protected resource
        AuthorizationManager authorizationManager = BMSClient.getInstance().getAuthorizationManager();
        String cachedAuthHeader = authorizationManager.getCachedAuthorizationHeader();
        if (cachedAuthHeader != null) {
            removeHeaders("Authorization");
            addHeader("Authorization", cachedAuthHeader);
        }

        savedRequestBody = requestBody;
        super.sendRequest(progressListener, listener, requestBody);
    }

    @Override
    protected Callback getCallback(final ProgressListener progressListener, final ResponseListener responseListener) {
        final RequestBody requestBody = savedRequestBody;
        final Request request = this;
        final Context ctx = this.context;

        return new Callback() {


            // The request failed to complete, so no response was received from the server.
            @Override
            public void onFailure(Call call, IOException e){
                // If auto-retries are enabled, and the request hasn't run out of retry attempts,
                // then try to send the same request again. Otherwise, delegate to the user's ResponseListener.
                // Note that we also retry requests that receive 504 responses, as seen in the onResponse() method.
                if (numberOfRetries > 0) {
                    numberOfRetries--;
                    logger.debug("Resending " + call.request().method() +  " request to " + call.request().toString());
                    sendOKHttpRequest(call.request(), getCallback(progressListener, responseListener));
                } else {
                    if (responseListener != null) {
                        responseListener.onFailure(null, e, null);
                    }
                }
            }

            // If this method is reached, a response (of any type) has been received from the server.
            // This does not always indicate a successful response.
            @Override
            public void onResponse(Call call, okhttp3.Response response) throws IOException {
                if (responseListener == null || response == null) {
                    return;
                }

                // If the request is made to a protected endpoint, see if we need to use AuthorizationManager
                // to authenticate by resending the request with the correct authorization header.
                AuthorizationManager authorizationManager = BMSClient.getInstance().getAuthorizationManager();

                int responseCode = response.code();
                Map<String, List<String>> responseHeaders = response.headers().toMultimap();
                boolean isAuthorizationRequired = authorizationManager.isAuthorizationRequired(responseCode, responseHeaders);

                if (isAuthorizationRequired) {

                    // The first oauthFailCounter gets triggered by a 401 (the server is requesting authentication)
                    // If the oauthFailCounter gets incremented again, then authentication has failed.
                    if (oauthFailCounter++ < 2) {
                        authorizationManager.obtainAuthorization(
                                ctx,
                                new ResponseListener() {
                                    @Override
                                    public void onSuccess(Response response) {
                                        // this will take the auth hader that has been cached by obtainAuthorizationHeader
                                        request.sendRequest(progressListener, responseListener, requestBody);
                                    }

                                    @Override
                                    public void onFailure(Response response, Throwable t, JSONObject extendedInfo) {
                                        responseListener.onFailure(response, t, extendedInfo);
                                    }
                                }
                        );
                    } else {
                        responseListener.onFailure(new ResponseImpl(response), null, null);
                    }
                } else {

                    // If the response is successful, delegate to the user's
                    //      1) ResponseListener
                    //      2) ProgressListener (if applicable)
                    if (response.isSuccessful() || response.isRedirect()) {
                        Response bmsResponse = new ResponseImpl(response);
                        if (progressListener != null) {
                            updateProgressListener(progressListener, bmsResponse);
                        }
                        responseListener.onSuccess(bmsResponse);

                        // If auto-retries are enabled, and the request hasn't run out of retry attempts,
                        // then try to send the same request again. Otherwise, delegate to the user's ResponseListener.
                    } else if (numberOfRetries > 0 && response.code() == 504) {
                        numberOfRetries--;
                        logger.debug("Resending " + request.getMethod() +  " request to " + request.getUrl());
                        sendOKHttpRequest(response.request(), getCallback(progressListener, responseListener));
                    } else {
                        responseListener.onFailure(new ResponseImpl(response), null, null);
                    }
                }
                response.body().close();
            }
        };
    }

    protected int getNumberOfRetries() {
        return numberOfRetries;
    }
} public interface Response {

    /**
     * Returns the URL that the request was made to.
     *
     * @return The URL of the request.
     */
    String getRequestURL();

    /**
     * This method gets the HTTP status of the response.
     *
     * @return The HTTP status of the response. Will be 0 when there was no response.
     */
    int getStatus();

    /**
     * This method parses the response body as a String.
     * If this method is called, then subsequent calls to {@link #getResponseByteStream()} or {@link #getResponseBytes()}
     * will return null unless the {@link Request} was made using a <code>download()</code> method.
     *
     * @return The body of the response as a String. Empty string if there is no body.
     */
    String getResponseText();

    /**
     * This method parses the response body as a JSONObject.
     * If this method is called, then subsequent calls to {@link #getResponseByteStream()} or {@link #getResponseBytes()}
     * will return null unless the {@link Request} was made using a <code>download()</code> method.
     *
     * @return The body of the response as a JSONObject.
     */
    JSONObject getResponseJSON();

    /**
     * This method gets the bytes of the response body.
     * If this method is called, then subsequent calls to {@link #getResponseByteStream()} or {@link #getResponseBytes()}
     * will return null unless the {@link Request} was made using a <code>download()</code> method.
     *
     * @return the bytes of the response body. Will be null if there is no body.
     */
    byte[] getResponseBytes();

    /**
     * This method gets the response body as an input stream.
     *
     * <p>
     * <b>Important: </b>This method may not be used for requests made with any of the {@link Request} download() methods,
     * since the stream will already be closed. Use {@link Response#getResponseBytes()} instead.
     * </p>
     *
     * @return The input stream representing the response body. Will be null if there is no body.
     */
    InputStream getResponseByteStream();

    /**
     * This method gets the Content-Length of the response body.
     *
     * @return The content length of the response.
     */
    long getContentLength();

    /**
     * Get the HTTP headers from the response.
     *
     * @return A map with all the headers, and the corresponding values for each one.
     */
    Map<String, List<String>> getHeaders();
} public interface ResponseListener {

	/**
	 * This method will be called only when a response from the server has been received with a status
	 * in the 200 range.
	 * @param response the server response
	 */
	void onSuccess(Response response);

	/**
	 * This method will be called in the following cases:
	 * <ul>
	 * <li>There is no response from the server.</li>
	 * <li>The status from the server response is in the 400 or 500 ranges.</li>
	 * <li>There is an operational failure such as: authentication failure, data validation failure, or custom failure.</li>
	 * </ul>
	 * @param response Contains detail regarding why the Http request failed. May be null if the request did not reach the server
	 * @param t Exception that could have caused the request to fail. null if no Exception thrown.
	 * @param extendedInfo Contains details regarding operational failure. null if no operational failure occurred.
	 */
	void onFailure(Response response, Throwable t, JSONObject extendedInfo);

} public class ResponseImpl implements Response {

    private okhttp3.Response okHttpResponse;
    private String requestURL;
    private Headers headers;
    private MediaType contentType;
    private InputStream responseByteStream;
    private byte[] bodyBytes;

    private static Logger logger = Logger.getLogger(Logger.INTERNAL_PREFIX + ResponseImpl.class.getSimpleName());

    // Convert OkHttp response into a BMSCore Response
    public ResponseImpl(okhttp3.Response response) {
        okHttpResponse = response;

        if (okHttpResponse != null) {
            requestURL = okHttpResponse.request().toString();
            headers = okHttpResponse.headers();
            try
            {
                contentType = okHttpResponse.body().contentType();
                responseByteStream = okHttpResponse.body().byteStream();
                this.bodyBytes = this.okHttpResponse.body().bytes();
            }
            catch (NullPointerException | IOException e)
            {
                logger.error("Response body bytes can't be read: " + e.getLocalizedMessage());
                this.bodyBytes = null;
            }
        }
    }

    /**
     * Returns the URL that the request was made to.
     *
     * @return The URL of the request.
     */
    public String getRequestURL() {
        return okHttpResponse.request().toString();
    }

    /**
     * This method gets the HTTP status of the response.
     *
     * @return The HTTP status of the response. Will be 0 when there was no response.
     */
    public int getStatus() {
        if (okHttpResponse == null) {
            return 0;
        }

        return okHttpResponse.code();
    }

    /**
     * This method gets the Content-Length of the response body.
     *
     * @return The content length of the response.
     */
    public long getContentLength() {
        try {
            return getInternalResponse().body().contentLength();
        } catch (NullPointerException e){
            logger.error("Failed to get the response content length from " + getRequestURL() + ". Error: " + e.getMessage());
            return 0;
        }
    }

    /**
     * This method parses the response body as a String.
     * If this method is called, then subsequent calls to {@link #getResponseByteStream()} or {@link #getResponseBytes()}
     * will return null unless the {@link Request} was made using a <code>download()</code> method.
     *
     * @return The body of the response as a String. Empty string if there is no body.
     */
    public String getResponseText() {
        if (this.bodyBytes == null) {
            return "";
        }

        Charset charset = contentType != null ? contentType.charset(Charset.forName("UTF-8"))
            : Charset.forName("UTF-8");
        try {
            return new String(this.bodyBytes, charset.name());
        } catch (UnsupportedEncodingException e) {
            logger.warn("Failed to extract text from response body. Error: " + e.getMessage());
            return null;
        }
    }

    /**
     * This method parses the response body as a JSONObject.
     * If this method is called, then subsequent calls to {@link #getResponseByteStream()} or {@link #getResponseBytes()}
     * will return null unless the {@link Request} was made using a <code>download()</code> method.
     *
     * @return The body of the response as a JSONObject.
     */
    public JSONObject getResponseJSON() {
        String responseText = getResponseText();

        if(responseText == null || responseText.length() == 0){
            return null;
        }

        try {
            return new JSONObject(responseText);
        } catch (JSONException e) {
            logger.warn("Failed to extract JSON from response body. Error: " + e.getMessage());
            return null;
        }
    }

    /**
     * This method gets the bytes of the response body.
     * If this method is called, then subsequent calls to {@link #getResponseByteStream()} or {@link #getResponseBytes()}
     * will return null unless the {@link Request} was made using a <code>download()</code> method.
     *
     * @return the bytes of the response body. Will be null if there is no body.
     */
    public byte[] getResponseBytes() {
        if (responseByteStream != null) {
            try {
                return IOUtils.toByteArray(responseByteStream);
            }
            catch (IOException e) {
                logger.warn("Failed to extract byte array from response body. Error: " + e.getMessage());
                return null;
            }
        }
        return this.bodyBytes;
    }

    protected void setResponseBytes(byte[] responseBytes) {
        this.bodyBytes = responseBytes;
    }

    /**
     * This method gets the response body as an input stream.
     *
     * <p>
     * <b>Important: </b>This method may not be used for requests made with any of the {@link Request} download() methods,
     * since the stream will already be closed. Use {@link Response#getResponseBytes()} instead.
     * </p>
     *
     * @return The input stream representing the response body. Will be null if there is no body.
     */
    public InputStream getResponseByteStream() {
        return this.responseByteStream;
    }

    /**
     * Returns true if this response redirects to another resource.
     */
    public boolean isRedirect() {
        if (okHttpResponse == null) {
            return false;
        }
        return okHttpResponse.isRedirect();
    }

    /**
     * Returns true if the code is in [200..300), which means the request was
     * successfully received, understood, and accepted.
     */
    public boolean isSuccessful() {
        if (okHttpResponse == null) {
            return false;
        }
        return okHttpResponse.isSuccessful();
    }

    /**
     * Get the HTTP headers from the response.
     *
     * @return A map with all the headers, and the corresponding values for each one.
     */
    public Map<String, List<String>> getHeaders() {
        if (headers == null) {
            return null;
        }
        return headers.toMultimap();
    }

    /**
     * Get the header values for the given header name, if it exists. There can be more than one value
     * for a given header name.
     *
     * @param name the name of the header to get
     * @return the values of the given header name
     */
    public List<String> getHeader(String name) {
        if (headers == null) {
            return null;
        }
        return headers.values(name);
    }

    /**
     * Get the first header value for the given header name, if it exists.
     *
     * @param name the name of the header to get
     * @return the first value of the given header name
     */
    public String getFirstHeader(String name) {
        List<String> headerValues = getHeader(name);

        if (headerValues == null || headerValues.size() == 0) {
            return null;
        }

        return headerValues.get(0);
    }

    /**
     * Get the names of all the HTTP headers in the response.
     *
     * @return The names of all the headers in the response
     */
    public Set<String> getResponseHeadersNames() {
        if (headers == null) {
            return null;
        }
        return headers.names();
    }

    @Override
    public String toString() {
        try {
            return "Response: Status=" + getStatus() + ", Response Text: " + getResponseText();
        } catch (RuntimeException e) {
            return "Response: Status=" + getStatus() + ", Exception occurred when constructing response text string: " + e.getLocalizedMessage();
        }
    }

    protected okhttp3.Response getInternalResponse(){
        return okHttpResponse;
    }
} public interface AppIdentity {
	String ID = "id";
	String VERSION = "version";

	/**
	 * @return application Id (bundleId)
	 */
	String getId();


	/**
	 * @return application version string
	 */
	String getVersion();

} public interface DeviceIdentity {
	String ID = "id";
	String OS = "platform";
	String OS_VERSION = "osVersion";
	String MODEL = "model";
	String BRAND = "brand";

	/**
	 * @return deviceId
	 */
	String getId();

	/**
	 * @return device OS
	 */
	String getOS();

	/**
	 * @return OS version
	 */
	String getOSVersion();

	/**
	 * @return device model
	 */
	String getModel();

	/**
	 * @return device brand
	 */
	String getBrand();
} public class BaseAppIdentity extends JSONObject implements AppIdentity {

    /**
     * Init the data using map
     * @param asMap hold the device data
     */
    public BaseAppIdentity (Map asMap) {
        super(asMap);
    }

    /**
     * Init the data using context
     * @param context android application context
     */
    public BaseAppIdentity (Context context) {
        try {
            String packageName = context.getPackageName();
            put(ID, packageName);
            put(VERSION, context.getPackageManager().getPackageInfo(packageName, 0).versionName);
        } catch (JSONException e) {
            throw new RuntimeException(e);
        } catch (PackageManager.NameNotFoundException e){
            throw new RuntimeException(e);
        }
    }

    /**
     * @return get application id (package name)
     */
    public String getId() {
        return optString(ID);
    }

    /**
     * @return get application version
     */
    public String getVersion() {
        return optString(VERSION);
    }
} public class BaseDeviceIdentity extends JSONObject implements DeviceIdentity {

    /**
     * Init the data using map
     * @param asMap hold the device data
     */
    public BaseDeviceIdentity (Map asMap) {
        super(asMap);
    }

    /**
     * Init the data using context
     * @param context android application context
     */
    public BaseDeviceIdentity (Context context) {
        try {
            put(ID, getDeviceUUID(context));
            put(OS, "android");
            put(OS_VERSION, Build.VERSION.RELEASE);
			put(BRAND, Build.BRAND);
            put(MODEL, Build.MODEL);
        } catch (JSONException e) {
            e.printStackTrace();
        }
    }

    /**
     * @return device unique id
     */
    public String getId() {
        return optString(ID);
    }

    /**
     * @return device OS
     */
    public String getOS() {
        return optString(OS);
    }

    /**
     * @return device model
     */
    public String getModel() {
        return optString(MODEL);
    }

	/**
	 * @return OS version
	 */
	public String getOSVersion(){
		return optString(OS_VERSION);
	}

	/**
	 * @return device brand
	 */
	public String getBrand(){
		return optString(BRAND);
	}

	/**
     * @param context android application context
     * @return device unique id
     */
    private String getDeviceUUID(Context context) {
        String uuid = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
        return UUID.nameUUIDFromBytes(uuid.getBytes()).toString();
    }
} public class CertificateStore {

    private static final String alias = "registration";
    File keyStoreFile;
    private char[] password;
    private KeyStore keyStore;

    public CertificateStore(File keyStoreFile, String password) {
        this.keyStoreFile = keyStoreFile;
        this.password = password.toCharArray();
    }

    public void saveCertificate(KeyPair keyPair, X509Certificate certificate) throws IOException, CertificateException, KeyStoreException, NoSuchAlgorithmException {

        loadKeyStore();
        keyStore.setKeyEntry(alias, keyPair.getPrivate(), password, new X509Certificate[]{certificate});

        //save the keystore
        FileOutputStream fos = new FileOutputStream(keyStoreFile);
        keyStore.store(fos,password);
        fos.close();
    }

    private void loadKeyStore() throws IOException, KeyStoreException, CertificateException {

        try {
            if (keyStore == null){
                keyStore = KeyStore.getInstance(KeyStore.getDefaultType());

                //load existing keystore
                if (keyStoreFile.exists() && keyStoreFile.length() > 0){
                    FileInputStream fileInputStream = new FileInputStream(keyStoreFile);
                    keyStore.load(fileInputStream,password);
                    fileInputStream.close();
                }

                //load empty keystore
                else{
                    keyStore.load(null, password);
                }
            }

        }  catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
    }

    public KeyPair getStoredKeyPair() throws CertificateException, KeyStoreException, IOException, UnrecoverableEntryException, NoSuchAlgorithmException {

        loadKeyStore();
        KeyPair keyPair = null;

        if (keyStore.containsAlias(alias)){
            KeyStore.PrivateKeyEntry pke = (KeyStore.PrivateKeyEntry) keyStore.getEntry(alias, new KeyStore.PasswordProtection(password));
            Certificate cert = keyStore.getCertificate(alias);
            keyPair = new KeyPair(cert.getPublicKey(), pke.getPrivateKey());
        }

        return keyPair;
    }

    public X509Certificate getCertificate() throws CertificateException, KeyStoreException, IOException {
        loadKeyStore();
        X509Certificate certificate = null;

        if (keyStore.containsAlias(alias)){
            Certificate[] chain = keyStore.getCertificateChain(alias);
            if (chain==null || chain.length==0){
                throw new IOException("No certificate found");
            }

            certificate = (X509Certificate) chain[0];
        }

        return certificate;
    }


    public boolean isCertificateStored(){
        try {
            loadKeyStore();

            Certificate[] chain = keyStore.getCertificateChain(alias);
            if (chain!=null && chain.length > 0){
                return true;
            }

        } catch (Exception e) {
            return false;
        }

        return false;
    }



} public class CertificatesUtility {

    public static X509Certificate base64StringToCertificate(String certificateString) throws CertificateException, IOException {

        if (certificateString == null){
            throw new IllegalArgumentException("certificateString cannot be null");
        }

        byte[] encodedCert = Base64.decode(certificateString,Base64.DEFAULT);
        InputStream inStream = new ByteArrayInputStream(encodedCert);
        CertificateFactory cf = CertificateFactory.getInstance("X.509");
        X509Certificate cert = (X509Certificate) cf.generateCertificate(inStream);
        inStream.close();

        return cert;
    }

    public static void checkValidityWithPublicKey(X509Certificate certificate, PublicKey publicKey) throws CertificateNotYetValidException, CertificateExpiredException {

        Date now = new Date();
        long nowTime = now.getTime();
        final int oneMinute = 60000;
        Date afterAddingOneMinute = new Date(nowTime + (5 * oneMinute));

        //we are checking the certificate against current time plus five minutes to prevent false failure because of sync problems
        certificate.checkValidity(afterAddingOneMinute);
        if (!certificate.getPublicKey().equals(publicKey)) {
            throw new RuntimeException("Failed to validate public key");
        }
    }

    public static String getClientIdFromCertificate(X509Certificate certificate){

        if (certificate == null){
            throw new IllegalArgumentException("Certificate cannot be null");
        }

        //subjectDN is of the form: "UID=<clientId>, DC=<some other value>" or "DC=<some other value>, UID=<clientId>"
        String clientId = null;

        String subjectDN = certificate.getSubjectDN().getName();
        String[] parts = subjectDN.split(Pattern.quote(","));
        for (String part: parts){
            if (part.contains("UID=")){
                String uid=part.substring(part.indexOf("UID="));
                clientId = uid.split(Pattern.quote("="))[1];
            }
        }

        return clientId;
    }
} public class DefaultJSONSigner implements JSONSigner {

    private static final String ALG = "alg";

    @Override
    public String sign(KeyPair keyPair, JSONObject json) throws Exception {

        if (keyPair == null || json == null){
            throw new IllegalArgumentException("parameter cannot be null");
        }

        RSAPublicKey publicKey = ((RSAPublicKey)keyPair.getPublic());
        PrivateKey privateKey = keyPair.getPrivate();

        // create CSR Header (based on public key)
        JSONObject jwsHeaderJson = new JSONObject();
        jwsHeaderJson.put(ALG, "RS256");

        JSONObject publicKeyDataJson = new JSONObject();
        publicKeyDataJson.put(ALG, "RSA");

        String mod = encodeUrlSafe(publicKey.getModulus().toByteArray());
        publicKeyDataJson.put("mod", mod);

        String exp = encodeUrlSafe(publicKey.getPublicExponent().toByteArray());
        publicKeyDataJson.put("exp", exp);

        jwsHeaderJson.put("jpk", publicKeyDataJson);

        String jwsHeader = jwsHeaderJson.toString();
        String payload = json.toString();

        // concatenate JWS Header and payload.
        String csrHeaderAndPayload = encodeUrlSafe(jwsHeader.getBytes()) + "." + encodeUrlSafe(payload.getBytes());

        // create CSR Signature
        String jwsSignature = encodeUrlSafe(signCsrData(csrHeaderAndPayload,  privateKey));

        // Concatenate them all, and return the result.
        return csrHeaderAndPayload + "." + jwsSignature;
    }

    private byte[] signCsrData(String csrJSONData, PrivateKey privateKey) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
        Signature signature = Signature.getInstance("SHA256withRSA");
        signature.initSign(privateKey);
        signature.update(csrJSONData.getBytes());
        return signature.sign();
    }

    private String encodeUrlSafe(byte[] data) throws UnsupportedEncodingException {
        return new String(Base64.encode(data, Base64.URL_SAFE | Base64.NO_WRAP),"UTF-8");
    }
} public class KeyPairUtility {

    private static final int RSA_KEY_SIZE = 512;

    public static KeyPair generateRandomKeyPair() {
        KeyPair keyPair = null;

        try {
            KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
            kpg.initialize(RSA_KEY_SIZE);
            keyPair = kpg.genKeyPair();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }

        return keyPair;
    }
} public class AuthorizationManagerPreferences extends SharedPreferencesManager {

    public PolicyPreference persistencePolicy = new PolicyPreference("persistencePolicy", MCAAuthorizationManager.PersistencePolicy.ALWAYS);
    public StringPreference clientId = new StringPreference("clientId");
    public TokenPreference accessToken = new TokenPreference("accessToken");
    public TokenPreference idToken = new TokenPreference("idToken");

    public JSONPreference userIdentity = new JSONPreference("userIdentity");
    public JSONPreference deviceIdentity = new JSONPreference("deviceIdentity");
    public JSONPreference appIdentity = new JSONPreference("appIdentity");

    public AuthorizationManagerPreferences(Context context) {
        super(context, "AuthorizationManagerPreferences", Context.MODE_PRIVATE);

        String uuid = Settings.Secure.getString(context.getContentResolver(), android.provider.Settings.Secure.ANDROID_ID);
        setStringEncryption(new AESStringEncryption(uuid));
    }

    /**
     * Holds authorization manager Policy preference
     */
    public class PolicyPreference {

        private MCAAuthorizationManager.PersistencePolicy value;
        private String prefName;

        public PolicyPreference(String prefName, MCAAuthorizationManager.PersistencePolicy defaultValue) {
            this.prefName = prefName;
            value = MCAAuthorizationManager.PersistencePolicy.valueOf(sharedPreferences.getString(prefName, defaultValue.toString()));
        }

        public MCAAuthorizationManager.PersistencePolicy get() {
            return value;
        }

        public void set(MCAAuthorizationManager.PersistencePolicy value) {
            this.value = value;
            editor.putString(prefName, value.toString());
            editor.commit();
        }
    }

    /**
     * Holds authorization manager Token preference
     */
    public class TokenPreference {

        String runtimeValue;
        StringPreference savedValue;

        public TokenPreference(String prefName) {
            savedValue = new StringPreference(prefName);
        }

        public void set(String value) {
            runtimeValue = value;
            if (persistencePolicy.get() == MCAAuthorizationManager.PersistencePolicy.ALWAYS) {
                savedValue.set(value);
            } else {
                savedValue.clear();
            }
        }

        public String get() {
            if (runtimeValue == null && persistencePolicy.get() == MCAAuthorizationManager.PersistencePolicy.ALWAYS) {
                return savedValue.get();
            }
            return runtimeValue;
        }

        public void updateStateByPolicy() {
            if (persistencePolicy.get() == MCAAuthorizationManager.PersistencePolicy.ALWAYS) {
                savedValue.set(runtimeValue);
            } else {
                savedValue.clear();
            }
        }

        public void clear() {
            savedValue.clear();
            runtimeValue = null;
        }
    }
}
import android.content.Context;
import android.provider.Settings;
import android.util.Base64;
import com.ibm.mobilefirstplatform.clientsdk.android.core.api.Request;
import com.ibm.mobilefirstplatform.clientsdk.android.core.api.Response;
import com.ibm.mobilefirstplatform.clientsdk.android.core.api.ResponseListener;
import com.ibm.mobilefirstplatform.clientsdk.android.core.internal.ResponseImpl;
import com.ibm.mobilefirstplatform.clientsdk.android.logger.api.Logger;
import com.ibm.mobilefirstplatform.clientsdk.android.security.api.AppIdentity;
import com.ibm.mobilefirstplatform.clientsdk.android.security.api.DeviceIdentity;
import com.ibm.mobilefirstplatform.clientsdk.android.security.identity.BaseAppIdentity;
import com.ibm.mobilefirstplatform.clientsdk.android.security.identity.BaseDeviceIdentity;
import com.ibm.mobilefirstplatform.clientsdk.android.security.mca.internal.certificate.CertificateStore;
import com.ibm.mobilefirstplatform.clientsdk.android.security.mca.internal.certificate.CertificatesUtility;
import com.ibm.mobilefirstplatform.clientsdk.android.security.mca.internal.certificate.DefaultJSONSigner;
import com.ibm.mobilefirstplatform.clientsdk.android.security.mca.internal.certificate.KeyPairUtility;
import com.ibm.mobilefirstplatform.clientsdk.android.security.mca.internal.preferences.AuthorizationManagerPreferences;
import org.json.JSONObject;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.KeyPair;
import java.security.cert.X509Certificate;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.ConcurrentLinkedQueue;
/*
 *     Copyright 2015 IBM Corp.
 *     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.ibm.mobilefirstplatform.clientsdk.android.security.mca.internal;





/**
 * Handles the complete authorization process cycle
 * Created by cirilla on 8/3/15.
 */
public class AuthorizationProcessManager {

    private static final String HTTP_LOCALHOST = "http://localhost";
    private AuthorizationManagerPreferences preferences;
    private ConcurrentLinkedQueue<ResponseListener> authorizationQueue;
    private KeyPair registrationKeyPair;
    private DefaultJSONSigner jsonSigner;

    private CertificateStore certificateStore;
    private Logger logger;
    private String sessionId;

    public AuthorizationProcessManager(Context context, AuthorizationManagerPreferences preferences) {
        this.logger = Logger.getLogger(Logger.INTERNAL_PREFIX + AuthorizationProcessManager.class.getSimpleName());

        this.preferences = preferences;
        this.authorizationQueue = new ConcurrentLinkedQueue<>();
        this.jsonSigner = new DefaultJSONSigner();

        File keyStoreFile = new File(context.getFilesDir().getAbsolutePath(), "mfp.keystore");
        String uuid = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
        certificateStore = new CertificateStore(keyStoreFile, uuid);

        //case where the shared preferences were deleted but the certificate is saved in the keystore
        if (preferences.clientId.get() == null && certificateStore.isCertificateStored()) {
            try {
                X509Certificate certificate = certificateStore.getCertificate();
                preferences.clientId.set(CertificatesUtility.getClientIdFromCertificate(certificate));
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        //generate new random session id
        sessionId = UUID.randomUUID().toString();
    }

    /**
     * Main method to start authorization process
     * @param context android context
     * @param listener response listener that will get the result of the process
     */
    public void startAuthorizationProcess(final Context context, ResponseListener listener) {
        authorizationQueue.add(listener);

        //start the authorization process only if this is the first time we ask for authorization
        if (authorizationQueue.size() == 1) {
            try {
                if (preferences.clientId.get() == null) {
                    logger.info("starting registration process");
                    invokeInstanceRegistrationRequest(context);
                } else {
                    logger.info("starting authorization process");
                    invokeAuthorizationRequest(context);
                }
            } catch (Throwable t) {
                handleAuthorizationFailure(t);
            }
        }

        else{
            logger.info("authorization process already running, adding response listener to the queue");
            logger.debug(String.format("authorization process currently handling %d requests", authorizationQueue.size()));
        }
    }

    /**
     * logs out user
     * @param context Android Activity that will handle the authorization (like facebook or google)
     * @param listener Response listener
     */
    public void logout(Context context, ResponseListener listener) {
        AuthorizationRequestManager.RequestOptions options = new AuthorizationRequestManager.RequestOptions();
        options.parameters = new HashMap<String,String>(1);
        options.parameters.put("client_id", preferences.clientId.get());
        options.headers = new HashMap<>(1);
        addSessionIdHeader(options.headers);
        options.requestMethod = Request.GET;
        try {
            authorizationRequestSend(context,"logout", options, listener);
        } catch (Exception e) {
            logger.debug("Could not log out");
        }
    }
    /**
     * Invoke request for registration, the result of the request should contain ClientId.
     * @param context android context
     */
    private void invokeInstanceRegistrationRequest(final Context context) {

        AuthorizationRequestManager.RequestOptions options = new AuthorizationRequestManager.RequestOptions();

        options.parameters = createRegistrationParams();
        options.headers = createRegistrationHeaders();
        options.requestMethod = Request.POST;

        InnerAuthorizationResponseListener listener = new InnerAuthorizationResponseListener() {
            @Override
            public void handleAuthorizationSuccessResponse(Response response) throws Exception {