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