900c4e0b6d794d6311fec9ffb7bafa0d970154be
[aai/rest-client.git] / src / main / java / org / openecomp / restclient / client / RestClient.java
1 /**
2  * ============LICENSE_START=======================================================
3  * RestClient
4  * ================================================================================
5  * Copyright © 2017 AT&T Intellectual Property.
6  * Copyright © 2017 Amdocs
7  * All rights reserved.
8  * ================================================================================
9  * Licensed under the Apache License, Version 2.0 (the "License");
10  * you may not use this file except in compliance with the License.
11  * You may obtain a copy of the License at
12  *
13  *    http://www.apache.org/licenses/LICENSE-2.0
14  *
15  * Unless required by applicable law or agreed to in writing, software
16  * distributed under the License is distributed on an "AS IS" BASIS,
17  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18  * See the License for the specific language governing permissions and
19  * limitations under the License.
20  * ============LICENSE_END=========================================================
21  *
22  * ECOMP and OpenECOMP are trademarks
23  * and service marks of AT&T Intellectual Property.
24  */
25 package org.openecomp.restclient.client;
26
27 import com.sun.jersey.api.client.Client;
28 import com.sun.jersey.api.client.ClientResponse;
29 import com.sun.jersey.api.client.WebResource;
30 import com.sun.jersey.api.client.WebResource.Builder;
31 import com.sun.jersey.core.util.MultivaluedMapImpl;
32
33 import org.openecomp.cl.api.LogFields;
34 import org.openecomp.cl.api.LogLine;
35 import org.openecomp.cl.api.Logger;
36 import org.openecomp.cl.eelf.LoggerFactory;
37 import org.openecomp.cl.mdc.MdcContext;
38 import org.openecomp.cl.mdc.MdcOverride;
39 import org.openecomp.restclient.logging.RestClientMsgs;
40 import org.openecomp.restclient.rest.RestClientBuilder;
41
42 import java.io.ByteArrayOutputStream;
43 import java.text.SimpleDateFormat;
44 import java.util.Arrays;
45 import java.util.List;
46 import java.util.Map;
47 import java.util.Map.Entry;
48 import java.util.UUID;
49
50 import javax.ws.rs.core.MediaType;
51 import javax.ws.rs.core.MultivaluedMap;
52 import javax.ws.rs.core.Response;
53
54
55 /**
56  * This class provides a general client implementation that micro services can use for communicating
57  * with the endpoints via their exposed REST interfaces.
58  */
59 public class RestClient {
60
61   /**
62    * This is a generic builder that is used for constructing the REST client that we will use to
63    * communicate with the REST endpoint.
64    */
65   private RestClientBuilder clientBuilder;
66
67   /**
68    * The low level instance of the REST client that will be used to communicate with the endpoint.
69    */
70   private Client restClient = null;
71
72   /** Standard logger for producing log statements. */
73   private Logger logger = LoggerFactory.getInstance().getLogger("AAIRESTClient");
74
75   /** Standard logger for producing metric statements. */
76   private Logger metricsLogger = LoggerFactory.getInstance().getMetricsLogger("AAIRESTClient");
77
78   private SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX");
79
80   /** Reusable function call for GET REST operations. */
81   private final RestOperation getOp = new GetRestOperation();
82
83   /** Reusable function call for PUT REST operations. */
84   private final RestOperation putOp = new PutRestOperation();
85
86   /** Reusable function call for POST REST operations. */
87   private final RestOperation postOp = new PostRestOperation();
88
89   /** Reusable function call for DELETE REST operations. */
90   private final RestOperation deleteOp = new DeleteRestOperation();
91
92   /**
93    * Creates a new instance of the {@link RestClient}.
94    */
95   public RestClient() {
96     clientBuilder = new RestClientBuilder();
97   }
98
99
100   /**
101    * Creates a new instance of the {@link RestClient} using the supplied {@link RestClientBuilder}.
102    *
103    * @param rcBuilder - The REST client builder that this instance of the {@link RestClient} should
104    *        use.
105    */
106   public RestClient(RestClientBuilder rcBuilder) {
107     clientBuilder = rcBuilder;
108   }
109
110
111   /**
112    * Sets the flag to indicate whether or not validation should be performed against the host name
113    * of the server we are trying to communicate with.
114    *
115    * @parameter validate - Set to true to enable validation, false to disable
116    *
117    * @return The AAIRESTClient instance. This is useful for chaining parameter assignments.
118    */
119   public RestClient validateServerHostname(boolean validate) {
120     logger.debug("Set validate server hostname = " + validate);
121     clientBuilder.setValidateServerHostname(validate);
122     return this;
123   }
124
125
126   /**
127    * Sets the flag to indicate whether or not validation should be performed against the certificate
128    * chain.
129    *
130    * @parameter validate - Set to true to enable validation, false to disable.
131    *
132    * @return The AAIRESTClient instance. This is useful for chaining parameter assignments.
133    */
134   public RestClient validateServerCertChain(boolean validate) {
135     logger.debug("Set validate server certificate chain = " + validate);
136     clientBuilder.setValidateServerCertChain(validate);
137     return this;
138   }
139
140
141   /**
142    * Assigns the client certificate file to use.
143    *
144    * @param filename - The name of the certificate file.
145    *
146    * @return The AAIRESTClient instance. This is useful for chaining parameter assignments.
147    */
148   public RestClient clientCertFile(String filename) {
149     logger.debug("Set client certificate filename = " + filename);
150     clientBuilder.setClientCertFileName(filename);
151     return this;
152   }
153
154
155   /**
156    * Assigns the client certificate password to use.
157    *
158    * @param password - The certificate password.
159    *
160    * @return The AAIRESTClient instance. This is useful for chaining parameter assignments.
161    */
162   public RestClient clientCertPassword(String password) {
163     logger.debug("Set client certificate password = " + password);
164     clientBuilder.setClientCertPassword(password);
165     return this;
166   }
167
168
169   /**
170    * Assigns the name of the trust store file to use.
171    *
172    * @param filename - the name of the trust store file.
173    *
174    * @return The AAIRESTClient instance. This is useful for chaining parameter assignments.
175    */
176   public RestClient trustStore(String filename) {
177     logger.debug("Set trust store filename = " + filename);
178     clientBuilder.setTruststoreFilename(filename);
179     return this;
180   }
181
182
183   /**
184    * Assigns the connection timeout (in ms) to use when connecting to the target server.
185    *
186    * @param timeout - The length of time to wait in ms before timing out.
187    *
188    * @return The AAIRESTClient instance. This is useful for chaining parameter assignments.
189    */
190   public RestClient connectTimeoutMs(int timeout) {
191     logger.debug("Set connection timeout = " + timeout + " ms");
192     clientBuilder.setConnectTimeoutInMs(timeout);
193     return this;
194   }
195
196
197   /**
198    * Assigns the read timeout (in ms) to use when communicating with the target server.
199    *
200    * @param timeout The read timeout in milliseconds.
201    *
202    * @return The AAIRESTClient instance. This is useful for chaining parameter assignments.
203    */
204   public RestClient readTimeoutMs(int timeout) {
205     logger.debug("Set read timeout = " + timeout + " ms");
206     clientBuilder.setReadTimeoutInMs(timeout);
207     return this;
208   }
209
210   /**
211    * This method operates on a REST endpoint by submitting an HTTP operation request against the
212    * supplied URL.    
213    * This variant of the method will perform a requested number of retries in the event that the
214    * first request is unsuccessful.
215    *
216    * @param operation - the REST operation type to send to the url
217    * @param url - The REST endpoint to submit the REST request to.
218    * @param payload - They payload to provide in the REST request, if applicable
219    * @param headers - The headers that should be passed in the request
220    * @param contentType - The content type of the payload
221    * @param responseType - The expected format of the response.
222    * 
223    * @return The result of the REST request.
224    */
225   protected OperationResult processRequest(RestOperation operation, String url, String payload,
226       Map<String, List<String>> headers, MediaType contentType, MediaType responseType,
227       int numRetries) {
228
229
230     OperationResult result = null;
231
232     long startTimeInMs = System.currentTimeMillis();
233     for (int retryCount = 0; retryCount < numRetries; retryCount++) {
234
235       logger.info(RestClientMsgs.HTTP_REQUEST_WITH_RETRIES, url, Integer.toString(retryCount + 1));
236
237       // Submit our query to the AAI.
238       result = processRequest(operation, url, payload, headers, contentType, responseType);
239
240       // If the submission was successful then we're done.
241       if (Integer.toString(result.getResultCode()).charAt(0) == '2') {
242         logger.info(RestClientMsgs.HTTP_REQUEST_TIME_WITH_RETRIES,
243             Long.toString(System.currentTimeMillis() - startTimeInMs), url,
244             Integer.toString(retryCount));
245         return result;
246       }
247
248       // Our submission was unsuccessful...
249       try {
250         // Sleep between re-tries to be nice to the target system.
251         Thread.sleep(500);
252
253       } catch (InterruptedException e) {
254         logger.error(RestClientMsgs.HTTP_REQUEST_INTERRUPTED, url, e.getLocalizedMessage());
255         break;
256       }
257     }
258
259     // If we've gotten this far, then we failed all of our retries.
260     result.setResultCode(504);
261     result.setFailureCause(
262         "Failed to get a successful result " + "after multiple retries to target server");
263
264     return result;
265   }
266
267   /**
268    * This method operates on a REST endpoint by submitting an HTTP operation request against the
269    * supplied URL.
270    *
271    * @param operation - the REST operation type to send to the url
272    * @param url - The REST endpoint to submit the REST request to.
273    * @param payload - They payload to provide in the REST request, if applicable
274    * @param headers - The headers that should be passed in the request
275    * @param contentType - The content type of the payload
276    * @param responseType - The expected format of the response.
277    *
278    * @return The result of the REST request.
279    */
280   protected OperationResult processRequest(RestOperation operation, String url, String payload,
281       Map<String, List<String>> headers, MediaType contentType, MediaType responseType) {
282
283     ClientResponse clientResponse = null;
284     OperationResult operationResult = new OperationResult();
285     ByteArrayOutputStream baos = new ByteArrayOutputStream();
286
287     String requestType = operation.getRequestType().name();
288
289     // Grab the current time so that we can log how long the
290     // query took once we are done.
291     long startTimeInMs = System.currentTimeMillis();
292     MdcOverride override = new MdcOverride();
293     override.addAttribute(MdcContext.MDC_START_TIME, formatter.format(startTimeInMs));
294
295     logger.info(RestClientMsgs.HTTP_REQUEST, requestType, url);
296
297     try {
298
299       // Get a REST client instance for our request.
300       Client client = getClient();
301
302       // Debug log the request
303       debugRequest(url, payload, headers, responseType);
304
305       // Get a client request builder, and submit our GET request.
306       Builder builder = getClientBuilder(client, url, payload, headers, contentType, responseType);
307       clientResponse = operation.processOperation(builder);
308
309       populateOperationResult(clientResponse, operationResult);
310
311       // Debug log the response
312       debugResponse(operationResult, clientResponse.getHeaders());
313
314     } catch (Exception ex) {
315
316       logger.error(RestClientMsgs.HTTP_REQUEST_ERROR, requestType, url, ex.getLocalizedMessage());
317       operationResult.setResultCode(500);
318       operationResult.setFailureCause(
319           "Error during GET operation to AAI with message = " + ex.getLocalizedMessage());
320
321     } finally {
322
323       if (logger.isDebugEnabled()) {
324         logger.debug(baos.toString());
325       }
326
327       // Not every valid response code is actually represented by the Response.Status
328       // object, so we need to guard against missing codes, otherwise we throw null
329       // pointer exceptions when we try to generate our metrics logs...
330       Response.Status responseStatus =
331           Response.Status.fromStatusCode(operationResult.getResultCode());
332       String responseStatusCodeString = "";
333       if (responseStatus != null) {
334         responseStatusCodeString = responseStatus.toString();
335       }
336
337       metricsLogger.info(RestClientMsgs.HTTP_REQUEST_TIME,
338           new LogFields().setField(LogLine.DefinedFields.STATUS_CODE, responseStatusCodeString)
339               .setField(LogLine.DefinedFields.RESPONSE_CODE, operationResult.getResultCode())
340               .setField(LogLine.DefinedFields.RESPONSE_DESCRIPTION, operationResult.getResult()),
341           override, requestType, Long.toString(System.currentTimeMillis() - startTimeInMs), url);
342       logger.info(RestClientMsgs.HTTP_REQUEST_TIME, requestType,
343           Long.toString(System.currentTimeMillis() - startTimeInMs), url);
344       logger.info(RestClientMsgs.HTTP_RESPONSE, url,
345           operationResult.getResultCode() + " " + responseStatusCodeString);
346     }
347
348     return operationResult;
349   }
350
351   /**
352    * This method submits an HTTP PUT request against the supplied URL.
353    *
354    * @param url - The REST endpoint to submit the PUT request to.
355    * @param payload - the payload to send to the supplied URL
356    * @param headers - The headers that should be passed in the request
357    * @param contentType - The content type of the payload
358    * @param responseType - The expected format of the response.
359    *
360    * @return The result of the PUT request.
361    */
362   public OperationResult put(String url, String payload, Map<String, List<String>> headers,
363       MediaType contentType, MediaType responseType) {
364     return processRequest(putOp, url, payload, headers, contentType, responseType);
365   }
366
367   /**
368    * This method submits an HTTP POST request against the supplied URL.
369    *
370    * @param url - The REST endpoint to submit the POST request to.
371    * @param payload - the payload to send to the supplied URL
372    * @param headers - The headers that should be passed in the request
373    * @param contentType - The content type of the payload
374    * @param responseType - The expected format of the response.
375    *
376    * @return The result of the POST request.
377    */
378   public OperationResult post(String url, String payload, Map<String, List<String>> headers,
379       MediaType contentType, MediaType responseType) {
380     return processRequest(postOp, url, payload, headers, contentType, responseType);
381   }
382
383   /**
384    * This method submits an HTTP GET request against the supplied URL.
385    *
386    * @param url - The REST endpoint to submit the GET request to.
387    * @param headers - The headers that should be passed in the request
388    * @param responseType - The expected format of the response.
389    *
390    * @return The result of the GET request.
391    */
392   public OperationResult get(String url, Map<String, List<String>> headers,
393       MediaType responseType) {
394     return processRequest(getOp, url, null, headers, null, responseType);
395   }
396
397   /**
398    * This method submits an HTTP GET request against the supplied URL. 
399    * This variant of the method will perform a requested number of retries in the event that the
400    * first request is unsuccessful.
401    * 
402    * @param url - The REST endpoint to submit the GET request to.
403    * @param headers - The headers that should be passed in the request
404    * @param responseType - The expected format of the response.
405    * @param numRetries - The number of times to try resubmitting the request in the event of a
406    *        failure.
407    * 
408    * @return The result of the GET request.
409    */
410   public OperationResult get(String url, Map<String, List<String>> headers, MediaType responseType,
411       int numRetries) {
412     return processRequest(getOp, url, null, headers, null, responseType, numRetries);
413   }
414
415   /**
416    * This method submits an HTTP DELETE request against the supplied URL.
417    *
418    * @param url - The REST endpoint to submit the DELETE request to.
419    * @param headers - The headers that should be passed in the request
420    * @param responseType - The expected format of the response.
421    *
422    * @return The result of the DELETE request.
423    */
424   public OperationResult delete(String url, Map<String, List<String>> headers,
425       MediaType responseType) {
426     return processRequest(deleteOp, url, null, headers, null, responseType);
427   }
428
429   /**
430    * This method does a health check ("ping") against the supplied URL.
431    *
432    * @param url - The REST endpoint to attempt a health check.
433    * @param srcAppName - The name of the application using this client.
434    * @param destAppName - The name of the destination app.
435    *
436    * @return A boolean value. True if connection attempt was successful, false otherwise.
437    *
438    */
439   public boolean healthCheck(String url, String srcAppName, String destAppName) {
440     return healthCheck(url, srcAppName, destAppName, MediaType.TEXT_PLAIN_TYPE);
441
442   }
443
444   /**
445    * This method does a health check ("ping") against the supplied URL.
446    *
447    * @param url - The REST endpoint to attempt a health check.
448    * @param srcAppName - The name of the application using this client.
449    * @param destAppName - The name of the destination app.
450    * @param responseType - The response type.
451    *
452    * @return A boolean value. True if connection attempt was successful, false otherwise.
453    *
454    */
455   public boolean healthCheck(String url, String srcAppName, String destAppName,
456       MediaType responseType) {
457     MultivaluedMap<String, String> headers = new MultivaluedMapImpl();
458     headers.put(Headers.FROM_APP_ID, Arrays.asList(new String[] {srcAppName}));
459     headers.put(Headers.TRANSACTION_ID, Arrays.asList(new String[] {UUID.randomUUID().toString()}));
460
461     try {
462       logger.info(RestClientMsgs.HEALTH_CHECK_ATTEMPT, destAppName, url);
463       OperationResult result = get(url, headers, responseType);
464
465       if (result != null && result.getFailureCause() == null) {
466         logger.info(RestClientMsgs.HEALTH_CHECK_SUCCESS, destAppName, url);
467         return true;
468       } else {
469         logger.error(RestClientMsgs.HEALTH_CHECK_FAILURE, destAppName, url,
470             result.getFailureCause());
471         return false;
472       }
473     } catch (Exception e) {
474       logger.error(RestClientMsgs.HEALTH_CHECK_FAILURE, destAppName, url, e.getMessage());
475       return false;
476     }
477   }
478
479   /**
480    * This method constructs a client request builder that can be used for submitting REST requests
481    * to the supplied URL endpoint.
482    *
483    * @param client - The REST client we will be using to talk to the server.
484    * @param url - The URL endpoint that our request will be submitted to.
485    * @param headers - The headers that should be passed in the request
486    * @param contentType - the content type of the payload
487    * @param responseType - The expected format of the response.
488    *
489    * @return A client request builder.
490    */
491   private Builder getClientBuilder(Client client, String url, String payload,
492       Map<String, List<String>> headers, MediaType contentType, MediaType responseType) {
493
494     WebResource resource = client.resource(url);
495     Builder builder = null;
496
497     builder = resource.accept(responseType);
498
499     if (contentType != null) {
500       builder.type(contentType);
501     }
502
503     if (payload != null) {
504       builder.entity(payload);
505     }
506
507     if (headers != null) {
508       for (Entry<String, List<String>> header : headers.entrySet()) {
509         builder.header(header.getKey(), header.getValue());
510       }
511     }
512
513     return builder;
514   }
515
516   private void debugRequest(String url, String payload, Map<String, List<String>> headers,
517       MediaType responseType) {
518     if (logger.isDebugEnabled()) {
519       StringBuilder debugRequest = new StringBuilder("REQUEST:\n");
520       debugRequest.append("URL: ").append(url).append("\n");
521       debugRequest.append("Payload: ").append(payload).append("\n");
522       debugRequest.append("Response Type: ").append(responseType).append("\n");
523       if (headers != null) {
524         debugRequest.append("Headers: ");
525         for (Entry<String, List<String>> header : headers.entrySet()) {
526           debugRequest.append("\n\t").append(header.getKey()).append(":");
527           for (String headerEntry : header.getValue()) {
528             debugRequest.append("\"").append(headerEntry).append("\" ");
529           }
530         }
531       }
532       logger.debug(debugRequest.toString());
533     }
534   }
535
536   private void debugResponse(OperationResult operationResult,
537       MultivaluedMap<String, String> headers) {
538     if (logger.isDebugEnabled()) {
539       StringBuilder debugResponse = new StringBuilder("RESPONSE:\n");
540       debugResponse.append("Result: ").append(operationResult.getResultCode()).append("\n");
541       debugResponse.append("Failure Cause: ").append(operationResult.getFailureCause())
542           .append("\n");
543       debugResponse.append("Payload: ").append(operationResult.getResult()).append("\n");
544       if (headers != null) {
545         debugResponse.append("Headers: ");
546         for (Entry<String, List<String>> header : headers.entrySet()) {
547           debugResponse.append("\n\t").append(header.getKey()).append(":");
548           for (String headerEntry : header.getValue()) {
549             debugResponse.append("\"").append(headerEntry).append("\" ");
550           }
551         }
552       }
553       logger.debug(debugResponse.toString());
554     }
555   }
556
557
558   /**
559    * This method creates an instance of the low level REST client to use for communicating with the
560    * AAI, if one has not already been created, otherwise it returns the already created instance.
561    *
562    * @return A {@link Client} instance.
563    */
564   private synchronized Client getClient() throws Exception {
565
566     if (restClient == null) {
567
568       if (logger.isDebugEnabled()) {
569         logger.debug("Instantiating REST client with following parameters:");
570         logger.debug(
571             "  validate server hostname          = " + clientBuilder.isValidateServerHostname());
572         logger.debug(
573             "  validate server certificate chain = " + clientBuilder.isValidateServerCertChain());
574         logger.debug(
575             "  client certificate filename       = " + clientBuilder.getClientCertFileName());
576         logger.debug(
577             "  client certificate password       = " + clientBuilder.getClientCertPassword());
578         logger.debug(
579             "  trust store filename              = " + clientBuilder.getTruststoreFilename());
580         logger.debug("  connection timeout                = "
581             + clientBuilder.getConnectTimeoutInMs() + " ms");
582         logger.debug(
583             "  read timeout                      = " + clientBuilder.getReadTimeoutInMs() + " ms");
584       }
585
586       restClient = clientBuilder.getClient();
587     }
588
589     return restClient;
590   }
591
592
593   /**
594    * This method populates the fields of an {@link OperationResult} instance based on the contents
595    * of a {@link ClientResponse} received in response to a REST request.
596    */
597   private void populateOperationResult(ClientResponse response, OperationResult opResult) {
598
599     // If we got back a NULL response, then just produce a generic
600     // error code and result indicating this.
601     if (response == null) {
602       opResult.setResultCode(500);
603       opResult.setFailureCause("Client response was null");
604       return;
605     }
606
607     int statusCode = response.getStatus();
608     String payload = response.getEntity(String.class);
609
610     opResult.setResultCode(statusCode);
611
612     if ((statusCode < 200) || (statusCode > 299)) {
613       opResult.setFailureCause(payload);
614     } else {
615       opResult.setResult(payload);
616     }
617
618     opResult.setHeaders(response.getHeaders());
619   }
620
621   private class GetRestOperation implements RestOperation {
622     public ClientResponse processOperation(Builder builder) {
623       return builder.get(ClientResponse.class);
624     }
625
626     public RequestType getRequestType() {
627       return RequestType.GET;
628     }
629   }
630
631   private class PutRestOperation implements RestOperation {
632     public ClientResponse processOperation(Builder builder) {
633       return builder.put(ClientResponse.class);
634     }
635
636     public RequestType getRequestType() {
637       return RequestType.PUT;
638     }
639   }
640
641   private class PostRestOperation implements RestOperation {
642     public ClientResponse processOperation(Builder builder) {
643       return builder.post(ClientResponse.class);
644     }
645
646     public RequestType getRequestType() {
647       return RequestType.POST;
648     }
649   }
650
651   private class DeleteRestOperation implements RestOperation {
652     public ClientResponse processOperation(Builder builder) {
653       return builder.delete(ClientResponse.class);
654     }
655
656     public RequestType getRequestType() {
657       return RequestType.DELETE;
658     }
659   }
660
661   /**
662    * Interface used wrap a Jersey REST call using a functional interface.
663    */
664   private interface RestOperation {
665
666     /**
667      * Method used to wrap the functionality of making a REST call out to the endpoint.
668      *
669      * @param builder the Jersey builder used to make the request
670      * @return the response from the REST endpoint
671      */
672     public ClientResponse processOperation(Builder builder);
673
674     /**
675      * Returns the REST request type.
676      */
677     public RequestType getRequestType();
678
679     /**
680      * The supported REST request types.
681      */
682     public enum RequestType {
683       GET, PUT, POST, DELETE;
684     }
685   }
686 }