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

com.adobe.pdfservices.operation.internal.http.HttpClientWrapper Maven / Gradle / Ivy

Go to download

Adobe PDF Services SDK allows you to access RESTful APIs to create, convert, and manipulate PDFs within your applications.

There is a newer version: 4.1.0
Show newest version
/*
 * Copyright 2019 Adobe
 * All Rights Reserved.
 *
 * NOTICE: Adobe permits you to use, modify, and distribute this file in
 * accordance with the terms of the Adobe license agreement accompanying
 * it. If you have received this file from a source other than Adobe,
 * then your use, modification, or distribution of it requires the prior
 * written permission of Adobe.
 */

package com.adobe.pdfservices.operation.internal.http;

import java.io.IOException;
import java.io.InputStream;
import java.util.Objects;
import java.util.Set;
import javax.mail.MessagingException;

import com.adobe.pdfservices.operation.internal.cpf.constants.CustomErrorMessages;
import com.adobe.pdfservices.operation.internal.cpf.constants.RequestKey;
import com.adobe.pdfservices.operation.internal.cpf.dto.response.CPFErrorResponse;
import com.adobe.pdfservices.operation.internal.cpf.dto.response.platform.CPFContentAnalyzerResponse;
import com.adobe.pdfservices.operation.internal.cpf.dto.response.platform.CPFStatus;
import com.adobe.pdfservices.operation.internal.dto.response.ErrorResponse;
import com.adobe.pdfservices.operation.internal.dto.response.ImsErrorResponse;
import com.adobe.pdfservices.operation.internal.exception.OperationException;
import com.adobe.pdfservices.operation.internal.exception.UnauthorizedClientException;
import com.adobe.pdfservices.operation.internal.http.config.HttpClientConfig;
import com.adobe.pdfservices.operation.internal.util.JsonUtil;
import com.adobe.pdfservices.operation.internal.util.StringUtil;
import com.adobe.pdfservices.operation.internal.GlobalConfig;
import com.adobe.pdfservices.operation.internal.auth.Authenticator;
import net.jodah.failsafe.Failsafe;
import net.jodah.failsafe.RetryPolicy;
import org.apache.http.HttpHeaders;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.adobe.pdfservices.operation.exception.SdkException;
import com.adobe.pdfservices.operation.exception.ServiceUsageException;

/**
 * A wrapper class for the underlying http client.
 */
public class HttpClientWrapper implements HttpClient {

    private static final Logger LOGGER = LoggerFactory.getLogger(HttpClient.class);
    private static final int ACCEPTED_SUCCESS_STATUS_CODE = 202;
    private static final int SERVICE_USAGE_EXCEPTION_STATUS_CODE = 429;
    private static final String MULTIPART_IDENTIFIER_STRING = "multipart";
    private static final String UNAUTHORIZED_ERROR_CODE = "401013";

    // IMS error handling specific constants
    private static final String IMS_INVALID_TOKEN_ERROR_STRING = "invalid_token";
    private static final String IMS_CERTIFICATE_EXPIRY_ERROR_DESCRIPTION_STRING = "Could not match JWT signature to any of the bindings";

    //Service usage and quota exhaustion specific error code constants
    private static final String SERVICE_USAGE_EXCEPTION_STATUS_CODE_429001_STRING = "429001";
    private static final String SERVICE_USAGE_EXCEPTION_STATUS_CODE_429002_STRING = "429002";

    private int connectTimeout;
    private int socketTimeout;
    private org.apache.http.client.HttpClient httpClient;
    private int maxRetries;
//  private double retryDelayFactor;
//  private int maxRetryInterval;
//  private int retryBackoffInterval;

    private Set retriableErrorCodes;
    private Set successResponseCodes;


    public HttpClientWrapper(HttpClientConfig httpClientConfig) {
        this.connectTimeout = httpClientConfig.getConnectTimeout();
        this.socketTimeout = httpClientConfig.getSocketTimeout();
        this.maxRetries = httpClientConfig.getMaxRetries();
//    Properties for configuring retries. Not needed currently since we only retry once
//    this.retryBackoffInterval = httpClientConfig.getRetryBackoffInterval();
//    this.retryDelayFactor = httpClientConfig.getRetryDelayFactor();
//    this.maxRetryInterval = httpClientConfig.getMaxRetryInterval();


        PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
        cm.setMaxTotal(GlobalConfig.getMaxApacheConnections());
        cm.setDefaultMaxPerRoute(GlobalConfig.getMaxApacheConnectionsPerRoute());
        RequestConfig requestConfig = RequestConfig.custom()
                .setConnectTimeout(connectTimeout)
                .setSocketTimeout(socketTimeout)
                .build();
        this.httpClient = HttpClients.custom()
                .setConnectionManager(cm)
                .setDefaultRequestConfig(requestConfig)
                .disableAutomaticRetries()
                .build();
        retriableErrorCodes = GlobalConfig.getRetriableResponseCodes();
        successResponseCodes = GlobalConfig.getSuccessResponseCodes();

    }


