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