/**
* 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.FileNotFoundException;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.core.MediaType;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.config.Registry;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.glassfish.jersey.apache.connector.ApacheClientProperties;
import org.glassfish.jersey.apache.connector.ApacheConnectorProvider;
import org.glassfish.jersey.apache.connector.VitamClientProperties;
import org.glassfish.jersey.client.ClientConfig;
import org.glassfish.jersey.client.ClientProperties;
import org.glassfish.jersey.client.JerseyInvocation;
import org.glassfish.jersey.client.RequestEntityProcessing;
import org.glassfish.jersey.jackson.JacksonFeature;
import org.glassfish.jersey.media.multipart.MultiPartFeature;
import com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider;
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.client.configuration.ClientConfigurationImpl;
import fr.gouv.vitam.common.client.configuration.SSLConfiguration;
import fr.gouv.vitam.common.client.configuration.SecureClientConfiguration;
import fr.gouv.vitam.common.exception.VitamException;
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.thread.VitamThreadPoolExecutor;
import fr.gouv.vitam.common.thread.VitamThreadPoolExecutorProvider;
/**
* General VitamClientFactory for non SSL client
*
* @param <T> MockOrRestClient class
*
*/
public abstract class VitamClientFactory<T extends MockOrRestClient> implements VitamClientFactoryInterface<T> {
private static final VitamLogger LOGGER = VitamLoggerFactory.getInstance(VitamClientFactory.class);
/**
* Multipart response from Server side
*/
public static final String MULTIPART_MIXED = "multipart/mixed";
/**
* Multipart response from Server side
*/
public static final MediaType MULTIPART_MIXED_TYPE = new MediaType("multipart", "mixed");
static final AtomicBoolean INIT_STATIC_CONFIG = new AtomicBoolean(false);
/**
* Global configuration for Apache: Pooling connection
*/
static final PoolingHttpClientConnectionManager POOLING_CONNECTION_MANAGER =
new PoolingHttpClientConnectionManager(VitamConfiguration.getMaxDelayUnusedConnection(), TimeUnit.MILLISECONDS);
/**
* Global configuration for Apache: Pooling connection
*/
static final PoolingHttpClientConnectionManager POOLING_CONNECTION_MANAGER_NOT_CHUNKED =
new PoolingHttpClientConnectionManager(VitamConfiguration.getMaxDelayUnusedConnection(), TimeUnit.MILLISECONDS);
/**
* Global configuration for Apache: Idle Monitor
*/
static final AtomicBoolean STATIC_IDLE_MONITOR = new AtomicBoolean(false);
/**
* Global configuration for Apache: Idle Monitor
*/
final ExpiredConnectionMonitorThread idleMonitor;
/**
* Global configuration
*/
final ClientConfig config = new ClientConfig();
/**
* Global configuration not chunked
*/
final ClientConfig configNotChunked = new ClientConfig();
private String serviceUrl;
private String resourcePath;
protected ClientConfiguration clientConfiguration;
private boolean chunkedMode;
private boolean multipartMode;
private final Client givenClient;
private VitamClientType vitamClientType = VitamClientType.MOCK;
PoolingHttpClientConnectionManager chunkedPoolingManager;
PoolingHttpClientConnectionManager notChunkedPoolingManager;
VitamThreadPoolExecutor vitamThreadPoolExecutor = VitamThreadPoolExecutor.getDefaultExecutor();
SSLConfiguration sslConfiguration = null;
private boolean useAuthorizationFilter = true;
/**
* Constructor with standard configuration
*
* @param configuration The client configuration
* @param resourcePath the resource path of the server for the client calls
* @throws UnsupportedOperationException HTTPS not implemented yet
*/
protected VitamClientFactory(ClientConfiguration configuration, String resourcePath) {
this(configuration, resourcePath, true, false, true);
}
/**
* Constructor to allow to enable Multipart support (until all are removed)
*
* @param configuration The client configuration
* @param resourcePath the resource path of the server for the client calls
* @param suppressHttpCompliance define if client (Jetty Client feature) check if request id HTTP compliant
* @param multipart allow multipart and disabling chunked mode
* @throws UnsupportedOperationException HTTPS not implemented yet
*/
protected VitamClientFactory(ClientConfiguration configuration, String resourcePath,
boolean suppressHttpCompliance, boolean allowMultipart) {
this(configuration, resourcePath, suppressHttpCompliance, allowMultipart, !allowMultipart);
}
/**
* Constructor to allow to enable Multipart support or Chunked mode.
*
* @param configuration The client configuration
* @param resourcePath the resource path of the server for the client calls
* @param suppressHttpCompliance define if client (Jetty Client feature) check if request id HTTP compliant
* @param multipart allow multipart and disabling chunked mode
* @param chunkedMode if multipart is false, one can managed here if the client is in default chunkedMode or not
* @throws UnsupportedOperationException HTTPS not implemented yet
*/
protected VitamClientFactory(ClientConfiguration configuration, String resourcePath,
boolean suppressHttpCompliance, boolean allowMultipart, boolean chunkedMode) {
internalConfigure();
initialisation(configuration, resourcePath);
config.property(ClientProperties.SUPPRESS_HTTP_COMPLIANCE_VALIDATION, suppressHttpCompliance);
configNotChunked.property(ClientProperties.SUPPRESS_HTTP_COMPLIANCE_VALIDATION, suppressHttpCompliance);
this.chunkedMode = chunkedMode;
this.multipartMode = allowMultipart;
if (allowMultipart) {
this.chunkedMode = false;
LOGGER.warn("This client is using Multipart therefore not Chunked mode");
}
disableChunkMode(configNotChunked);
givenClient = null;
idleMonitor = new ExpiredConnectionMonitorThread(this);
startupMonitor();
if (configuration != null) {
useAuthorizationFilter = !configuration.isSecure();
}
}
/**
* Constructor to allow to enable Multipart support or Chunked mode.<br/>
* <br/>
* HACK: to support Storage DriverImpl!
*
* @param configuration The client configuration
* @param resourcePath the resource path of the server for the client calls
* @param suppressHttpCompliance define if client (Jetty Client feature) check if request id HTTP compliant
* @param multipart allow multipart and disabling chunked mode
* @param chunkedMode if multipart is false, one can managed here if the client is in default chunkedMode or not
* @param sharedMonitor If true, the Monitor is instantiate only once
* @throws UnsupportedOperationException HTTPS not implemented yet
*/
protected VitamClientFactory(ClientConfiguration configuration, String resourcePath,
boolean suppressHttpCompliance, boolean allowMultipart, boolean chunkedMode, boolean sharedMonitor) {
internalConfigure();
initialisation(configuration, resourcePath);
config.property(ClientProperties.SUPPRESS_HTTP_COMPLIANCE_VALIDATION, suppressHttpCompliance);
configNotChunked.property(ClientProperties.SUPPRESS_HTTP_COMPLIANCE_VALIDATION, suppressHttpCompliance);
this.chunkedMode = chunkedMode;
this.multipartMode = allowMultipart;
if (allowMultipart) {
this.chunkedMode = false;
LOGGER.warn("This client is using Multipart therefore not Chunked mode");
}
disableChunkMode(configNotChunked);
givenClient = null;
if (sharedMonitor) {
if (STATIC_IDLE_MONITOR.compareAndSet(false, true)) {
idleMonitor = new ExpiredConnectionMonitorThread(this);
startupMonitor();
} else {
idleMonitor = null;
}
} else {
idleMonitor = new ExpiredConnectionMonitorThread(this);
startupMonitor();
}
if (configuration != null) {
useAuthorizationFilter = !configuration.isSecure();
}
}
/**
* ONLY use this constructor in unit test. Remove this when JerseyTest will be fully compatible with Jetty
*
* @param configuration the client configuration
* @param resourcePath the resource path of the server for the client calls
* @param client the HTTP client to use
* @throws UnsupportedOperationException HTTPS not implemented yet
*/
protected VitamClientFactory(ClientConfiguration configuration, String resourcePath, Client client) {
ParametersChecker.checkParameter("Client cannot be null", client);
internalConfigure();
initialisation(configuration, resourcePath);
this.givenClient = client;
idleMonitor = new ExpiredConnectionMonitorThread(this);
}
protected void disableUseAuthorizationFilter() {
useAuthorizationFilter = false;
}
protected void enableUseAuthorizationFilter() {
useAuthorizationFilter = true;
}
boolean useAuthorizationFilter() {
return useAuthorizationFilter;
}
protected final void initialisation(ClientConfiguration configuration, String resourcePath) {
if (configuration == null) {
setVitamClientType(VitamClientType.MOCK);
clientConfiguration = new ClientConfigurationImpl();
} else {
setVitamClientType(VitamClientType.PRODUCTION);
clientConfiguration = configuration;
ParametersChecker.checkParameter("Host cannot be null", clientConfiguration.getServerHost());
ParametersChecker.checkValue("Port has invalid value", clientConfiguration.getServerPort(), 1);
}
ParametersChecker.checkParameter("resourcePath cannot be null", resourcePath);
this.resourcePath = Optional.ofNullable(resourcePath).orElse("/");
if (this.resourcePath.codePointAt(0) != '/') {
this.resourcePath = "/" + this.resourcePath;
}
serviceUrl = (clientConfiguration.isSecure() ? "https://"
: "http://") + clientConfiguration.getServerHost() + ":" + clientConfiguration.getServerPort() +
this.resourcePath;
if (chunkedPoolingManager != null && chunkedPoolingManager != POOLING_CONNECTION_MANAGER) {
chunkedPoolingManager.close();
}
if (notChunkedPoolingManager != null && notChunkedPoolingManager != POOLING_CONNECTION_MANAGER_NOT_CHUNKED) {
notChunkedPoolingManager.close();
}
if (clientConfiguration.isSecure()) {
final SecureClientConfiguration sclientConfiguration = (SecureClientConfiguration) clientConfiguration;
sslConfiguration = sclientConfiguration.getSslConfiguration();
ParametersChecker.checkParameter("sslConfiguration is a mandatory parameter", sslConfiguration);
Registry<ConnectionSocketFactory> registry;
try {
registry = sslConfiguration.getRegistry(sslConfiguration.createSSLContext());
} catch (FileNotFoundException | VitamException e) {
LOGGER.error(e);
throw new IllegalArgumentException("SSLConfiguration issue while reading KeyStore or TrustStore", e);
}
PoolingHttpClientConnectionManager pool = new PoolingHttpClientConnectionManager(registry, null, null, null,
VitamConfiguration.getMaxDelayUnusedConnection(), TimeUnit.MILLISECONDS);
setupApachePool(pool);
chunkedPoolingManager = pool;
pool = new PoolingHttpClientConnectionManager(registry, null, null, null,
VitamConfiguration.getMaxDelayUnusedConnection(), TimeUnit.MILLISECONDS);
setupApachePool(pool);
notChunkedPoolingManager = pool;
} else {
chunkedPoolingManager = POOLING_CONNECTION_MANAGER;
notChunkedPoolingManager = POOLING_CONNECTION_MANAGER_NOT_CHUNKED;
}
config.property(ApacheClientProperties.CONNECTION_MANAGER, chunkedPoolingManager);
configNotChunked.property(ApacheClientProperties.CONNECTION_MANAGER, notChunkedPoolingManager);
}
@Override
public void changeServerPort(int port) {
try {
ParametersChecker.checkParameter("Host cannot be null", clientConfiguration.getServerHost());
} catch (final IllegalArgumentException e) {
LOGGER.debug(e);
clientConfiguration.setServerHost(TestVitamClientFactory.LOCALHOST);
}
this.initialisation(clientConfiguration.setServerPort(port), getResourcePath());
}
@Override
public VitamClientType getVitamClientType() {
return vitamClientType;
}
@Override
public VitamClientFactory<T> setVitamClientType(VitamClientType vitamClientType) {
this.vitamClientType = vitamClientType;
return this;
}
/**
* To be overridden if necessary (Benchmark Test)
*/
void internalConfigure() {
startup();
internalConfigure(config, true);
internalConfigure(configNotChunked, false);
}
private void internalConfigure(ClientConfig config, boolean chunkedMode) {
commonConfigure(config);
commonApacheConfigure(config, chunkedMode);
}
/**
* Specific to handle Junit tests with given Client
*
* @param config
* @return the associated client
*/
private Client buildClient(ClientConfig config) {
if (givenClient != null) {
return givenClient;
}
return ClientBuilder.newClient(config);
}
/**
*
* @return the client chunked mode default configuration
*/
boolean getChunkedMode() {
return chunkedMode;
}
@Override
public Client getHttpClient() {
if (givenClient != null) {
return givenClient;
}
return getHttpClient(chunkedMode);
}
@Override
public Client getHttpClient(boolean useChunkedMode) {
if (givenClient != null) {
return givenClient;
}
if (useChunkedMode) {
return buildClient(config);
} else {
return buildClient(configNotChunked);
}
}
@Override
public String getResourcePath() {
return resourcePath;
}
@Override
public String getServiceUrl() {
return serviceUrl;
}
@Override
public String toString() {
return new StringBuilder("VitamFactory: { ")
.append("ServiceUrl: ").append(serviceUrl)
.append(", ResourcePath: ").append(resourcePath)
.append(", ChunkedMode: ").append(chunkedMode)
.append(", Configuration: { Classes: \"").append(config.getClasses())
.append("\", Properties: \"").append(config.getProperties())
.append("\" }")
.append(" }").toString();
}
@Override
public final ClientConfiguration getClientConfiguration() {
return clientConfiguration;
}
/**
* To allow specific client to disable ChunkMode until all API remove Multipart
*
* @param config
*/
private final void disableChunkMode(ClientConfig config) {
if (multipartMode) {
config.register(MultiPartFeature.class);
}
config.property(ClientProperties.CHUNKED_ENCODING_SIZE, 0)
.property(ClientProperties.REQUEST_ENTITY_PROCESSING, RequestEntityProcessing.BUFFERED);
}
@Override
public final ClientConfig getDefaultConfigCient() {
return config;
}
@Override
public final ClientConfig getDefaultConfigCient(boolean chunkedMode) {
if (chunkedMode) {
return config;
} else {
return configNotChunked;
}
}
/**
* Shutdown the global Connection Manager (cannot be restarted yet)
*/
public void shutdown() {
if (idleMonitor != null) {
idleMonitor.shutdown();
idleMonitor.interrupt();
}
if (chunkedPoolingManager != null) {
chunkedPoolingManager.close();
notChunkedPoolingManager.close();
}
}
static void startup() {
if (INIT_STATIC_CONFIG.compareAndSet(false, true)) {
setupApachePool(POOLING_CONNECTION_MANAGER);
setupApachePool(POOLING_CONNECTION_MANAGER_NOT_CHUNKED);
}
}
private static void setupApachePool(PoolingHttpClientConnectionManager manager) {
manager.setMaxTotal(VitamConfiguration.getMaxTotalClient());
manager.setDefaultMaxPerRoute(VitamConfiguration.getMaxClientPerHost());
manager
.setValidateAfterInactivity(VitamConfiguration.getDelayValidationAfterInactivity());
}
private void startupMonitor() {
// Apache configuration
if (idleMonitor != null) {
idleMonitor.setDaemon(true);
idleMonitor.start();
}
}
void commonConfigure(ClientConfig config) {
// Prevent Warning on misusage of non standard Calls
Logger.getLogger(JerseyInvocation.class.getName()).setLevel(Level.OFF);
config.register(JacksonJsonProvider.class)
.register(JacksonFeature.class)
.register(new VitamThreadPoolExecutorProvider("Vitam"))
// Not supported MultiPartFeature.class
.property(ClientProperties.CHUNKED_ENCODING_SIZE, VitamConfiguration.getChunkSize())
.property(ClientProperties.REQUEST_ENTITY_PROCESSING, RequestEntityProcessing.CHUNKED)
.property(ClientProperties.CONNECT_TIMEOUT, VitamConfiguration.getConnectTimeout())
.property(ClientProperties.READ_TIMEOUT, VitamConfiguration.getReadTimeout())
.property(ClientProperties.SUPPRESS_HTTP_COMPLIANCE_VALIDATION, true);
}
final void commonApacheConfigure(ClientConfig config, boolean chunkedMode) {
if (chunkedMode) {
config.property(ApacheClientProperties.CONNECTION_MANAGER, POOLING_CONNECTION_MANAGER);
} else {
config.property(ApacheClientProperties.CONNECTION_MANAGER, POOLING_CONNECTION_MANAGER_NOT_CHUNKED);
}
final RequestConfig requestConfig = RequestConfig.custom()
.setConnectionRequestTimeout(VitamConfiguration.getDelayGetClient()).build();
config.property(ApacheClientProperties.CONNECTION_MANAGER_SHARED, true)
.property(VitamClientProperties.DISABLE_AUTOMATIC_RETRIES, true)
.property(ApacheClientProperties.REQUEST_CONFIG, requestConfig);
config.connectorProvider(new ApacheConnectorProvider());
}
/**
*
* @return the VitamThreadPoolExecutor used by the server
*/
public VitamThreadPoolExecutor getVitamThreadPoolExecutor() {
return vitamThreadPoolExecutor;
}
/**
* Monitor to check Expired Connection (staled). Idle connections are managed directly in the Pool
*/
private static class ExpiredConnectionMonitorThread extends Thread {
volatile boolean shutdown;
final VitamClientFactory<?> factory;
/**
* Constructor
*/
ExpiredConnectionMonitorThread(VitamClientFactory<?> factory) {
this.factory = factory;
}
@Override
public void run() {
try {
while (!shutdown) {
synchronized (this) {
if (factory.chunkedPoolingManager == null) {
return;
}
wait(VitamConfiguration.getIntervalDelayCheckIdle());
// Close expired connections
factory.chunkedPoolingManager.closeExpiredConnections();
factory.notChunkedPoolingManager.closeExpiredConnections();
factory.chunkedPoolingManager.closeIdleConnections(
VitamConfiguration.getDelayValidationAfterInactivity(), TimeUnit.MILLISECONDS);
factory.notChunkedPoolingManager.closeIdleConnections(
VitamConfiguration.getDelayValidationAfterInactivity(), TimeUnit.MILLISECONDS);
}
}
} catch (final InterruptedException ex) {
SysErrLogger.FAKE_LOGGER.ignoreLog(ex);
// terminate
}
}
/**
* Shutdown this Daemon
*/
public void shutdown() {
shutdown = true;
synchronized (this) {
notifyAll();
}
}
}
}