2 * ============LICENSE_START=======================================================
4 * ================================================================================
5 * Copyright © 2017 AT&T Intellectual Property.
6 * Copyright © 2017 Amdocs
8 * ================================================================================
9 * Licensed under the Apache License, Version 2.0 (the "License");
10 * you may not use this file except in compliance with the License.
11 * You may obtain a copy of the License at
13 * http://www.apache.org/licenses/LICENSE-2.0
15 * Unless required by applicable law or agreed to in writing, software
16 * distributed under the License is distributed on an "AS IS" BASIS,
17 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18 * See the License for the specific language governing permissions and
19 * limitations under the License.
20 * ============LICENSE_END=========================================================
22 * ECOMP and OpenECOMP are trademarks
23 * and service marks of AT&T Intellectual Property.
25 package org.openecomp.restclient.client;
27 import java.io.ByteArrayOutputStream;
28 import java.text.SimpleDateFormat;
29 import java.util.Arrays;
30 import java.util.List;
32 import java.util.Map.Entry;
33 import java.util.UUID;
34 import java.util.concurrent.ConcurrentHashMap;
35 import java.util.concurrent.ConcurrentMap;
37 import javax.ws.rs.core.MediaType;
38 import javax.ws.rs.core.MultivaluedMap;
39 import javax.ws.rs.core.Response;
41 import org.openecomp.cl.api.LogFields;
42 import org.openecomp.cl.api.LogLine;
43 import org.openecomp.cl.api.Logger;
44 import org.openecomp.cl.eelf.LoggerFactory;
45 import org.openecomp.cl.mdc.MdcContext;
46 import org.openecomp.cl.mdc.MdcOverride;
47 import org.openecomp.restclient.enums.RestAuthenticationMode;
48 import org.openecomp.restclient.logging.RestClientMsgs;
49 import org.openecomp.restclient.rest.RestClientBuilder;
51 import com.sun.jersey.api.client.Client;
52 import com.sun.jersey.api.client.ClientResponse;
53 import com.sun.jersey.api.client.WebResource;
54 import com.sun.jersey.api.client.WebResource.Builder;
55 import com.sun.jersey.core.util.MultivaluedMapImpl;
59 * This class provides a general client implementation that micro services can use for communicating
60 * with the endpoints via their exposed REST interfaces.
64 public class RestClient {
67 * This is a generic builder that is used for constructing the REST client that we will use to
68 * communicate with the REST endpoint.
70 private RestClientBuilder clientBuilder;
72 private final ConcurrentMap<String,InitializedClient> CLIENT_CACHE = new ConcurrentHashMap<String,InitializedClient>();
73 private static final String REST_CLIENT_INSTANCE = "REST_CLIENT_INSTANCE";
75 /** Standard logger for producing log statements. */
76 private Logger logger = LoggerFactory.getInstance().getLogger("AAIRESTClient");
78 /** Standard logger for producing metric statements. */
79 private Logger metricsLogger = LoggerFactory.getInstance().getMetricsLogger("AAIRESTClient");
81 private SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX");
83 /** Reusable function call for GET REST operations. */
84 private final RestOperation getOp = new GetRestOperation();
86 /** Reusable function call for PUT REST operations. */
87 private final RestOperation putOp = new PutRestOperation();
89 /** Reusable function call for POST REST operations. */
90 private final RestOperation postOp = new PostRestOperation();
92 /** Reusable function call for DELETE REST operations. */
93 private final RestOperation deleteOp = new DeleteRestOperation();
95 /** Reusable function call for HEAD REST operations. */
96 private final RestOperation headOp = new HeadRestOperation();
98 /** Reusable function call for PATCH REST operations. */
99 private final RestOperation patchOp = new PatchRestOperation();
103 * Creates a new instance of the {@link RestClient}.
105 public RestClient() {
107 clientBuilder = new RestClientBuilder();
113 * Creates a new instance of the {@link RestClient} using the supplied {@link RestClientBuilder}.
115 * @param rcBuilder - The REST client builder that this instance of the {@link RestClient} should
118 public RestClient(RestClientBuilder rcBuilder) {
119 clientBuilder = rcBuilder;
122 public RestClient authenticationMode(RestAuthenticationMode mode) {
123 logger.debug("Set rest authentication mode= " + mode);
124 clientBuilder.setAuthenticationMode(mode);
128 public RestClient basicAuthUsername(String username) {
129 logger.debug("Set SSL BasicAuth username = " + username);
130 clientBuilder.setBasicAuthUsername(username);
134 public RestClient basicAuthPassword(String password) {
136 * purposely not logging out the password, I guess we could obfuscate it if we really want to
139 clientBuilder.setBasicAuthPassword(password);
145 * Sets the flag to indicate whether or not validation should be performed against the host name
146 * of the server we are trying to communicate with.
148 * @parameter validate - Set to true to enable validation, false to disable
150 * @return The AAIRESTClient instance. This is useful for chaining parameter assignments.
152 public RestClient validateServerHostname(boolean validate) {
153 logger.debug("Set validate server hostname = " + validate);
154 clientBuilder.setValidateServerHostname(validate);
160 * Sets the flag to indicate whether or not validation should be performed against the certificate
163 * @parameter validate - Set to true to enable validation, false to disable.
165 * @return The AAIRESTClient instance. This is useful for chaining parameter assignments.
167 public RestClient validateServerCertChain(boolean validate) {
168 logger.debug("Set validate server certificate chain = " + validate);
169 clientBuilder.setValidateServerCertChain(validate);
175 * Assigns the client certificate file to use.
177 * @param filename - The name of the certificate file.
179 * @return The AAIRESTClient instance. This is useful for chaining parameter assignments.
181 public RestClient clientCertFile(String filename) {
182 logger.debug("Set client certificate filename = " + filename);
183 clientBuilder.setClientCertFileName(filename);
189 * Assigns the client certificate password to use.
191 * @param password - The certificate password.
193 * @return The AAIRESTClient instance. This is useful for chaining parameter assignments.
195 public RestClient clientCertPassword(String password) {
196 clientBuilder.setClientCertPassword(password);
202 * Assigns the name of the trust store file to use.
204 * @param filename - the name of the trust store file.
206 * @return The AAIRESTClient instance. This is useful for chaining parameter assignments.
208 public RestClient trustStore(String filename) {
209 logger.debug("Set trust store filename = " + filename);
210 clientBuilder.setTruststoreFilename(filename);
216 * Assigns the connection timeout (in ms) to use when connecting to the target server.
218 * @param timeout - The length of time to wait in ms before timing out.
220 * @return The AAIRESTClient instance. This is useful for chaining parameter assignments.
222 public RestClient connectTimeoutMs(int timeout) {
223 logger.debug("Set connection timeout = " + timeout + " ms");
224 clientBuilder.setConnectTimeoutInMs(timeout);
230 * Assigns the read timeout (in ms) to use when communicating with the target server.
232 * @param timeout The read timeout in milliseconds.
234 * @return The AAIRESTClient instance. This is useful for chaining parameter assignments.
236 public RestClient readTimeoutMs(int timeout) {
237 logger.debug("Set read timeout = " + timeout + " ms");
238 clientBuilder.setReadTimeoutInMs(timeout);
242 private boolean shouldRetry(OperationResult operationResult) {
244 if (operationResult == null) {
248 int resultCode = operationResult.getResultCode();
250 if (resultCode == 200) {
254 if (resultCode == 404) {
263 * This method operates on a REST endpoint by submitting an HTTP operation request against the
265 * This variant of the method will perform a requested number of retries in the event that the
266 * first request is unsuccessful.
268 * @param operation - the REST operation type to send to the url
269 * @param url - The REST endpoint to submit the REST request to.
270 * @param payload - They payload to provide in the REST request, if applicable
271 * @param headers - The headers that should be passed in the request
272 * @param contentType - The content type of the payload
273 * @param responseType - The expected format of the response.
275 * @return The result of the REST request.
277 protected OperationResult processRequest(RestOperation operation, String url, String payload,
278 Map<String, List<String>> headers, MediaType contentType, MediaType responseType,
282 OperationResult result = null;
284 long startTimeInMs = System.currentTimeMillis();
285 for (int retryCount = 0; retryCount < numRetries; retryCount++) {
287 logger.info(RestClientMsgs.HTTP_REQUEST_WITH_RETRIES, operation.getRequestType().toString(),
288 url, Integer.toString(retryCount + 1));
290 // Submit our query to the AAI.
291 result = processRequest(operation, url, payload, headers, contentType, responseType);
293 // If the submission was successful then we're done.
295 if (!shouldRetry(result)) {
297 logger.info(RestClientMsgs.HTTP_REQUEST_TIME_WITH_RETRIES, operation.getRequestType().toString(),url,
298 Long.toString(System.currentTimeMillis() - startTimeInMs),
299 Integer.toString(retryCount));
301 result.setNumRetries(retryCount);
306 // Our submission was unsuccessful...
308 // Sleep between re-tries to be nice to the target system.
311 } catch (InterruptedException e) {
312 logger.error(RestClientMsgs.HTTP_REQUEST_INTERRUPTED, url, e.getLocalizedMessage());
317 // If we've gotten this far, then we failed all of our retries.
318 result.setNumRetries(numRetries);
319 result.setResultCode(504);
320 result.setFailureCause(
321 "Failed to get a successful result after multiple retries to target server.");
327 * This method operates on a REST endpoint by submitting an HTTP operation request against the
330 * @param operation - the REST operation type to send to the url
331 * @param url - The REST endpoint to submit the REST request to.
332 * @param payload - They payload to provide in the REST request, if applicable
333 * @param headers - The headers that should be passed in the request
334 * @param contentType - The content type of the payload
335 * @param responseType - The expected format of the response.
337 * @return The result of the REST request.
339 protected OperationResult processRequest(RestOperation operation, String url, String payload,
340 Map<String, List<String>> headers, MediaType contentType, MediaType responseType) {
342 ClientResponse clientResponse = null;
343 OperationResult operationResult = new OperationResult();
344 ByteArrayOutputStream baos = new ByteArrayOutputStream();
346 String requestType = operation.getRequestType().name();
348 // Grab the current time so that we can log how long the
349 // query took once we are done.
350 long startTimeInMs = System.currentTimeMillis();
351 MdcOverride override = new MdcOverride();
352 override.addAttribute(MdcContext.MDC_START_TIME, formatter.format(startTimeInMs));
354 logger.info(RestClientMsgs.HTTP_REQUEST, requestType, url);
358 // Get a REST client instance for our request.
359 Client client = getClient();
361 // Debug log the request
362 debugRequest(url, payload, headers, responseType);
364 // Get a client request builder, and submit our GET request.
365 Builder builder = getClientBuilder(client, url, payload, headers, contentType, responseType);
366 clientResponse = operation.processOperation(builder);
368 populateOperationResult(clientResponse, operationResult);
370 // Debug log the response
371 debugResponse(operationResult, clientResponse.getHeaders());
373 } catch (Exception ex) {
375 logger.error(RestClientMsgs.HTTP_REQUEST_ERROR, requestType, url, ex.getLocalizedMessage());
376 operationResult.setResultCode(500);
377 operationResult.setFailureCause(
378 "Error during GET operation to AAI with message = " + ex.getLocalizedMessage());
382 if (logger.isDebugEnabled()) {
383 logger.debug(baos.toString());
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 =
390 Response.Status.fromStatusCode(operationResult.getResultCode());
391 String responseStatusCodeString = "";
392 if (responseStatus != null) {
393 responseStatusCodeString = responseStatus.toString();
396 metricsLogger.info(RestClientMsgs.HTTP_REQUEST_TIME,
397 new LogFields().setField(LogLine.DefinedFields.STATUS_CODE, responseStatusCodeString)
398 .setField(LogLine.DefinedFields.RESPONSE_CODE, operationResult.getResultCode())
399 .setField(LogLine.DefinedFields.RESPONSE_DESCRIPTION, operationResult.getResult()),
400 override, requestType, Long.toString(System.currentTimeMillis() - startTimeInMs), url);
401 logger.info(RestClientMsgs.HTTP_REQUEST_TIME, requestType,
402 Long.toString(System.currentTimeMillis() - startTimeInMs), url);
403 logger.info(RestClientMsgs.HTTP_RESPONSE, url,
404 operationResult.getResultCode() + " " + responseStatusCodeString);
407 return operationResult;
411 * This method submits an HTTP PUT request against the supplied URL.
413 * @param url - The REST endpoint to submit the PUT request to.
414 * @param payload - the payload to send to the supplied URL
415 * @param headers - The headers that should be passed in the request
416 * @param contentType - The content type of the payload
417 * @param responseType - The expected format of the response.
419 * @return The result of the PUT request.
421 public OperationResult put(String url, String payload, Map<String, List<String>> headers,
422 MediaType contentType, MediaType responseType) {
423 return processRequest(putOp, url, payload, headers, contentType, responseType);
427 * This method submits an HTTP POST request against the supplied URL.
429 * @param url - The REST endpoint to submit the POST request to.
430 * @param payload - the payload to send to the supplied URL
431 * @param headers - The headers that should be passed in the request
432 * @param contentType - The content type of the payload
433 * @param responseType - The expected format of the response.
435 * @return The result of the POST request.
437 public OperationResult post(String url, String payload, Map<String, List<String>> headers,
438 MediaType contentType, MediaType responseType) {
439 return processRequest(postOp, url, payload, headers, contentType, responseType);
443 * This method submits an HTTP POST request against the supplied URL, and emulates a PATCH
444 * operation by setting a special header value
446 * @param url - The REST endpoint to submit the POST request to.
447 * @param payload - the payload to send to the supplied URL
448 * @param headers - The headers that should be passed in the request
449 * @param contentType - The content type of the payload
450 * @param responseType - The expected format of the response.
452 * @return The result of the POST request.
454 public OperationResult patch(String url, String payload, Map<String, List<String>> headers,
455 MediaType contentType, MediaType responseType) {
456 return processRequest(patchOp, url, payload, headers, contentType, responseType);
461 * This method submits an HTTP HEAD request against the supplied URL
463 * @param url - The REST endpoint to submit the POST request to.
464 * @param headers - The headers that should be passed in the request
465 * @param responseType - The expected format of the response.
467 * @return The result of the POST request.
469 public OperationResult head(String url, Map<String, List<String>> headers,
470 MediaType responseType) {
471 return processRequest(headOp, url, null, headers, null, responseType);
475 * This method submits an HTTP GET request against the supplied URL.
477 * @param url - The REST endpoint to submit the GET request to.
478 * @param headers - The headers that should be passed in the request
479 * @param responseType - The expected format of the response.
481 * @return The result of the GET request.
483 public OperationResult get(String url, Map<String, List<String>> headers,
484 MediaType responseType) {
485 return processRequest(getOp, url, null, headers, null, responseType);
489 * This method submits an HTTP GET request against the supplied URL.
490 * This variant of the method will perform a requested number of retries in the event that the
491 * first request is unsuccessful.
493 * @param url - The REST endpoint to submit the GET request to.
494 * @param headers - The headers that should be passed in the request
495 * @param responseType - The expected format of the response.
496 * @param numRetries - The number of times to try resubmitting the request in the event of a
499 * @return The result of the GET request.
501 public OperationResult get(String url, Map<String, List<String>> headers, MediaType responseType,
503 return processRequest(getOp, url, null, headers, null, responseType, numRetries);
507 * This method submits an HTTP DELETE request against the supplied URL.
509 * @param url - The REST endpoint to submit the DELETE request to.
510 * @param headers - The headers that should be passed in the request
511 * @param responseType - The expected format of the response.
513 * @return The result of the DELETE request.
515 public OperationResult delete(String url, Map<String, List<String>> headers,
516 MediaType responseType) {
517 return processRequest(deleteOp, url, null, headers, null, responseType);
521 * This method does a health check ("ping") against the supplied URL.
523 * @param url - The REST endpoint to attempt a health check.
524 * @param srcAppName - The name of the application using this client.
525 * @param destAppName - The name of the destination app.
527 * @return A boolean value. True if connection attempt was successful, false otherwise.
530 public boolean healthCheck(String url, String srcAppName, String destAppName) {
531 return healthCheck(url, srcAppName, destAppName, MediaType.TEXT_PLAIN_TYPE);
536 * This method does a health check ("ping") against the supplied URL.
538 * @param url - The REST endpoint to attempt a health check.
539 * @param srcAppName - The name of the application using this client.
540 * @param destAppName - The name of the destination app.
541 * @param responseType - The response type.
543 * @return A boolean value. True if connection attempt was successful, false otherwise.
546 public boolean healthCheck(String url, String srcAppName, String destAppName,
547 MediaType responseType) {
548 MultivaluedMap<String, String> headers = new MultivaluedMapImpl();
549 headers.put(Headers.FROM_APP_ID, Arrays.asList(new String[] {srcAppName}));
550 headers.put(Headers.TRANSACTION_ID, Arrays.asList(new String[] {UUID.randomUUID().toString()}));
553 logger.info(RestClientMsgs.HEALTH_CHECK_ATTEMPT, destAppName, url);
554 OperationResult result = get(url, headers, responseType);
556 if (result != null && result.getFailureCause() == null) {
557 logger.info(RestClientMsgs.HEALTH_CHECK_SUCCESS, destAppName, url);
560 logger.error(RestClientMsgs.HEALTH_CHECK_FAILURE, destAppName, url,
561 result.getFailureCause());
564 } catch (Exception e) {
565 logger.error(RestClientMsgs.HEALTH_CHECK_FAILURE, destAppName, url, e.getMessage());
571 * This method constructs a client request builder that can be used for submitting REST requests
572 * to the supplied URL endpoint.
574 * @param client - The REST client we will be using to talk to the server.
575 * @param url - The URL endpoint that our request will be submitted to.
576 * @param headers - The headers that should be passed in the request
577 * @param contentType - the content type of the payload
578 * @param responseType - The expected format of the response.
580 * @return A client request builder.
582 private Builder getClientBuilder(Client client, String url, String payload,
583 Map<String, List<String>> headers, MediaType contentType, MediaType responseType) {
585 WebResource resource = client.resource(url);
586 Builder builder = null;
588 builder = resource.accept(responseType);
590 if (contentType != null) {
591 builder.type(contentType);
594 if (payload != null) {
595 builder.entity(payload);
598 if (headers != null) {
599 for (Entry<String, List<String>> header : headers.entrySet()) {
600 builder.header(header.getKey(), header.getValue());
603 if (clientBuilder.getAuthenticationMode() == RestAuthenticationMode.SSL_BASIC) {
604 builder = builder.header(Headers.AUTHORIZATION,
605 clientBuilder.getBasicAuthenticationCredentials());
613 private void debugRequest(String url, String payload, Map<String, List<String>> headers,
614 MediaType responseType) {
615 if (logger.isDebugEnabled()) {
616 StringBuilder debugRequest = new StringBuilder("REQUEST:\n");
617 debugRequest.append("URL: ").append(url).append("\n");
618 debugRequest.append("Payload: ").append(payload).append("\n");
619 debugRequest.append("Response Type: ").append(responseType).append("\n");
620 if (headers != null) {
621 debugRequest.append("Headers: ");
622 for (Entry<String, List<String>> header : headers.entrySet()) {
623 debugRequest.append("\n\t").append(header.getKey()).append(":");
624 for (String headerEntry : header.getValue()) {
625 debugRequest.append("\"").append(headerEntry).append("\" ");
629 logger.debug(debugRequest.toString());
633 private void debugResponse(OperationResult operationResult,
634 MultivaluedMap<String, String> headers) {
635 if (logger.isDebugEnabled()) {
636 StringBuilder debugResponse = new StringBuilder("RESPONSE:\n");
637 debugResponse.append("Result: ").append(operationResult.getResultCode()).append("\n");
638 debugResponse.append("Failure Cause: ").append(operationResult.getFailureCause())
640 debugResponse.append("Payload: ").append(operationResult.getResult()).append("\n");
641 if (headers != null) {
642 debugResponse.append("Headers: ");
643 for (Entry<String, List<String>> header : headers.entrySet()) {
644 debugResponse.append("\n\t").append(header.getKey()).append(":");
645 for (String headerEntry : header.getValue()) {
646 debugResponse.append("\"").append(headerEntry).append("\" ");
650 logger.debug(debugResponse.toString());
655 * This method creates an instance of the low level REST client to use for communicating with the
656 * AAI, if one has not already been created, otherwise it returns the already created instance.
658 * @return A {@link Client} instance.
660 protected Client getClient() throws Exception {
663 * Attempting a new way of doing non-blocking thread-safe lazy-initialization by using Java 1.8
664 * computeIfAbsent functionality. A null value will not be stored, but once a valid mapping has
665 * been established, then the same value will be returned.
667 * One awkwardness of the computeIfAbsent is the lack of support for thrown exceptions, which
668 * required a bit of hoop jumping to preserve the original exception for the purpose of
669 * maintaining the pre-existing this API signature.
672 final InitializedClient clientInstance =
673 CLIENT_CACHE.computeIfAbsent(REST_CLIENT_INSTANCE, k -> loggedClientInitialization());
675 if (clientInstance.getCaughtException() != null) {
676 throw new InstantiationException(clientInstance.getCaughtException().getMessage());
679 return clientInstance.getClient();
684 * This method will only be called if computerIfAbsent is true. The return value is null, then the result is not
687 * @return a new client instance or null
689 private InitializedClient loggedClientInitialization() {
691 if (logger.isDebugEnabled()) {
692 logger.debug("Instantiating REST client with following parameters:");
693 logger.debug(clientBuilder.toString());
696 InitializedClient initClient = new InitializedClient();
699 initClient.setClient(clientBuilder.getClient());
700 } catch ( Throwable error ) {
701 initClient.setCaughtException(error);
710 * This method populates the fields of an {@link OperationResult} instance based on the contents
711 * of a {@link ClientResponse} received in response to a REST request.
713 private void populateOperationResult(ClientResponse response, OperationResult opResult) {
715 // If we got back a NULL response, then just produce a generic
716 // error code and result indicating this.
717 if (response == null) {
718 opResult.setResultCode(500);
719 opResult.setFailureCause("Client response was null");
723 int statusCode = response.getStatus();
724 String payload = response.getEntity(String.class);
726 opResult.setResultCode(statusCode);
728 if (opResult.wasSuccessful()) {
729 opResult.setResult(payload);
731 opResult.setFailureCause(payload);
734 opResult.setHeaders(response.getHeaders());
737 private class GetRestOperation implements RestOperation {
738 public ClientResponse processOperation(Builder builder) {
739 return builder.get(ClientResponse.class);
742 public RequestType getRequestType() {
743 return RequestType.GET;
747 private class PutRestOperation implements RestOperation {
748 public ClientResponse processOperation(Builder builder) {
749 return builder.put(ClientResponse.class);
752 public RequestType getRequestType() {
753 return RequestType.PUT;
757 private class PostRestOperation implements RestOperation {
758 public ClientResponse processOperation(Builder builder) {
759 return builder.post(ClientResponse.class);
762 public RequestType getRequestType() {
763 return RequestType.POST;
767 private class DeleteRestOperation implements RestOperation {
768 public ClientResponse processOperation(Builder builder) {
769 return builder.delete(ClientResponse.class);
772 public RequestType getRequestType() {
773 return RequestType.DELETE;
777 private class HeadRestOperation implements RestOperation {
778 public ClientResponse processOperation(Builder builder) {
779 return builder.head();
782 public RequestType getRequestType() {
783 return RequestType.HEAD;
787 private class PatchRestOperation implements RestOperation {
790 * Technically there is no standarized PATCH operation for the
791 * jersey client, but we can use the method-override approach
794 public ClientResponse processOperation(Builder builder) {
795 builder = builder.header("X-HTTP-Method-Override", "PATCH");
796 return builder.post(ClientResponse.class);
799 public RequestType getRequestType() {
800 return RequestType.PATCH;
806 * Interface used wrap a Jersey REST call using a functional interface.
808 private interface RestOperation {
811 * Method used to wrap the functionality of making a REST call out to the endpoint.
813 * @param builder the Jersey builder used to make the request
814 * @return the response from the REST endpoint
816 public ClientResponse processOperation(Builder builder);
819 * Returns the REST request type.
821 public RequestType getRequestType();
824 * The supported REST request types.
826 public enum RequestType {
827 GET, PUT, POST, DELETE, PATCH, HEAD
832 * An entity to encapsulate an expected result and a potential failure cause when returning from a
833 * functional interface during the computeIfAbsent call.
835 private class InitializedClient {
836 private Client client;
837 private Throwable caughtException;
839 public InitializedClient() {
841 caughtException = null;
844 public Client getClient() {
847 public void setClient(Client client) {
848 this.client = client;
850 public Throwable getCaughtException() {
851 return caughtException;
853 public void setCaughtException(Throwable caughtException) {
854 this.caughtException = caughtException;