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