diff options
author | Lee, Tian (tl5884) <TianL@amdocs.com> | 2018-11-21 12:02:49 +0000 |
---|---|---|
committer | Lee, Tian (tl5884) <TianL@amdocs.com> | 2018-11-21 12:11:43 +0000 |
commit | c782aa5250b32e8aa3c7ecd088f47fd87334a8ef (patch) | |
tree | 75d544b2b7a023de4fe5e96fe57eae6ef00952a1 /src/main | |
parent | ef858ed661134e651082675c091db056f8add98d (diff) |
Rewrite rest-client to use Jersey 2.x API1.4.0
This is to fix clashes with the rs-api 2.x jars that Spring Boot
requires on the classpath.
Change-Id: Ic36cdf0d650f96c54824e2f51c7f8060b2588c66
Issue-ID: AAI-1939
Signed-off-by: Lee, Tian (tl5884) <TianL@amdocs.com>
Diffstat (limited to 'src/main')
3 files changed, 980 insertions, 1010 deletions
diff --git a/src/main/java/org/onap/aai/restclient/client/RestClient.java b/src/main/java/org/onap/aai/restclient/client/RestClient.java index f4f184c..89d5c62 100644 --- a/src/main/java/org/onap/aai/restclient/client/RestClient.java +++ b/src/main/java/org/onap/aai/restclient/client/RestClient.java @@ -23,869 +23,854 @@ package org.onap.aai.restclient.client; import java.io.ByteArrayOutputStream; import java.text.SimpleDateFormat; import java.util.Arrays; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; - +import java.util.stream.Collectors; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.Invocation.Builder; +import javax.ws.rs.client.WebTarget; import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.MultivaluedHashMap; +import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; - -import org.onap.aai.restclient.enums.RestAuthenticationMode; -import org.onap.aai.restclient.logging.RestClientMsgs; -import org.onap.aai.restclient.rest.RestClientBuilder; import org.onap.aai.cl.api.LogFields; import org.onap.aai.cl.api.LogLine; import org.onap.aai.cl.api.Logger; import org.onap.aai.cl.eelf.LoggerFactory; import org.onap.aai.cl.mdc.MdcContext; import org.onap.aai.cl.mdc.MdcOverride; +import org.onap.aai.restclient.enums.RestAuthenticationMode; +import org.onap.aai.restclient.logging.RestClientMsgs; +import org.onap.aai.restclient.rest.RestClientBuilder; -import com.sun.jersey.api.client.Client; -import com.sun.jersey.api.client.ClientResponse; -import com.sun.jersey.api.client.WebResource; -import com.sun.jersey.api.client.WebResource.Builder; -import com.sun.jersey.core.util.MultivaluedMapImpl; /** - * This class provides a general client implementation that micro services can use for communicating - * with the endpoints via their exposed REST interfaces. + * This class provides a general client implementation that micro services can use for communicating with the endpoints + * via their exposed REST interfaces. * */ public class RestClient { - /** - * This is a generic builder that is used for constructing the REST client that we will use to - * communicate with the REST endpoint. - */ - private RestClientBuilder clientBuilder; - - private final ConcurrentMap<String,InitializedClient> CLIENT_CACHE = new ConcurrentHashMap<>(); - private static final String REST_CLIENT_INSTANCE = "REST_CLIENT_INSTANCE"; - - /** Standard logger for producing log statements. */ - private Logger logger = LoggerFactory.getInstance().getLogger("AAIRESTClient"); - - /** Standard logger for producing metric statements. */ - private Logger metricsLogger = LoggerFactory.getInstance().getMetricsLogger("AAIRESTClient"); - - private SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX"); - - /** Reusable function call for GET REST operations. */ - private final RestOperation getOp = new GetRestOperation(); - - /** Reusable function call for PUT REST operations. */ - private final RestOperation putOp = new PutRestOperation(); - - /** Reusable function call for POST REST operations. */ - private final RestOperation postOp = new PostRestOperation(); - - /** Reusable function call for DELETE REST operations. */ - private final RestOperation deleteOp = new DeleteRestOperation(); - - /** Reusable function call for HEAD REST operations. */ - private final RestOperation headOp = new HeadRestOperation(); - - /** Reusable function call for PATCH REST operations. */ - private final RestOperation patchOp = new PatchRestOperation(); - - - /** - * Creates a new instance of the {@link RestClient}. - */ - public RestClient() { - - clientBuilder = new RestClientBuilder(); - - } - - - /** - * Creates a new instance of the {@link RestClient} using the supplied {@link RestClientBuilder}. - * - * @param rcBuilder - The REST client builder that this instance of the {@link RestClient} should - * use. - */ - public RestClient(RestClientBuilder rcBuilder) { - clientBuilder = rcBuilder; - } - - public RestClient authenticationMode(RestAuthenticationMode mode) { - logger.debug("Set rest authentication mode= " + mode); - clientBuilder.setAuthenticationMode(mode); - return this; - } - - public RestClient basicAuthUsername(String username) { - logger.debug("Set SSL BasicAuth username = " + username); - clientBuilder.setBasicAuthUsername(username); - return this; - } - - public RestClient basicAuthPassword(String password) { - /* - * purposely not logging out the password, I guess we could obfuscate it if we really want to - * see it in the logs + /** + * This is a generic builder that is used for constructing the REST client that we will use to communicate with the + * REST endpoint. */ - clientBuilder.setBasicAuthPassword(password); - return this; - } - - - /** - * Sets the flag to indicate whether or not validation should be performed against the host name - * of the server we are trying to communicate with. - * - * @parameter validate - Set to true to enable validation, false to disable - * - * @return The AAIRESTClient instance. This is useful for chaining parameter assignments. - */ - public RestClient validateServerHostname(boolean validate) { - logger.debug("Set validate server hostname = " + validate); - clientBuilder.setValidateServerHostname(validate); - return this; - } - - - /** - * Sets the flag to indicate whether or not validation should be performed against the certificate - * chain. - * - * @parameter validate - Set to true to enable validation, false to disable. - * - * @return The AAIRESTClient instance. This is useful for chaining parameter assignments. - */ - public RestClient validateServerCertChain(boolean validate) { - logger.debug("Set validate server certificate chain = " + validate); - clientBuilder.setValidateServerCertChain(validate); - return this; - } - - - /** - * Assigns the client certificate file to use. - * - * @param filename - The name of the certificate file. - * - * @return The AAIRESTClient instance. This is useful for chaining parameter assignments. - */ - public RestClient clientCertFile(String filename) { - logger.debug("Set client certificate filename = " + filename); - clientBuilder.setClientCertFileName(filename); - return this; - } - - - /** - * Assigns the client certificate password to use. - * - * @param password - The certificate password. - * - * @return The AAIRESTClient instance. This is useful for chaining parameter assignments. - */ - public RestClient clientCertPassword(String password) { - clientBuilder.setClientCertPassword(password); - return this; - } - - - /** - * Assigns the name of the trust store file to use. - * - * @param filename - the name of the trust store file. - * - * @return The AAIRESTClient instance. This is useful for chaining parameter assignments. - */ - public RestClient trustStore(String filename) { - logger.debug("Set trust store filename = " + filename); - clientBuilder.setTruststoreFilename(filename); - return this; - } - - - /** - * Assigns the connection timeout (in ms) to use when connecting to the target server. - * - * @param timeout - The length of time to wait in ms before timing out. - * - * @return The AAIRESTClient instance. This is useful for chaining parameter assignments. - */ - public RestClient connectTimeoutMs(int timeout) { - logger.debug("Set connection timeout = " + timeout + " ms"); - clientBuilder.setConnectTimeoutInMs(timeout); - return this; - } - - - /** - * Assigns the read timeout (in ms) to use when communicating with the target server. - * - * @param timeout The read timeout in milliseconds. - * - * @return The AAIRESTClient instance. This is useful for chaining parameter assignments. - */ - public RestClient readTimeoutMs(int timeout) { - logger.debug("Set read timeout = " + timeout + " ms"); - clientBuilder.setReadTimeoutInMs(timeout); - return this; - } - - /** - * Configures the client for a specific SSL protocol - * - * @param sslProtocol - protocol string constant such as TLS, TLSv1, TLSv1.1, TLSv1.2 - * - * @return The AAIRESTClient instance. - */ - public RestClient sslProtocol(String sslProtocol) { - logger.debug("Set sslProtocol = " + sslProtocol); - clientBuilder.setSslProtocol(sslProtocol); - return this; - } - - private boolean shouldRetry(OperationResult operationResult) { - - if (operationResult == null) { - return true; - } - - int resultCode = operationResult.getResultCode(); - - if (resultCode == 200) { - return false; - } - - if (resultCode == 404) { - return false; - } - - return true; - - } - - /** - * This method operates on a REST endpoint by submitting an HTTP operation request against the - * supplied URL. - * This variant of the method will perform a requested number of retries in the event that the - * first request is unsuccessful. - * - * @param operation - the REST operation type to send to the url - * @param url - The REST endpoint to submit the REST request to. - * @param payload - They payload to provide in the REST request, if applicable - * @param headers - The headers that should be passed in the request - * @param contentType - The content type of the payload - * @param responseType - The expected format of the response. - * - * @return The result of the REST request. - */ - protected OperationResult processRequest(RestOperation operation, String url, String payload, - Map<String, List<String>> headers, MediaType contentType, MediaType responseType, - int numRetries) { - - - OperationResult result = null; - - long startTimeInMs = System.currentTimeMillis(); - for (int retryCount = 0; retryCount < numRetries; retryCount++) { - - logger.info(RestClientMsgs.HTTP_REQUEST_WITH_RETRIES, operation.getRequestType().toString(), - url, Integer.toString(retryCount + 1)); - - // Submit our query to the AAI. - result = processRequest(operation, url, payload, headers, contentType, responseType); - - // If the submission was successful then we're done. - - if (!shouldRetry(result)) { - - logger.info(RestClientMsgs.HTTP_REQUEST_TIME_WITH_RETRIES, operation.getRequestType().toString(),url, - Long.toString(System.currentTimeMillis() - startTimeInMs), - Integer.toString(retryCount)); - - result.setNumRetries(retryCount); - - return result; - } + private RestClientBuilder clientBuilder; + + private final ConcurrentMap<String, InitializedClient> CLIENT_CACHE = new ConcurrentHashMap<>(); + private static final String REST_CLIENT_INSTANCE = "REST_CLIENT_INSTANCE"; + + /** Standard logger for producing log statements. */ + private Logger logger = LoggerFactory.getInstance().getLogger("AAIRESTClient"); + + /** Standard logger for producing metric statements. */ + private Logger metricsLogger = LoggerFactory.getInstance().getMetricsLogger("AAIRESTClient"); - // Our submission was unsuccessful... - try { - // Sleep between re-tries to be nice to the target system. - Thread.sleep(50); - - } catch (InterruptedException e) { - logger.error(RestClientMsgs.HTTP_REQUEST_INTERRUPTED, url, e.getLocalizedMessage()); - Thread.currentThread().interrupt(); - break; - } + private SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX"); + + /** Reusable function call for GET REST operations. */ + private final RestOperation getOp = new GetRestOperation(); + + /** Reusable function call for PUT REST operations. */ + private final RestOperation putOp = new PutRestOperation(); + + /** Reusable function call for POST REST operations. */ + private final RestOperation postOp = new PostRestOperation(); + + /** Reusable function call for DELETE REST operations. */ + private final RestOperation deleteOp = new DeleteRestOperation(); + + /** Reusable function call for HEAD REST operations. */ + private final RestOperation headOp = new HeadRestOperation(); + + /** Reusable function call for PATCH REST operations. */ + private final RestOperation patchOp = new PatchRestOperation(); + + /** + * Creates a new instance of the {@link RestClient}. + */ + public RestClient() { + clientBuilder = new RestClientBuilder(); } - // If we've gotten this far, then we failed all of our retries. - if (result == null) { - result = new OperationResult(); - } - - result.setNumRetries(numRetries); - result.setResultCode(504); - result.setFailureCause("Failed to get a successful result after multiple retries to target server."); - - - return result; - } - - /** - * This method operates on a REST endpoint by submitting an HTTP operation request against the - * supplied URL. - * - * @param operation - the REST operation type to send to the url - * @param url - The REST endpoint to submit the REST request to. - * @param payload - They payload to provide in the REST request, if applicable - * @param headers - The headers that should be passed in the request - * @param contentType - The content type of the payload - * @param responseType - The expected format of the response. - * - * @return The result of the REST request. - */ - protected OperationResult processRequest(RestOperation operation, String url, String payload, - Map<String, List<String>> headers, MediaType contentType, MediaType responseType) { - - ClientResponse clientResponse = null; - OperationResult operationResult = new OperationResult(); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - - String requestType = operation.getRequestType().name(); - - // Grab the current time so that we can log how long the - // query took once we are done. - long startTimeInMs = System.currentTimeMillis(); - MdcOverride override = new MdcOverride(); - override.addAttribute(MdcContext.MDC_START_TIME, formatter.format(startTimeInMs)); - - logger.info(RestClientMsgs.HTTP_REQUEST, requestType, url); - - try { - - // Get a REST client instance for our request. - Client client = getClient(); - - // Debug log the request - debugRequest(url, payload, headers, responseType); - - // Get a client request builder, and submit our GET request. - Builder builder = getClientBuilder(client, url, payload, headers, contentType, responseType); - clientResponse = operation.processOperation(builder); - - populateOperationResult(clientResponse, operationResult); - - // Debug log the response - if (clientResponse != null) { - debugResponse(operationResult, clientResponse.getHeaders()); - } - - } catch (Exception ex) { - - logger.error(RestClientMsgs.HTTP_REQUEST_ERROR, requestType, url, ex.getLocalizedMessage()); - operationResult.setResultCode(500); - operationResult.setFailureCause( - "Error during GET operation to AAI with message = " + ex.getLocalizedMessage()); - - } finally { - - if (logger.isDebugEnabled()) { - logger.debug(baos.toString()); - } - - // Not every valid response code is actually represented by the Response.Status - // object, so we need to guard against missing codes, otherwise we throw null - // pointer exceptions when we try to generate our metrics logs... - Response.Status responseStatus = - Response.Status.fromStatusCode(operationResult.getResultCode()); - String responseStatusCodeString = ""; - if (responseStatus != null) { - responseStatusCodeString = responseStatus.toString(); - } - - metricsLogger.info(RestClientMsgs.HTTP_REQUEST_TIME, - new LogFields().setField(LogLine.DefinedFields.STATUS_CODE, responseStatusCodeString) - .setField(LogLine.DefinedFields.RESPONSE_CODE, operationResult.getResultCode()) - .setField(LogLine.DefinedFields.RESPONSE_DESCRIPTION, operationResult.getResult()), - override, requestType, Long.toString(System.currentTimeMillis() - startTimeInMs), url); - logger.info(RestClientMsgs.HTTP_REQUEST_TIME, requestType, - Long.toString(System.currentTimeMillis() - startTimeInMs), url); - logger.info(RestClientMsgs.HTTP_RESPONSE, url, - operationResult.getResultCode() + " " + responseStatusCodeString); - } - - return operationResult; - } - - /** - * This method submits an HTTP PUT request against the supplied URL. - * - * @param url - The REST endpoint to submit the PUT request to. - * @param payload - the payload to send to the supplied URL - * @param headers - The headers that should be passed in the request - * @param contentType - The content type of the payload - * @param responseType - The expected format of the response. - * - * @return The result of the PUT request. - */ - public OperationResult put(String url, String payload, Map<String, List<String>> headers, - MediaType contentType, MediaType responseType) { - return processRequest(putOp, url, payload, headers, contentType, responseType); - } - - /** - * This method submits an HTTP POST request against the supplied URL. - * - * @param url - The REST endpoint to submit the POST request to. - * @param payload - the payload to send to the supplied URL - * @param headers - The headers that should be passed in the request - * @param contentType - The content type of the payload - * @param responseType - The expected format of the response. - * - * @return The result of the POST request. - */ - public OperationResult post(String url, String payload, Map<String, List<String>> headers, - MediaType contentType, MediaType responseType) { - return processRequest(postOp, url, payload, headers, contentType, responseType); - } - - /** - * This method submits an HTTP POST request against the supplied URL, and emulates a PATCH - * operation by setting a special header value - * - * @param url - The REST endpoint to submit the POST request to. - * @param payload - the payload to send to the supplied URL - * @param headers - The headers that should be passed in the request - * @param contentType - The content type of the payload - * @param responseType - The expected format of the response. - * - * @return The result of the POST request. - */ - public OperationResult patch(String url, String payload, Map<String, List<String>> headers, - MediaType contentType, MediaType responseType) { - return processRequest(patchOp, url, payload, headers, contentType, responseType); - } - - - /** - * This method submits an HTTP HEAD request against the supplied URL - * - * @param url - The REST endpoint to submit the POST request to. - * @param headers - The headers that should be passed in the request - * @param responseType - The expected format of the response. - * - * @return The result of the POST request. - */ - public OperationResult head(String url, Map<String, List<String>> headers, - MediaType responseType) { - return processRequest(headOp, url, null, headers, null, responseType); - } - - /** - * This method submits an HTTP GET request against the supplied URL. - * - * @param url - The REST endpoint to submit the GET request to. - * @param headers - The headers that should be passed in the request - * @param responseType - The expected format of the response. - * - * @return The result of the GET request. - */ - public OperationResult get(String url, Map<String, List<String>> headers, - MediaType responseType) { - return processRequest(getOp, url, null, headers, null, responseType); - } - - /** - * This method submits an HTTP GET request against the supplied URL. - * This variant of the method will perform a requested number of retries in the event that the - * first request is unsuccessful. - * - * @param url - The REST endpoint to submit the GET request to. - * @param headers - The headers that should be passed in the request - * @param responseType - The expected format of the response. - * @param numRetries - The number of times to try resubmitting the request in the event of a - * failure. - * - * @return The result of the GET request. - */ - public OperationResult get(String url, Map<String, List<String>> headers, MediaType responseType, - int numRetries) { - return processRequest(getOp, url, null, headers, null, responseType, numRetries); - } - - /** - * This method submits an HTTP DELETE request against the supplied URL. - * - * @param url - The REST endpoint to submit the DELETE request to. - * @param headers - The headers that should be passed in the request - * @param responseType - The expected format of the response. - * - * @return The result of the DELETE request. - */ - public OperationResult delete(String url, Map<String, List<String>> headers, - MediaType responseType) { - return processRequest(deleteOp, url, null, headers, null, responseType); - } - - /** - * This method does a health check ("ping") against the supplied URL. - * - * @param url - The REST endpoint to attempt a health check. - * @param srcAppName - The name of the application using this client. - * @param destAppName - The name of the destination app. - * - * @return A boolean value. True if connection attempt was successful, false otherwise. - * - */ - public boolean healthCheck(String url, String srcAppName, String destAppName) { - return healthCheck(url, srcAppName, destAppName, MediaType.TEXT_PLAIN_TYPE); - - } - - /** - * This method does a health check ("ping") against the supplied URL. - * - * @param url - The REST endpoint to attempt a health check. - * @param srcAppName - The name of the application using this client. - * @param destAppName - The name of the destination app. - * @param responseType - The response type. - * - * @return A boolean value. True if connection attempt was successful, false otherwise. - * - */ - public boolean healthCheck(String url, String srcAppName, String destAppName, - MediaType responseType) { - MultivaluedMap<String, String> headers = new MultivaluedMapImpl(); - headers.put(Headers.FROM_APP_ID, Arrays.asList(new String[] {srcAppName})); - headers.put(Headers.TRANSACTION_ID, Arrays.asList(new String[] {UUID.randomUUID().toString()})); - - try { - logger.info(RestClientMsgs.HEALTH_CHECK_ATTEMPT, destAppName, url); - OperationResult result = get(url, headers, responseType); - - if (result != null && result.getFailureCause() == null) { - logger.info(RestClientMsgs.HEALTH_CHECK_SUCCESS, destAppName, url); - return true; - } else { - logger.error(RestClientMsgs.HEALTH_CHECK_FAILURE, destAppName, url, result != null ? result.getFailureCause() - : null); - return false; - } - } catch (Exception e) { - logger.error(RestClientMsgs.HEALTH_CHECK_FAILURE, destAppName, url, e.getMessage()); - return false; + /** + * Creates a new instance of the {@link RestClient} using the supplied {@link RestClientBuilder}. + * + * @param rcBuilder - The REST client builder that this instance of the {@link RestClient} should use. + */ + public RestClient(RestClientBuilder rcBuilder) { + clientBuilder = rcBuilder; } - } - /** - * This method constructs a client request builder that can be used for submitting REST requests - * to the supplied URL endpoint. - * - * @param client - The REST client we will be using to talk to the server. - * @param url - The URL endpoint that our request will be submitted to. - * @param headers - The headers that should be passed in the request - * @param contentType - the content type of the payload - * @param responseType - The expected format of the response. - * - * @return A client request builder. - */ - private Builder getClientBuilder(Client client, String url, String payload, - Map<String, List<String>> headers, MediaType contentType, MediaType responseType) { + public RestClient authenticationMode(RestAuthenticationMode mode) { + logger.debug("Set rest authentication mode= " + mode); + clientBuilder.setAuthenticationMode(mode); + return this; + } - WebResource resource = client.resource(url); - Builder builder = resource.accept(responseType); + public RestClient basicAuthUsername(String username) { + logger.debug("Set SSL BasicAuth username = " + username); + clientBuilder.setBasicAuthUsername(username); + return this; + } - if (contentType != null) { - builder.type(contentType); + public RestClient basicAuthPassword(String password) { + /* + * purposely not logging out the password, I guess we could obfuscate it if we really want to see it in the logs + */ + clientBuilder.setBasicAuthPassword(password); + return this; } - if (payload != null) { - builder.entity(payload); + /** + * Sets the flag to indicate whether or not validation should be performed against the host name of the server we + * are trying to communicate with. + * + * @parameter validate - Set to true to enable validation, false to disable + * + * @return The AAIRESTClient instance. This is useful for chaining parameter assignments. + */ + public RestClient validateServerHostname(boolean validate) { + logger.debug("Set validate server hostname = " + validate); + clientBuilder.setValidateServerHostname(validate); + return this; } - if (headers != null) { - for (Entry<String, List<String>> header : headers.entrySet()) { - builder.header(header.getKey(), String.join(";",header.getValue())); - } - - //Added additional check to prevent adding duplicate authorization header if client is already sending the authorization header - // AAI-1097 - For AAI calls when Rest authentication mode is selected as SSL_BASIC getting 403 error - if (clientBuilder.getAuthenticationMode() == RestAuthenticationMode.SSL_BASIC && headers.get(Headers.AUTHORIZATION) == null) { - builder = builder.header(Headers.AUTHORIZATION, - clientBuilder.getBasicAuthenticationCredentials()); - } - + /** + * Sets the flag to indicate whether or not validation should be performed against the certificate chain. + * + * @parameter validate - Set to true to enable validation, false to disable. + * + * @return The AAIRESTClient instance. This is useful for chaining parameter assignments. + */ + public RestClient validateServerCertChain(boolean validate) { + logger.debug("Set validate server certificate chain = " + validate); + clientBuilder.setValidateServerCertChain(validate); + return this; } - return builder; - } + /** + * Assigns the client certificate file to use. + * + * @param filename - The name of the certificate file. + * + * @return The AAIRESTClient instance. This is useful for chaining parameter assignments. + */ + public RestClient clientCertFile(String filename) { + logger.debug("Set client certificate filename = " + filename); + clientBuilder.setClientCertFileName(filename); + return this; + } - private void debugRequest(String url, String payload, Map<String, List<String>> headers, - MediaType responseType) { - if (!logger.isDebugEnabled()) { - return; + /** + * Assigns the client certificate password to use. + * + * @param password - The certificate password. + * + * @return The AAIRESTClient instance. This is useful for chaining parameter assignments. + */ + public RestClient clientCertPassword(String password) { + clientBuilder.setClientCertPassword(password); + return this; } - StringBuilder debugRequest = new StringBuilder("REQUEST:\n"); - debugRequest.append("URL: ").append(url).append("\n"); - debugRequest.append("Payload: ").append(payload).append("\n"); - debugRequest.append("Response Type: ").append(responseType).append("\n"); + /** + * Assigns the name of the trust store file to use. + * + * @param filename - the name of the trust store file. + * + * @return The AAIRESTClient instance. This is useful for chaining parameter assignments. + */ + public RestClient trustStore(String filename) { + logger.debug("Set trust store filename = " + filename); + clientBuilder.setTruststoreFilename(filename); + return this; + } - if (headers == null) { - logger.debug(debugRequest.toString()); - return; + /** + * Assigns the connection timeout (in ms) to use when connecting to the target server. + * + * @param timeout - The length of time to wait in ms before timing out. + * + * @return The AAIRESTClient instance. This is useful for chaining parameter assignments. + */ + public RestClient connectTimeoutMs(int timeout) { + logger.debug("Set connection timeout = " + timeout + " ms"); + clientBuilder.setConnectTimeoutInMs(timeout); + return this; } - debugRequest.append("Headers: "); - for (Entry<String, List<String>> header : headers.entrySet()) { - debugRequest.append("\n\t").append(header.getKey()).append(":"); - for (String headerEntry : header.getValue()) { - debugRequest.append("\"").append(headerEntry).append("\" "); - } + /** + * Assigns the read timeout (in ms) to use when communicating with the target server. + * + * @param timeout The read timeout in milliseconds. + * + * @return The AAIRESTClient instance. This is useful for chaining parameter assignments. + */ + public RestClient readTimeoutMs(int timeout) { + logger.debug("Set read timeout = " + timeout + " ms"); + clientBuilder.setReadTimeoutInMs(timeout); + return this; } - logger.debug(debugRequest.toString()); + /** + * Configures the client for a specific SSL protocol + * + * @param sslProtocol - protocol string constant such as TLS, TLSv1, TLSv1.1, TLSv1.2 + * + * @return The AAIRESTClient instance. + */ + public RestClient sslProtocol(String sslProtocol) { + logger.debug("Set sslProtocol = " + sslProtocol); + clientBuilder.setSslProtocol(sslProtocol); + return this; + } + + private boolean shouldRetry(OperationResult operationResult) { + + if (operationResult == null) { + return true; + } + + int resultCode = operationResult.getResultCode(); + + if (resultCode == 200) { + return false; + } - } + if (resultCode == 404) { + return false; + } - private void debugResponse(OperationResult operationResult, - MultivaluedMap<String, String> headers) { + return true; - if (!logger.isDebugEnabled()) { - return; } - StringBuilder debugResponse = new StringBuilder("RESPONSE:\n"); - debugResponse.append("Result: ").append(operationResult.getResultCode()).append("\n"); - debugResponse.append("Failure Cause: ").append(operationResult.getFailureCause()).append("\n"); - debugResponse.append("Payload: ").append(operationResult.getResult()).append("\n"); + /** + * This method operates on a REST endpoint by submitting an HTTP operation request against the supplied URL. This + * variant of the method will perform a requested number of retries in the event that the first request is + * unsuccessful. + * + * @param operation - the REST operation type to send to the url + * @param url - The REST endpoint to submit the REST request to. + * @param payload - They payload to provide in the REST request, if applicable + * @param headers - The headers that should be passed in the request + * @param contentType - The content type of the payload + * @param responseType - The expected format of the response. + * + * @return The result of the REST request. + */ + protected OperationResult processRequest(RestOperation operation, String url, String payload, + Map<String, List<String>> headers, MediaType contentType, MediaType responseType, int numRetries) { + + + OperationResult result = null; - if (headers == null) { - logger.debug(debugResponse.toString()); - return; + long startTimeInMs = System.currentTimeMillis(); + for (int retryCount = 0; retryCount < numRetries; retryCount++) { + + logger.info(RestClientMsgs.HTTP_REQUEST_WITH_RETRIES, operation.getRequestType().toString(), url, + Integer.toString(retryCount + 1)); + + // Submit our query to the AAI. + result = processRequest(operation, url, payload, headers, contentType, responseType); + + // If the submission was successful then we're done. + + if (!shouldRetry(result)) { + + logger.info(RestClientMsgs.HTTP_REQUEST_TIME_WITH_RETRIES, operation.getRequestType().toString(), url, + Long.toString(System.currentTimeMillis() - startTimeInMs), Integer.toString(retryCount)); + + result.setNumRetries(retryCount); + + return result; + } + + // Our submission was unsuccessful... + try { + // Sleep between re-tries to be nice to the target system. + Thread.sleep(50); + + } catch (InterruptedException e) { + logger.error(RestClientMsgs.HTTP_REQUEST_INTERRUPTED, url, e.getLocalizedMessage()); + Thread.currentThread().interrupt(); + break; + } + } + + // If we've gotten this far, then we failed all of our retries. + if (result == null) { + result = new OperationResult(); + } + + result.setNumRetries(numRetries); + result.setResultCode(504); + result.setFailureCause("Failed to get a successful result after multiple retries to target server."); + + + return result; } - debugResponse.append("Headers: "); - for (Entry<String, List<String>> header : headers.entrySet()) { - debugResponse.append("\n\t").append(header.getKey()).append(":"); - for (String headerEntry : header.getValue()) { - debugResponse.append("\"").append(headerEntry).append("\" "); - } + /** + * This method operates on a REST endpoint by submitting an HTTP operation request against the supplied URL. + * + * @param operation - the REST operation type to send to the url + * @param url - The REST endpoint to submit the REST request to. + * @param payload - They payload to provide in the REST request, if applicable + * @param headers - The headers that should be passed in the request + * @param contentType - The content type of the payload + * @param responseType - The expected format of the response. + * + * @return The result of the REST request. + */ + protected OperationResult processRequest(RestOperation operation, String url, String payload, + Map<String, List<String>> headers, MediaType contentType, MediaType responseType) { + + Response clientResponse = null; + OperationResult operationResult = new OperationResult(); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + String requestType = operation.getRequestType().name(); + + // Grab the current time so that we can log how long the + // query took once we are done. + long startTimeInMs = System.currentTimeMillis(); + MdcOverride override = new MdcOverride(); + override.addAttribute(MdcContext.MDC_START_TIME, formatter.format(startTimeInMs)); + + logger.info(RestClientMsgs.HTTP_REQUEST, requestType, url); + + try { + + // Get a REST client instance for our request. + Client client = getClient(); + + // Debug log the request + debugRequest(url, payload, headers, responseType); + + // Get a client request builder, and submit our GET request. + Builder builder = getClientBuilder(client, url, headers, responseType); + clientResponse = operation.processOperation(builder, payload, contentType); + + populateOperationResult(clientResponse, operationResult); + + // Debug log the response + if (clientResponse != null) { + debugResponse(operationResult, clientResponse.getHeaders()); + } + + } catch (Exception ex) { + + logger.error(RestClientMsgs.HTTP_REQUEST_ERROR, requestType, url, ex.getLocalizedMessage()); + operationResult.setResultCode(500); + operationResult + .setFailureCause("Error during GET operation to AAI with message = " + ex.getLocalizedMessage()); + + } finally { + + if (logger.isDebugEnabled()) { + logger.debug(baos.toString()); + } + + // Not every valid response code is actually represented by the Response.Status + // object, so we need to guard against missing codes, otherwise we throw null + // pointer exceptions when we try to generate our metrics logs... + Response.Status responseStatus = Response.Status.fromStatusCode(operationResult.getResultCode()); + String responseStatusCodeString = ""; + if (responseStatus != null) { + responseStatusCodeString = responseStatus.toString(); + } + + metricsLogger.info(RestClientMsgs.HTTP_REQUEST_TIME, + new LogFields().setField(LogLine.DefinedFields.STATUS_CODE, responseStatusCodeString) + .setField(LogLine.DefinedFields.RESPONSE_CODE, operationResult.getResultCode()) + .setField(LogLine.DefinedFields.RESPONSE_DESCRIPTION, operationResult.getResult()), + override, requestType, Long.toString(System.currentTimeMillis() - startTimeInMs), url); + logger.info(RestClientMsgs.HTTP_REQUEST_TIME, requestType, + Long.toString(System.currentTimeMillis() - startTimeInMs), url); + logger.info(RestClientMsgs.HTTP_RESPONSE, url, + operationResult.getResultCode() + " " + responseStatusCodeString); + } + + return operationResult; } - logger.debug(debugResponse.toString()); - } + /** + * This method submits an HTTP PUT request against the supplied URL. + * + * @param url - The REST endpoint to submit the PUT request to. + * @param payload - the payload to send to the supplied URL + * @param headers - The headers that should be passed in the request + * @param contentType - The content type of the payload + * @param responseType - The expected format of the response. + * + * @return The result of the PUT request. + */ + public OperationResult put(String url, String payload, Map<String, List<String>> headers, MediaType contentType, + MediaType responseType) { + return processRequest(putOp, url, payload, headers, contentType, responseType); + } - /** - * This method creates an instance of the low level REST client to use for communicating with the - * AAI, if one has not already been created, otherwise it returns the already created instance. - * - * @return A {@link Client} instance. - */ - protected Client getClient() throws Exception { + /** + * This method submits an HTTP POST request against the supplied URL. + * + * @param url - The REST endpoint to submit the POST request to. + * @param payload - the payload to send to the supplied URL + * @param headers - The headers that should be passed in the request + * @param contentType - The content type of the payload + * @param responseType - The expected format of the response. + * + * @return The result of the POST request. + */ + public OperationResult post(String url, String payload, Map<String, List<String>> headers, MediaType contentType, + MediaType responseType) { + return processRequest(postOp, url, payload, headers, contentType, responseType); + } - /* - * Attempting a new way of doing non-blocking thread-safe lazy-initialization by using Java 1.8 - * computeIfAbsent functionality. A null value will not be stored, but once a valid mapping has - * been established, then the same value will be returned. + /** + * This method submits an HTTP POST request against the supplied URL, and emulates a PATCH operation by setting a + * special header value + * + * @param url - The REST endpoint to submit the POST request to. + * @param payload - the payload to send to the supplied URL + * @param headers - The headers that should be passed in the request + * @param contentType - The content type of the payload + * @param responseType - The expected format of the response. + * + * @return The result of the PATCH request. + */ + public OperationResult patch(String url, String payload, Map<String, List<String>> headers, MediaType contentType, + MediaType responseType) { + return processRequest(patchOp, url, payload, headers, contentType, responseType); + } + + /** + * This method submits an HTTP HEAD request against the supplied URL + * + * @param url - The REST endpoint to submit the POST request to. + * @param headers - The headers that should be passed in the request + * @param responseType - The expected format of the response. + * + * @return The result of the HEAD request. + */ + public OperationResult head(String url, Map<String, List<String>> headers, MediaType responseType) { + return processRequest(headOp, url, null, headers, null, responseType); + } + + /** + * This method submits an HTTP GET request against the supplied URL. + * + * @param url - The REST endpoint to submit the GET request to. + * @param headers - The headers that should be passed in the request + * @param responseType - The expected format of the response. + * + * @return The result of the GET request. + */ + public OperationResult get(String url, Map<String, List<String>> headers, MediaType responseType) { + return processRequest(getOp, url, null, headers, null, responseType); + } + + /** + * This method submits an HTTP GET request against the supplied URL. This variant of the method will perform a + * requested number of retries in the event that the first request is unsuccessful. + * + * @param url - The REST endpoint to submit the GET request to. + * @param headers - The headers that should be passed in the request + * @param responseType - The expected format of the response. + * @param numRetries - The number of times to try resubmitting the request in the event of a failure. * - * One awkwardness of the computeIfAbsent is the lack of support for thrown exceptions, which - * required a bit of hoop jumping to preserve the original exception for the purpose of - * maintaining the pre-existing this API signature. + * @return The result of the GET request. */ - - final InitializedClient clientInstance = - CLIENT_CACHE.computeIfAbsent(REST_CLIENT_INSTANCE, k -> loggedClientInitialization()); - - if (clientInstance.getCaughtException() != null) { - throw new InstantiationException(clientInstance.getCaughtException().getMessage()); + public OperationResult get(String url, Map<String, List<String>> headers, MediaType responseType, int numRetries) { + return processRequest(getOp, url, null, headers, null, responseType, numRetries); } - return clientInstance.getClient(); + /** + * This method submits an HTTP DELETE request against the supplied URL. + * + * @param url - The REST endpoint to submit the DELETE request to. + * @param headers - The headers that should be passed in the request + * @param responseType - The expected format of the response. + * + * @return The result of the DELETE request. + */ + public OperationResult delete(String url, Map<String, List<String>> headers, MediaType responseType) { + return processRequest(deleteOp, url, null, headers, null, responseType); + } - } + /** + * This method does a health check ("ping") against the supplied URL. + * + * @param url - The REST endpoint to attempt a health check. + * @param srcAppName - The name of the application using this client. + * @param destAppName - The name of the destination app. + * + * @return A boolean value. True if connection attempt was successful, false otherwise. + * + */ + public boolean healthCheck(String url, String srcAppName, String destAppName) { + return healthCheck(url, srcAppName, destAppName, MediaType.TEXT_PLAIN_TYPE); - /** - * This method will only be called if computerIfAbsent is true. The return value is null, then the result is not - * stored in the map. - * - * @return a new client instance or null - */ - private InitializedClient loggedClientInitialization() { + } - if (logger.isDebugEnabled()) { - logger.debug("Instantiating REST client with following parameters:"); - logger.debug(clientBuilder.toString()); + /** + * This method does a health check ("ping") against the supplied URL. + * + * @param url - The REST endpoint to attempt a health check. + * @param srcAppName - The name of the application using this client. + * @param destAppName - The name of the destination app. + * @param responseType - The response type. + * + * @return A boolean value. True if connection attempt was successful, false otherwise. + * + */ + public boolean healthCheck(String url, String srcAppName, String destAppName, MediaType responseType) { + Map<String, List<String>> headers = new HashMap<>(); + headers.put(Headers.FROM_APP_ID, Arrays.asList(new String[] {srcAppName})); + headers.put(Headers.TRANSACTION_ID, Arrays.asList(new String[] {UUID.randomUUID().toString()})); + + try { + logger.info(RestClientMsgs.HEALTH_CHECK_ATTEMPT, destAppName, url); + OperationResult result = get(url, headers, responseType); + + if (result != null && result.getFailureCause() == null) { + logger.info(RestClientMsgs.HEALTH_CHECK_SUCCESS, destAppName, url); + return true; + } else { + logger.error(RestClientMsgs.HEALTH_CHECK_FAILURE, destAppName, url, + result != null ? result.getFailureCause() : null); + return false; + } + } catch (Exception e) { + logger.error(RestClientMsgs.HEALTH_CHECK_FAILURE, destAppName, url, e.getMessage()); + return false; + } } - - InitializedClient initClient = new InitializedClient(); - - try { - initClient.setClient(clientBuilder.getClient()); - } catch ( Exception error) { - initClient.setCaughtException(error); + + /** + * This method constructs a client request builder that can be used for submitting REST requests to the supplied URL + * endpoint. + * + * @param client - The REST client we will be using to talk to the server. + * @param url - The URL endpoint that our request will be submitted to. + * @param headers - The headers that should be passed in the request + * @param responseType - The expected format of the response. + * + * @return A client request builder. + */ + private Builder getClientBuilder(Client client, String url, Map<String, List<String>> headers, + MediaType responseType) { + + WebTarget target = client.target(url); + + Builder builder = target.request().accept(responseType); + + if (headers != null) { + for (Entry<String, List<String>> header : headers.entrySet()) { + builder.header(header.getKey(), String.join(";", header.getValue())); + } + + // Added additional check to prevent adding duplicate authorization header if client is already sending the + // authorization header + // AAI-1097 - For AAI calls when Rest authentication mode is selected as SSL_BASIC getting 403 error + if (clientBuilder.getAuthenticationMode() == RestAuthenticationMode.SSL_BASIC + && headers.get(Headers.AUTHORIZATION) == null) { + builder = builder.header(Headers.AUTHORIZATION, clientBuilder.getBasicAuthenticationCredentials()); + } + + } + + return builder; } - - return initClient; - } + private void debugRequest(String url, String payload, Map<String, List<String>> headers, MediaType responseType) { + if (!logger.isDebugEnabled()) { + return; + } + + StringBuilder debugRequest = new StringBuilder("REQUEST:\n"); + debugRequest.append("URL: ").append(url).append("\n"); + debugRequest.append("Payload: ").append(payload).append("\n"); + debugRequest.append("Response Type: ").append(responseType).append("\n"); + + if (headers == null) { + logger.debug(debugRequest.toString()); + return; + } + debugRequest.append("Headers: "); + for (Entry<String, List<String>> header : headers.entrySet()) { + debugRequest.append("\n\t").append(header.getKey()).append(":"); + for (String headerEntry : header.getValue()) { + debugRequest.append("\"").append(headerEntry).append("\" "); + } + } - /** - * This method populates the fields of an {@link OperationResult} instance based on the contents - * of a {@link ClientResponse} received in response to a REST request. - */ - private void populateOperationResult(ClientResponse response, OperationResult opResult) { + logger.debug(debugRequest.toString()); - // If we got back a NULL response, then just produce a generic - // error code and result indicating this. - if (response == null) { - opResult.setResultCode(500); - opResult.setFailureCause("Client response was null"); - return; } - - int statusCode = response.getStatus(); - opResult.setResultCode(statusCode); - if (opResult.wasSuccessful()) { - if (statusCode != Response.Status.NO_CONTENT.getStatusCode()) { - opResult.setResult(response.getEntity(String.class)); + private void debugResponse(OperationResult operationResult, MultivaluedMap<String, Object> headers) { + + if (!logger.isDebugEnabled()) { + return; + } + + StringBuilder debugResponse = new StringBuilder("RESPONSE:\n"); + debugResponse.append("Result: ").append(operationResult.getResultCode()).append("\n"); + debugResponse.append("Failure Cause: ").append(operationResult.getFailureCause()).append("\n"); + debugResponse.append("Payload: ").append(operationResult.getResult()).append("\n"); + + if (headers == null) { + logger.debug(debugResponse.toString()); + return; } - } else { - opResult.setFailureCause(response.getEntity(String.class)); + + debugResponse.append("Headers: "); + for (Entry<String, List<Object>> header : headers.entrySet()) { + debugResponse.append("\n\t").append(header.getKey()).append(":"); + for (Object headerEntry : header.getValue()) { + debugResponse.append("\"").append(headerEntry).append("\" "); + } + } + + logger.debug(debugResponse.toString()); } - opResult.setHeaders(response.getHeaders()); - } + /** + * This method creates an instance of the low level REST client to use for communicating with the AAI, if one has + * not already been created, otherwise it returns the already created instance. + * + * @return A {@link Client} instance. + */ + protected Client getClient() throws Exception { + + /* + * Attempting a new way of doing non-blocking thread-safe lazy-initialization by using Java 1.8 computeIfAbsent + * functionality. A null value will not be stored, but once a valid mapping has been established, then the same + * value will be returned. + * + * One awkwardness of the computeIfAbsent is the lack of support for thrown exceptions, which required a bit of + * hoop jumping to preserve the original exception for the purpose of maintaining the pre-existing this API + * signature. + */ + + final InitializedClient clientInstance = + CLIENT_CACHE.computeIfAbsent(REST_CLIENT_INSTANCE, k -> loggedClientInitialization()); + + if (clientInstance.getCaughtException() != null) { + throw new InstantiationException(clientInstance.getCaughtException().getMessage()); + } + + return clientInstance.getClient(); - private class GetRestOperation implements RestOperation { - public ClientResponse processOperation(Builder builder) { - return builder.get(ClientResponse.class); } - public RequestType getRequestType() { - return RequestType.GET; + /** + * This method will only be called if computerIfAbsent is true. The return value is null, then the result is not + * stored in the map. + * + * @return a new client instance or null + */ + private InitializedClient loggedClientInitialization() { + + if (logger.isDebugEnabled()) { + logger.debug("Instantiating REST client with following parameters:"); + logger.debug(clientBuilder.toString()); + } + + InitializedClient initClient = new InitializedClient(); + + try { + initClient.setClient(clientBuilder.getClient()); + } catch (Exception error) { + initClient.setCaughtException(error); + } + + return initClient; + } - } - private class PutRestOperation implements RestOperation { - public ClientResponse processOperation(Builder builder) { - return builder.put(ClientResponse.class); + + /** + * This method populates the fields of an {@link OperationResult} instance based on the contents of a + * {@link Response} received in response to a REST request. + */ + private void populateOperationResult(Response response, OperationResult opResult) { + + // If we got back a NULL response, then just produce a generic + // error code and result indicating this. + if (response == null) { + opResult.setResultCode(500); + opResult.setFailureCause("Client response was null"); + return; + } + + int statusCode = response.getStatus(); + opResult.setResultCode(statusCode); + + if (opResult.wasSuccessful()) { + if (statusCode != Response.Status.NO_CONTENT.getStatusCode()) { + opResult.setResult(response.readEntity(String.class)); + } + } else { + opResult.setFailureCause(response.readEntity(String.class)); + } + + opResult.setHeaders(convertHeaderObjectsToString(response.getHeaders())); } - public RequestType getRequestType() { - return RequestType.PUT; + private MultivaluedMap<String, String> convertHeaderObjectsToString(MultivaluedMap<String, Object> headers) { + MultivaluedMap<String, String> result = new MultivaluedHashMap<>(); + headers.forEach((k, v) -> result.addAll(k, v.stream().map(Object::toString).collect(Collectors.toList()))); + return result; } - } - private class PostRestOperation implements RestOperation { - public ClientResponse processOperation(Builder builder) { - return builder.post(ClientResponse.class); + private class GetRestOperation implements RestOperation { + @Override + public Response processOperation(Builder builder, String payload, MediaType contentType) { + return builder.get(); + } + + @Override + public RequestType getRequestType() { + return RequestType.GET; + } } - public RequestType getRequestType() { - return RequestType.POST; + private class PutRestOperation implements RestOperation { + @Override + public Response processOperation(Builder builder, String payload, MediaType contentType) { + return builder.put(Entity.entity(payload, contentType)); + } + + @Override + public RequestType getRequestType() { + return RequestType.PUT; + } } - } - private class DeleteRestOperation implements RestOperation { - public ClientResponse processOperation(Builder builder) { - return builder.delete(ClientResponse.class); + private class PostRestOperation implements RestOperation { + @Override + public Response processOperation(Builder builder, String payload, MediaType contentType) { + return builder.post(Entity.entity(payload, contentType)); + } + + @Override + public RequestType getRequestType() { + return RequestType.POST; + } } - public RequestType getRequestType() { - return RequestType.DELETE; + private class DeleteRestOperation implements RestOperation { + @Override + public Response processOperation(Builder builder, String payload, MediaType contentType) { + return builder.delete(); + } + + @Override + public RequestType getRequestType() { + return RequestType.DELETE; + } } - } - - private class HeadRestOperation implements RestOperation { - public ClientResponse processOperation(Builder builder) { - return builder.head(); + + private class HeadRestOperation implements RestOperation { + @Override + public Response processOperation(Builder builder, String payload, MediaType contentType) { + return builder.head(); + } + + @Override + public RequestType getRequestType() { + return RequestType.HEAD; + } } - public RequestType getRequestType() { - return RequestType.HEAD; + private class PatchRestOperation implements RestOperation { + + /** + * Technically there is no standarized PATCH operation for the jersey client, but we can use the method-override + * approach instead. + */ + @Override + public Response processOperation(Builder builder, String payload, MediaType contentType) { + builder = builder.header("X-HTTP-Method-Override", "PATCH"); + return builder.post(Entity.entity(payload, contentType)); + } + + @Override + public RequestType getRequestType() { + return RequestType.PATCH; + } } - } - private class PatchRestOperation implements RestOperation { /** - * Technically there is no standarized PATCH operation for the - * jersey client, but we can use the method-override approach - * instead. + * Interface used wrap a Jersey REST call using a functional interface. */ - public ClientResponse processOperation(Builder builder) { - builder = builder.header("X-HTTP-Method-Override", "PATCH"); - return builder.post(ClientResponse.class); + private interface RestOperation { + + /** + * Method used to wrap the functionality of making a REST call out to the endpoint. + * + * @param builder the Jersey builder used to make the request + * @param payload the request payload + * @param contentType the content type of the payload + * @return the response from the REST endpoint + */ + public Response processOperation(Builder builder, String payload, MediaType contentType); + + /** + * Returns the REST request type. + */ + public RequestType getRequestType(); + + /** + * The supported REST request types. + */ + public enum RequestType { + GET, PUT, POST, DELETE, PATCH, HEAD + } } - public RequestType getRequestType() { - return RequestType.PATCH; - } - } + /* + * An entity to encapsulate an expected result and a potential failure cause when returning from a functional + * interface during the computeIfAbsent call. + */ + private class InitializedClient { + private Client client; + private Throwable caughtException; + public InitializedClient() { + client = null; + caughtException = null; + } - /** - * Interface used wrap a Jersey REST call using a functional interface. - */ - private interface RestOperation { + public Client getClient() { + return client; + } - /** - * Method used to wrap the functionality of making a REST call out to the endpoint. - * - * @param builder the Jersey builder used to make the request - * @return the response from the REST endpoint - */ - public ClientResponse processOperation(Builder builder); + public void setClient(Client client) { + this.client = client; + } - /** - * Returns the REST request type. - */ - public RequestType getRequestType(); + public Throwable getCaughtException() { + return caughtException; + } + + public void setCaughtException(Throwable caughtException) { + this.caughtException = caughtException; + } + + } - /** - * The supported REST request types. - */ - public enum RequestType { - GET, PUT, POST, DELETE, PATCH, HEAD - } - } - - /* - * An entity to encapsulate an expected result and a potential failure cause when returning from a - * functional interface during the computeIfAbsent call. - */ - private class InitializedClient { - private Client client; - private Throwable caughtException; - - public InitializedClient() { - client = null; - caughtException = null; - } - - public Client getClient() { - return client; - } - public void setClient(Client client) { - this.client = client; - } - public Throwable getCaughtException() { - return caughtException; - } - public void setCaughtException(Throwable caughtException) { - this.caughtException = caughtException; - } - - } - } diff --git a/src/main/java/org/onap/aai/restclient/rest/RestClientBuilder.java b/src/main/java/org/onap/aai/restclient/rest/RestClientBuilder.java index 26c5fdf..38a8558 100644 --- a/src/main/java/org/onap/aai/restclient/rest/RestClientBuilder.java +++ b/src/main/java/org/onap/aai/restclient/rest/RestClientBuilder.java @@ -22,281 +22,262 @@ package org.onap.aai.restclient.rest; import java.io.FileInputStream;
import java.security.KeyStore;
-import java.security.cert.X509Certificate;
-
-import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
-import javax.net.ssl.X509TrustManager;
-
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.ClientBuilder;
+import org.glassfish.jersey.client.ClientProperties;
import org.onap.aai.restclient.enums.RestAuthenticationMode;
-import com.sun.jersey.api.client.Client;
-import com.sun.jersey.api.client.config.ClientConfig;
-import com.sun.jersey.api.client.config.DefaultClientConfig;
-import com.sun.jersey.client.urlconnection.HTTPSProperties;
-
/**
- * This is a generic REST Client builder with flexible security validation. Sometimes it's nice to
- * be able to disable server chain cert validation and hostname validation to work-around lab
- * issues, but at the same time be able to provide complete validation with client cert + hostname +
- * server cert chain validation. I used the ModelLoader REST client as a base and merged in the TSUI
- * client I wrote which also validates the server hostname and server certificate chain.
+ * This is a generic REST Client builder with flexible security validation. Sometimes it's nice to be able to disable
+ * server chain cert validation and hostname validation to work-around lab issues, but at the same time be able to
+ * provide complete validation with client cert + hostname + server cert chain validation. I used the ModelLoader REST
+ * client as a base and merged in the TSUI client I wrote which also validates the server hostname and server
+ * certificate chain.
*/
public class RestClientBuilder {
- public static final boolean DEFAULT_VALIDATE_SERVER_HOST = false;
- public static final boolean DEFAULT_VALIDATE_CERT_CHAIN = false;
- public static final String DEFAULT_CLIENT_CERT_FILENAME = null;
- public static final String DEFAULT_CERT_PASSWORD = null;
- public static final String DEFAULT_TRUST_STORE_FILENAME = null;
- public static final int DEFAULT_CONNECT_TIMEOUT_MS = 60000;
- public static final int DEFAULT_READ_TIMEOUT_MS = 60000;
- public static final RestAuthenticationMode DEFAULT_AUTH_MODE = RestAuthenticationMode.SSL_CERT;
- public static final String DEFAULT_BASIC_AUTH_USERNAME = "";
- public static final String DEFAULT_BASIC_AUTH_PASSWORD = "";
- public static final String DEFAULT_SSL_PROTOCOL = "TLS";
-
- private static final String KEYSTORE_ALGORITHM = "SunX509";
- private static final String KEYSTORE_TYPE = "PKCS12";
- private static final String TRUST_STORE_PROPERTY = "javax.net.ssl.trustStore";
-
- private boolean validateServerHostname;
- private boolean validateServerCertChain;
- private String clientCertFileName;
- private String clientCertPassword;
- private String truststoreFilename;
- private int connectTimeoutInMs;
- private int readTimeoutInMs;
- private RestAuthenticationMode authenticationMode;
- private String basicAuthUsername;
- private String basicAuthPassword;
- private String sslProtocol;
-
- /**
- * Rest Client Builder.
- */
- public RestClientBuilder() {
- validateServerHostname = DEFAULT_VALIDATE_SERVER_HOST;
- validateServerCertChain = DEFAULT_VALIDATE_CERT_CHAIN;
- clientCertFileName = DEFAULT_CLIENT_CERT_FILENAME;
- clientCertPassword = DEFAULT_CERT_PASSWORD;
- truststoreFilename = DEFAULT_TRUST_STORE_FILENAME;
- connectTimeoutInMs = DEFAULT_CONNECT_TIMEOUT_MS;
- readTimeoutInMs = DEFAULT_READ_TIMEOUT_MS;
- authenticationMode = DEFAULT_AUTH_MODE;
- basicAuthUsername = DEFAULT_BASIC_AUTH_USERNAME;
- basicAuthPassword = DEFAULT_BASIC_AUTH_PASSWORD;
- sslProtocol = DEFAULT_SSL_PROTOCOL;
- }
-
- public boolean isValidateServerHostname() {
- return validateServerHostname;
- }
-
- public void setValidateServerHostname(boolean validateServerHostname) {
- this.validateServerHostname = validateServerHostname;
- }
-
- public boolean isValidateServerCertChain() {
- return validateServerCertChain;
- }
-
- public void setValidateServerCertChain(boolean validateServerCertChain) {
- this.validateServerCertChain = validateServerCertChain;
- }
-
- public String getClientCertFileName() {
- return clientCertFileName;
- }
-
- public void setClientCertFileName(String clientCertFileName) {
- this.clientCertFileName = clientCertFileName;
- }
-
- public String getClientCertPassword() {
- return clientCertPassword;
- }
-
- public void setClientCertPassword(String clientCertPassword) {
- this.clientCertPassword = clientCertPassword;
- }
-
- public String getTruststoreFilename() {
- return truststoreFilename;
- }
-
- public void setTruststoreFilename(String truststoreFilename) {
- this.truststoreFilename = truststoreFilename;
- }
-
- public int getConnectTimeoutInMs() {
- return connectTimeoutInMs;
- }
-
- public void setConnectTimeoutInMs(int connectTimeoutInMs) {
- this.connectTimeoutInMs = connectTimeoutInMs;
- }
-
- public int getReadTimeoutInMs() {
- return readTimeoutInMs;
- }
-
- public void setReadTimeoutInMs(int readTimeoutInMs) {
- this.readTimeoutInMs = readTimeoutInMs;
- }
-
- public RestAuthenticationMode getAuthenticationMode() {
- return authenticationMode;
- }
-
- public void setAuthenticationMode(RestAuthenticationMode authenticationMode) {
- this.authenticationMode = authenticationMode;
- }
-
- public String getBasicAuthUsername() {
- return basicAuthUsername;
- }
-
- public void setBasicAuthUsername(String basicAuthUsername) {
- this.basicAuthUsername = basicAuthUsername;
- }
-
- public String getBasicAuthPassword() {
- return basicAuthPassword;
- }
-
- public void setBasicAuthPassword(String basicAuthPassword) {
- this.basicAuthPassword = basicAuthPassword;
- }
-
- public String getSslProtocol() {
- return sslProtocol;
- }
-
- public void setSslProtocol(String sslProtocol) {
- this.sslProtocol = sslProtocol;
- }
-
- /**
- * Returns Client configured for SSL
- */
- public Client getClient() throws Exception {
-
- switch (authenticationMode) {
- case SSL_BASIC:
- case SSL_CERT:
- return getClient(true);
-
- default:
- // return basic non-authenticating HTTP client
- return getClient(false);
+ public static final boolean DEFAULT_VALIDATE_SERVER_HOST = false;
+ public static final boolean DEFAULT_VALIDATE_CERT_CHAIN = false;
+ public static final String DEFAULT_CLIENT_CERT_FILENAME = null;
+ public static final String DEFAULT_CERT_PASSWORD = null;
+ public static final String DEFAULT_TRUST_STORE_FILENAME = null;
+ public static final int DEFAULT_CONNECT_TIMEOUT_MS = 60000;
+ public static final int DEFAULT_READ_TIMEOUT_MS = 60000;
+ public static final RestAuthenticationMode DEFAULT_AUTH_MODE = RestAuthenticationMode.SSL_CERT;
+ public static final String DEFAULT_BASIC_AUTH_USERNAME = "";
+ public static final String DEFAULT_BASIC_AUTH_PASSWORD = "";
+ public static final String DEFAULT_SSL_PROTOCOL = "TLS";
+
+ private static final String KEYSTORE_ALGORITHM = "SunX509";
+ private static final String KEYSTORE_TYPE = "PKCS12";
+ private static final String TRUST_STORE_PROPERTY = "javax.net.ssl.trustStore";
+
+ private boolean validateServerHostname;
+ private boolean validateServerCertChain;
+ private String clientCertFileName;
+ private String clientCertPassword;
+ private String truststoreFilename;
+ private int connectTimeoutInMs;
+ private int readTimeoutInMs;
+ private RestAuthenticationMode authenticationMode;
+ private String basicAuthUsername;
+ private String basicAuthPassword;
+ private String sslProtocol;
+
+ /**
+ * Rest Client Builder.
+ */
+ public RestClientBuilder() {
+ validateServerHostname = DEFAULT_VALIDATE_SERVER_HOST;
+ validateServerCertChain = DEFAULT_VALIDATE_CERT_CHAIN;
+ clientCertFileName = DEFAULT_CLIENT_CERT_FILENAME;
+ clientCertPassword = DEFAULT_CERT_PASSWORD;
+ truststoreFilename = DEFAULT_TRUST_STORE_FILENAME;
+ connectTimeoutInMs = DEFAULT_CONNECT_TIMEOUT_MS;
+ readTimeoutInMs = DEFAULT_READ_TIMEOUT_MS;
+ authenticationMode = DEFAULT_AUTH_MODE;
+ basicAuthUsername = DEFAULT_BASIC_AUTH_USERNAME;
+ basicAuthPassword = DEFAULT_BASIC_AUTH_PASSWORD;
+ sslProtocol = DEFAULT_SSL_PROTOCOL;
+ }
+
+ public boolean isValidateServerHostname() {
+ return validateServerHostname;
+ }
+
+ public void setValidateServerHostname(boolean validateServerHostname) {
+ this.validateServerHostname = validateServerHostname;
+ }
+
+ public boolean isValidateServerCertChain() {
+ return validateServerCertChain;
+ }
+
+ public void setValidateServerCertChain(boolean validateServerCertChain) {
+ this.validateServerCertChain = validateServerCertChain;
+ }
+
+ public String getClientCertFileName() {
+ return clientCertFileName;
+ }
+
+ public void setClientCertFileName(String clientCertFileName) {
+ this.clientCertFileName = clientCertFileName;
+ }
+
+ public String getClientCertPassword() {
+ return clientCertPassword;
+ }
+
+ public void setClientCertPassword(String clientCertPassword) {
+ this.clientCertPassword = clientCertPassword;
+ }
+
+ public String getTruststoreFilename() {
+ return truststoreFilename;
+ }
+
+ public void setTruststoreFilename(String truststoreFilename) {
+ this.truststoreFilename = truststoreFilename;
+ }
+
+ public int getConnectTimeoutInMs() {
+ return connectTimeoutInMs;
+ }
+
+ public void setConnectTimeoutInMs(int connectTimeoutInMs) {
+ this.connectTimeoutInMs = connectTimeoutInMs;
}
- }
-
- protected void setupSecureSocketLayerClientConfig(ClientConfig clientConfig) throws Exception {
- // Check to see if we need to perform proper validation of
- // the certificate chains.
- TrustManager[] trustAllCerts = null;
- if (truststoreFilename != null) {
- System.setProperty(TRUST_STORE_PROPERTY, truststoreFilename);
- } else {
- throw new IllegalArgumentException("Trust store filename must be set!");
- }
-
- // Set up the SSL context, keystore, etc. to use for our connection
- // to the AAI.
- SSLContext ctx = SSLContext.getInstance(sslProtocol);
- KeyManagerFactory kmf = KeyManagerFactory.getInstance(KEYSTORE_ALGORITHM);
- KeyStore ks = KeyStore.getInstance(KEYSTORE_TYPE);
-
- char[] pwd = null;
- if (clientCertPassword != null) {
- pwd = clientCertPassword.toCharArray();
+ public int getReadTimeoutInMs() {
+ return readTimeoutInMs;
}
- if (clientCertFileName != null) {
- FileInputStream fin = new FileInputStream(clientCertFileName);
+ public void setReadTimeoutInMs(int readTimeoutInMs) {
+ this.readTimeoutInMs = readTimeoutInMs;
+ }
+
+ public RestAuthenticationMode getAuthenticationMode() {
+ return authenticationMode;
+ }
- // Load the keystore and initialize the key manager factory.
- ks.load(fin, pwd);
- kmf.init(ks, pwd);
+ public void setAuthenticationMode(RestAuthenticationMode authenticationMode) {
+ this.authenticationMode = authenticationMode;
+ }
- ctx.init(kmf.getKeyManagers(), trustAllCerts, null);
- } else {
- ctx.init(null, trustAllCerts, null);
+ public String getBasicAuthUsername() {
+ return basicAuthUsername;
}
- // Are we performing validation of the server host name?
- if (validateServerHostname) {
- clientConfig.getProperties().put(HTTPSProperties.PROPERTY_HTTPS_PROPERTIES,
- new HTTPSProperties(null, ctx));
-
- } else {
- clientConfig.getProperties().put(HTTPSProperties.PROPERTY_HTTPS_PROPERTIES,
- new HTTPSProperties(new HostnameVerifier() {
- @Override
- public boolean verify(String str, SSLSession sslSession) {
- return true;
- }
- }, ctx));
+ public void setBasicAuthUsername(String basicAuthUsername) {
+ this.basicAuthUsername = basicAuthUsername;
}
- }
+ public String getBasicAuthPassword() {
+ return basicAuthPassword;
+ }
- /**
- * Returns client instance
- *
- * @param useSsl - used to configure the client with an ssl-context or just plain http
- */
- protected Client getClient(boolean useSsl) throws Exception {
+ public void setBasicAuthPassword(String basicAuthPassword) {
+ this.basicAuthPassword = basicAuthPassword;
+ }
- ClientConfig clientConfig = new DefaultClientConfig();
+ public String getSslProtocol() {
+ return sslProtocol;
+ }
- if (useSsl) {
- setupSecureSocketLayerClientConfig(clientConfig);
+ public void setSslProtocol(String sslProtocol) {
+ this.sslProtocol = sslProtocol;
}
- // Finally, create and initialize our client...
- Client client = null;
- client = Client.create(clientConfig);
- client.setConnectTimeout(connectTimeoutInMs);
- client.setReadTimeout(readTimeoutInMs);
-
- // ...and return it to the caller.
- return client;
- }
-
- public String getBasicAuthenticationCredentials() {
-
- String usernameAndPassword = getBasicAuthUsername() + ":" + getBasicAuthPassword();
- return "Basic " + java.util.Base64.getEncoder().encodeToString(usernameAndPassword.getBytes());
- }
-
- /*
- * Added a little bit of logic to obfuscate passwords that could be logged out
- * (non-Javadoc)
- * @see java.lang.Object#toString()
- */
- @Override
- public String toString() {
- return "RestClientBuilder [validateServerHostname=" + validateServerHostname
- + ", validateServerCertChain=" + validateServerCertChain + ", "
- + (clientCertFileName != null ? "clientCertFileName=" + clientCertFileName + ", " : "")
- + (clientCertPassword != null
- ? "clientCertPassword="
- + java.util.Base64.getEncoder().encodeToString(clientCertPassword.getBytes()) + ", "
- : "")
- + (truststoreFilename != null ? "truststoreFilename=" + truststoreFilename + ", " : "")
- + "connectTimeoutInMs=" + connectTimeoutInMs + ", readTimeoutInMs=" + readTimeoutInMs + ", "
- + (authenticationMode != null ? "authenticationMode=" + authenticationMode + ", " : "")
- + (basicAuthUsername != null ? "basicAuthUsername=" + basicAuthUsername + ", " : "")
- + (basicAuthPassword != null ? "basicAuthPassword="
- + java.util.Base64.getEncoder().encodeToString(basicAuthPassword.getBytes()) : "")
- + "]";
- }
+ /**
+ * Returns Client configured for SSL
+ */
+ public Client getClient() throws Exception {
+
+ switch (authenticationMode) {
+ case SSL_BASIC:
+ case SSL_CERT:
+ return getClient(true);
+
+ default:
+ // return basic non-authenticating HTTP client
+ return getClient(false);
+ }
+
+ }
+
+ protected void setupSecureSocketLayerClientConfig(ClientBuilder builder) throws Exception {
+ // Check to see if we need to perform proper validation of
+ // the certificate chains.
+ TrustManager[] trustAllCerts = null;
+ if (truststoreFilename != null) {
+ System.setProperty(TRUST_STORE_PROPERTY, truststoreFilename);
+ } else {
+ throw new IllegalArgumentException("Trust store filename must be set!");
+ }
+
+ // Set up the SSL context, keystore, etc. to use for our connection
+ // to the AAI.
+ SSLContext ctx = SSLContext.getInstance(sslProtocol);
+ KeyManagerFactory kmf = KeyManagerFactory.getInstance(KEYSTORE_ALGORITHM);
+ KeyStore ks = KeyStore.getInstance(KEYSTORE_TYPE);
+
+ char[] pwd = null;
+ if (clientCertPassword != null) {
+ pwd = clientCertPassword.toCharArray();
+ }
+
+ if (clientCertFileName != null) {
+ FileInputStream fin = new FileInputStream(clientCertFileName);
+
+ // Load the keystore and initialize the key manager factory.
+ ks.load(fin, pwd);
+ kmf.init(ks, pwd);
+
+ ctx.init(kmf.getKeyManagers(), trustAllCerts, null);
+ } else {
+ ctx.init(null, trustAllCerts, null);
+ }
+
+ builder.sslContext(ctx);
+
+ // Are we performing validation of the server host name?
+ if (!validateServerHostname) {
+ builder.hostnameVerifier((String str, SSLSession sslSession) -> true);
+ }
+ }
+
+ /**
+ * Returns client instance
+ *
+ * @param useSsl - used to configure the client with an ssl-context or just plain http
+ */
+ protected Client getClient(boolean useSsl) throws Exception {
+
+ // Finally, create and initialize our client...
+ ClientBuilder builder = ClientBuilder.newBuilder();
+ if (useSsl) {
+ setupSecureSocketLayerClientConfig(builder);
+ }
+ Client client = builder.build();
+ client.property(ClientProperties.CONNECT_TIMEOUT, connectTimeoutInMs);
+ client.property(ClientProperties.READ_TIMEOUT, readTimeoutInMs);
+
+ // ...and return it to the caller.
+ return client;
+ }
+
+ public String getBasicAuthenticationCredentials() {
+
+ String usernameAndPassword = getBasicAuthUsername() + ":" + getBasicAuthPassword();
+ return "Basic " + java.util.Base64.getEncoder().encodeToString(usernameAndPassword.getBytes());
+ }
+
+ /*
+ * Added a little bit of logic to obfuscate passwords that could be logged out (non-Javadoc)
+ *
+ * @see java.lang.Object#toString()
+ */
+ @Override
+ public String toString() {
+ return "RestClientBuilder [validateServerHostname=" + validateServerHostname + ", validateServerCertChain="
+ + validateServerCertChain + ", "
+ + (clientCertFileName != null ? "clientCertFileName=" + clientCertFileName + ", " : "")
+ + (clientCertPassword != null
+ ? "clientCertPassword="
+ + java.util.Base64.getEncoder().encodeToString(clientCertPassword.getBytes()) + ", "
+ : "")
+ + (truststoreFilename != null ? "truststoreFilename=" + truststoreFilename + ", " : "")
+ + "connectTimeoutInMs=" + connectTimeoutInMs + ", readTimeoutInMs=" + readTimeoutInMs + ", "
+ + (authenticationMode != null ? "authenticationMode=" + authenticationMode + ", " : "")
+ + (basicAuthUsername != null ? "basicAuthUsername=" + basicAuthUsername + ", " : "")
+ + (basicAuthPassword != null ? "basicAuthPassword="
+ + java.util.Base64.getEncoder().encodeToString(basicAuthPassword.getBytes()) : "")
+ + "]";
+ }
}
diff --git a/src/main/resources/logging/RESTClientMsgs.properties b/src/main/resources/logging/RESTClientMsgs.properties index 75d2b8d..2d1ce83 100644 --- a/src/main/resources/logging/RESTClientMsgs.properties +++ b/src/main/resources/logging/RESTClientMsgs.properties @@ -44,6 +44,10 @@ HEALTH_CHECK_SUCCESS=\ AC0006I|\ Successfully established connection to {0} at {1} +HTTP_REQUEST_TIME_WITH_RETRIES=\ + AC0007I|\ + {0} request at url = {1} operation time = {2} attempt number = {3} + HTTP_REQUEST_INTERRUPTED=\ AC2001E|\ {0} request interrupted while sleeping at url = {1} with cause = {2} |