001package ca.uhn.fhir.rest.client.impl; 002 003/* 004 * #%L 005 * HAPI FHIR - Client Framework 006 * %% 007 * Copyright (C) 2014 - 2018 University Health Network 008 * %% 009 * Licensed under the Apache License, Version 2.0 (the "License"); 010 * you may not use this file except in compliance with the License. 011 * You may obtain a copy of the License at 012 * 013 * http://www.apache.org/licenses/LICENSE-2.0 014 * 015 * Unless required by applicable law or agreed to in writing, software 016 * distributed under the License is distributed on an "AS IS" BASIS, 017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 018 * See the License for the specific language governing permissions and 019 * limitations under the License. 020 * #L% 021 */ 022 023import ca.uhn.fhir.context.*; 024import ca.uhn.fhir.parser.DataFormatException; 025import ca.uhn.fhir.parser.IParser; 026import ca.uhn.fhir.rest.api.*; 027import ca.uhn.fhir.rest.client.api.*; 028import ca.uhn.fhir.rest.client.exceptions.FhirClientConnectionException; 029import ca.uhn.fhir.rest.client.exceptions.InvalidResponseException; 030import ca.uhn.fhir.rest.client.exceptions.NonFhirResponseException; 031import ca.uhn.fhir.rest.client.method.HttpGetClientInvocation; 032import ca.uhn.fhir.rest.client.method.IClientResponseHandler; 033import ca.uhn.fhir.rest.client.method.IClientResponseHandlerHandlesBinary; 034import ca.uhn.fhir.rest.client.method.MethodUtil; 035import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; 036import ca.uhn.fhir.util.OperationOutcomeUtil; 037import ca.uhn.fhir.util.XmlDetectionUtil; 038import org.apache.commons.io.IOUtils; 039import org.apache.commons.lang3.StringUtils; 040import org.apache.commons.lang3.Validate; 041import org.hl7.fhir.instance.model.api.*; 042 043import java.io.IOException; 044import java.io.InputStream; 045import java.io.Reader; 046import java.io.StringReader; 047import java.util.*; 048 049import static org.apache.commons.lang3.StringUtils.isNotBlank; 050 051public abstract class BaseClient implements IRestfulClient { 052 053 /** 054 * This property is used by unit tests - do not rely on it in production code 055 * as it may change at any time. If you want to capture responses in a reliable 056 * way in your own code, just use client interceptors 057 */ 058 public static final String HAPI_CLIENT_KEEPRESPONSES = "hapi.client.keepresponses"; 059 060 private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseClient.class); 061 062 private final IHttpClient myClient; 063 private final RestfulClientFactory myFactory; 064 private final String myUrlBase; 065 private boolean myDontValidateConformance; 066 private EncodingEnum myEncoding = null; // default unspecified (will be XML) 067 private List<IClientInterceptor> myInterceptors = new ArrayList<IClientInterceptor>(); 068 private boolean myKeepResponses = false; 069 private IHttpResponse myLastResponse; 070 private String myLastResponseBody; 071 private Boolean myPrettyPrint = false; 072 private SummaryEnum mySummary; 073 private RequestFormatParamStyleEnum myRequestFormatParamStyle = RequestFormatParamStyleEnum.SHORT; 074 075 BaseClient(IHttpClient theClient, String theUrlBase, RestfulClientFactory theFactory) { 076 super(); 077 myClient = theClient; 078 myUrlBase = theUrlBase; 079 myFactory = theFactory; 080 081 /* 082 * This property is used by unit tests - do not rely on it in production code 083 * as it may change at any time. If you want to capture responses in a reliable 084 * way in your own code, just use client interceptors 085 */ 086 if ("true".equals(System.getProperty(HAPI_CLIENT_KEEPRESPONSES))) { 087 setKeepResponses(true); 088 } 089 090 if (XmlDetectionUtil.isStaxPresent() == false) { 091 myEncoding = EncodingEnum.JSON; 092 } 093 094 } 095 096 protected Map<String, List<String>> createExtraParams() { 097 HashMap<String, List<String>> retVal = new LinkedHashMap<String, List<String>>(); 098 099 if (myRequestFormatParamStyle == RequestFormatParamStyleEnum.SHORT) { 100 if (getEncoding() == EncodingEnum.XML) { 101 retVal.put(Constants.PARAM_FORMAT, Collections.singletonList("xml")); 102 } else if (getEncoding() == EncodingEnum.JSON) { 103 retVal.put(Constants.PARAM_FORMAT, Collections.singletonList("json")); 104 } 105 } 106 107 if (isPrettyPrint()) { 108 retVal.put(Constants.PARAM_PRETTY, Collections.singletonList(Constants.PARAM_PRETTY_VALUE_TRUE)); 109 } 110 111 return retVal; 112 } 113 114 @Override 115 public <T extends IBaseResource> T fetchResourceFromUrl(Class<T> theResourceType, String theUrl) { 116 BaseHttpClientInvocation clientInvocation = new HttpGetClientInvocation(getFhirContext(), theUrl); 117 ResourceResponseHandler<T> binding = new ResourceResponseHandler<T>(theResourceType); 118 return invokeClient(getFhirContext(), binding, clientInvocation, null, false, false, null, null, null); 119 } 120 121 void forceConformanceCheck() { 122 myFactory.validateServerBase(myUrlBase, myClient, this); 123 } 124 125 @Override 126 public EncodingEnum getEncoding() { 127 return myEncoding; 128 } 129 130 /** 131 * Sets the encoding that will be used on requests. Default is <code>null</code>, which means the client will not 132 * explicitly request an encoding. (This is perfectly acceptable behaviour according to the FHIR specification. In 133 * this case, the server will choose which encoding to return, and the client can handle either XML or JSON) 134 */ 135 @Override 136 public void setEncoding(EncodingEnum theEncoding) { 137 myEncoding = theEncoding; 138 // return this; 139 } 140 141 /** 142 * {@inheritDoc} 143 */ 144 @Override 145 public IHttpClient getHttpClient() { 146 return myClient; 147 } 148 149 /** 150 * {@inheritDoc} 151 */ 152 @Override 153 public List<IClientInterceptor> getInterceptors() { 154 return Collections.unmodifiableList(myInterceptors); 155 } 156 157 /** 158 * For now, this is a part of the internal API of HAPI - Use with caution as this method may change! 159 */ 160 public IHttpResponse getLastResponse() { 161 return myLastResponse; 162 } 163 164 /** 165 * For now, this is a part of the internal API of HAPI - Use with caution as this method may change! 166 */ 167 public String getLastResponseBody() { 168 return myLastResponseBody; 169 } 170 171 /** 172 * {@inheritDoc} 173 */ 174 @Override 175 public String getServerBase() { 176 return myUrlBase; 177 } 178 179 public SummaryEnum getSummary() { 180 return mySummary; 181 } 182 183 @Override 184 public void setSummary(SummaryEnum theSummary) { 185 mySummary = theSummary; 186 } 187 188 public String getUrlBase() { 189 return myUrlBase; 190 } 191 192 @Override 193 public void setFormatParamStyle(RequestFormatParamStyleEnum theRequestFormatParamStyle) { 194 Validate.notNull(theRequestFormatParamStyle, "theRequestFormatParamStyle must not be null"); 195 myRequestFormatParamStyle = theRequestFormatParamStyle; 196 } 197 198 <T> T invokeClient(FhirContext theContext, IClientResponseHandler<T> binding, BaseHttpClientInvocation clientInvocation) { 199 return invokeClient(theContext, binding, clientInvocation, false); 200 } 201 202 <T> T invokeClient(FhirContext theContext, IClientResponseHandler<T> binding, BaseHttpClientInvocation clientInvocation, boolean theLogRequestAndResponse) { 203 return invokeClient(theContext, binding, clientInvocation, null, null, theLogRequestAndResponse, null, null, null); 204 } 205 206 <T> T invokeClient(FhirContext theContext, IClientResponseHandler<T> binding, BaseHttpClientInvocation clientInvocation, EncodingEnum theEncoding, Boolean thePrettyPrint, 207 boolean theLogRequestAndResponse, SummaryEnum theSummaryMode, Set<String> theSubsetElements, CacheControlDirective theCacheControlDirective) { 208 209 if (!myDontValidateConformance) { 210 myFactory.validateServerBaseIfConfiguredToDoSo(myUrlBase, myClient, this); 211 } 212 213 // TODO: handle non 2xx status codes by throwing the correct exception, 214 // and ensure it's passed upwards 215 IHttpRequest httpRequest = null; 216 IHttpResponse response = null; 217 try { 218 Map<String, List<String>> params = createExtraParams(); 219 220 if (clientInvocation instanceof HttpGetClientInvocation) { 221 if (myRequestFormatParamStyle == RequestFormatParamStyleEnum.SHORT) { 222 if (theEncoding == EncodingEnum.XML) { 223 params.put(Constants.PARAM_FORMAT, Collections.singletonList("xml")); 224 } else if (theEncoding == EncodingEnum.JSON) { 225 params.put(Constants.PARAM_FORMAT, Collections.singletonList("json")); 226 } 227 } 228 } 229 230 if (theSummaryMode != null) { 231 params.put(Constants.PARAM_SUMMARY, Collections.singletonList(theSummaryMode.getCode())); 232 } else if (mySummary != null) { 233 params.put(Constants.PARAM_SUMMARY, Collections.singletonList(mySummary.getCode())); 234 } 235 236 if (thePrettyPrint == Boolean.TRUE) { 237 params.put(Constants.PARAM_PRETTY, Collections.singletonList(Constants.PARAM_PRETTY_VALUE_TRUE)); 238 } 239 240 if (theSubsetElements != null && theSubsetElements.isEmpty() == false) { 241 params.put(Constants.PARAM_ELEMENTS, Collections.singletonList(StringUtils.join(theSubsetElements, ','))); 242 } 243 244 EncodingEnum encoding = getEncoding(); 245 if (theEncoding != null) { 246 encoding = theEncoding; 247 } 248 249 httpRequest = clientInvocation.asHttpRequest(myUrlBase, params, encoding, thePrettyPrint); 250 251 if (theCacheControlDirective != null) { 252 StringBuilder b = new StringBuilder(); 253 addToCacheControlHeader(b, Constants.CACHE_CONTROL_NO_CACHE, theCacheControlDirective.isNoCache()); 254 addToCacheControlHeader(b, Constants.CACHE_CONTROL_NO_STORE, theCacheControlDirective.isNoStore()); 255 if (theCacheControlDirective.getMaxResults() != null) { 256 addToCacheControlHeader(b, Constants.CACHE_CONTROL_MAX_RESULTS + "=" + Integer.toString(theCacheControlDirective.getMaxResults().intValue()), true); 257 } 258 if (b.length() > 0) { 259 httpRequest.addHeader(Constants.HEADER_CACHE_CONTROL, b.toString()); 260 } 261 } 262 263 if (theLogRequestAndResponse) { 264 ourLog.info("Client invoking: {}", httpRequest); 265 String body = httpRequest.getRequestBodyFromStream(); 266 if (body != null) { 267 ourLog.info("Client request body: {}", body); 268 } 269 } 270 271 for (IClientInterceptor nextInterceptor : myInterceptors) { 272 nextInterceptor.interceptRequest(httpRequest); 273 } 274 275 response = httpRequest.execute(); 276 277 for (IClientInterceptor nextInterceptor : myInterceptors) { 278 nextInterceptor.interceptResponse(response); 279 } 280 281 String mimeType; 282 if (Constants.STATUS_HTTP_204_NO_CONTENT == response.getStatus()) { 283 mimeType = null; 284 } else { 285 mimeType = response.getMimeType(); 286 } 287 288 Map<String, List<String>> headers = response.getAllHeaders(); 289 290 if (response.getStatus() < 200 || response.getStatus() > 299) { 291 String body = null; 292 Reader reader = null; 293 try { 294 reader = response.createReader(); 295 body = IOUtils.toString(reader); 296 } catch (Exception e) { 297 ourLog.debug("Failed to read input stream", e); 298 } finally { 299 IOUtils.closeQuietly(reader); 300 } 301 302 String message = "HTTP " + response.getStatus() + " " + response.getStatusInfo(); 303 IBaseOperationOutcome oo = null; 304 if (Constants.CT_TEXT.equals(mimeType)) { 305 message = message + ": " + body; 306 } else { 307 EncodingEnum enc = EncodingEnum.forContentType(mimeType); 308 if (enc != null) { 309 IParser p = enc.newParser(theContext); 310 try { 311 // TODO: handle if something other than OO comes back 312 oo = (IBaseOperationOutcome) p.parseResource(body); 313 String details = OperationOutcomeUtil.getFirstIssueDetails(getFhirContext(), oo); 314 if (isNotBlank(details)) { 315 message = message + ": " + details; 316 } 317 } catch (Exception e) { 318 ourLog.debug("Failed to process OperationOutcome response"); 319 } 320 } 321 } 322 323 keepResponseAndLogIt(theLogRequestAndResponse, response, body); 324 325 BaseServerResponseException exception = BaseServerResponseException.newInstance(response.getStatus(), message); 326 exception.setOperationOutcome(oo); 327 328 if (body != null) { 329 exception.setResponseBody(body); 330 } 331 332 throw exception; 333 } 334 if (binding instanceof IClientResponseHandlerHandlesBinary) { 335 IClientResponseHandlerHandlesBinary<T> handlesBinary = (IClientResponseHandlerHandlesBinary<T>) binding; 336 if (handlesBinary.isBinary()) { 337 InputStream reader = response.readEntity(); 338 try { 339 return handlesBinary.invokeClient(mimeType, reader, response.getStatus(), headers); 340 } finally { 341 IOUtils.closeQuietly(reader); 342 } 343 } 344 } 345 346 Reader reader = response.createReader(); 347 348 if (ourLog.isTraceEnabled() || myKeepResponses || theLogRequestAndResponse) { 349 String responseString = IOUtils.toString(reader); 350 keepResponseAndLogIt(theLogRequestAndResponse, response, responseString); 351 reader = new StringReader(responseString); 352 } 353 354 try { 355 return binding.invokeClient(mimeType, reader, response.getStatus(), headers); 356 } finally { 357 IOUtils.closeQuietly(reader); 358 } 359 360 } catch (DataFormatException e) { 361 String msg; 362 if (httpRequest != null) { 363 msg = getFhirContext().getLocalizer().getMessage(BaseClient.class, "failedToParseResponse", httpRequest.getHttpVerbName(), httpRequest.getUri(), e.toString()); 364 } else { 365 msg = getFhirContext().getLocalizer().getMessage(BaseClient.class, "failedToParseResponse", "UNKNOWN", "UNKNOWN", e.toString()); 366 } 367 throw new FhirClientConnectionException(msg, e); 368 } catch (IllegalStateException e) { 369 throw new FhirClientConnectionException(e); 370 } catch (IOException e) { 371 String msg; 372 msg = getFhirContext().getLocalizer().getMessage(BaseClient.class, "failedToParseResponse", httpRequest.getHttpVerbName(), httpRequest.getUri(), e.toString()); 373 throw new FhirClientConnectionException(msg, e); 374 } catch (RuntimeException e) { 375 throw e; 376 } catch (Exception e) { 377 throw new FhirClientConnectionException(e); 378 } finally { 379 if (response != null) { 380 response.close(); 381 } 382 } 383 } 384 385 private void addToCacheControlHeader(StringBuilder theBuilder, String theDirective, boolean theActive) { 386 if (theActive) { 387 if (theBuilder.length() > 0) { 388 theBuilder.append(", "); 389 } 390 theBuilder.append(theDirective); 391 } 392 } 393 394 /** 395 * For now, this is a part of the internal API of HAPI - Use with caution as this method may change! 396 */ 397 public boolean isKeepResponses() { 398 return myKeepResponses; 399 } 400 401 /** 402 * For now, this is a part of the internal API of HAPI - Use with caution as this method may change! 403 */ 404 public void setKeepResponses(boolean theKeepResponses) { 405 myKeepResponses = theKeepResponses; 406 } 407 408 /** 409 * Returns the pretty print flag, which is a request to the server for it to return "pretty printed" responses. Note 410 * that this is currently a non-standard flag (_pretty) which is supported only by HAPI based servers (and any other 411 * servers which might implement it). 412 */ 413 public boolean isPrettyPrint() { 414 return Boolean.TRUE.equals(myPrettyPrint); 415 } 416 417 /** 418 * Sets the pretty print flag, which is a request to the server for it to return "pretty printed" responses. Note 419 * that this is currently a non-standard flag (_pretty) which is supported only by HAPI based servers (and any other 420 * servers which might implement it). 421 */ 422 @Override 423 public void setPrettyPrint(Boolean thePrettyPrint) { 424 myPrettyPrint = thePrettyPrint; 425 // return this; 426 } 427 428 private void keepResponseAndLogIt(boolean theLogRequestAndResponse, IHttpResponse response, String responseString) { 429 if (myKeepResponses) { 430 myLastResponse = response; 431 myLastResponseBody = responseString; 432 } 433 if (theLogRequestAndResponse) { 434 String message = "HTTP " + response.getStatus() + " " + response.getStatusInfo(); 435 if (StringUtils.isNotBlank(responseString)) { 436 ourLog.info("Client response: {}\n{}", message, responseString); 437 } else { 438 ourLog.info("Client response: {}", message, responseString); 439 } 440 } else { 441 ourLog.trace("FHIR response:\n{}\n{}", response, responseString); 442 } 443 } 444 445 @Override 446 public void registerInterceptor(IClientInterceptor theInterceptor) { 447 Validate.notNull(theInterceptor, "Interceptor can not be null"); 448 myInterceptors.add(theInterceptor); 449 } 450 451 /** 452 * This method is an internal part of the HAPI API and may change, use with caution. If you want to disable the 453 * loading of conformance statements, use 454 * {@link IRestfulClientFactory#setServerValidationModeEnum(ServerValidationModeEnum)} 455 */ 456 public void setDontValidateConformance(boolean theDontValidateConformance) { 457 myDontValidateConformance = theDontValidateConformance; 458 } 459 460 @Override 461 public void unregisterInterceptor(IClientInterceptor theInterceptor) { 462 Validate.notNull(theInterceptor, "Interceptor can not be null"); 463 myInterceptors.remove(theInterceptor); 464 } 465 466 protected final class ResourceResponseHandler<T extends IBaseResource> implements IClientResponseHandler<T> { 467 468 private boolean myAllowHtmlResponse; 469 private IIdType myId; 470 private List<Class<? extends IBaseResource>> myPreferResponseTypes; 471 private Class<T> myReturnType; 472 473 public ResourceResponseHandler() { 474 this(null); 475 } 476 477 public ResourceResponseHandler(Class<T> theReturnType) { 478 this(theReturnType, null, null); 479 } 480 481 public ResourceResponseHandler(Class<T> theReturnType, Class<? extends IBaseResource> thePreferResponseType, IIdType theId) { 482 this(theReturnType, thePreferResponseType, theId, false); 483 } 484 485 public ResourceResponseHandler(Class<T> theReturnType, Class<? extends IBaseResource> thePreferResponseType, IIdType theId, boolean theAllowHtmlResponse) { 486 this(theReturnType, toTypeList(thePreferResponseType), theId, theAllowHtmlResponse); 487 } 488 489 public ResourceResponseHandler(Class<T> theClass, List<Class<? extends IBaseResource>> thePreferResponseTypes) { 490 this(theClass, thePreferResponseTypes, null, false); 491 } 492 493 public ResourceResponseHandler(Class<T> theReturnType, List<Class<? extends IBaseResource>> thePreferResponseTypes, IIdType theId, boolean theAllowHtmlResponse) { 494 myReturnType = theReturnType; 495 myId = theId; 496 myPreferResponseTypes = thePreferResponseTypes; 497 myAllowHtmlResponse = theAllowHtmlResponse; 498 } 499 500 @Override 501 public T invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map<String, List<String>> theHeaders) throws BaseServerResponseException { 502 EncodingEnum respType = EncodingEnum.forContentType(theResponseMimeType); 503 if (respType == null) { 504 if (myAllowHtmlResponse && theResponseMimeType.toLowerCase().contains(Constants.CT_HTML) && myReturnType != null) { 505 return readHtmlResponse(theResponseReader); 506 } 507 throw NonFhirResponseException.newInstance(theResponseStatusCode, theResponseMimeType, theResponseReader); 508 } 509 IParser parser = respType.newParser(getFhirContext()); 510 parser.setServerBaseUrl(getUrlBase()); 511 if (myPreferResponseTypes != null) { 512 parser.setPreferTypes(myPreferResponseTypes); 513 } 514 T retVal = parser.parseResource(myReturnType, theResponseReader); 515 516 MethodUtil.parseClientRequestResourceHeaders(myId, theHeaders, retVal); 517 518 return retVal; 519 } 520 521 @SuppressWarnings("unchecked") 522 private T readHtmlResponse(Reader theResponseReader) { 523 RuntimeResourceDefinition resDef = getFhirContext().getResourceDefinition(myReturnType); 524 IBaseResource instance = resDef.newInstance(); 525 BaseRuntimeChildDefinition textChild = resDef.getChildByName("text"); 526 BaseRuntimeElementCompositeDefinition<?> textElement = (BaseRuntimeElementCompositeDefinition<?>) textChild.getChildByName("text"); 527 IBase textInstance = textElement.newInstance(); 528 textChild.getMutator().addValue(instance, textInstance); 529 530 BaseRuntimeChildDefinition divChild = textElement.getChildByName("div"); 531 BaseRuntimeElementDefinition<?> divElement = divChild.getChildByName("div"); 532 IPrimitiveType<?> divInstance = (IPrimitiveType<?>) divElement.newInstance(); 533 try { 534 divInstance.setValueAsString(IOUtils.toString(theResponseReader)); 535 } catch (Exception e) { 536 throw new InvalidResponseException(400, "Failed to process HTML response from server: " + e.getMessage(), e); 537 } 538 divChild.getMutator().addValue(textInstance, divInstance); 539 return (T) instance; 540 } 541 542 public void setPreferResponseTypes(List<Class<? extends IBaseResource>> thePreferResponseTypes) { 543 myPreferResponseTypes = thePreferResponseTypes; 544 } 545 } 546 547 static ArrayList<Class<? extends IBaseResource>> toTypeList(Class<? extends IBaseResource> thePreferResponseType) { 548 ArrayList<Class<? extends IBaseResource>> preferResponseTypes = null; 549 if (thePreferResponseType != null) { 550 preferResponseTypes = new ArrayList<Class<? extends IBaseResource>>(1); 551 preferResponseTypes.add(thePreferResponseType); 552 } 553 return preferResponseTypes; 554 } 555 556}