com.adobe.pdfservices.operation.internal.http.HttpClientWrapper Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of pdfservices-sdk Show documentation
Show all versions of pdfservices-sdk Show documentation
Adobe PDF Services SDK allows you to access RESTful APIs to create, convert, and manipulate PDFs within your applications.
/*
* 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);
}*/
}