f4f184c9389a91d19fa5ff86ba5220eb0c2260c3
[aai/rest-client.git] / src / main / java / org / onap / aai / restclient / client / RestClient.java
1 /**
2  * ============LICENSE_START=======================================================
3  * org.onap.aai
4  * ================================================================================
5  * Copyright © 2017-2018 AT&T Intellectual Property. All rights reserved.
6  * Copyright © 2017-2018 Amdocs
7  * ================================================================================
8  * Licensed under the Apache License, Version 2.0 (the "License");
9  * you may not use this file except in compliance with the License.
10  * You may obtain a copy of the License at
11  *
12  *       http://www.apache.org/licenses/LICENSE-2.0
13  *
14  * Unless required by applicable law or agreed to in writing, software
15  * distributed under the License is distributed on an "AS IS" BASIS,
16  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17  * See the License for the specific language governing permissions and
18  * limitations under the License.
19  * ============LICENSE_END=========================================================
20  */
21 package org.onap.aai.restclient.client;
22
23 import java.io.ByteArrayOutputStream;
24 import java.text.SimpleDateFormat;
25 import java.util.Arrays;
26 import java.util.List;
27 import java.util.Map;
28 import java.util.Map.Entry;
29 import java.util.UUID;
30 import java.util.concurrent.ConcurrentHashMap;
31 import java.util.concurrent.ConcurrentMap;
32
33 import javax.ws.rs.core.MediaType;
34 import javax.ws.rs.core.MultivaluedMap;
35 import javax.ws.rs.core.MultivaluedHashMap;
36 import javax.ws.rs.core.Response;
37
38 import org.onap.aai.restclient.enums.RestAuthenticationMode;
39 import org.onap.aai.restclient.logging.RestClientMsgs;
40 import org.onap.aai.restclient.rest.RestClientBuilder;
41 import org.onap.aai.cl.api.LogFields;
42 import org.onap.aai.cl.api.LogLine;
43 import org.onap.aai.cl.api.Logger;
44 import org.onap.aai.cl.eelf.LoggerFactory;
45 import org.onap.aai.cl.mdc.MdcContext;
46 import org.onap.aai.cl.mdc.MdcOverride;
47
48 import com.sun.jersey.api.client.Client;
49 import com.sun.jersey.api.client.ClientResponse;
50 import com.sun.jersey.api.client.WebResource;
51 import com.sun.jersey.api.client.WebResource.Builder;
52 import com.sun.jersey.core.util.MultivaluedMapImpl;
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  */
60
61 public class RestClient {
62
63   /**
64    * This is a generic builder that is used for constructing the REST client that we will use to
65    * communicate with the REST endpoint.
66    */
67   private RestClientBuilder clientBuilder;
68   
69   private final ConcurrentMap<String,InitializedClient> CLIENT_CACHE = new ConcurrentHashMap<>();
70   private static final String REST_CLIENT_INSTANCE = "REST_CLIENT_INSTANCE";
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   /** Reusable function call for HEAD REST operations. */
93   private final RestOperation headOp = new HeadRestOperation();
94
95   /** Reusable function call for PATCH REST operations. */
96   private final RestOperation patchOp = new PatchRestOperation();
97   
98   
99   /**
100    * Creates a new instance of the {@link RestClient}.
101    */
102   public RestClient() {
103     
104     clientBuilder = new RestClientBuilder();
105   
106   }
107
108
109   /**
110    * Creates a new instance of the {@link RestClient} using the supplied {@link RestClientBuilder}.
111    *
112    * @param rcBuilder - The REST client builder that this instance of the {@link RestClient} should
113    *        use.
114    */
115   public RestClient(RestClientBuilder rcBuilder) {
116     clientBuilder = rcBuilder;
117   }
118   
119   public RestClient authenticationMode(RestAuthenticationMode mode) {
120     logger.debug("Set rest authentication mode= " + mode);
121     clientBuilder.setAuthenticationMode(mode);
122     return this;
123   }
124   
125   public RestClient basicAuthUsername(String username) {
126     logger.debug("Set SSL BasicAuth username = " + username);
127     clientBuilder.setBasicAuthUsername(username);
128     return this;
129   }
130   
131   public RestClient basicAuthPassword(String password) {
132     /*
133      * purposely not logging out the password, I guess we could obfuscate it if we really want to
134      * see it in the logs
135      */
136     clientBuilder.setBasicAuthPassword(password);
137     return this;
138   }
139
140
141   /**
142    * Sets the flag to indicate whether or not validation should be performed against the host name
143    * of the server we are trying to communicate with.
144    *
145    * @parameter validate - Set to true to enable validation, false to disable
146    *
147    * @return The AAIRESTClient instance. This is useful for chaining parameter assignments.
148    */
149   public RestClient validateServerHostname(boolean validate) {
150     logger.debug("Set validate server hostname = " + validate);
151     clientBuilder.setValidateServerHostname(validate);
152     return this;
153   }
154
155
156   /**
157    * Sets the flag to indicate whether or not validation should be performed against the certificate
158    * chain.
159    *
160    * @parameter validate - Set to true to enable validation, false to disable.
161    *
162    * @return The AAIRESTClient instance. This is useful for chaining parameter assignments.
163    */
164   public RestClient validateServerCertChain(boolean validate) {
165     logger.debug("Set validate server certificate chain = " + validate);
166     clientBuilder.setValidateServerCertChain(validate);
167     return this;
168   }
169
170
171   /**
172    * Assigns the client certificate file to use.
173    *
174    * @param filename - The name of the certificate file.
175    *
176    * @return The AAIRESTClient instance. This is useful for chaining parameter assignments.
177    */
178   public RestClient clientCertFile(String filename) {
179     logger.debug("Set client certificate filename = " + filename);
180     clientBuilder.setClientCertFileName(filename);
181     return this;
182   }
183
184
185   /**
186    * Assigns the client certificate password to use.
187    *
188    * @param password - The certificate password.
189    *
190    * @return The AAIRESTClient instance. This is useful for chaining parameter assignments.
191    */
192   public RestClient clientCertPassword(String password) {
193     clientBuilder.setClientCertPassword(password);
194     return this;
195   }
196
197
198   /**
199    * Assigns the name of the trust store file to use.
200    *
201    * @param filename - the name of the trust store file.
202    *
203    * @return The AAIRESTClient instance. This is useful for chaining parameter assignments.
204    */
205   public RestClient trustStore(String filename) {
206     logger.debug("Set trust store filename = " + filename);
207     clientBuilder.setTruststoreFilename(filename);
208     return this;
209   }
210
211
212   /**
213    * Assigns the connection timeout (in ms) to use when connecting to the target server.
214    *
215    * @param timeout - The length of time to wait in ms before timing out.
216    *
217    * @return The AAIRESTClient instance. This is useful for chaining parameter assignments.
218    */
219   public RestClient connectTimeoutMs(int timeout) {
220     logger.debug("Set connection timeout = " + timeout + " ms");
221     clientBuilder.setConnectTimeoutInMs(timeout);
222     return this;
223   }
224
225
226   /**
227    * Assigns the read timeout (in ms) to use when communicating with the target server.
228    *
229    * @param timeout The read timeout in milliseconds.
230    *
231    * @return The AAIRESTClient instance. This is useful for chaining parameter assignments.
232    */
233   public RestClient readTimeoutMs(int timeout) {
234     logger.debug("Set read timeout = " + timeout + " ms");
235     clientBuilder.setReadTimeoutInMs(timeout);
236     return this;
237   }
238   
239   /**
240    * Configures the client for a specific SSL protocol
241    *
242    * @param sslProtocol - protocol string constant such as TLS, TLSv1, TLSv1.1, TLSv1.2
243    *
244    * @return The AAIRESTClient instance. 
245    */
246   public RestClient sslProtocol(String sslProtocol) {
247     logger.debug("Set sslProtocol = " + sslProtocol);
248     clientBuilder.setSslProtocol(sslProtocol);
249     return this;
250   }
251
252   private boolean shouldRetry(OperationResult operationResult) {
253
254     if (operationResult == null) {
255       return true;
256     }
257
258     int resultCode = operationResult.getResultCode();
259
260     if (resultCode == 200) {
261       return false;
262     }
263
264     if (resultCode == 404) {
265       return false;
266     }
267
268     return true;
269
270   }
271   
272   /**
273    * This method operates on a REST endpoint by submitting an HTTP operation request against the
274    * supplied URL.    
275    * This variant of the method will perform a requested number of retries in the event that the
276    * first request is unsuccessful.
277    *
278    * @param operation - the REST operation type to send to the url
279    * @param url - The REST endpoint to submit the REST request to.
280    * @param payload - They payload to provide in the REST request, if applicable
281    * @param headers - The headers that should be passed in the request
282    * @param contentType - The content type of the payload
283    * @param responseType - The expected format of the response.
284    * 
285    * @return The result of the REST request.
286    */
287   protected OperationResult processRequest(RestOperation operation, String url, String payload,
288       Map<String, List<String>> headers, MediaType contentType, MediaType responseType,
289       int numRetries) {
290
291
292     OperationResult result = null;
293
294     long startTimeInMs = System.currentTimeMillis();
295     for (int retryCount = 0; retryCount < numRetries; retryCount++) {
296
297       logger.info(RestClientMsgs.HTTP_REQUEST_WITH_RETRIES, operation.getRequestType().toString(),
298           url, Integer.toString(retryCount + 1));
299       
300       // Submit our query to the AAI.
301       result = processRequest(operation, url, payload, headers, contentType, responseType);
302
303       // If the submission was successful then we're done.
304       
305       if (!shouldRetry(result)) {
306         
307         logger.info(RestClientMsgs.HTTP_REQUEST_TIME_WITH_RETRIES, operation.getRequestType().toString(),url,
308             Long.toString(System.currentTimeMillis() - startTimeInMs), 
309             Integer.toString(retryCount));
310         
311         result.setNumRetries(retryCount);
312         
313         return result;
314       }
315
316       // Our submission was unsuccessful...
317       try {
318         // Sleep between re-tries to be nice to the target system.
319         Thread.sleep(50);
320
321       } catch (InterruptedException e) {
322         logger.error(RestClientMsgs.HTTP_REQUEST_INTERRUPTED, url, e.getLocalizedMessage());
323         Thread.currentThread().interrupt();
324         break;
325       }
326     }
327
328     // If we've gotten this far, then we failed all of our retries.
329     if (result == null) {
330         result = new OperationResult();
331     }
332
333     result.setNumRetries(numRetries);
334     result.setResultCode(504);
335     result.setFailureCause("Failed to get a successful result after multiple retries to target server.");
336
337
338     return result;
339   }
340
341   /**
342    * This method operates on a REST endpoint by submitting an HTTP operation request against the
343    * supplied URL.
344    *
345    * @param operation - the REST operation type to send to the url
346    * @param url - The REST endpoint to submit the REST request to.
347    * @param payload - They payload to provide in the REST request, if applicable
348    * @param headers - The headers that should be passed in the request
349    * @param contentType - The content type of the payload
350    * @param responseType - The expected format of the response.
351    *
352    * @return The result of the REST request.
353    */
354   protected OperationResult processRequest(RestOperation operation, String url, String payload,
355       Map<String, List<String>> headers, MediaType contentType, MediaType responseType) {
356
357     ClientResponse clientResponse = null;
358     OperationResult operationResult = new OperationResult();
359     ByteArrayOutputStream baos = new ByteArrayOutputStream();
360
361     String requestType = operation.getRequestType().name();
362
363     // Grab the current time so that we can log how long the
364     // query took once we are done.
365     long startTimeInMs = System.currentTimeMillis();
366     MdcOverride override = new MdcOverride();
367     override.addAttribute(MdcContext.MDC_START_TIME, formatter.format(startTimeInMs));
368
369     logger.info(RestClientMsgs.HTTP_REQUEST, requestType, url);
370
371     try {
372
373       // Get a REST client instance for our request.
374       Client client = getClient();
375
376       // Debug log the request
377       debugRequest(url, payload, headers, responseType);
378
379       // Get a client request builder, and submit our GET request.
380       Builder builder = getClientBuilder(client, url, payload, headers, contentType, responseType);
381       clientResponse = operation.processOperation(builder);
382
383       populateOperationResult(clientResponse, operationResult);
384
385       // Debug log the response
386       if (clientResponse != null) {
387         debugResponse(operationResult, clientResponse.getHeaders());
388       }
389
390     } catch (Exception ex) {
391
392       logger.error(RestClientMsgs.HTTP_REQUEST_ERROR, requestType, url, ex.getLocalizedMessage());
393       operationResult.setResultCode(500);
394       operationResult.setFailureCause(
395           "Error during GET operation to AAI with message = " + ex.getLocalizedMessage());
396
397     } finally {
398
399       if (logger.isDebugEnabled()) {
400         logger.debug(baos.toString());
401       }
402
403       // Not every valid response code is actually represented by the Response.Status
404       // object, so we need to guard against missing codes, otherwise we throw null
405       // pointer exceptions when we try to generate our metrics logs...
406       Response.Status responseStatus =
407           Response.Status.fromStatusCode(operationResult.getResultCode());
408       String responseStatusCodeString = "";
409       if (responseStatus != null) {
410         responseStatusCodeString = responseStatus.toString();
411       }
412
413       metricsLogger.info(RestClientMsgs.HTTP_REQUEST_TIME,
414           new LogFields().setField(LogLine.DefinedFields.STATUS_CODE, responseStatusCodeString)
415               .setField(LogLine.DefinedFields.RESPONSE_CODE, operationResult.getResultCode())
416               .setField(LogLine.DefinedFields.RESPONSE_DESCRIPTION, operationResult.getResult()),
417           override, requestType, Long.toString(System.currentTimeMillis() - startTimeInMs), url);
418       logger.info(RestClientMsgs.HTTP_REQUEST_TIME, requestType,
419           Long.toString(System.currentTimeMillis() - startTimeInMs), url);
420       logger.info(RestClientMsgs.HTTP_RESPONSE, url,
421           operationResult.getResultCode() + " " + responseStatusCodeString);
422     }
423
424     return operationResult;
425   }
426
427   /**
428    * This method submits an HTTP PUT request against the supplied URL.
429    *
430    * @param url - The REST endpoint to submit the PUT request to.
431    * @param payload - the payload to send to the supplied URL
432    * @param headers - The headers that should be passed in the request
433    * @param contentType - The content type of the payload
434    * @param responseType - The expected format of the response.
435    *
436    * @return The result of the PUT request.
437    */
438   public OperationResult put(String url, String payload, Map<String, List<String>> headers,
439       MediaType contentType, MediaType responseType) {
440     return processRequest(putOp, url, payload, headers, contentType, responseType);
441   }
442
443   /**
444    * This method submits an HTTP POST request against the supplied URL.
445    *
446    * @param url - The REST endpoint to submit the POST request to.
447    * @param payload - the payload to send to the supplied URL
448    * @param headers - The headers that should be passed in the request
449    * @param contentType - The content type of the payload
450    * @param responseType - The expected format of the response.
451    *
452    * @return The result of the POST request.
453    */
454   public OperationResult post(String url, String payload, Map<String, List<String>> headers,
455       MediaType contentType, MediaType responseType) {
456     return processRequest(postOp, url, payload, headers, contentType, responseType);
457   }
458   
459   /**
460    * This method submits an HTTP POST request against the supplied URL, and emulates a PATCH
461    * operation by setting a special header value
462    *
463    * @param url - The REST endpoint to submit the POST request to.
464    * @param payload - the payload to send to the supplied URL
465    * @param headers - The headers that should be passed in the request
466    * @param contentType - The content type of the payload
467    * @param responseType - The expected format of the response.
468    *
469    * @return The result of the POST request.
470    */
471   public OperationResult patch(String url, String payload, Map<String, List<String>> headers,
472       MediaType contentType, MediaType responseType) {
473     return processRequest(patchOp, url, payload, headers, contentType, responseType);
474   }
475
476   
477   /**
478    * This method submits an HTTP HEAD request against the supplied URL
479    *
480    * @param url - The REST endpoint to submit the POST request to.
481    * @param headers - The headers that should be passed in the request
482    * @param responseType - The expected format of the response.
483    *
484    * @return The result of the POST request.
485    */
486   public OperationResult head(String url, Map<String, List<String>> headers,
487       MediaType responseType) {
488     return processRequest(headOp, url, null, headers, null, responseType);
489   }
490   
491   /**
492    * This method submits an HTTP GET request against the supplied URL.
493    *
494    * @param url - The REST endpoint to submit the GET request to.
495    * @param headers - The headers that should be passed in the request
496    * @param responseType - The expected format of the response.
497    *
498    * @return The result of the GET request.
499    */
500   public OperationResult get(String url, Map<String, List<String>> headers,
501       MediaType responseType) {
502     return processRequest(getOp, url, null, headers, null, responseType);
503   }
504
505   /**
506    * This method submits an HTTP GET request against the supplied URL. 
507    * This variant of the method will perform a requested number of retries in the event that the
508    * first request is unsuccessful.
509    * 
510    * @param url - The REST endpoint to submit the GET request to.
511    * @param headers - The headers that should be passed in the request
512    * @param responseType - The expected format of the response.
513    * @param numRetries - The number of times to try resubmitting the request in the event of a
514    *        failure.
515    * 
516    * @return The result of the GET request.
517    */
518   public OperationResult get(String url, Map<String, List<String>> headers, MediaType responseType,
519       int numRetries) {
520     return processRequest(getOp, url, null, headers, null, responseType, numRetries);
521   }
522
523   /**
524    * This method submits an HTTP DELETE request against the supplied URL.
525    *
526    * @param url - The REST endpoint to submit the DELETE request to.
527    * @param headers - The headers that should be passed in the request
528    * @param responseType - The expected format of the response.
529    *
530    * @return The result of the DELETE request.
531    */
532   public OperationResult delete(String url, Map<String, List<String>> headers,
533       MediaType responseType) {
534     return processRequest(deleteOp, url, null, headers, null, responseType);
535   }
536
537   /**
538    * This method does a health check ("ping") against the supplied URL.
539    *
540    * @param url - The REST endpoint to attempt a health check.
541    * @param srcAppName - The name of the application using this client.
542    * @param destAppName - The name of the destination app.
543    *
544    * @return A boolean value. True if connection attempt was successful, false otherwise.
545    *
546    */
547   public boolean healthCheck(String url, String srcAppName, String destAppName) {
548     return healthCheck(url, srcAppName, destAppName, MediaType.TEXT_PLAIN_TYPE);
549
550   }
551
552   /**
553    * This method does a health check ("ping") against the supplied URL.
554    *
555    * @param url - The REST endpoint to attempt a health check.
556    * @param srcAppName - The name of the application using this client.
557    * @param destAppName - The name of the destination app.
558    * @param responseType - The response type.
559    *
560    * @return A boolean value. True if connection attempt was successful, false otherwise.
561    *
562    */
563   public boolean healthCheck(String url, String srcAppName, String destAppName,
564       MediaType responseType) {
565     MultivaluedMap<String, String> headers = new MultivaluedMapImpl();
566     headers.put(Headers.FROM_APP_ID, Arrays.asList(new String[] {srcAppName}));
567     headers.put(Headers.TRANSACTION_ID, Arrays.asList(new String[] {UUID.randomUUID().toString()}));
568
569     try {
570       logger.info(RestClientMsgs.HEALTH_CHECK_ATTEMPT, destAppName, url);
571       OperationResult result = get(url, headers, responseType);
572
573       if (result != null && result.getFailureCause() == null) {
574         logger.info(RestClientMsgs.HEALTH_CHECK_SUCCESS, destAppName, url);
575         return true;
576       } else {
577         logger.error(RestClientMsgs.HEALTH_CHECK_FAILURE, destAppName, url, result != null ? result.getFailureCause()
578                                                                                            : null);
579         return false;
580       }
581     } catch (Exception e) {
582       logger.error(RestClientMsgs.HEALTH_CHECK_FAILURE, destAppName, url, e.getMessage());
583       return false;
584     }
585   }
586
587   /**
588    * This method constructs a client request builder that can be used for submitting REST requests
589    * to the supplied URL endpoint.
590    *
591    * @param client - The REST client we will be using to talk to the server.
592    * @param url - The URL endpoint that our request will be submitted to.
593    * @param headers - The headers that should be passed in the request
594    * @param contentType - the content type of the payload
595    * @param responseType - The expected format of the response.
596    *
597    * @return A client request builder.
598    */
599   private Builder getClientBuilder(Client client, String url, String payload,
600       Map<String, List<String>> headers, MediaType contentType, MediaType responseType) {
601
602     WebResource resource = client.resource(url);
603     Builder builder = resource.accept(responseType);
604
605     if (contentType != null) {
606       builder.type(contentType);
607     }
608
609     if (payload != null) {
610       builder.entity(payload);
611     }
612
613     if (headers != null) {
614       for (Entry<String, List<String>> header : headers.entrySet()) {
615         builder.header(header.getKey(), String.join(";",header.getValue()));
616       }
617       
618       //Added additional check to prevent adding duplicate authorization header if client is already sending the authorization header 
619       // AAI-1097 - For AAI calls when Rest authentication mode is selected as SSL_BASIC getting 403 error
620       if (clientBuilder.getAuthenticationMode() == RestAuthenticationMode.SSL_BASIC && headers.get(Headers.AUTHORIZATION) == null) {
621         builder = builder.header(Headers.AUTHORIZATION,
622             clientBuilder.getBasicAuthenticationCredentials());
623       }
624       
625     }
626
627     return builder;
628   }
629
630   private void debugRequest(String url, String payload, Map<String, List<String>> headers,
631       MediaType responseType) {
632     if (!logger.isDebugEnabled()) {
633       return;
634     }
635
636     StringBuilder debugRequest = new StringBuilder("REQUEST:\n");
637     debugRequest.append("URL: ").append(url).append("\n");
638     debugRequest.append("Payload: ").append(payload).append("\n");
639     debugRequest.append("Response Type: ").append(responseType).append("\n");
640
641     if (headers == null) {
642       logger.debug(debugRequest.toString());
643       return;
644     }
645
646     debugRequest.append("Headers: ");
647     for (Entry<String, List<String>> header : headers.entrySet()) {
648       debugRequest.append("\n\t").append(header.getKey()).append(":");
649       for (String headerEntry : header.getValue()) {
650         debugRequest.append("\"").append(headerEntry).append("\" ");
651       }
652     }
653
654     logger.debug(debugRequest.toString());
655
656   }
657
658   private void debugResponse(OperationResult operationResult,
659       MultivaluedMap<String, String> headers) {
660
661     if (!logger.isDebugEnabled()) {
662       return;
663     }
664
665     StringBuilder debugResponse = new StringBuilder("RESPONSE:\n");
666     debugResponse.append("Result: ").append(operationResult.getResultCode()).append("\n");
667     debugResponse.append("Failure Cause: ").append(operationResult.getFailureCause()).append("\n");
668     debugResponse.append("Payload: ").append(operationResult.getResult()).append("\n");
669
670     if (headers == null) {
671       logger.debug(debugResponse.toString());
672       return;
673     }
674
675     debugResponse.append("Headers: ");
676     for (Entry<String, List<String>> header : headers.entrySet()) {
677       debugResponse.append("\n\t").append(header.getKey()).append(":");
678       for (String headerEntry : header.getValue()) {
679         debugResponse.append("\"").append(headerEntry).append("\" ");
680       }
681     }
682
683     logger.debug(debugResponse.toString());
684   }
685
686   /**
687    * This method creates an instance of the low level REST client to use for communicating with the
688    * AAI, if one has not already been created, otherwise it returns the already created instance.
689    *
690    * @return A {@link Client} instance.
691    */
692   protected Client getClient() throws Exception {
693
694     /*
695      * Attempting a new way of doing non-blocking thread-safe lazy-initialization by using Java 1.8
696      * computeIfAbsent functionality. A null value will not be stored, but once a valid mapping has
697      * been established, then the same value will be returned.
698      * 
699      * One awkwardness of the computeIfAbsent is the lack of support for thrown exceptions, which
700      * required a bit of hoop jumping to preserve the original exception for the purpose of
701      * maintaining the pre-existing this API signature.
702      */
703  
704     final InitializedClient clientInstance =
705         CLIENT_CACHE.computeIfAbsent(REST_CLIENT_INSTANCE, k -> loggedClientInitialization());
706     
707     if (clientInstance.getCaughtException() != null) {
708       throw new InstantiationException(clientInstance.getCaughtException().getMessage());
709     }
710
711     return clientInstance.getClient();
712
713   }
714
715   /**
716    * This method will only be called if computerIfAbsent is true.  The return value is null, then the result is not
717    * stored in the map. 
718    * 
719    * @return a new client instance or null
720    */
721   private InitializedClient loggedClientInitialization() {
722
723     if (logger.isDebugEnabled()) {
724       logger.debug("Instantiating REST client with following parameters:");
725       logger.debug(clientBuilder.toString());
726     }
727     
728     InitializedClient initClient = new InitializedClient();
729     
730     try {
731       initClient.setClient(clientBuilder.getClient());
732     } catch ( Exception error) {
733       initClient.setCaughtException(error);
734     }
735     
736     return initClient;
737
738   }
739
740
741   /**
742    * This method populates the fields of an {@link OperationResult} instance based on the contents
743    * of a {@link ClientResponse} received in response to a REST request.
744    */
745   private void populateOperationResult(ClientResponse response, OperationResult opResult) {
746
747     // If we got back a NULL response, then just produce a generic
748     // error code and result indicating this.
749     if (response == null) {
750       opResult.setResultCode(500);
751       opResult.setFailureCause("Client response was null");
752       return;
753     }
754         
755     int statusCode = response.getStatus();
756     opResult.setResultCode(statusCode);
757
758     if (opResult.wasSuccessful()) {
759         if (statusCode != Response.Status.NO_CONTENT.getStatusCode()) {
760             opResult.setResult(response.getEntity(String.class));
761         }
762     } else {
763         opResult.setFailureCause(response.getEntity(String.class));
764     }
765
766     opResult.setHeaders(response.getHeaders());
767   }
768
769   private class GetRestOperation implements RestOperation {
770     public ClientResponse processOperation(Builder builder) {
771       return builder.get(ClientResponse.class);
772     }
773
774     public RequestType getRequestType() {
775       return RequestType.GET;
776     }
777   }
778
779   private class PutRestOperation implements RestOperation {
780     public ClientResponse processOperation(Builder builder) {
781       return builder.put(ClientResponse.class);
782     }
783
784     public RequestType getRequestType() {
785       return RequestType.PUT;
786     }
787   }
788
789   private class PostRestOperation implements RestOperation {
790     public ClientResponse processOperation(Builder builder) {
791       return builder.post(ClientResponse.class);
792     }
793
794     public RequestType getRequestType() {
795       return RequestType.POST;
796     }
797   }
798
799   private class DeleteRestOperation implements RestOperation {
800     public ClientResponse processOperation(Builder builder) {
801       return builder.delete(ClientResponse.class);
802     }
803
804     public RequestType getRequestType() {
805       return RequestType.DELETE;
806     }
807   }
808   
809   private class HeadRestOperation implements RestOperation {
810     public ClientResponse processOperation(Builder builder) {
811       return builder.head();
812     }
813
814     public RequestType getRequestType() {
815       return RequestType.HEAD;
816     }
817   }
818
819   private class PatchRestOperation implements RestOperation {
820
821     /**
822      * Technically there is no standarized PATCH operation for the 
823      * jersey client, but we can use the method-override approach 
824      * instead.
825      */
826     public ClientResponse processOperation(Builder builder) {
827       builder = builder.header("X-HTTP-Method-Override", "PATCH");
828       return builder.post(ClientResponse.class);
829     }
830
831     public RequestType getRequestType() {
832       return RequestType.PATCH;
833     }
834   }
835
836
837   /**
838    * Interface used wrap a Jersey REST call using a functional interface.
839    */
840   private interface RestOperation {
841
842     /**
843      * Method used to wrap the functionality of making a REST call out to the endpoint.
844      *
845      * @param builder the Jersey builder used to make the request
846      * @return the response from the REST endpoint
847      */
848     public ClientResponse processOperation(Builder builder);
849
850     /**
851      * Returns the REST request type.
852      */
853     public RequestType getRequestType();
854
855     /**
856      * The supported REST request types.
857      */
858     public enum RequestType {
859       GET, PUT, POST, DELETE, PATCH, HEAD
860     }
861   }
862   
863   /*
864    * An entity to encapsulate an expected result and a potential failure cause when returning from a
865    * functional interface during the computeIfAbsent call.
866    */
867   private class InitializedClient {
868     private Client client;
869     private Throwable caughtException;
870     
871     public InitializedClient() {
872       client = null;
873       caughtException = null;
874     }
875     
876     public Client getClient() {
877       return client;
878     }
879     public void setClient(Client client) {
880       this.client = client;
881     }
882     public Throwable getCaughtException() {
883       return caughtException;
884     }
885     public void setCaughtException(Throwable caughtException) {
886       this.caughtException = caughtException;
887     }
888   
889   }
890   
891 }