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