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