From 8a665d85c9ea91f024e9a378779aad107550b832 Mon Sep 17 00:00:00 2001 From: Shwetank Dave Date: Mon, 8 May 2017 15:36:26 -0400 Subject: [PATCH] Initial OpenEcomp A&AI Rest Client commit. Change-Id: Ic6949778061bdf141431c4b14ea2417da6aa1e57 Signed-off-by: Shwetank Dave --- .gitignore | 6 + .gitreview | 4 + License.txt | 22 + README.md | 60 ++ pom.xml | 167 +++++ .../org/openecomp/restclient/client/Headers.java | 37 ++ .../restclient/client/OperationResult.java | 83 +++ .../openecomp/restclient/client/RestClient.java | 686 +++++++++++++++++++++ .../restclient/logging/RestClientMsgs.java | 114 ++++ .../org/openecomp/restclient/rest/HttpUtil.java | 115 ++++ .../restclient/rest/RestClientBuilder.java | 229 +++++++ .../resources/logging/RESTClientMsgs.properties | 57 ++ .../restclient/client/RESTClientTest.java | 391 ++++++++++++ .../restclient/rest/RestClientBuilderTest.java | 166 +++++ version.properties | 13 + 15 files changed, 2150 insertions(+) create mode 100644 .gitignore create mode 100644 .gitreview create mode 100644 License.txt create mode 100644 README.md create mode 100644 pom.xml create mode 100644 src/main/java/org/openecomp/restclient/client/Headers.java create mode 100644 src/main/java/org/openecomp/restclient/client/OperationResult.java create mode 100644 src/main/java/org/openecomp/restclient/client/RestClient.java create mode 100644 src/main/java/org/openecomp/restclient/logging/RestClientMsgs.java create mode 100644 src/main/java/org/openecomp/restclient/rest/HttpUtil.java create mode 100644 src/main/java/org/openecomp/restclient/rest/RestClientBuilder.java create mode 100644 src/main/resources/logging/RESTClientMsgs.properties create mode 100644 src/test/java/org/openecomp/restclient/client/RESTClientTest.java create mode 100644 src/test/java/org/openecomp/restclient/rest/RestClientBuilderTest.java create mode 100644 version.properties diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e2b927a --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.classpath +.project +.settings/ +target/ +logs/ +debug-logs/ diff --git a/.gitreview b/.gitreview new file mode 100644 index 0000000..449c2df --- /dev/null +++ b/.gitreview @@ -0,0 +1,4 @@ +[gerrit] +host=gerrit.onap.org +port=29418 +project=aai/rest-client.git diff --git a/License.txt b/License.txt new file mode 100644 index 0000000..8a1e022 --- /dev/null +++ b/License.txt @@ -0,0 +1,22 @@ +============LICENSE_START======================================================= +RestClient +================================================================================ +Copyright © 2017 AT&T Intellectual Property. +Copyright © 2017 Amdocs +All rights reserved. +================================================================================ +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +============LICENSE_END========================================================= + +ECOMP and OpenECOMP are trademarks +and service marks of AT&T Intellectual Property. diff --git a/README.md b/README.md new file mode 100644 index 0000000..d17a448 --- /dev/null +++ b/README.md @@ -0,0 +1,60 @@ +# Common REST Client Library + +This library provides a single client implementation to be used by micro services for communicating via its REST API. + +--- + +## Usage +In order to make the _REST Client_ library available to your microservice, include the following dependency in your service's pom.xml: + + + + org.openecomp.aai + rest-client + 1.0.0-SNAPSHOT + + +## Code Examples + +### Creating and Configuring a Client Instance +In order to start talking to a service, you need to create a client instance and configure it. The _RestClient_ uses a fluent interface which allows it to be both instantiated and configured as in the following example: + + // Create an instance of the Rest Client and configure it. + RestClient myClient = new RestClient() + .validateServerHostname(false) + .validateServerCertChain(true) + .clientCertFile("certificate_filename") + .trustStroe("trust_store_filename") + .connectTimeoutMs(1000) + .readTimeoutMs(1000) + +Note, that all of the above configuration parameters are optional and will be set to default values if they are not specified. + +### Querying The A&AI +Once your service has a client instance, it can query the _Active & Available Inventory_ by specifying an HTTP endpoint, headers, and the expected response format: + + MultivaluedMap headers = new MultivaluedMapImpl(); + headers.put("Accept", Arrays.asList(new String[]{"application/json"})); + headers.put("X-FromAppId", Arrays.asList(new String[]{"APP-ID"})); + headers.put("X-TransactionId", Arrays.asList(new String[]{UUID.randomUUID().toString()})); + + OperationResult result = myClient.queryActiveInventory("http://some/endpoint", headers, RestClient.RESPONSE_MIME_TYPE.JSON); + + // You can also specify number of re-tries: + int retries = 3 + OperationResult result = myClient.queryActiveInventory("http://some/endpoint", headers, RestClient.RESPONSE_MIME_TYPE.JSON, retries); + + +The result of the query is returned as an _OperationResult_ object, which can be unpacked in the following manner: + +The standard HTTP result code received back from the _A&AI_ is accessible as follows: + + int resultCode = getResultCode() + +The actual result payload is accessible in the following manner: + + String resultPayload = result.getResult() + +Finally, in the event of a failure, a failure cause message will be populated and can be accessed as follows: + + String failureCause = result.getFailureCause() diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..8d98e17 --- /dev/null +++ b/pom.xml @@ -0,0 +1,167 @@ + + 4.0.0 + + org.openecomp.aai + rest-client + 1.0.0-SNAPSHOT + REST Client + + + google_checks.xml + https://nexus.onap.org + + java + jacoco + ${project.build.directory}/surefire-reports + ${project.build.directory}/coverage-reports/jacoco.exec + false + ${project.version} + + + + + com.sun.jersey + jersey-client + 1.18 + + + + org.openecomp.aai.logging-service + common-logging + 1.0.0 + + + + com.sun.jersey.jersey-test-framework + jersey-test-framework-grizzly2 + 1.18 + test + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.8 + 1.8 + + + + maven-release-plugin + 2.4.2 + + + org.apache.maven.scm + maven-scm-provider-gitexe + 1.8.1 + + + + + + + + org.apache.maven.plugins + maven-site-plugin + 3.3 + + + + org.apache.maven.plugins + maven-checkstyle-plugin + 2.17 + + + + checkstyle + + + + + + + + + + + + + com.mycila + license-maven-plugin + 3.0 + +
License.txt
+ + src/main/java/** + +
+ + + + format + + process-sources + + +
+ + + org.sonatype.plugins + nexus-staging-maven-plugin + 1.6.7 + true + + ${nexusproxy} + 176c31dfe190a + ecomp-staging + + + + + org.codehaus.mojo + sonar-maven-plugin + 3.2 + + + org.jacoco + jacoco-maven-plugin + 0.7.7.201606060606 + + true + + + + jacoco-initialize-unit-tests + + prepare-agent + + + ${project.build.directory}/coverage-reports/jacoco.exec + + + + + +
+
+ + + + ecomp-releases + ECOMP Release Repository + ${nexusproxy}/content/repositories/releases/ + + + ecomp-snapshots + ECOMP Snapshot Repository + ${nexusproxy}/content/repositories/snapshots/ + + + +
diff --git a/src/main/java/org/openecomp/restclient/client/Headers.java b/src/main/java/org/openecomp/restclient/client/Headers.java new file mode 100644 index 0000000..aac817e --- /dev/null +++ b/src/main/java/org/openecomp/restclient/client/Headers.java @@ -0,0 +1,37 @@ +/** + * ============LICENSE_START======================================================= + * RestClient + * ================================================================================ + * Copyright © 2017 AT&T Intellectual Property. + * Copyright © 2017 Amdocs + * All rights reserved. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + * + * ECOMP and OpenECOMP are trademarks + * and service marks of AT&T Intellectual Property. + */ +package org.openecomp.restclient.client; + +public final class Headers { + + public static final String FROM_APP_ID = "X-FromAppId"; + public static final String TRANSACTION_ID = "X-TransactionId"; + public static final String RESOURCE_VERSION = "resourceVersion"; + public static final String ETAG = "ETag"; + public static final String IF_MATCH = "If-Match"; + public static final String IF_NONE_MATCH = "If-None-Match"; + public static final String ACCEPT = "Accept"; + +} diff --git a/src/main/java/org/openecomp/restclient/client/OperationResult.java b/src/main/java/org/openecomp/restclient/client/OperationResult.java new file mode 100644 index 0000000..c9d0f9c --- /dev/null +++ b/src/main/java/org/openecomp/restclient/client/OperationResult.java @@ -0,0 +1,83 @@ +/** + * ============LICENSE_START======================================================= + * RestClient + * ================================================================================ + * Copyright © 2017 AT&T Intellectual Property. + * Copyright © 2017 Amdocs + * All rights reserved. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + * + * ECOMP and OpenECOMP are trademarks + * and service marks of AT&T Intellectual Property. + */ +package org.openecomp.restclient.client; + +import javax.ws.rs.core.MultivaluedMap; + +public class OperationResult { + + private String result; + private String failureCause; + private int resultCode; + private MultivaluedMap headers; + + /** + * Get the HTTP headers of the response. + * + * @return the HTTP headers of the response. + */ + public MultivaluedMap getHeaders() { + return headers; + } + + public void setHeaders(MultivaluedMap headers) { + this.headers = headers; + } + + + public String getResult() { + return result; + } + + public void setResult(String result) { + this.result = result; + } + + public int getResultCode() { + return resultCode; + } + + public String getFailureCause() { + return failureCause; + } + + public void setFailureCause(String failureCause) { + this.failureCause = failureCause; + } + + public void setResultCode(int resultCode) { + this.resultCode = resultCode; + } + + public OperationResult() { + super(); + } + + @Override + public String toString() { + return "OperationResult [result=" + result + ", resultCode=" + resultCode + "]"; + } + +} diff --git a/src/main/java/org/openecomp/restclient/client/RestClient.java b/src/main/java/org/openecomp/restclient/client/RestClient.java new file mode 100644 index 0000000..900c4e0 --- /dev/null +++ b/src/main/java/org/openecomp/restclient/client/RestClient.java @@ -0,0 +1,686 @@ +/** + * ============LICENSE_START======================================================= + * RestClient + * ================================================================================ + * Copyright © 2017 AT&T Intellectual Property. + * Copyright © 2017 Amdocs + * All rights reserved. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + * + * ECOMP and OpenECOMP are trademarks + * and service marks of AT&T Intellectual Property. + */ +package org.openecomp.restclient.client; + +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; + +import org.openecomp.cl.api.LogFields; +import org.openecomp.cl.api.LogLine; +import org.openecomp.cl.api.Logger; +import org.openecomp.cl.eelf.LoggerFactory; +import org.openecomp.cl.mdc.MdcContext; +import org.openecomp.cl.mdc.MdcOverride; +import org.openecomp.restclient.logging.RestClientMsgs; +import org.openecomp.restclient.rest.RestClientBuilder; + +import java.io.ByteArrayOutputStream; +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.UUID; + +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; + + +/** + * 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; + + /** + * The low level instance of the REST client that will be used to communicate with the endpoint. + */ + private Client restClient = null; + + /** 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(); + + /** + * 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; + } + + + /** + * 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) { + logger.debug("Set client certificate password = " + 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; + } + + /** + * 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> 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, 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 (Integer.toString(result.getResultCode()).charAt(0) == '2') { + logger.info(RestClientMsgs.HTTP_REQUEST_TIME_WITH_RETRIES, + Long.toString(System.currentTimeMillis() - startTimeInMs), url, + Integer.toString(retryCount)); + return result; + } + + // Our submission was unsuccessful... + try { + // Sleep between re-tries to be nice to the target system. + Thread.sleep(500); + + } catch (InterruptedException e) { + logger.error(RestClientMsgs.HTTP_REQUEST_INTERRUPTED, url, e.getLocalizedMessage()); + break; + } + } + + // If we've gotten this far, then we failed all of our retries. + 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> 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 + 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> 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> headers, + MediaType contentType, MediaType responseType) { + return processRequest(postOp, url, payload, headers, contentType, 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> 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> 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> 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 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.getFailureCause()); + return false; + } + } catch (Exception e) { + logger.error(RestClientMsgs.HEALTH_CHECK_FAILURE, destAppName, url, e.getMessage()); + return false; + } + } + + /** + * 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> headers, MediaType contentType, MediaType responseType) { + + WebResource resource = client.resource(url); + Builder builder = null; + + builder = resource.accept(responseType); + + if (contentType != null) { + builder.type(contentType); + } + + if (payload != null) { + builder.entity(payload); + } + + if (headers != null) { + for (Entry> header : headers.entrySet()) { + builder.header(header.getKey(), header.getValue()); + } + } + + return builder; + } + + private void debugRequest(String url, String payload, Map> headers, + MediaType responseType) { + if (logger.isDebugEnabled()) { + 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) { + debugRequest.append("Headers: "); + for (Entry> header : headers.entrySet()) { + debugRequest.append("\n\t").append(header.getKey()).append(":"); + for (String headerEntry : header.getValue()) { + debugRequest.append("\"").append(headerEntry).append("\" "); + } + } + } + logger.debug(debugRequest.toString()); + } + } + + private void debugResponse(OperationResult operationResult, + MultivaluedMap headers) { + if (logger.isDebugEnabled()) { + 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) { + debugResponse.append("Headers: "); + for (Entry> header : headers.entrySet()) { + debugResponse.append("\n\t").append(header.getKey()).append(":"); + for (String headerEntry : header.getValue()) { + debugResponse.append("\"").append(headerEntry).append("\" "); + } + } + } + logger.debug(debugResponse.toString()); + } + } + + + /** + * 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. + */ + private synchronized Client getClient() throws Exception { + + if (restClient == null) { + + if (logger.isDebugEnabled()) { + logger.debug("Instantiating REST client with following parameters:"); + logger.debug( + " validate server hostname = " + clientBuilder.isValidateServerHostname()); + logger.debug( + " validate server certificate chain = " + clientBuilder.isValidateServerCertChain()); + logger.debug( + " client certificate filename = " + clientBuilder.getClientCertFileName()); + logger.debug( + " client certificate password = " + clientBuilder.getClientCertPassword()); + logger.debug( + " trust store filename = " + clientBuilder.getTruststoreFilename()); + logger.debug(" connection timeout = " + + clientBuilder.getConnectTimeoutInMs() + " ms"); + logger.debug( + " read timeout = " + clientBuilder.getReadTimeoutInMs() + " ms"); + } + + restClient = clientBuilder.getClient(); + } + + return restClient; + } + + + /** + * 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) { + + // 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(); + String payload = response.getEntity(String.class); + + opResult.setResultCode(statusCode); + + if ((statusCode < 200) || (statusCode > 299)) { + opResult.setFailureCause(payload); + } else { + opResult.setResult(payload); + } + + opResult.setHeaders(response.getHeaders()); + } + + private class GetRestOperation implements RestOperation { + public ClientResponse processOperation(Builder builder) { + return builder.get(ClientResponse.class); + } + + public RequestType getRequestType() { + return RequestType.GET; + } + } + + private class PutRestOperation implements RestOperation { + public ClientResponse processOperation(Builder builder) { + return builder.put(ClientResponse.class); + } + + public RequestType getRequestType() { + return RequestType.PUT; + } + } + + private class PostRestOperation implements RestOperation { + public ClientResponse processOperation(Builder builder) { + return builder.post(ClientResponse.class); + } + + public RequestType getRequestType() { + return RequestType.POST; + } + } + + private class DeleteRestOperation implements RestOperation { + public ClientResponse processOperation(Builder builder) { + return builder.delete(ClientResponse.class); + } + + public RequestType getRequestType() { + return RequestType.DELETE; + } + } + + /** + * Interface used wrap a Jersey REST call using a functional interface. + */ + 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 + * @return the response from the REST endpoint + */ + public ClientResponse processOperation(Builder builder); + + /** + * Returns the REST request type. + */ + public RequestType getRequestType(); + + /** + * The supported REST request types. + */ + public enum RequestType { + GET, PUT, POST, DELETE; + } + } +} diff --git a/src/main/java/org/openecomp/restclient/logging/RestClientMsgs.java b/src/main/java/org/openecomp/restclient/logging/RestClientMsgs.java new file mode 100644 index 0000000..0b59139 --- /dev/null +++ b/src/main/java/org/openecomp/restclient/logging/RestClientMsgs.java @@ -0,0 +1,114 @@ +/** + * ============LICENSE_START======================================================= + * RestClient + * ================================================================================ + * Copyright © 2017 AT&T Intellectual Property. + * Copyright © 2017 Amdocs + * All rights reserved. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + * + * ECOMP and OpenECOMP are trademarks + * and service marks of AT&T Intellectual Property. + */ +package org.openecomp.restclient.logging; + +import com.att.eelf.i18n.EELFResourceManager; +import org.openecomp.cl.eelf.LogMessageEnum; + +public enum RestClientMsgs implements LogMessageEnum { + + /** + * Arguments: + * {0} = HTTP operation + * {1} = URL + */ + HTTP_REQUEST, + + /** + * Arguments: + * {0} = HTTP operation + * {1} = URL + * {2} = Attempt count. + */ + HTTP_REQUEST_WITH_RETRIES, + + /** + * Arguments: + * {0} = HTTP operation + * {1} - URL + * {2} - Operation time in ms. + */ + HTTP_REQUEST_TIME, + + /** + * Arguments: + * {0} = HTTP operation + * {1} - URL + * {2} - Operation time in ms. + * {3} - Retry count. + */ + HTTP_REQUEST_TIME_WITH_RETRIES, + + /** + * Arguments: + * {0} = HTTP operation + * {1} - URL + * {2} - Error message. + */ + HTTP_REQUEST_INTERRUPTED, + + /** + * Arguments: + * {0} = HTTP operation + * {1} - URL + * {2} - Error message. + */ + HTTP_REQUEST_ERROR, + + /** + * . Arguments: + * {0} = Target URL + */ + HEALTH_CHECK_ATTEMPT, + + /** + * . Arguments: + * {0} = Target URL + */ + HEALTH_CHECK_SUCCESS, + + /** + * . Arguments: + * {0} = Target URL + * {1} = failure cause + */ + HEALTH_CHECK_FAILURE, + + + /** + * . Arguments: + * {0} = URL + * {1} - Response code + */ + HTTP_RESPONSE; + + + /** + * Static initializer to ensure the resource bundles for this class are loaded... + */ + static { + EELFResourceManager.loadMessageBundle("logging/RESTClientMsgs"); + } +} diff --git a/src/main/java/org/openecomp/restclient/rest/HttpUtil.java b/src/main/java/org/openecomp/restclient/rest/HttpUtil.java new file mode 100644 index 0000000..89af684 --- /dev/null +++ b/src/main/java/org/openecomp/restclient/rest/HttpUtil.java @@ -0,0 +1,115 @@ +/** + * ============LICENSE_START======================================================= + * RestClient + * ================================================================================ + * Copyright © 2017 AT&T Intellectual Property. + * Copyright © 2017 Amdocs + * All rights reserved. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + * + * ECOMP and OpenECOMP are trademarks + * and service marks of AT&T Intellectual Property. + */ +package org.openecomp.restclient.rest; + +public class HttpUtil { + + /** + * Determines if the provided HTTP response is present in the provided list of acceptable response + * codes. + * + * @param response the http response we got from our request + * @param list the list of acceptable response codes + * @return true if the http response is in the provided list + */ + public static boolean isHttpResponseInList(int response, int... list) { + for (int checkCode : list) { + if (checkCode == response) { + return true; + } + } + return false; + } + + /** + * Determines if the provided http response is of the information class. + * + * @param response the http response we got from our request + * @return true if the response is of the informational class and false otherwise + */ + public static boolean isHttpResponseClassInformational(int response) { + return isExpectedHttpResponseClass(response, '1'); + } + + /** + * Determines if the provided http response is of the success class. + * + * @param response the http response we got from our request + * @return true if the response is of the success class and false otherwise + */ + public static boolean isHttpResponseClassSuccess(int response) { + return isExpectedHttpResponseClass(response, '2'); + } + + /** + * Determines if the provided http response is of the redirection class. + * + * @param response the http response we got from our request + * @return true if the response is of the redirection class and false otherwise + */ + public static boolean isHttpResponseClassRedirection(int response) { + return isExpectedHttpResponseClass(response, '3'); + } + + /** + * Determines if the provided http response is of the client error class. + * + * @param response the http response we got from our request + * @return true if the response is of the client error class and false otherwise + */ + public static boolean isHttpResponseClassClientError(int response) { + return isExpectedHttpResponseClass(response, '4'); + } + + /** + * Determines if the provided http response is of the server error class. + * + * @param response the http response we got from our request + * @return true if the response is of the server error class and false otherwise + */ + public static boolean isHttpResponseClassServerError(int response) { + return isExpectedHttpResponseClass(response, '5'); + } + + /** + * Helper method to determine if we have received the response class we are expecting. + * + * @param response the http response we got from our request + * @param expectedClass the expected http response class ie: 1, 2, 3, 4, 5 which maps to 1xx, 2xx, + * 3xx, 4xx, 5xx respectively + * @return true if the response if of our expected class and false if not + */ + private static boolean isExpectedHttpResponseClass(int response, char expectedClass) { + if (response < 100 || response >= 600) { + return false; + } + + if (Integer.toString(response).charAt(0) == expectedClass) { + return true; + } + + return false; + } +} diff --git a/src/main/java/org/openecomp/restclient/rest/RestClientBuilder.java b/src/main/java/org/openecomp/restclient/rest/RestClientBuilder.java new file mode 100644 index 0000000..3d546fe --- /dev/null +++ b/src/main/java/org/openecomp/restclient/rest/RestClientBuilder.java @@ -0,0 +1,229 @@ +/** + * ============LICENSE_START======================================================= + * RestClient + * ================================================================================ + * Copyright © 2017 AT&T Intellectual Property. + * Copyright © 2017 Amdocs + * All rights reserved. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + * + * ECOMP and OpenECOMP are trademarks + * and service marks of AT&T Intellectual Property. + */ +package org.openecomp.restclient.rest; + +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; + +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; + +/** + * 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. + * + * @author DAVEA + * + */ +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; + + private static final String SSL_PROTOCOL = "TLS"; + private static final String KEYSTORE_ALGORITHM = "SunX509"; + private static final String KEYSTORE_TYPE = "PKCS12"; + + /* + * TODO: implement fluent interface? + */ + + private boolean validateServerHostname; + private boolean validateServerCertChain; + private String clientCertFileName; + private String clientCertPassword; + private String truststoreFilename; + private int connectTimeoutInMs; + private int readTimeoutInMs; + + /** + * 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; + } + + 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; + } + + /** + * Returns Client. + */ + public Client getClient() throws Exception { + + ClientConfig clientConfig = new DefaultClientConfig(); + + // Check to see if we need to perform proper validation of + // the certificate chains. + TrustManager[] trustAllCerts = null; + if (validateServerCertChain) { + if (truststoreFilename != null) { + System.setProperty("javax.net.ssl.trustStore", truststoreFilename); + } else { + throw new IllegalArgumentException("Trust store filename must be set!"); + } + + } else { + + // We aren't validating certificates, so create a trust manager that does + // not validate certificate chains. + trustAllCerts = new TrustManager[] {new X509TrustManager() { + public X509Certificate[] getAcceptedIssuers() { + return null; + } + + public void checkClientTrusted(X509Certificate[] certs, String authType) {} + + public void checkServerTrusted(X509Certificate[] certs, String authType) {} + }}; + } + + // Set up the SSL context, keystore, etc. to use for our connection + // to the AAI. + SSLContext ctx = SSLContext.getInstance(SSL_PROTOCOL); + 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); + } + + + // 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)); + } + + // 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; + } +} diff --git a/src/main/resources/logging/RESTClientMsgs.properties b/src/main/resources/logging/RESTClientMsgs.properties new file mode 100644 index 0000000..9df0764 --- /dev/null +++ b/src/main/resources/logging/RESTClientMsgs.properties @@ -0,0 +1,57 @@ +#Resource key=Error Code|Message text|Resolution text |Description text +####### +#Newlines can be utilized to add some clarity ensuring continuing line +#has atleast one leading space +#ResourceKey=\ +# ERR0000E\ +# Sample error msg txt\ +# Sample resolution msg\ +# Sample description txt +# +###### +#Error code classification category +#000 Info/Debug +#100 Permission errors +#200 Availability errors/Timeouts +#300 Data errors +#400 Schema Interface type/validation errors +#500 Business process errors +#900 Unknown errors +# +######################################################################## + +HTTP_REQUEST=\ + AC0001I|\ + {0} request at url = {1} + +HTTP_REQUEST_WITH_RETRIES=\ + AC0002I|\ + {0} request at url = {1} attempt number = {2} + +HTTP_REQUEST_TIME=\ + AC0003I|\ + {0} request operation time = {1} ms for link = {2} + +HTTP_RESPONSE=\ + AC0004I|\ + request at url = {0} resulted in http response: {1} + +HEALTH_CHECK_ATTEMPT=\ + AC0005I|\ + Attempting to connect to {0} at {1} + +HEALTH_CHECK_SUCCESS=\ + AC0006I|\ + Successfully established connection to {0} at {1} + +HTTP_REQUEST_INTERRUPTED=\ + AC2001E|\ + {0} request interrupted while sleeping at url = {1} with cause = {2} + +HTTP_REQUEST_ERROR=\ + AC2002E|\ + Error during {0} operation to endpoint at url = {1} with error = {2} + +HEALTH_CHECK_FAILURE=\ + AC2003E|\ + Failed to establish connection to {0} at {1}. Cause {2} diff --git a/src/test/java/org/openecomp/restclient/client/RESTClientTest.java b/src/test/java/org/openecomp/restclient/client/RESTClientTest.java new file mode 100644 index 0000000..b049c38 --- /dev/null +++ b/src/test/java/org/openecomp/restclient/client/RESTClientTest.java @@ -0,0 +1,391 @@ +package org.openecomp.restclient.client; + +import org.junit.Before; +import org.junit.Test; +import org.openecomp.restclient.client.OperationResult; +import org.openecomp.restclient.client.RestClient; +import org.openecomp.restclient.rest.RestClientBuilder; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertEquals; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.core.MediaType; + +import com.sun.jersey.test.framework.AppDescriptor; +import com.sun.jersey.test.framework.JerseyTest; +import com.sun.jersey.test.framework.WebAppDescriptor; + +import com.sun.jersey.api.client.Client; + + +/** + * This suite of tests is intended to exercise the behaviour of the {@link RestClient}. + */ +public class RESTClientTest extends JerseyTest { + + private static final String GOOD_AAI_ENDPOINT = "testaai/good"; + private static final String FAIL_ALWAYS_AAI_ENDPOINT = "testaai/failalways"; + private static final String FAIL_THEN_SUCCEED_ENDPOINT = "testaai/failthensucceed"; + private static final String INVALID_AAI_ENDPOINT = "testaai/bad"; + + private static final String AAI_GET_REPLY_PAYLOAD = "Reply from AAI"; + + private static final int SUCCESS_RESULT_CODE = 200; + private static final int INVALID_END_POINT_RESULT_CODE = 404; + private static final int INTERNAL_ERR_RESULT_CODE = 500; + private static final int TIMEOUT_RESULT_CODE = 504; + + + /** + * Creates a new instance of the {@link RESTClientTest} test suite. + */ + public RESTClientTest() throws Exception { + + // Tell our in memory container to look here for resource endpoints. + super("org.openecomp.restclient.client"); + } + + + @Override + protected AppDescriptor configure() { + return new WebAppDescriptor.Builder().build(); + } + + + /** + * Perform common initialization actions that need to run before every unit test. + */ + @Before + public void setup() { + + // Initialize our test endpoints to make sure that all of their + // counters have valid starting values + AAI_FailAlways_Stub.initialize(); + AAI_FailThenSucceed_Stub.initialize(); + } + + + /** + * This test validates that all of the {@link RestClient}'s configurable parameters can be set via + * its fluent interface and that those values are successfully passed down to the underlying + * {@link RestClientBuilder} instance. + */ + @Test + public void configureAAIClientTest() { + + final boolean VALIDATE_SERVER = true; + final boolean VALIDATE_CERT_CHAIN = true; + final String CLIENT_CERT_FILE = "myCertFile"; + final String CLIENT_CERT_PASSWORD = "My voice is my password"; + final String TRUST_STORE = "myTrustStore"; + final int CONNECT_TIMEOUT = 5000; + final int READ_TIMEOUT = 5000; + + // Create an instance of our test version of the REST client builder. + TestRestClientBuilder clientBuilder = new TestRestClientBuilder(); + + // Now, create a new instance of the {@link AAIClient} and configure + // its parameters. + RestClient testClient = + new RestClient(clientBuilder).validateServerHostname(true).validateServerCertChain(true) + .clientCertFile("myCertFile").clientCertPassword("My voice is my password") + .trustStore("myTrustStore").connectTimeoutMs(5000).readTimeoutMs(5000); + + // Validate that the parameters of the test REST client builder that + // we passed to the AAI client have been set according to what we + // passed in when we instantiated the AAI client. + assertEquals("Unexpected 'validate server host name' value", VALIDATE_SERVER, + clientBuilder.isValidateServerHostname()); + assertEquals("Unexpected 'validate certificat chain' value", VALIDATE_CERT_CHAIN, + clientBuilder.isValidateServerCertChain()); + assertTrue("Unexpected client certificate filename", + CLIENT_CERT_FILE.equals(clientBuilder.getClientCertFileName())); + assertTrue("Unexpected client certificate password", + CLIENT_CERT_PASSWORD.equals(clientBuilder.getClientCertPassword())); + assertTrue("Unexpected trust store filename", + TRUST_STORE.equals(clientBuilder.getTruststoreFilename())); + assertEquals("Unexpected connection timeout value", CONNECT_TIMEOUT, + clientBuilder.getConnectTimeoutInMs()); + assertEquals("Unexpected read timeout value", READ_TIMEOUT, clientBuilder.getReadTimeoutInMs()); + } + + + /** + * This test validates that the {@link RestClient} can submit a GET request to a valid REST + * endpoint and receive a valid response. + */ + @Test + public void queryAAI_SuccessTest() { + + // Create an instance of the AAIClient that uses our test version of + // the REST client builder. + RestClient testClient = new RestClient(new TestRestClientBuilder()); + + // Query our stubbed out AAI with a URL that we expecte to get a successful + // reply from. + OperationResult or = + testClient.get(getBaseURI() + GOOD_AAI_ENDPOINT, null, MediaType.APPLICATION_JSON_TYPE); + + // Validate that a successful query returns a result code of 200. + assertEquals("Unexpected result code", SUCCESS_RESULT_CODE, or.getResultCode()); + + // Validate that no error cause gets set on a successful query. + assertNull("Operation result failure code should not be set for successful GET", + or.getFailureCause()); + + // Validate that our query returned the expected payload from our dummy + // AAI. + assertTrue("Incorrect payload returned from AAI query", + AAI_GET_REPLY_PAYLOAD.equals(or.getResult())); + } + + + /** + * This test validates that the {@link RestClient} behaves as expected when query requests are + * unsuccessful. + *

+ * Specifically, the following scenarios are covered:
+ * 1) Submitting a GET request to an invalid REST endpoint 2) Submitting a GET request to a valid + * endpoint which throws an error rather than replying successfully. + *

