/**
* Copyright French Prime minister Office/SGMAP/DINSIC/Vitam Program (2015-2019)
*
* contact.vitam@culture.gouv.fr
*
* This software is a computer program whose purpose is to implement a digital archiving back-office system managing
* high volumetry securely and efficiently.
*
* This software is governed by the CeCILL 2.1 license under French law and abiding by the rules of distribution of free
* software. You can use, modify and/ or redistribute the software under the terms of the CeCILL 2.1 license as
* circulated by CEA, CNRS and INRIA at the following URL "http://www.cecill.info".
*
* As a counterpart to the access to the source code and rights to copy, modify and redistribute granted by the license,
* users are provided only with a limited warranty and the software's author, the holder of the economic rights, and the
* successive licensors have only limited liability.
*
* In this respect, the user's attention is drawn to the risks associated with loading, using, modifying and/or
* developing or reproducing the software by the user in light of its specific status of free software, that may mean
* that it is complicated to manipulate, and that also therefore means that it is reserved for developers and
* experienced professionals having in-depth computer knowledge. Users are therefore encouraged to load and test the
* software's suitability as regards their requirements in conditions enabling the security of their systems and/or data
* to be ensured and, more generally, to use and operate it in the same conditions as regards security.
*
* The fact that you are presently reading this means that you have had knowledge of the CeCILL 2.1 license and that you
* accept its terms.
*/
package fr.gouv.vitam.common.client;
import java.io.InputStream;
import java.net.UnknownHostException;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Random;
import java.util.concurrent.Future;
import javax.ws.rs.HttpMethod;
import javax.ws.rs.ProcessingException;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.Entity;
import javax.ws.rs.client.Invocation.Builder;
import javax.ws.rs.client.InvocationCallback;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedHashMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import org.apache.http.conn.ConnectTimeoutException;
import fr.gouv.vitam.common.GlobalDataRest;
import fr.gouv.vitam.common.ParametersChecker;
import fr.gouv.vitam.common.VitamConfiguration;
import fr.gouv.vitam.common.client.configuration.ClientConfiguration;
import fr.gouv.vitam.common.exception.VitamApplicationServerException;
import fr.gouv.vitam.common.exception.VitamClientInternalException;
import fr.gouv.vitam.common.logging.SysErrLogger;
import fr.gouv.vitam.common.logging.VitamLogger;
import fr.gouv.vitam.common.logging.VitamLoggerFactory;
import fr.gouv.vitam.common.security.filter.AuthorizationFilterHelper;
import fr.gouv.vitam.common.stream.StreamUtils;
/**
* Abstract Partial client class for all vitam clients
*/
abstract class AbstractCommonClient implements BasicClient {
private static final VitamLogger LOGGER = VitamLoggerFactory.getInstance(AbstractCommonClient.class);
private static final String BODY_AND_CONTENT_TYPE_CANNOT_BE_NULL = "Body and ContentType cannot be null";
/**
* Multipart response from Server side
*/
public static final String MULTIPART_MIXED = "multipart/mixed";
public static final MediaType MULTIPART_MIXED_TYPE = new MediaType("multipart", "mixed");
private static final String ARGUMENT_CANNOT_BE_NULL_EXCEPT_HEADERS = "Argument cannot be null except headers";
protected static final String INTERNAL_SERVER_ERROR = "Internal Server Error";
/**
* Client Factory
*/
final VitamClientFactory<?> clientFactory;
private final Client client;
private final Client clientNotChunked;
private final Random random = new Random(System.currentTimeMillis());
/**
* Constructor with standard configuration
*
* @param factory The client factory
*/
protected AbstractCommonClient(VitamClientFactoryInterface<?> factory) {
clientFactory = (VitamClientFactory<?>) factory;
client = clientFactory.getHttpClient();
clientNotChunked = clientFactory.getHttpClient(false);
// External client or with no Session context are excluded
// TODO: Find a better check (a specific one, instead of inferring the context from another constraint ?);
if (clientFactory.useAuthorizationFilter()) {
client.register(RequestIdClientFilter.class);
client.register(TenantIdClientFilter.class);
clientNotChunked.register(RequestIdClientFilter.class);
clientNotChunked.register(TenantIdClientFilter.class);
}
}
@Override
public final void consumeAnyEntityAndClose(Response response) {
staticConsumeAnyEntityAndClose(response);
}
/**
* This method consume everything (in particular InpuStream) and close the response.
*
* @param response
*/
public static final void staticConsumeAnyEntityAndClose(Response response) {
try {
if (response != null && response.hasEntity()) {
final Object object = response.getEntity();
if (object instanceof InputStream) {
StreamUtils.closeSilently((InputStream) object);
}
}
} catch (final Exception e) {
SysErrLogger.FAKE_LOGGER.ignoreLog(e);
} finally {
if (response != null) {
try {
response.close();
} catch (Exception e) {
SysErrLogger.FAKE_LOGGER.ignoreLog(e);
}
}
}
}
@Override
public void checkStatus() throws VitamApplicationServerException {
Response response = null;
try {
response = performRequest(HttpMethod.GET, STATUS_URL, null, MediaType.APPLICATION_JSON_TYPE, false);
final Response.Status status = Response.Status.fromStatusCode(response.getStatus());
if (status == Status.OK || status == Status.NO_CONTENT) {
return;
}
final String messageText = INTERNAL_SERVER_ERROR + " : " + status.getReasonPhrase();
LOGGER.error(messageText);
throw new VitamApplicationServerException(messageText);
} catch (ProcessingException | VitamClientInternalException e) {
final String messageText = INTERNAL_SERVER_ERROR + " : " + e.getMessage();
LOGGER.error(messageText);
throw new VitamApplicationServerException(messageText, e);
} finally {
consumeAnyEntityAndClose(response);
}
}
/**
* Perform a HTTP request to the server for synchronous call using default chunked mode configured in this client
*
* @param httpMethod HTTP method to use for request
* @param path URL to request
* @param headers headers HTTP to add to request, may be null
* @param accept asked type of response
* @return the response from the server
* @throws VitamClientInternalException
*/
protected Response performRequest(String httpMethod, String path, MultivaluedHashMap<String, Object> headers,
MediaType accept) throws VitamClientInternalException {
final boolean chunkFinalMode =
getChunkedMode() && !HttpMethod.HEAD.equals(path) && !HttpMethod.OPTIONS.equals(path);
return performRequest(httpMethod, path, headers, accept, chunkFinalMode);
}
/**
* Helper for retry request when unreachable or Connect timeout
*
* @param retry retry count
* @param e the original ProcessingException
* @return the original exception allowing to continue and store the last one
* @throws ProcessingException
*/
private final ProcessingException checkSpecificExceptionForRetry(int retry, ProcessingException e)
throws ProcessingException {
Throwable source = e.getCause();
if (source instanceof ConnectTimeoutException || source instanceof UnknownHostException
|| source instanceof org.apache.http.conn.HttpHostConnectException
|| source.getMessage().startsWith("Unable to establish route:")) {
LOGGER.info("TimeoutOccurs or DNS probe error, retry: " + retry, source);
try {
long sleep = random.nextInt(50) + 20;
Thread.sleep(sleep);
} catch (InterruptedException e1) {
LOGGER.warn("TimeoutOccurs or DNS probe error, retry: " + retry, source);
throw new ProcessingException("Interruption received", e1);
}
return e;
} else {
LOGGER.warn("TimeoutOccurs or DNS probe error, retry: " + retry, source);
throw e;
}
}
/**
* @param httpMethod
* @param body may be null
* @param contentType may be null
* @param builder
* @return the final response
* @throws VitamClientInternalException if retry is not possible and http call is failed
*/
private final Response retryIfNecessary(String httpMethod, Object body, MediaType contentType, Builder builder)
throws VitamClientInternalException {
if (body instanceof InputStream) {
Entity<Object> entity = Entity.entity(body, contentType);
return builder.method(httpMethod, entity);
}
ProcessingException lastException = null;
for (int i = 0; i < VitamConfiguration.getRetryNumber(); i++) {
try {
if (body == null) {
return builder.method(httpMethod);
} else {
Entity<Object> entity = Entity.entity(body, contentType);
return builder.method(httpMethod, entity);
}
} catch (ProcessingException e) {
lastException = checkSpecificExceptionForRetry(i, e);
continue;
}
}
if (lastException != null) {
throw lastException;
} else {
throw new VitamClientInternalException("Unknown error in client");
}
}
/**
* Perform a HTTP request to the server for synchronous call
*
* @param httpMethod HTTP method to use for request
* @param path URL to request
* @param headers headers HTTP to add to request, may be null
* @param accept asked type of response
* @param chunkedMode True use default client, else False use non Chunked mode client
* @return the response from the server
* @throws VitamClientInternalException
*/
protected Response performRequest(String httpMethod, String path, MultivaluedHashMap<String, Object> headers,
MediaType accept, boolean chunkedMode) throws VitamClientInternalException {
try {
final Builder builder = buildRequest(httpMethod, path, headers, accept, chunkedMode);
return retryIfNecessary(httpMethod, null, null, builder);
} catch (final ProcessingException e) {
throw new VitamClientInternalException(e);
}
}
/**
* Perform a HTTP request to the server for synchronous call
*
* @param httpMethod HTTP method to use for request
* @param path URL to request
* @param headers headers HTTP to add to request, may be null
* @param body body content of type contentType, may be null
* @param contentType the media type of the body to send, null if body is null
* @param accept asked type of response
* @return the response from the server
* @throws VitamClientInternalException
*/
protected Response performRequest(String httpMethod, String path, MultivaluedHashMap<String, Object> headers,
Object body,
MediaType contentType, MediaType accept) throws VitamClientInternalException {
if (body == null) {
return performRequest(httpMethod, path, headers, accept, getChunkedMode());
}
try {
ParametersChecker.checkParameter(BODY_AND_CONTENT_TYPE_CANNOT_BE_NULL, body, contentType);
final Builder builder = buildRequest(httpMethod, path, headers, accept, getChunkedMode());
if (body instanceof InputStream) {
Entity<Object> entity = Entity.entity(body, contentType);
return builder.method(httpMethod, entity);
} else {
return retryIfNecessary(httpMethod, body, contentType, builder);
}
} catch (final ProcessingException e) {
throw new VitamClientInternalException(e);
}
}
/**
* Perform a HTTP request to the server for synchronous call
*
* @param httpMethod HTTP method to use for request
* @param path URL to request
* @param headers headers HTTP to add to request, may be null
* @param body body content of type contentType, may be null
* @param contentType the media type of the body to send, null if body is null
* @param accept asked type of response
* @param chunkedMode True use default client, else False use non Chunked mode client
* @return the response from the server
* @throws VitamClientInternalException
*/
protected Response performRequest(String httpMethod, String path, MultivaluedHashMap<String, Object> headers,
Object body,
MediaType contentType, MediaType accept, boolean chunkedMode) throws VitamClientInternalException {
if (body == null) {
return performRequest(httpMethod, path, headers, accept, getChunkedMode());
}
try {
ParametersChecker.checkParameter(BODY_AND_CONTENT_TYPE_CANNOT_BE_NULL, body, contentType);
final Builder builder = buildRequest(httpMethod, path, headers, accept, chunkedMode);
if (body instanceof InputStream) {
Entity<Object> entity = Entity.entity(body, contentType);
return builder.method(httpMethod, entity);
} else {
return retryIfNecessary(httpMethod, body, contentType, builder);
}
} catch (final ProcessingException e) {
throw new VitamClientInternalException(e);
}
}
/**
* Perform an Async HTTP request to the server with callback
*
* @param httpMethod HTTP method to use for request
* @param path URL to request
* @param headers headers HTTP to add to request, may be null
* @param body body content of type contentType, may be null
* @param contentType the media type of the body to send, null if body is null
* @param accept asked type of response
* @param callback
* @param <T> the type of the Future result (generally Response)
* @return the response from the server
* @throws VitamClientInternalException
*/
protected <T> Future<T> performAsyncRequest(String httpMethod, String path,
MultivaluedHashMap<String, Object> headers,
Object body, MediaType contentType, MediaType accept, InvocationCallback<T> callback)
throws VitamClientInternalException {
try {
ParametersChecker.checkParameter(ARGUMENT_CANNOT_BE_NULL_EXCEPT_HEADERS, callback);
if (body != null) {
ParametersChecker.checkParameter(BODY_AND_CONTENT_TYPE_CANNOT_BE_NULL, body, contentType);
}
final Builder builder = buildRequest(httpMethod, path, headers, accept, getChunkedMode());
if (body != null) {
if (body instanceof InputStream) {
Entity<Object> entity = Entity.entity(body, contentType);
return builder.async().method(httpMethod, entity, callback);
} else {
ProcessingException lastException = null;
for (int i = 0; i < VitamConfiguration.getRetryNumber(); i++) {
try {
Entity<Object> entity = Entity.entity(body, contentType);
return builder.async().method(httpMethod, entity, callback);
} catch (ProcessingException e) {
lastException = checkSpecificExceptionForRetry(i, e);
continue;
}
}
if (lastException != null) {
throw lastException;
} else {
throw new VitamClientInternalException("Unknown error in client");
}
}
} else {
ProcessingException lastException = null;
for (int i = 0; i < VitamConfiguration.getRetryNumber(); i++) {
try {
return builder.async().method(httpMethod, callback);
} catch (ProcessingException e) {
lastException = checkSpecificExceptionForRetry(i, e);
continue;
}
}
if (lastException != null) {
throw lastException;
} else {
throw new VitamClientInternalException("Unknown error in client");
}
}
} catch (final ProcessingException e) {
throw new VitamClientInternalException(e);
}
}
/**
* Perform an Async HTTP request to the server with full control of action on caller
*
* @param httpMethod HTTP method to use for request
* @param path URL to request
* @param headers headers HTTP to add to request, may be null
* @param body body content of type contentType, may be null
* @param contentType the media type of the body to send, null if body is null
* @param accept asked type of response
* @return the response from the server as a Future
* @throws VitamClientInternalException
*/
protected Future<Response> performAsyncRequest(String httpMethod, String path,
MultivaluedHashMap<String, Object> headers,
Object body, MediaType contentType, MediaType accept) throws VitamClientInternalException {
try {
if (body != null) {
ParametersChecker.checkParameter(BODY_AND_CONTENT_TYPE_CANNOT_BE_NULL, body, contentType);
}
final Builder builder = buildRequest(httpMethod, path, headers, accept, getChunkedMode());
if (body != null) {
if (body instanceof InputStream) {
Entity<Object> entity = Entity.entity(body, contentType);
return builder.async().method(httpMethod, entity);
} else {
ProcessingException lastException = null;
for (int i = 0; i < VitamConfiguration.getRetryNumber(); i++) {
try {
Entity<Object> entity = Entity.entity(body, contentType);
return builder.async().method(httpMethod, entity);
} catch (ProcessingException e) {
lastException = checkSpecificExceptionForRetry(i, e);
continue;
}
}
if (lastException != null) {
throw lastException;
} else {
throw new VitamClientInternalException("Unknown error in client");
}
}
} else {
ProcessingException lastException = null;
for (int i = 0; i < VitamConfiguration.getRetryNumber(); i++) {
try {
return builder.async().method(httpMethod);
} catch (ProcessingException e) {
lastException = checkSpecificExceptionForRetry(i, e);
continue;
}
}
if (lastException != null) {
throw lastException;
} else {
throw new VitamClientInternalException("Unknown error in client");
}
}
} catch (final ProcessingException e) {
throw new VitamClientInternalException(e);
}
}
@Override
public String getResourcePath() {
return clientFactory.getResourcePath();
}
@Override
public String getServiceUrl() {
return clientFactory.getServiceUrl();
}
@Override
public void close() {
if (client != null) {
client.close();
}
if (clientNotChunked != null) {
clientNotChunked.close();
}
}
@Override
public String toString() {
return new StringBuilder("VitamClient: { ").append(clientFactory.toString()).append(" }").toString();
}
/**
* Build a HTTP request to the server for synchronous call without Body
*
* @param httpMethod HTTP method to use for request
* @param path URL to request
* @param headers headers HTTP to add to request, may be null
* @param accept asked type of response
* @param chunkedMode True use default client, else False use non Chunked mode client
* @return the builder ready to be performed
*/
final Builder buildRequest(String httpMethod, String path, MultivaluedHashMap<String, Object> headers,
MediaType accept,
boolean chunkedMode) {
return buildRequest(httpMethod, getServiceUrl(), path, headers, accept, chunkedMode);
}
/**
* Build a HTTP request to the server for synchronous call without Body
*
* @param httpMethod HTTP method to use for request
* @param url base url
* @param path URL to request
* @param headers headers HTTP to add to request, may be null
* @param accept asked type of response
* @param chunkedMode True use default client, else False use non Chunked mode client
* @return the builder ready to be performed
*/
final Builder buildRequest(String httpMethod, String url, String path, MultivaluedHashMap<String, Object> headers,
MediaType accept, boolean chunkedMode) {
ParametersChecker.checkParameter(ARGUMENT_CANNOT_BE_NULL_EXCEPT_HEADERS, httpMethod, path, accept);
final Builder builder = getHttpClient(chunkedMode).target(url).path(path).request().accept(accept);
if (headers != null) {
for (final Entry<String, List<Object>> entry : headers.entrySet()) {
for (final Object value : entry.getValue()) {
builder.header(entry.getKey(), value);
}
}
}
String newPath = path;
if (newPath.codePointAt(0) != '/') {
newPath = "/" + newPath;
}
String baseUri = getResourcePath() + newPath;
if (url.endsWith(VitamConfiguration.ADMIN_PATH)) {
baseUri = VitamConfiguration.ADMIN_PATH + newPath;
}
// add Authorization Headers (X_TIMESTAMP, X_PLATFORM_ID)
if (clientFactory.useAuthorizationFilter()) {
final Map<String, String> authorizationHeaders =
AuthorizationFilterHelper.getAuthorizationHeaders(httpMethod,
baseUri);
if (authorizationHeaders.size() == 2) {
builder.header(GlobalDataRest.X_TIMESTAMP, authorizationHeaders.get(GlobalDataRest.X_TIMESTAMP));
builder.header(GlobalDataRest.X_PLATFORM_ID, authorizationHeaders.get(GlobalDataRest.X_PLATFORM_ID));
}
}
return builder;
}
/**
* Get the internal Http client
*
* @return the client
*/
Client getHttpClient() {
return getHttpClient(getChunkedMode());
}
/**
* Get the internal Http client according to the chunk mode
*
* @param useChunkedMode
* @return the client
*/
Client getHttpClient(boolean useChunkedMode) {
if (useChunkedMode) {
return client;
} else {
return clientNotChunked;
}
}
final ClientConfiguration getClientConfiguration() {
return clientFactory.getClientConfiguration();
}
/**
* @return the client chunked mode default configuration
*/
boolean getChunkedMode() {
return clientFactory.getChunkedMode();
}
/**
* @return the VitamClientFactory
*/
public VitamClientFactory<?> getVitamClientFactory() {
return clientFactory;
}
}