    @Override
    public  HttpResponse send(HttpRequest httpRequest, Class responseType) {

        RetryPolicy> retryPolicy = getAuthenticationRetryPolicy(httpRequest);

        return Failsafe.with(retryPolicy).get(() -> executeRequest(httpRequest, responseType));

    }

    /**
     * This method can be moved to the appropriate authenticator to define custom policies
     *
     * @param 
     * @param httpRequest
     * @return
     */
    private  RetryPolicy> getAuthenticationRetryPolicy(HttpRequest httpRequest) {

        //Max retries 'x' means total attempts will be 'x+1', 1 for the original execution and x in case of failures
        //We don't need to refresh session token once we've reached the retry limit is reached, hence the if condition
        //Retry parameters can be added to the http request config, needn't be binded to the http client
        return new RetryPolicy>()
                .withMaxRetries(this.maxRetries)
                .handle(UnauthorizedClientException.class)
                .onFailedAttempt(listener -> {
                    LOGGER.debug(" Retry attempt count {} ", listener.getAttemptCount());
                    if (listener.getAttemptCount() <= this.maxRetries) {
                        httpRequest.forceAuthenticate();
                    }
                });
    }

    private  HttpResponse executeRequest(HttpRequest httpRequest,
                                               Class responseType) throws IOException, MessagingException {
        httpRequest.authenticate();
        HttpUriRequest apacheHttpRequest = ApacheHttpUtil.getApacheRequest(httpRequest);
        org.apache.http.HttpResponse httpResponse = executeApacheHttpRequest(httpRequest, apacheHttpRequest);
        return handleResponse(httpResponse, httpRequest, responseType);

    }

    private org.apache.http.HttpResponse executeApacheHttpRequest(HttpRequest httpRequest,
                                                                  HttpUriRequest apacheHttpRequest) {
        org.apache.http.HttpResponse httpResponse;
        try {
            httpResponse = this.httpClient.execute(apacheHttpRequest);
        } catch (ClientProtocolException e) {
            LOGGER.error("Error in http protocol, request could not be completed ", e);
            throw new SdkException("Http protocol exception encountered while executing request ", e);

        } catch (IOException e) {
            LOGGER.warn("Connection exception encountered while executing request {} ", httpRequest.getRequestKey().getValue());
            throw new SdkException("Request could not be completed. Possible cause attached!", e);

        }
        return httpResponse;
    }

    /**
     * Throws ServiceApiException if non success status code is received in cpf:status
     * Required here because some error conditions returned in CPFContentAnalyzerResponse comes back with a 200
     * Treating this condition as ServiceApiException instead of OperationException because of the above mentioned reason
     */
    private void checkPlatformContentResponseStatusCode(CPFContentAnalyzerResponse CPFContentAnalyzerResponse,
                                                        String requestId) throws OperationException {

        // For empty 202 responses
        if(CPFContentAnalyzerResponse == null)
            return;

        // For non empty responses
        if (Objects.nonNull(CPFContentAnalyzerResponse.getCPFStatus())) {
            CPFStatus CPFStatus = CPFContentAnalyzerResponse.getCPFStatus();
            String reportErrorCode = null;
            if (Objects.nonNull(CPFStatus.getReport())) {
                reportErrorCode = CPFStatus.getReport().getErrorCode();
            }
            if(!successResponseCodes.contains(CPFStatus)) {
                throw new OperationException(CPFStatus.getTitle(), CPFStatus.getStatus(),
                        requestId, null, CPFStatus.getTitle(), null, reportErrorCode);
            }
        }
    }

