All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.arm.mbed.cloud.sdk.common.CloudCaller Maven / Gradle / Ivy

Go to download

The Pelion Cloud SDK (formerly known as Mbed Cloud SDK) provides a simplified interface to the Pelion Cloud APIs by exposing functionality using conventions and paradigms familiar to Java developers.

There is a newer version: 2.5.0
Show newest version
package com.arm.mbed.cloud.sdk.common;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.Date;

import com.arm.mbed.cloud.sdk.annotations.Internal;
import com.arm.mbed.cloud.sdk.annotations.Preamble;
import com.arm.mbed.cloud.sdk.common.GenericAdapter.Mapper;

import okhttp3.Headers;
import okhttp3.Request;
import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.Response;

@Preamble(description = "Utility in charge of calling Arm Mbed Cloud APIs")
@Internal
public class CloudCaller {
    private static final String UNCHECKED = "unchecked";
    protected static final String DATE_HEADER = "Date";
    protected static final String DATE_HEADER_LOWERCASE = "date";
    protected static final String REQUEST_ID_HEADER = "X-Request-ID";
    protected static final String REQUEST_ID_HEADER_LOWERCASE = "x-request-id";
    protected static final String ETAG_HEADER = "ETag";
    protected static final String ETAG_HEADER_LOWERCASE = "etag";
    private final CloudCall caller;
    private final Mapper mapper;
    private final SdkLogger logger;
    private final String apiName;
    private final boolean storeMetadata;
    private final boolean throwExceptionOnNotFound;
    private final AbstractApi module;

    /**
     * Defines a request to Arm Mbed Cloud.
     *
     * @param 
     *            type of response object.
     */
    public interface CloudCall {
        Call call();
    }

    private CloudCaller(String apiName, CloudCall caller, Mapper mapper, AbstractApi module,
                        boolean storeMetada, boolean throwExceptionOnNotFound) {
        super();
        this.caller = caller;
        this.mapper = mapper;
        this.logger = module.logger;
        this.apiName = apiName;
        this.module = module;
        this.storeMetadata = storeMetada;
        this.throwExceptionOnNotFound = throwExceptionOnNotFound;
    }

