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