Java tutorial
/* * Copyright (c) Mirth Corporation. All rights reserved. * * http://www.mirthcorp.com * * The software in this package is published under the terms of the MPL license a copy of which has * been included with this distribution in the LICENSE.txt file. */ package com.mirth.connect.client.core; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.Collection; import java.util.List; import java.util.Map.Entry; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicBoolean; import javax.net.ssl.SSLContext; import javax.ws.rs.ProcessingException; import javax.ws.rs.core.MultivaluedHashMap; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response.Status; import org.apache.commons.lang3.StringUtils; import org.apache.http.Header; import org.apache.http.HttpEntity; import org.apache.http.HttpStatus; import org.apache.http.StatusLine; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpDelete; import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpOptions; import org.apache.http.client.methods.HttpPatch; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPut; import org.apache.http.client.methods.HttpRequestBase; import org.apache.http.client.protocol.HttpClientContext; import org.apache.http.client.utils.HttpClientUtils; import org.apache.http.config.Registry; import org.apache.http.config.RegistryBuilder; import org.apache.http.config.SocketConfig; import org.apache.http.conn.socket.ConnectionSocketFactory; import org.apache.http.conn.socket.PlainConnectionSocketFactory; import org.apache.http.conn.ssl.NoopHostnameVerifier; import org.apache.http.conn.ssl.SSLConnectionSocketFactory; import org.apache.http.conn.ssl.TrustSelfSignedStrategy; import org.apache.http.entity.AbstractHttpEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.client.HttpClients; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.apache.http.ssl.SSLContexts; import org.apache.log4j.Logger; import org.glassfish.jersey.client.ClientRequest; import org.glassfish.jersey.client.ClientResponse; import org.glassfish.jersey.client.spi.AsyncConnectorCallback; import org.glassfish.jersey.client.spi.Connector; import org.glassfish.jersey.message.internal.OutboundMessageContext; import org.glassfish.jersey.message.internal.Statuses; import com.mirth.connect.client.core.Operation.ExecuteType; import com.mirth.connect.util.HttpUtil; import com.mirth.connect.util.MirthSSLUtil; public class ServerConnection implements Connector { public static final String EXECUTE_TYPE_PROPERTY = "executeType"; public static final String OPERATION_PROPERTY = "operation"; private static final int CONNECT_TIMEOUT = 10000; private Logger logger = Logger.getLogger(getClass()); private CloseableHttpClient client; private RequestConfig requestConfig; private final Operation currentOp = new Operation(null, null, null, false); private HttpRequestBase syncRequestBase; private HttpRequestBase abortPendingRequestBase; private HttpClientContext abortPendingClientContext = null; private final AbortTask abortTask = new AbortTask(); private ExecutorService abortExecutor = Executors.newSingleThreadExecutor(); public ServerConnection(int timeout, String[] httpsProtocols, String[] httpsCipherSuites) { this(timeout, httpsProtocols, httpsCipherSuites, false); } public ServerConnection(int timeout, String[] httpsProtocols, String[] httpsCipherSuites, boolean allowHTTP) { SSLContext sslContext = null; try { sslContext = SSLContexts.custom().loadTrustMaterial(null, new TrustSelfSignedStrategy()).build(); } catch (Exception e) { logger.error("Unable to build SSL context.", e); } String[] enabledProtocols = MirthSSLUtil.getEnabledHttpsProtocols(httpsProtocols); String[] enabledCipherSuites = MirthSSLUtil.getEnabledHttpsCipherSuites(httpsCipherSuites); SSLConnectionSocketFactory sslConnectionSocketFactory = new SSLConnectionSocketFactory(sslContext, enabledProtocols, enabledCipherSuites, NoopHostnameVerifier.INSTANCE); RegistryBuilder<ConnectionSocketFactory> builder = RegistryBuilder.<ConnectionSocketFactory>create() .register("https", sslConnectionSocketFactory); if (allowHTTP) { builder.register("http", PlainConnectionSocketFactory.getSocketFactory()); } Registry<ConnectionSocketFactory> socketFactoryRegistry = builder.build(); PoolingHttpClientConnectionManager httpClientConnectionManager = new PoolingHttpClientConnectionManager( socketFactoryRegistry); httpClientConnectionManager.setDefaultMaxPerRoute(5); httpClientConnectionManager.setDefaultSocketConfig(SocketConfig.custom().setSoTimeout(timeout).build()); // MIRTH-3962: The stale connection settings has been deprecated, and this is recommended instead httpClientConnectionManager.setValidateAfterInactivity(5000); HttpClientBuilder clientBuilder = HttpClients.custom().setConnectionManager(httpClientConnectionManager); HttpUtil.configureClientBuilder(clientBuilder); client = clientBuilder.build(); requestConfig = RequestConfig.custom().setConnectTimeout(CONNECT_TIMEOUT) .setConnectionRequestTimeout(CONNECT_TIMEOUT).setSocketTimeout(timeout).build(); } @Override public ClientResponse apply(ClientRequest request) { Operation operation = (Operation) request.getConfiguration().getProperty(OPERATION_PROPERTY); if (operation == null) { throw new ProcessingException("No operation provided for request: " + request); } ExecuteType executeType = (ExecuteType) request.getConfiguration().getProperty(EXECUTE_TYPE_PROPERTY); if (executeType == null) { executeType = operation.getExecuteType(); } if (logger.isDebugEnabled()) { StringBuilder debugMessage = new StringBuilder(operation.getDisplayName()).append('\n'); debugMessage.append(request.getMethod()).append(' ').append(request.getUri()); logger.debug(debugMessage.toString()); } try { switch (executeType) { case SYNC: return executeSync(request, operation); case ASYNC: return executeAsync(request); case ABORT_PENDING: return executeAbortPending(request); } } catch (ClientException e) { throw new ProcessingException(e); } return null; } @Override public Future<?> apply(ClientRequest request, AsyncConnectorCallback callback) { throw new UnsupportedOperationException(); } @Override public String getName() { return "Mirth Server Connection"; } @Override public void close() { // Do nothing } public void shutdown() { // Shutdown the abort thread abortExecutor.shutdownNow(); HttpClientUtils.closeQuietly(client); } /** * Aborts the request if the currentOp is equal to the passed operation, or if the passed * operation is null * * @param operation */ public void abort(Collection<Operation> operations) { synchronized (currentOp) { if (operations.contains(currentOp)) { syncRequestBase.abort(); } } } /** * Allows one request at a time. */ private synchronized ClientResponse executeSync(ClientRequest request, Operation operation) throws ClientException { synchronized (currentOp) { currentOp.setName(operation.getName()); currentOp.setDisplayName(operation.getDisplayName()); currentOp.setAuditable(operation.isAuditable()); } HttpRequestBase requestBase = null; CloseableHttpResponse response = null; boolean shouldClose = true; try { requestBase = setupRequestBase(request, ExecuteType.SYNC); response = client.execute(requestBase); ClientResponse responseContext = handleResponse(request, requestBase, response, true); if (responseContext.hasEntity()) { shouldClose = false; } return responseContext; } catch (Exception e) { if (requestBase != null && requestBase.isAborted()) { throw new RequestAbortedException(e); } else if (e instanceof ClientException) { throw (ClientException) e; } throw new ClientException(e); } finally { if (shouldClose) { HttpClientUtils.closeQuietly(response); synchronized (currentOp) { currentOp.setName(null); currentOp.setDisplayName(null); currentOp.setAuditable(false); } } } } /** * Allows multiple simultaneous requests. */ private ClientResponse executeAsync(ClientRequest request) throws ClientException { HttpRequestBase requestBase = null; CloseableHttpResponse response = null; boolean shouldClose = true; try { requestBase = setupRequestBase(request, ExecuteType.ASYNC); response = client.execute(requestBase); ClientResponse responseContext = handleResponse(request, requestBase, response); if (responseContext.hasEntity()) { shouldClose = false; } return responseContext; } catch (Exception e) { if (requestBase != null && requestBase.isAborted()) { throw new RequestAbortedException(e); } else if (e instanceof ClientException) { throw (ClientException) e; } throw new ClientException(e); } finally { if (shouldClose) { HttpClientUtils.closeQuietly(response); } } } /** * The requests sent through this channel will be aborted on the client side when a new request * arrives. Currently there is no guarantee of the order that pending requests will be sent. */ private ClientResponse executeAbortPending(ClientRequest request) throws ClientException { // TODO: Make order sequential abortTask.incrementRequestsInQueue(); synchronized (abortExecutor) { if (!abortExecutor.isShutdown() && !abortTask.isRunning()) { abortExecutor.execute(abortTask); } HttpRequestBase requestBase = null; CloseableHttpResponse response = null; boolean shouldClose = true; try { abortPendingClientContext = HttpClientContext.create(); abortPendingClientContext.setRequestConfig(requestConfig); requestBase = setupRequestBase(request, ExecuteType.ABORT_PENDING); abortTask.setAbortAllowed(true); response = client.execute(requestBase, abortPendingClientContext); abortTask.setAbortAllowed(false); ClientResponse responseContext = handleResponse(request, requestBase, response); if (responseContext.hasEntity()) { shouldClose = false; } return responseContext; } catch (Exception e) { if (requestBase != null && requestBase.isAborted()) { return new ClientResponse(Status.NO_CONTENT, request); } else if (e instanceof ClientException) { throw (ClientException) e; } throw new ClientException(e); } finally { abortTask.decrementRequestsInQueue(); if (shouldClose) { HttpClientUtils.closeQuietly(response); } } } } private HttpRequestBase createRequestBase(String method) { HttpRequestBase requestBase = null; if (StringUtils.equalsIgnoreCase(HttpGet.METHOD_NAME, method)) { requestBase = new HttpGet(); } else if (StringUtils.equalsIgnoreCase(HttpPost.METHOD_NAME, method)) { requestBase = new HttpPost(); } else if (StringUtils.equalsIgnoreCase(HttpPut.METHOD_NAME, method)) { requestBase = new HttpPut(); } else if (StringUtils.equalsIgnoreCase(HttpDelete.METHOD_NAME, method)) { requestBase = new HttpDelete(); } else if (StringUtils.equalsIgnoreCase(HttpOptions.METHOD_NAME, method)) { requestBase = new HttpOptions(); } else if (StringUtils.equalsIgnoreCase(HttpPatch.METHOD_NAME, method)) { requestBase = new HttpPatch(); } requestBase.setConfig(requestConfig); return requestBase; } private HttpRequestBase getRequestBase(ExecuteType executeType, String method) { HttpRequestBase requestBase = createRequestBase(method); if (executeType == ExecuteType.SYNC) { syncRequestBase = requestBase; } else if (executeType == ExecuteType.ABORT_PENDING) { abortPendingRequestBase = requestBase; } return requestBase; } private HttpRequestBase setupRequestBase(ClientRequest request, ExecuteType executeType) { HttpRequestBase requestBase = getRequestBase(executeType, request.getMethod()); requestBase.setURI(request.getUri()); for (Entry<String, List<String>> entry : request.getStringHeaders().entrySet()) { for (String value : entry.getValue()) { requestBase.addHeader(entry.getKey(), value); } } if (request.hasEntity() && requestBase instanceof HttpEntityEnclosingRequestBase) { final HttpEntityEnclosingRequestBase entityRequestBase = (HttpEntityEnclosingRequestBase) requestBase; entityRequestBase.setEntity(new ClientRequestEntity(request)); } return requestBase; } private ClientResponse handleResponse(ClientRequest request, HttpRequestBase requestBase, CloseableHttpResponse response) throws IOException, ClientException { return handleResponse(request, requestBase, response, false); } private ClientResponse handleResponse(ClientRequest request, HttpRequestBase requestBase, CloseableHttpResponse response, boolean sync) throws IOException, ClientException { StatusLine statusLine = response.getStatusLine(); int statusCode = statusLine.getStatusCode(); ClientResponse responseContext = new MirthClientResponse(Statuses.from(statusCode), request); MultivaluedMap<String, String> headerMap = new MultivaluedHashMap<String, String>(); for (Header header : response.getAllHeaders()) { headerMap.add(header.getName(), header.getValue()); } responseContext.headers(headerMap); HttpEntity responseEntity = response.getEntity(); if (responseEntity != null) { responseContext .setEntityStream(new EntityInputStreamWrapper(response, responseEntity.getContent(), sync)); } if (statusCode == HttpStatus.SC_UNAUTHORIZED) { if (responseContext.hasEntity()) { try { Object entity = responseContext.readEntity(Object.class); throw new UnauthorizedException(statusLine.toString(), entity); } catch (ProcessingException e) { } } throw new UnauthorizedException(statusLine.toString()); } else if (statusCode == HttpStatus.SC_FORBIDDEN) { throw new ForbiddenException(statusLine.toString()); } if (statusCode >= 400) { if (responseContext.hasEntity()) { try { Object entity = responseContext.readEntity(Object.class); if (entity instanceof Throwable) { throw new ClientException("Method failed: " + statusLine, (Throwable) entity); } } catch (ProcessingException e) { } } throw new ClientException("Method failed: " + statusLine); } return responseContext; } private class ClientRequestEntity extends AbstractHttpEntity { private ClientRequest request; public ClientRequestEntity(ClientRequest request) { this.request = request; } @Override public boolean isRepeatable() { return false; } @Override public long getContentLength() { return -1; } @Override public InputStream getContent() throws UnsupportedOperationException { throw new UnsupportedOperationException(); } @Override public void writeTo(final OutputStream outstream) throws IOException { request.setStreamProvider(new OutboundMessageContext.StreamProvider() { @Override public OutputStream getOutputStream(int contentLength) throws IOException { return outstream; } }); request.writeEntity(); } @Override public boolean isStreaming() { return true; } } private class EntityInputStreamWrapper extends InputStream { private CloseableHttpResponse response; private InputStream delegate; private boolean sync; public EntityInputStreamWrapper(CloseableHttpResponse response, InputStream delegate, boolean sync) { this.response = response; this.delegate = delegate; this.sync = sync; } @Override public int read() throws IOException { return delegate.read(); } @Override public int read(byte[] b) throws IOException { return delegate.read(b); } @Override public int read(byte[] b, int off, int len) throws IOException { return delegate.read(b, off, len); } @Override public long skip(long n) throws IOException { return delegate.skip(n); } @Override public int available() throws IOException { return delegate.available(); } @Override public void close() throws IOException { try { delegate.close(); } finally { HttpClientUtils.closeQuietly(response); if (sync) { synchronized (currentOp) { currentOp.setName(null); currentOp.setDisplayName(null); currentOp.setAuditable(false); } } } } @Override public synchronized void mark(int readlimit) { delegate.mark(readlimit); } @Override public synchronized void reset() throws IOException { delegate.reset(); } @Override public boolean markSupported() { return delegate.markSupported(); } } private class AbortTask implements Runnable { private final AtomicBoolean running = new AtomicBoolean(false); private int requestsInQueue = 0; private boolean abortAllowed = false; public synchronized void incrementRequestsInQueue() { requestsInQueue++; } public synchronized void decrementRequestsInQueue() { requestsInQueue--; } public synchronized void setAbortAllowed(boolean abortAllowed) { this.abortAllowed = abortAllowed; } public boolean isRunning() { return running.get(); } @Override public void run() { try { running.set(true); while (true) { synchronized (this) { if (requestsInQueue == 0) { return; } if (requestsInQueue > 1 && abortAllowed && abortPendingClientContext.isRequestSent()) { abortPendingRequestBase.abort(); abortAllowed = false; } } try { Thread.sleep(100); } catch (InterruptedException e) { return; } } } finally { running.set(false); } } } }