    /**
     * Executes a call to Arm Mbed Cloud.
     * 

* Note: call metadata are recorded * * @param module * API module * @param functionName * API function name. * @param mapper * object mapper * @param caller * request * * @param * type of HTTP response object. * @param * type of API response object. * @return request result * @throws MbedCloudException * if an error occurred during the call */ public static U call(AbstractApi module, String functionName, Mapper mapper, CloudCall caller) throws MbedCloudException { return call(module, functionName, mapper, caller, false); } /** * Executes a call to Arm Mbed Cloud. *

* Note: call metadata are recorded * * @param module * API module * @param functionName * API function name. * @param mapper * object mapper * @param caller * request * @param throwExceptionOnNotFound * states whether to throw an exception when 404 Not found is received from the server * @param * type of HTTP response object. * @param * type of API response object. * @return request result * @throws MbedCloudException * if an error occurred during the call */ public static U call(AbstractApi module, String functionName, Mapper mapper, CloudCall caller, boolean throwExceptionOnNotFound) throws MbedCloudException { return call(module, functionName, mapper, caller, true, throwExceptionOnNotFound); } /** * Executes a call to Arm Mbed Cloud. * * @param module * API module * @param functionName * API function name. * @param mapper * object mapper * @param caller * request * @param storeMetadata * states whether metadata should be recorded * @param throwExceptionOnNotFound * states whether to throw an exception when 404 Not found is received from the server * @param * type of HTTP response object. * @param * type of API response object. * @return request result * @throws MbedCloudException * if an error occurred during the call */ public static U call(AbstractApi module, String functionName, Mapper mapper, CloudCall caller, boolean storeMetadata, boolean throwExceptionOnNotFound) throws MbedCloudException { return callWithFeedback(module, functionName, mapper, caller, storeMetadata, throwExceptionOnNotFound).getResult(); } /** * Executes a call to Arm Mbed Cloud. *

* Note: call metadata are recorded * * @param module * API module * @param functionName * API function name. * @param caller * request * @param throwExceptionOnNotFound * states whether to throw an exception when 404 Not found is received from the server * @param * type of HTTP response object. * @return raw result * @throws MbedCloudException * if an error occurred during the call */ public static String callRaw(AbstractApi module, String functionName, CloudCall caller, boolean throwExceptionOnNotFound) throws MbedCloudException { return callWithRawFeedback(module, functionName, caller, true, throwExceptionOnNotFound).getResult(); } /** * Executes a call to Arm Mbed Cloud. * * @param module * API module * @param functionName * API function name. * @param mapper * object mapper * @param * type of HTTP response object. * @param * type of API response object. * @param caller * request * @param storeMetadata * states whether metadata should be recorded * @param throwExceptionOnNotFound * states whether to throw an exception when 404 Not found is received from the server * @return CallFeedback @see {@link CallFeedback} * @throws MbedCloudException * if an error occurred during the call */ public static CallFeedback callWithFeedback(AbstractApi module, String functionName, Mapper mapper, CloudCall caller, boolean storeMetadata, boolean throwExceptionOnNotFound) throws MbedCloudException { return new CloudCaller<>(functionName, caller, mapper, module, storeMetadata, throwExceptionOnNotFound).execute(); } /** * Executes a raw call to Arm Mbed Cloud. * * @param module * API module * @param functionName * API function name. * @param * type of HTTP response object. * @param caller * request * @param storeMetadata * states whether metadata should be recorded * @param throwExceptionOnNotFound * states whether to throw an exception when 404 Not found is received from the server * @return a raw call feedback @see {@link RawCallFeedback} * @throws MbedCloudException * if an error occurred during the call */ public static RawCallFeedback callWithRawFeedback(AbstractApi module, String functionName, CloudCall caller, boolean storeMetadata, boolean throwExceptionOnNotFound) throws MbedCloudException { return new CloudCaller<>(functionName, caller, null, module, storeMetadata, throwExceptionOnNotFound).executeRaw(); } /** * Stores API Metadata to the module. * * @param module * module. * @param metadata * api metadata */ public static void storeApiMetadata(AbstractApi module, ApiMetadata metadata) { new CloudCaller<>(null, null, null, module, true, false).storeApiMetadataInTheCache(metadata); } /** * Executes a call to Arm Mbed Cloud. * * @return raw result objects * @throws MbedCloudException * if an error occurred during the call */ @SuppressWarnings({ UNCHECKED }) private RawCallFeedback executeRaw() throws MbedCloudException { return execute(RawCallFeedback.class); } /** * Executes a call to Arm Mbed Cloud. * * @return result objects of type U * @throws MbedCloudException * if an error occurred during the call */ @SuppressWarnings(UNCHECKED) private CallFeedback execute() throws MbedCloudException { return execute(CallFeedback.class); } /** * Executes a call to Arm Mbed Cloud. * * @return result objects * @throws MbedCloudException * if an error occurred during the call */ private > C execute(Class callFeedback) throws MbedCloudException { Call call = null; try { final boolean isRawCall = callFeedback != null && callFeedback.isAssignableFrom(RawCallFeedback.class); logger.logInfo("Calling Arm Mbed Cloud API: " + apiName); clearPreviousApiMetadata(); call = caller.call(); final CloudResponse response = executeRequest(call, isRawCall); @SuppressWarnings(UNCHECKED) final C comms = (C) (isRawCall ? new RawCallFeedback(logger) : new CallFeedback<>(logger, mapper)); comms.setMetadataFromResponse(response.getResponse()); if (storeMetadata) { storeApiMetadataInTheCache(comms.getMetadata()); } checkResponse(response, comms); comms.setResultFromResponse(response); return comms; } catch (Exception exception) { Exception detailedException = exception; if (call != null) { if (call.isCanceled()) { detailedException = new Exception("the call to Mbed Cloud has been cancelled.", exception); } call.cancel(); } logger.throwSdkException("An error occurred when calling SDK function [" + apiName + "]", detailedException); } return null; } private CloudResponse executeRequest(Call call, final boolean isRawCall) throws IOException { return isRawCall ? rawExecute(call) : new CloudResponse<>(call.execute(), null); } @SuppressWarnings(UNCHECKED) private CloudResponse rawExecute(Call call) throws IOException { try { final Method method = call.getClass().getDeclaredMethod("createRawCall"); method.setAccessible(true); final okhttp3.Call rawCall = (okhttp3.Call) method.invoke(call); CloudResponse response = null; try (okhttp3.Response rawResponse = rawCall.execute()) { final int code = rawResponse.code(); try (ResponseBody rawBody = rawResponse.body()) { if (code < 200 || code >= 300) { response = new CloudResponse<>((Response) Response.error(rawBody, rawResponse), rawBody.string()); } else if (code == 204 || code == 205) { response = new CloudResponse<>((Response) Response.success(null, rawResponse), null); } else { response = new CloudResponse<>((Response) Response.success(null, rawResponse), rawBody.string()); } } } return response; } catch (Exception exception) { logger.logError("An error occurred when trying to fetch the raw HTTP response: " + exception.getMessage()); return new CloudResponse<>(call.execute(), null); } } /** * Stores API metadata. * * @param metadata * API metadata */ public void storeApiMetadataInTheCache(ApiMetadata metadata) { module.metadataCache.storeMetadata(metadata); } private void clearPreviousApiMetadata() { module.metadataCache.clearMetadata(); } private > void checkResponse(CloudResponse response, C comms) throws MbedCloudException { if (response == null) { logger.throwSdkException("An error occurred when calling Arm Mbed Cloud: no response was received"); } if (response != null && !response.isSuccessful()) { final String errorMessage = retrieveErrorMessageFromBody(response); final Error error = retrieveErrorDetails(errorMessage, response.getResponse()); if (comms != null) { comms.setErrorMessage(error); } // In the case of a 404 Not found error, consider that the request result is actually NULL and not // erroneous. if (response.isNotFound() && !throwExceptionOnNotFound) { return; } logger.throwSdkException("An error occurred when calling Arm Mbed Cloud: [" + response.code() + "] " + response.message(), new MbedCloudException(error.toPrettyString())); } } protected static String retrieveErrorMessageFromBody(CloudResponse response) { String errorMessage = null; try { errorMessage = response.getRawBody() == null ? response.getResponse().errorBody().string() : response.getRawBody(); } catch (Exception exception) { // Nothing to do } return errorMessage; } protected static Error retrieveErrorDetails(String errorMessageFromBody, Response response) { Error error = null; try { error = ErrorJsonConverter.INSTANCE.convert(errorMessageFromBody); } catch (Exception exception) { // Nothing to do } if (error == null) { error = generateErrorFromResponse(errorMessageFromBody, response); } return error; } private static Error generateErrorFromResponse(String errorMessageFromBody, Response response) { final StringBuilder errorMessageBuilder = new StringBuilder(); errorMessageBuilder.append(response.message()); if (errorMessageFromBody != null && !errorMessageFromBody.isEmpty()) { errorMessageBuilder.append(": ").append(errorMessageFromBody); } return new Error(response.code(), "Mbed Cloud call", errorMessageBuilder.toString(), retrieveRequestId(response)); } public static String retrieveRequestId(Response response) { final String requestId = response.headers().get(REQUEST_ID_HEADER); return requestId == null || requestId.isEmpty() ? fetchRequestUrlString(response) : requestId; } private static String fetchRequestUrlString(Response response) { final URL url = fetchRequestUrl(fetchRequest(response)); return (url == null) ? null : url.toString(); } static URL fetchRequestUrl(Request request) { if (request == null) { return null; } return request.url().url(); } static Request fetchRequest(Response response) { return response.raw().request(); } protected static class ErrorJsonConverter { private final JsonSerialiser jsonSerialiser = new JsonSerialiser(); public static final ErrorJsonConverter INSTANCE = new ErrorJsonConverter(); public Error convert(String body) { if (body == null) { return null; } return jsonSerialiser.fromJson(body, ErrorHack.class).getError(); } } /** * Workaroud to handle the fact that request id is snake case. */ @SuppressWarnings("PMD.DoNotExtendJavaLangError") private static class ErrorHack extends Error { /** * Serialisation Id. */ private static final long serialVersionUID = 4818490051889482443L; @SuppressWarnings({ "checkstyle:membername", "PMD.VariableNamingConventions" }) private String request_id; /** * Gets the underlying error. * * @return the underlying error. */ public Error getError() { final Error error = clone(); error.setRequestId(request_id); return error; } } public static class CloudResponse { private final Response response; private final String rawBody; /** * Constructor. * * @param response * response * @param rawBody * raw body */ public CloudResponse(Response response, String rawBody) { super(); this.response = response; this.rawBody = rawBody; } public String message() { return response == null ? null : response.message(); } public int code() { return response == null ? 0 : response.code(); } public boolean isNotFound() { return code() == 404; } public boolean isSuccessful() { return response == null ? false : response.isSuccessful(); } /** * Gets the response. * * @return the response */ public Response getResponse() { return response; } /** * Gets the raw body. * * @return the rawBody */ public String getRawBody() { return rawBody; } } private abstract static class AbstractCallFeedBack { protected final SdkLogger logger; ApiMetadata metadata; public AbstractCallFeedBack(SdkLogger logger) { super(); this.logger = logger; } /** * Gets call metadata. * * @see ApiMetadata * @return the metadata */ public ApiMetadata getMetadata() { return metadata; } /** * Sets call metadata. * * @see ApiMetadata * @param metadata * the metadata to set */ public void setMetadata(ApiMetadata metadata) { this.metadata = metadata; } /** * Sets metadata from an HTTP response. * * @param * type of the result * @param response * HTTP response */ public void setMetadataFromResponse(Response response) { setMetadata(retrieveMetadata(response)); } /** * Sets error message. * * @param error * error message @see Error */ public void setErrorMessage(Error error) { if (metadata != null) { metadata.setError(error); } } private ApiMetadata retrieveMetadata(Response response) { if (response == null) { return null; } final ApiMetadata callMetadata = new ApiMetadata(); final Request request = fetchRequest(response); if (request != null) { callMetadata.setMethod(request.method()); callMetadata.setUrl(fetchRequestUrl(request)); } callMetadata.setStatusCode(response.code()); final Headers headers = response.headers(); if (headers != null) { callMetadata.setHeaders(headers.toMultimap()); callMetadata.setRequestId(headers.get(REQUEST_ID_HEADER_LOWERCASE)); if (!callMetadata.hasRequestId()) { callMetadata.setRequestId(headers.get(REQUEST_ID_HEADER)); } callMetadata.setEtag(headers.get(ETAG_HEADER_LOWERCASE)); if (!callMetadata.hasEtag()) { callMetadata.setEtag(headers.get(ETAG_HEADER)); } try { String dateHeader = headers.get(DATE_HEADER); if (dateHeader == null) { dateHeader = headers.get(DATE_HEADER_LOWERCASE); } callMetadata.setDateFromString(dateHeader); } catch (Exception exception) { logger.logWarn("An error occurred when trying to fetch server date from API metadata", exception); callMetadata.setDate(new Date()); } } final T body = response.body(); if (body != null) { callMetadata.setObject(body.getClass()); final String etag = fetchEtagField(body); if (etag != null) { callMetadata.setEtag(etag); } } return callMetadata; } private String fetchEtagField(T body) { try { final Method getEtagMethod = body.getClass().getMethod("getEtag"); if (getEtagMethod != null) { final Object etag = getEtagMethod.invoke(body); return (etag == null) ? null : (etag instanceof String) ? (String) etag : etag.toString(); } } catch (SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException exception) { logger.logError("Error occurred when trying to fetch etag from API metadata", exception); } catch (NoSuchMethodException exception) { return null; } return null; } /** * Sets result from an HTTP response. * * @param response * HTTP response */ public abstract void setResultFromResponse(CloudResponse response); } /** * * Defines a call (Metadata + raw response) of a call to Arm Mbed Cloud. * * @param * type of the response object */ public static class RawCallFeedback extends AbstractCallFeedBack { String result; /** * Constructor. * * @param logger * logger */ public RawCallFeedback(SdkLogger logger) { super(logger); } /** * Gets call result. * * @return the result */ public String getResult() { return result; } /** * Sets call result. * * @param result * the result to set */ public void setResult(String result) { this.result = result; } /** * Sets result from an HTTP response. * * @param response * HTTP response */ @Override public void setResultFromResponse(CloudResponse response) { if (!response.isNotFound()) { setResult(response.getRawBody()); } } } /** * * Defines a call (Metadata + response) of a call to Arm Mbed Cloud. * * @param * type of the result object * @param * type of the response object */ public static class CallFeedback extends AbstractCallFeedBack { private U result; private final Mapper mapper; /** * Constructor. * * @param logger * logger * @param mapper * mapper */ public CallFeedback(SdkLogger logger, Mapper mapper) { super(logger); this.mapper = mapper; } /** * Gets call result. * * @return the result */ public U getResult() { return result; } /** * Sets call result. * * @param result * the result to set */ public void setResult(U result) { this.result = result; } /** * Sets result from an HTTP response. * * @param response * HTTP response */ @Override public void setResultFromResponse(CloudResponse response) { if (!response.isNotFound()) { setResult((mapper == null) ? null : mapper.map(response.getResponse().body())); } } } }