+ * Note that this test exercises the 'single attempt' variant of the query method. + */ + @Test + public void queryAAI_FailureTest() { + + // Create an instance of the AAIClient that uses our test version of + // the REST client builder. + RestClient testClient = new RestClient(new TestRestClientBuilder()); + + // Query our stubbed out AAI with a URL that we expecte to get a successful + // reply from. + OperationResult or = + testClient.get(getBaseURI() + INVALID_AAI_ENDPOINT, null, MediaType.APPLICATION_JSON_TYPE); + + // Validate that an attempt to query a non-existing endpoint results in + // a 404 error. + assertEquals("Unexpected result code", INVALID_END_POINT_RESULT_CODE, or.getResultCode()); + + // Validate that no payload was set since the query failed. + assertNull("Payload should not be set on 404 error", or.getResult()); + + // Now, submit a query request to the stubbed AAI. + or = testClient.get(getBaseURI() + FAIL_ALWAYS_AAI_ENDPOINT, null, + MediaType.APPLICATION_JSON_TYPE); + + // Validate that a query to a avalid returns a result code of 500. + assertEquals("Unexpected result code", INTERNAL_ERR_RESULT_CODE, or.getResultCode()); + } + + + /** + * This test validates the behaviour of querying the AAI with a number of retries requested in the + * case where we never get a successful reply. + */ + @Test + public void queryAAIWithRetries_TimeoutTest() { + + int NUM_RETRIES = 3; + + + // Create an instance of the AAIClient that uses our test version of + // the REST client builder. + RestClient testClient = new RestClient(new TestRestClientBuilder()); + + // Initialize our test endpoint to make sure that all of its + // counters have valid starting values + // AAI_FailAlways_Stub.initialize(); + + // Perform a query against the stubbed AAI, specifying a number of times + // to retry in the event of an error. + OperationResult or = testClient.get(getBaseURI() + FAIL_ALWAYS_AAI_ENDPOINT, null, + MediaType.APPLICATION_JSON_TYPE, NUM_RETRIES); + + // Validate that failing for all of our retry attempts results in a + // 504 error. + assertEquals("Unexpected result code", TIMEOUT_RESULT_CODE, or.getResultCode()); + + // Validate that our stubbed AAI actually received the expected number + // of retried requests. + assertEquals("Unexpected number of retries", NUM_RETRIES, AAI_FailAlways_Stub.getCount); + } + + + /** + * This test validates the behaviour of querying the AAI with a number of retries requested in the + * case where our query initially fails but then succeeds on one of the subsequent retries. + */ + @Test + public void queryAAIWithRetries_FailThenSucceedTest() { + + int num_retries = AAI_FailThenSucceed_Stub.MAX_FAILURES + 2; + + // Create an instance of the AAIClient that uses our test version of + // the REST client builder. + RestClient testClient = new RestClient(new TestRestClientBuilder()); + + // Initialize our test endpoint to make sure that all of its + // counters have valid starting values. + // AAI_FailThenSucceed_Stub.initialize(); + + // Perform a query against the stubbed AAI, specifying a number of times + // to retry in the event of an error. + OperationResult or = testClient.get(getBaseURI() + FAIL_THEN_SUCCEED_ENDPOINT, null, + MediaType.APPLICATION_JSON_TYPE, num_retries); + + // Validate that after failing a few attempts we finally got back a + // success code. + assertEquals("Unexpected result code", SUCCESS_RESULT_CODE, or.getResultCode()); + + // Validate that our stubbed AAI actually received the expected number + // of retried requests. + assertEquals("Unexpected number of retries", AAI_FailThenSucceed_Stub.MAX_FAILURES + 1, + AAI_FailThenSucceed_Stub.getCount); + } + + + /** + * This class provides a simple in-memory REST end point to stand in for a real AAI. + *

+ * This endpoint always returns a valid reply to a GET request and is used for success path + * testing. + */ + @Path(GOOD_AAI_ENDPOINT) + public static class AAI_Success_Stub { + + /** + * This is the end point for GET requests. It just returns a simple, pre-canned response + * payload. + * + * @return - A pre-canned response. + */ + @GET + public String getEndpoint() { + return AAI_GET_REPLY_PAYLOAD; + } + } + + + /** + * This class provides a simple in-memory REST end point to stand in for a real AAI. + *

+ * This endpoint always returns throws an error instead of responding successfully and is used for + * certain failure path tests. + */ + @Path(FAIL_ALWAYS_AAI_ENDPOINT) + public static class AAI_FailAlways_Stub { + + /** + * Maintains a running count of the number of GET requests that have been received. + */ + public static int getCount; + + + /** + * Resets all of the endpoints counters. + */ + public static void initialize() { + getCount = 0; + } + + + /** + * This is the end point for GET requests. It just throws an error instead of returning a valid + * response. + * + * @return - NONE. We actually throw an exception intentionally instead of returning. + */ + @GET + public String getEndpoint() { + + // Keep track of the number of get requests that we have received + // so that this value can be used for validation purposes later. + getCount++; + + // Always just throw an error instead of replying successfully. + throw new UnsupportedOperationException("Intentional Failure"); + } + } + + + /** + * This class provides a simple in-memory REST end point to stand in for a real AAI. + *

+ * This end point will throw errors instead of responding for a certain number of requests, after + * which it will return a valid, pre-canned response. + * + * @return - A pre-canned response. + */ + @Path(FAIL_THEN_SUCCEED_ENDPOINT) + public static class AAI_FailThenSucceed_Stub { + + /** + * The number of requests for which we should throw errors before responding successfully. + */ + public static int MAX_FAILURES = 2; + + /** + * Maintains a running count of the number of GET requests that have been received. + */ + public static int getCount; + + /** + * Maintains a running count of the number of requests which we have failed, so that we will + * know when to stop failing and return a valid response. + */ + private static int failCount; + + + /** + * Resets all of the endpoints counters. + */ + public static void initialize() { + getCount = 0; + failCount = 0; + } + + + /** + * This is the end point for GET requests. It will throw errors for a certain number of requests + * and then return a valid response. + * + * @return - A pre-canned response string. + */ + @GET + public String getEndpoint() { + + // Keep track of the number of get requests that we have received + // so that this value can be used for validation purposes later. + getCount++; + + // We only want to fail a set number of times, so check now to + // see what we should do. + if (failCount < MAX_FAILURES) { + failCount++; + throw new UnsupportedOperationException("Intentional Failure"); + + } else { + // We've failed as often as we need to. Time to reply + // successfully. + failCount = 0; + return AAI_GET_REPLY_PAYLOAD; + } + } + } + + + /** + * This class overrides the behaviour of the {@link RestClientBuilder} used by the + * {@link RestClient} to just return the in memory client provided by the JerseyTest framework. + */ + private class TestRestClientBuilder extends RestClientBuilder { + + @Override + public Client getClient() throws Exception { + return client(); + } + } +} diff --git a/src/test/java/org/openecomp/restclient/rest/RestClientBuilderTest.java b/src/test/java/org/openecomp/restclient/rest/RestClientBuilderTest.java new file mode 100644 index 0000000..93e5520 --- /dev/null +++ b/src/test/java/org/openecomp/restclient/rest/RestClientBuilderTest.java @@ -0,0 +1,166 @@ +package org.openecomp.restclient.rest; + +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; +import org.openecomp.restclient.rest.RestClientBuilder; + +import static org.junit.Assert.*; + +import com.sun.jersey.api.client.Client; + +import com.sun.jersey.client.urlconnection.HTTPSProperties; + + +/** + * This suite of tests is intended to exercise the functionality of the generice REST client + * builder. + */ +public class RestClientBuilderTest { + + /** + * This test validates that we can enable and disable certificate chain verification and that the + * associated parameters are correctly set. + */ + @Test + public void certificateChainVerificationTest() throws Exception { + + final String TRUST_STORE_FILENAME = "myTrustStore"; + + + // Instantiate a RestClientBuilder with default parameters and + // get a client instance. + RestClientBuilder builder = new RestClientBuilder(); + Client client = builder.getClient(); + + // Validate that, by default, no trust store has been set. + assertNull("Trust store filename should not be set for default builder", + System.getProperty("javax.net.ssl.trustStore")); + + // Now, enable certificate chain verification, but don't specify + // a trust store filename. + builder.setValidateServerCertChain(true); + + // Now, get a new client instance. We expect the builder to complain + // because there is no trust store filename. + try { + Client client2 = builder.getClient(); + fail("Expected exception due to no trust store filename."); + + } catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("Trust store filename must be set")); + } + + // Now, set a value for the trust store filename and try again to + // get a client instance. This time it should succeed and we should + // see that our trust name filename was set. + builder.setTruststoreFilename(TRUST_STORE_FILENAME); + Client client3 = builder.getClient(); + + // Validate that the trust store filename was set. + assertNotNull("Expected trust store filename to be set", + System.getProperty("javax.net.ssl.trustStore")); + + // Validate that the filename is set to the value we specified. + assertTrue( + "Unexpected trust store filename value " + System.getProperty("javax.net.ssl.trustStore"), + System.getProperty("javax.net.ssl.trustStore").equals(TRUST_STORE_FILENAME)); + } + + + /** + * This test validates that we can set timeout values in our client builder and that those values + * are reflected in the client produced by the builder. + */ + @Test + public void timeoutValuesTest() throws Exception { + + // Instantiate a RestClientBuilder with default parameters. + RestClientBuilder builder = new RestClientBuilder(); + + // Now, get a client instance and retrieve the client properties. + Client client = builder.getClient(); + + Map props = client.getProperties(); + + // Validate that the connection and read timeouts are set to the + // default values. + assertEquals("Unexpected connect timeout parameter", + props.get("com.sun.jersey.client.property.connectTimeout"), + RestClientBuilder.DEFAULT_CONNECT_TIMEOUT_MS); + assertEquals("Unexpected read timeout parameter", + props.get("com.sun.jersey.client.property.readTimeout"), + RestClientBuilder.DEFAULT_READ_TIMEOUT_MS); + + // Now, change the timeouts in the builder to non-default values. + builder.setConnectTimeoutInMs(RestClientBuilder.DEFAULT_CONNECT_TIMEOUT_MS + 100); + builder.setReadTimeoutInMs(RestClientBuilder.DEFAULT_READ_TIMEOUT_MS + 100); + + // Retrieve a new client instance and get the client properties. + Client client2 = builder.getClient(); + props = client2.getProperties(); + + // Validate that the connection and read timeouts are set to the + // new values. + assertEquals("Unexpected connect timeout parameter", + props.get("com.sun.jersey.client.property.connectTimeout"), + RestClientBuilder.DEFAULT_CONNECT_TIMEOUT_MS + 100); + assertEquals("Unexpected read timeout parameter", + props.get("com.sun.jersey.client.property.readTimeout"), + RestClientBuilder.DEFAULT_READ_TIMEOUT_MS + 100); + } + + + /** + * This test validates that we can enable and disable host name verification in the clients + * produced by our builder. + */ + @Test + public void hostNameVerifierTest() throws Exception { + + // Instantiate a RestClientBuilder with default parameters. + RestClientBuilder builder = new RestClientBuilder(); + + // Now, get a client instance. + Client client1 = builder.getClient(); + + // Retrieve the client's HTTPS properties. + HTTPSProperties httpProps = getHTTPSProperties(client1); + + // By default, hostname verification should be disabled, which means + // that our builder will have injected its own {@link HostnameVerifier} + // which just always returns true. + assertNotNull(httpProps.getHostnameVerifier()); + + // Verify that the host name verifier returns true regardless of what + // hostname we pass in. + assertTrue("Default hostname verifier should always return true", + httpProps.getHostnameVerifier().verify("not_a_valid_hostname", null)); + + + // Now, enable hostname verification for our client builder, and + // get a new client. + builder.setValidateServerHostname(true); + Client client2 = builder.getClient(); + + // Retrieve the client's HTTPS properties. + httpProps = getHTTPSProperties(client2); + + // Verify that with hostname verification enabled, our builder did not + // insert its own stubbed verifier. + assertNull(httpProps.getHostnameVerifier()); + } + + + /** + * This is a convenience method which extracts the HTTPS properties from a supplied client. + * + * @parameter aClient - The client to retrieve the HTTPS properties from. + */ + private HTTPSProperties getHTTPSProperties(Client aClient) { + + Map props = aClient.getProperties(); + return (HTTPSProperties) props.get(HTTPSProperties.PROPERTY_HTTPS_PROPERTIES); + } +} diff --git a/version.properties b/version.properties new file mode 100644 index 0000000..2ccc7c2 --- /dev/null +++ b/version.properties @@ -0,0 +1,13 @@ +# Versioning variables +# Note that these variables cannot be structured (e.g. : version.release or version.snapshot etc... ) +# because they are used in Jenkins, whose plug-in doesn't support + +major=0 +minor=9 +patch=0 + +base_version=${major}.${minor}.${patch} + +# Release must be completed with git revision # in Jenkins +release_version=${base_version} +snapshot_version=${base_version}-SNAPSHOT \ No newline at end of file -- 2.16.6