    private  HttpResponse handleResponse(org.apache.http.HttpResponse httpResponse,
                                               HttpRequest httpRequest,
                                               Class responseType) throws IOException, MessagingException {

        if (httpResponse == null) {
            return null;
        }
        int statusCode = httpResponse.getStatusLine().getStatusCode();
        LOGGER.debug("Response received {} ", httpResponse);
        InputStream responseContent = httpResponse.getEntity().getContent();
        RequestKey requestKey = httpRequest.getRequestKey();
        if (successResponseCodes.contains(statusCode)) {
            LOGGER.debug("Success response code {} received for request {}", statusCode, requestKey.getValue());
            if(statusCode == ACCEPTED_SUCCESS_STATUS_CODE) {
                responseContent.close();
                return new BaseHttpResponse<>(statusCode, ApacheHttpUtil.getHeaders(httpResponse.getAllHeaders()));
            } else if (httpResponse.getFirstHeader(HttpHeaders.CONTENT_TYPE)
                    .getValue().contains(MULTIPART_IDENTIFIER_STRING)) {
                return new MultiPartHttpResponse<>(statusCode,
                        ApacheHttpUtil.getHeaders(httpResponse.getAllHeaders()), responseContent, responseType);
            } else {
                T dcBaseResponseDto = JsonUtil.deserializeJsonStream(responseContent, responseType);
                if (dcBaseResponseDto instanceof CPFContentAnalyzerResponse) {
                    checkPlatformContentResponseStatusCode((CPFContentAnalyzerResponse) dcBaseResponseDto,
                            ApacheHttpUtil.getHeaders(httpResponse.getAllHeaders()).get(DefaultRequestHeaders.DC_REQUEST_ID_HEADER_KEY));
                }
                return new BaseHttpResponse<>(statusCode, ApacheHttpUtil.getHeaders(httpResponse.getAllHeaders()), dcBaseResponseDto);
            }
        } else {
            String errorResponseBody = EntityUtils.toString(httpResponse.getEntity());
            LOGGER.error("Failure response code {} encountered from backend", statusCode);
            // Check if we need a custom error message for this status code
            if (GlobalConfig.isCustomErrorMessageRequired((statusCode))) {
                throw new OperationException("Error response received for request",
                        statusCode, getRequestTrackingIdFromResponse(httpResponse, requestKey.getValue()),
                        GlobalConfig.getErrorCodeForHttpStatusCode(statusCode),
                        GlobalConfig.getErrorMessageForHttpStatusCode(statusCode), errorResponseBody);
            }
            handleIMSCallFailure(httpResponse, statusCode, requestKey, errorResponseBody);
            // Special handling for service usage exception cases
            if(statusCode == SERVICE_USAGE_EXCEPTION_STATUS_CODE) {
                handleServiceUsageFailure(errorResponseBody, httpResponse, requestKey, statusCode);
            }
            ErrorResponse errorResponse = getErrorResponseFromBody(errorResponseBody, statusCode);
            if (retriableErrorCodes.contains(statusCode)) {
                // we don't want to retry when authentication calls fail with 401, that needs to be dealt with
                if (statusCode == 401 && errorResponse.getCode().equalsIgnoreCase(UNAUTHORIZED_ERROR_CODE)) {
                    LOGGER.debug("Request was not authenticated. Will retry with refreshed session token");
                    throw new UnauthorizedClientException(String.format("Error response received for request %s", requestKey.getValue()),
                            statusCode, getRequestTrackingIdFromResponse(httpResponse, requestKey.getValue()),
                            errorResponse.getCode(), errorResponse.getMessage(), errorResponseBody);
                }
            }
            throw new OperationException("Error response received for request",
                    statusCode, getRequestTrackingIdFromResponse(httpResponse, requestKey.getValue()),
                    errorResponse.getCode(), errorResponse.getMessage(), errorResponseBody, errorResponse.getReportErrorCode());

        }
    }

    private ErrorResponse getErrorResponseFromBody(String errorResponseBody, int statusCode) {
        CPFErrorResponse errorResponse = JsonUtil.deserializeJsonString(errorResponseBody, CPFErrorResponse.class);
        String errorCode = errorResponse.getErrorCode();
        String errorMessage = errorResponse.getMessage();
        String reportErrorCode = null;
        if (Objects.nonNull(errorResponse.getStatus())) {
            errorCode = String.valueOf(errorResponse.getStatus());
            if (StringUtil.isNotBlank(errorResponse.getTitle())) {
                errorMessage = errorResponse.getTitle();
            }
        } else if (Objects.nonNull(errorResponse.getReason())) {
            errorCode = String.valueOf(statusCode);
            errorMessage = errorResponse.getReason();
        } else if (Objects.nonNull(errorResponse.getCPFStatus())) {
            if (Objects.nonNull(errorResponse.getCPFStatus().getReport())) {
                reportErrorCode = errorResponse.getCPFStatus().getReport().getErrorCode();
            }
            errorCode = String.valueOf(errorResponse.getCPFStatus().getStatus());
            errorMessage = errorResponse.getCPFStatus().getTitle();
        } else if (Objects.nonNull(errorResponse.getCpfError())) {
            errorCode = String.valueOf(statusCode);
            errorMessage = errorResponse.getCpfError().getMessage();
        }
        return new ErrorResponse(errorCode, errorMessage, reportErrorCode);
    }

