summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorShwetank Dave <shwetank.dave@amdocs.com>2017-05-08 15:36:26 -0400
committerShwetank Dave <shwetank.dave@amdocs.com>2017-05-08 15:37:11 -0400
commit8a665d85c9ea91f024e9a378779aad107550b832 (patch)
tree9b334e3d5dbc7a6cd518fae0872bac14792fbdfc /src
parent72744d5784f7227ace9bceac0f5576d5b18b544c (diff)
Initial OpenEcomp A&AI Rest Client commit.
Change-Id: Ic6949778061bdf141431c4b14ea2417da6aa1e57 Signed-off-by: Shwetank Dave <shwetank.dave@amdocs.com>
Diffstat (limited to 'src')
-rw-r--r--src/main/java/org/openecomp/restclient/client/Headers.java37
-rw-r--r--src/main/java/org/openecomp/restclient/client/OperationResult.java83
-rw-r--r--src/main/java/org/openecomp/restclient/client/RestClient.java686
-rw-r--r--src/main/java/org/openecomp/restclient/logging/RestClientMsgs.java114
-rw-r--r--src/main/java/org/openecomp/restclient/rest/HttpUtil.java115
-rw-r--r--src/main/java/org/openecomp/restclient/rest/RestClientBuilder.java229
-rw-r--r--src/main/resources/logging/RESTClientMsgs.properties57
-rw-r--r--src/test/java/org/openecomp/restclient/client/RESTClientTest.java391
-rw-r--r--src/test/java/org/openecomp/restclient/rest/RestClientBuilderTest.java166
9 files changed, 1878 insertions, 0 deletions
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<String, String> headers;
+
+ /**
+ * Get the HTTP headers of the response.
+ *
+ * @return the HTTP headers of the response.
+ */
+ public MultivaluedMap<String, String> getHeaders() {
+ return headers;
+ }
+
+ public void setHeaders(MultivaluedMap<String, String> 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<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, 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<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
+ 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 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.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<String, List<String>> 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<String, List<String>> header : headers.entrySet()) {
+ builder.header(header.getKey(), header.getValue());
+ }
+ }
+
+ return builder;
+ }
+
+ private void debugRequest(String url, String payload, Map<String, List<String>> 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<String, List<String>> 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<String, String> 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<String, List<String>> 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.
+ * <p>
+ * Specifically, the following scenarios are covered:<br>
+ * 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.
+ * <p>
+ * 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.
+ * <p>
+ * 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.
+ * <p>
+ * 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.
+ * <p>
+ * 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<String, Object> 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<String, Object> props = aClient.getProperties();
+ return (HTTPSProperties) props.get(HTTPSProperties.PROPERTY_HTTPS_PROPERTIES);
+ }
+}