    private void handleIMSCallFailure(org.apache.http.HttpResponse httpResponse, int statusCode,
                                      RequestKey requestKey, String errorResponseBody) {
        // Any error from IMS is not to be retried currently, and should throw an exception
        if (!isNotAuthenticationRequest(requestKey)) {
            LOGGER.error("IMS call failed with status code {}", statusCode);
            ImsErrorResponse imsErrorResponse = JsonUtil.deserializeJsonString(errorResponseBody, ImsErrorResponse.class);

            // When error is returned with no error description
            if(imsErrorResponse.getErrorDescription() == null) {
                imsErrorResponse.setErrorDescription(imsErrorResponse.getError());
            }

            // Special handling for invalid token and certificate expiry cases
            if(imsErrorResponse.getError().equals(IMS_INVALID_TOKEN_ERROR_STRING)) {
                if(imsErrorResponse.getErrorDescription().equals(IMS_CERTIFICATE_EXPIRY_ERROR_DESCRIPTION_STRING)) {
                    imsErrorResponse.setErrorDescription(CustomErrorMessages.IMS_CERTIFICATE_EXPIRED_ERROR_MESSAGE);
                } else {
                    imsErrorResponse.setErrorDescription(CustomErrorMessages.IMS_INVALID_TOKEN_GENERIC_ERROR_MESSAGE);
                }
            }
            throw new OperationException(String.format("Error response received for request %s", requestKey),
                    statusCode, getRequestTrackingIdFromResponse(httpResponse, requestKey.getValue()),
                    imsErrorResponse.getError(), imsErrorResponse.getErrorDescription(), errorResponseBody);
        }
    }

    private void handleServiceUsageFailure(String errorResponseBody,
                                           org.apache.http.HttpResponse httpResponse,
                                           RequestKey requestKey,
                                           int statusCode) {
        CPFErrorResponse errorResponse = JsonUtil.deserializeJsonString(errorResponseBody, CPFErrorResponse.class);
        String errorCode = null;
        if (Objects.nonNull(errorResponse.getCPFStatus())) {
            if (Objects.nonNull(errorResponse.getCPFStatus().getReport())) {
                errorCode = errorResponse.getCPFStatus().getReport().getErrorCode();
            }
            errorResponse.setMessage(CustomErrorMessages.QUOTA_ERROR_MESSAGE);
        } else {
            if(errorResponse.getErrorCode().equals(SERVICE_USAGE_EXCEPTION_STATUS_CODE_429001_STRING)) {
                errorResponse.setMessage(CustomErrorMessages.SERVICE_USAGE_LIMIT_REACHED_ERROR_MESSAGE);
            } else if (errorResponse.getErrorCode().equals(SERVICE_USAGE_EXCEPTION_STATUS_CODE_429002_STRING)) {
                errorResponse.setMessage(CustomErrorMessages.INTEGRATION_SERVICE_USAGE_LIMIT_REACHED_ERROR_MESSAGE);
            }
        }
        throw new ServiceUsageException(errorResponse.getMessage(),
                getRequestTrackingIdFromResponse(httpResponse, requestKey.getValue()), statusCode, errorCode);
    }

    private boolean isNotAuthenticationRequest(RequestKey requestKey) {
        return !(requestKey == RequestKey.AUTHN);
    }

    private String getRequestTrackingIdFromResponse(org.apache.http.HttpResponse httpResponse, String requestKey) {

        if (Authenticator.SESSION_TOKEN_REQUEST_GROUP_KEY.equalsIgnoreCase(requestKey)) {
            return httpResponse.getFirstHeader(DefaultRequestHeaders.SESSION_TOKEN_REQUEST_ID_HEADER_KEY).getValue();
        } else {
            return httpResponse.getFirstHeader(DefaultRequestHeaders.DC_REQUEST_ID_HEADER_KEY).getValue();
        }
    }

  /*@Override
  public  CompletableFuture> sendAsync(HttpRequest dcRequest, Class responseType) {
    return CompletableFuture.supplyAsync(() -> send(auth, dcRequest, responseType), this.asyncExecutor);
  }*/


}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy