001package ca.uhn.fhir.rest.client; 002 003import static org.apache.commons.lang3.StringUtils.isNotBlank; 004 005/* 006 * #%L 007 * HAPI FHIR - Core Library 008 * %% 009 * Copyright (C) 2014 - 2016 University Health Network 010 * %% 011 * Licensed under the Apache License, Version 2.0 (the "License"); 012 * you may not use this file except in compliance with the License. 013 * You may obtain a copy of the License at 014 * 015 * http://www.apache.org/licenses/LICENSE-2.0 016 * 017 * Unless required by applicable law or agreed to in writing, software 018 * distributed under the License is distributed on an "AS IS" BASIS, 019 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 020 * See the License for the specific language governing permissions and 021 * limitations under the License. 022 * #L% 023 */ 024 025import java.io.ByteArrayInputStream; 026import java.io.IOException; 027import java.io.InputStream; 028import java.io.InputStreamReader; 029import java.io.Reader; 030import java.io.StringReader; 031import java.nio.charset.Charset; 032import java.util.ArrayList; 033import java.util.Collections; 034import java.util.HashMap; 035import java.util.LinkedHashMap; 036import java.util.List; 037import java.util.Map; 038import java.util.Set; 039 040import org.apache.commons.io.IOUtils; 041import org.apache.commons.lang3.StringUtils; 042import org.apache.commons.lang3.Validate; 043import org.apache.http.Header; 044import org.apache.http.HttpEntity; 045import org.apache.http.HttpEntityEnclosingRequest; 046import org.apache.http.HttpResponse; 047import org.apache.http.client.HttpClient; 048import org.apache.http.client.methods.CloseableHttpResponse; 049import org.apache.http.client.methods.HttpRequestBase; 050import org.apache.http.entity.ContentType; 051import org.hl7.fhir.instance.model.api.IBase; 052import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; 053import org.hl7.fhir.instance.model.api.IBaseResource; 054import org.hl7.fhir.instance.model.api.IIdType; 055import org.hl7.fhir.instance.model.api.IPrimitiveType; 056 057import ca.uhn.fhir.context.BaseRuntimeChildDefinition; 058import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition; 059import ca.uhn.fhir.context.BaseRuntimeElementDefinition; 060import ca.uhn.fhir.context.FhirContext; 061import ca.uhn.fhir.context.RuntimeResourceDefinition; 062import ca.uhn.fhir.parser.DataFormatException; 063import ca.uhn.fhir.parser.IParser; 064import ca.uhn.fhir.rest.api.SummaryEnum; 065import ca.uhn.fhir.rest.client.api.IRestfulClient; 066import ca.uhn.fhir.rest.client.exceptions.FhirClientConnectionException; 067import ca.uhn.fhir.rest.client.exceptions.InvalidResponseException; 068import ca.uhn.fhir.rest.client.exceptions.NonFhirResponseException; 069import ca.uhn.fhir.rest.method.HttpGetClientInvocation; 070import ca.uhn.fhir.rest.method.IClientResponseHandler; 071import ca.uhn.fhir.rest.method.IClientResponseHandlerHandlesBinary; 072import ca.uhn.fhir.rest.method.MethodUtil; 073import ca.uhn.fhir.rest.server.Constants; 074import ca.uhn.fhir.rest.server.EncodingEnum; 075import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; 076import ca.uhn.fhir.util.OperationOutcomeUtil; 077 078public abstract class BaseClient implements IRestfulClient { 079 080 private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseClient.class); 081 082 private final HttpClient myClient; 083 private boolean myDontValidateConformance; 084 private EncodingEnum myEncoding = null; // default unspecified (will be XML) 085 private final RestfulClientFactory myFactory; 086 private List<IClientInterceptor> myInterceptors = new ArrayList<IClientInterceptor>(); 087 private boolean myKeepResponses = false; 088 private HttpResponse myLastResponse; 089 private String myLastResponseBody; 090 private Boolean myPrettyPrint = false; 091 private SummaryEnum mySummary; 092 private final String myUrlBase; 093 094 BaseClient(HttpClient theClient, String theUrlBase, RestfulClientFactory theFactory) { 095 super(); 096 myClient = theClient; 097 myUrlBase = theUrlBase; 098 myFactory = theFactory; 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 void forceConformanceCheck() { 118 myFactory.validateServerBase(myUrlBase, myClient, this); 119 } 120 121 /** 122 * Returns the encoding that will be used on requests. Default is <code>null</code>, which means the client will not 123 * explicitly request an encoding. (This is standard behaviour according to the FHIR specification) 124 */ 125 public EncodingEnum getEncoding() { 126 return myEncoding; 127 } 128 129 /** 130 * {@inheritDoc} 131 */ 132 @Override 133 public HttpClient getHttpClient() { 134 return myClient; 135 } 136 137 public List<IClientInterceptor> getInterceptors() { 138 return Collections.unmodifiableList(myInterceptors); 139 } 140 141 /** 142 * For now, this is a part of the internal API of HAPI - Use with caution as this method may change! 143 */ 144 public HttpResponse getLastResponse() { 145 return myLastResponse; 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 String getLastResponseBody() { 152 return myLastResponseBody; 153 } 154 155 /** 156 * Returns the pretty print flag, which is a request to the server for it to return "pretty printed" responses. Note 157 * that this is currently a non-standard flag (_pretty) which is supported only by HAPI based servers (and any other 158 * servers which might implement it). 159 */ 160 public Boolean getPrettyPrint() { 161 return myPrettyPrint; 162 } 163 164 /** 165 * {@inheritDoc} 166 */ 167 @Override 168 public String getServerBase() { 169 return myUrlBase; 170 } 171 172 public SummaryEnum getSummary() { 173 return mySummary; 174 } 175 176 public String getUrlBase() { 177 return myUrlBase; 178 } 179 180 <T> T invokeClient(FhirContext theContext, IClientResponseHandler<T> binding, BaseHttpClientInvocation clientInvocation) { 181 return invokeClient(theContext, binding, clientInvocation, false); 182 } 183 184 <T> T invokeClient(FhirContext theContext, IClientResponseHandler<T> binding, BaseHttpClientInvocation clientInvocation, boolean theLogRequestAndResponse) { 185 return invokeClient(theContext, binding, clientInvocation, null, null, theLogRequestAndResponse, null, null); 186 } 187 188 <T> T invokeClient(FhirContext theContext, IClientResponseHandler<T> binding, BaseHttpClientInvocation clientInvocation, EncodingEnum theEncoding, Boolean thePrettyPrint, boolean theLogRequestAndResponse, SummaryEnum theSummaryMode, Set<String> theSubsetElements) { 189 190 if (!myDontValidateConformance) { 191 myFactory.validateServerBaseIfConfiguredToDoSo(myUrlBase, myClient, this); 192 } 193 194 // TODO: handle non 2xx status codes by throwing the correct exception, 195 // and ensure it's passed upwards 196 HttpRequestBase httpRequest; 197 HttpResponse response; 198 try { 199 Map<String, List<String>> params = createExtraParams(); 200 201 if (theEncoding == EncodingEnum.XML) { 202 params.put(Constants.PARAM_FORMAT, Collections.singletonList("xml")); 203 } else if (theEncoding == EncodingEnum.JSON) { 204 params.put(Constants.PARAM_FORMAT, Collections.singletonList("json")); 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 if (httpRequest instanceof HttpEntityEnclosingRequest) { 231 HttpEntity entity = ((HttpEntityEnclosingRequest) httpRequest).getEntity(); 232 if (entity.isRepeatable()) { 233 String content = IOUtils.toString(entity.getContent()); 234 ourLog.info("Client request body: {}", content); 235 } 236 } 237 } 238 239 for (IClientInterceptor nextInterceptor : myInterceptors) { 240 nextInterceptor.interceptRequest(httpRequest); 241 } 242 243 response = myClient.execute(httpRequest); 244 245 for (IClientInterceptor nextInterceptor : myInterceptors) { 246 nextInterceptor.interceptResponse(response); 247 } 248 249 } catch (DataFormatException e) { 250 throw new FhirClientConnectionException(e); 251 } catch (IOException e) { 252 throw new FhirClientConnectionException(e); 253 } 254 255 try { 256 String mimeType; 257 if (Constants.STATUS_HTTP_204_NO_CONTENT == response.getStatusLine().getStatusCode()) { 258 mimeType = null; 259 } else { 260 ContentType ct = ContentType.get(response.getEntity()); 261 mimeType = ct != null ? ct.getMimeType() : null; 262 } 263 264 Map<String, List<String>> headers = new HashMap<String, List<String>>(); 265 if (response.getAllHeaders() != null) { 266 for (Header next : response.getAllHeaders()) { 267 String name = next.getName().toLowerCase(); 268 List<String> list = headers.get(name); 269 if (list == null) { 270 list = new ArrayList<String>(); 271 headers.put(name, list); 272 } 273 list.add(next.getValue()); 274 } 275 } 276 277 if (response.getStatusLine().getStatusCode() < 200 || response.getStatusLine().getStatusCode() > 299) { 278 String body = null; 279 Reader reader = null; 280 try { 281 reader = createReaderFromResponse(response); 282 body = IOUtils.toString(reader); 283 } catch (Exception e) { 284 ourLog.debug("Failed to read input stream", e); 285 } finally { 286 IOUtils.closeQuietly(reader); 287 } 288 289 String message = "HTTP " + response.getStatusLine().getStatusCode() + " " + response.getStatusLine().getReasonPhrase(); 290 IBaseOperationOutcome oo = null; 291 if (Constants.CT_TEXT.equals(mimeType)) { 292 message = message + ": " + body; 293 } else { 294 EncodingEnum enc = EncodingEnum.forContentType(mimeType); 295 if (enc != null) { 296 IParser p = enc.newParser(theContext); 297 try { 298 // TODO: handle if something other than OO comes back 299 oo = (IBaseOperationOutcome) p.parseResource(body); 300 String details = OperationOutcomeUtil.getFirstIssueDetails(getFhirContext(), oo); 301 if (isNotBlank(details)) { 302 message = message + ": " + details; 303 } 304 } catch (Exception e) { 305 ourLog.debug("Failed to process OperationOutcome response"); 306 } 307 } 308 } 309 310 keepResponseAndLogIt(theLogRequestAndResponse, response, body); 311 312 BaseServerResponseException exception = BaseServerResponseException.newInstance(response.getStatusLine().getStatusCode(), message); 313 exception.setOperationOutcome(oo); 314 315 if (body != null) { 316 exception.setResponseBody(body); 317 } 318 319 throw exception; 320 } 321 if (binding instanceof IClientResponseHandlerHandlesBinary) { 322 IClientResponseHandlerHandlesBinary<T> handlesBinary = (IClientResponseHandlerHandlesBinary<T>) binding; 323 if (handlesBinary.isBinary()) { 324 InputStream reader = response.getEntity().getContent(); 325 try { 326 327 if (ourLog.isTraceEnabled() || myKeepResponses || theLogRequestAndResponse) { 328 byte[] responseBytes = IOUtils.toByteArray(reader); 329 if (myKeepResponses) { 330 myLastResponse = response; 331 myLastResponseBody = null; 332 } 333 String message = "HTTP " + response.getStatusLine().getStatusCode() + " " + response.getStatusLine().getReasonPhrase(); 334 if (theLogRequestAndResponse) { 335 ourLog.info("Client response: {} - {} bytes", message, responseBytes.length); 336 } else { 337 ourLog.trace("Client response: {} - {} bytes", message, responseBytes.length); 338 } 339 reader = new ByteArrayInputStream(responseBytes); 340 } 341 342 return handlesBinary.invokeClient(mimeType, reader, response.getStatusLine().getStatusCode(), headers); 343 } finally { 344 IOUtils.closeQuietly(reader); 345 } 346 } 347 } 348 349 Reader reader = createReaderFromResponse(response); 350 351 if (ourLog.isTraceEnabled() || myKeepResponses || theLogRequestAndResponse) { 352 String responseString = IOUtils.toString(reader); 353 keepResponseAndLogIt(theLogRequestAndResponse, response, responseString); 354 reader = new StringReader(responseString); 355 } 356 357 try { 358 return binding.invokeClient(mimeType, reader, response.getStatusLine().getStatusCode(), headers); 359 } finally { 360 IOUtils.closeQuietly(reader); 361 } 362 363 } catch (IllegalStateException e) { 364 throw new FhirClientConnectionException(e); 365 } catch (IOException e) { 366 throw new FhirClientConnectionException(e); 367 } finally { 368 if (response instanceof CloseableHttpResponse) { 369 try { 370 ((CloseableHttpResponse) response).close(); 371 } catch (IOException e) { 372 ourLog.debug("Failed to close response", e); 373 } 374 } 375 } 376 } 377 378 /** 379 * For now, this is a part of the internal API of HAPI - Use with caution as this method may change! 380 */ 381 public boolean isKeepResponses() { 382 return myKeepResponses; 383 } 384 385 /** 386 * Returns the pretty print flag, which is a request to the server for it to return "pretty printed" responses. Note 387 * that this is currently a non-standard flag (_pretty) which is supported only by HAPI based servers (and any other 388 * servers which might implement it). 389 */ 390 public boolean isPrettyPrint() { 391 return Boolean.TRUE.equals(myPrettyPrint); 392 } 393 394 private void keepResponseAndLogIt(boolean theLogRequestAndResponse, HttpResponse response, String responseString) { 395 if (myKeepResponses) { 396 myLastResponse = response; 397 myLastResponseBody = responseString; 398 } 399 if (theLogRequestAndResponse) { 400 String message = "HTTP " + response.getStatusLine().getStatusCode() + " " + response.getStatusLine().getReasonPhrase(); 401 if (StringUtils.isNotBlank(responseString)) { 402 ourLog.info("Client response: {}\n{}", message, responseString); 403 } else { 404 ourLog.info("Client response: {}", message, responseString); 405 } 406 } else { 407 ourLog.trace("FHIR response:\n{}\n{}", response, responseString); 408 } 409 } 410 411 @Override 412 public void registerInterceptor(IClientInterceptor theInterceptor) { 413 Validate.notNull(theInterceptor, "Interceptor can not be null"); 414 myInterceptors.add(theInterceptor); 415 } 416 417 /** 418 * This method is an internal part of the HAPI API and may change, use with caution. If you want to disable the 419 * loading of conformance statements, use 420 * {@link IRestfulClientFactory#setServerValidationModeEnum(ServerValidationModeEnum)} 421 */ 422 public void setDontValidateConformance(boolean theDontValidateConformance) { 423 myDontValidateConformance = theDontValidateConformance; 424 } 425 426 /** 427 * Sets the encoding that will be used on requests. Default is <code>null</code>, which means the client will not 428 * explicitly request an encoding. (This is perfectly acceptable behaviour according to the FHIR specification. In 429 * this case, the server will choose which encoding to return, and the client can handle either XML or JSON) 430 */ 431 @Override 432 public void setEncoding(EncodingEnum theEncoding) { 433 myEncoding = theEncoding; 434 // return this; 435 } 436 437 /** 438 * For now, this is a part of the internal API of HAPI - Use with caution as this method may change! 439 */ 440 public void setKeepResponses(boolean theKeepResponses) { 441 myKeepResponses = theKeepResponses; 442 } 443 444 /** 445 * For now, this is a part of the internal API of HAPI - Use with caution as this method may change! 446 */ 447 public void setLastResponse(HttpResponse theLastResponse) { 448 myLastResponse = theLastResponse; 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 setLastResponseBody(String theLastResponseBody) { 455 myLastResponseBody = theLastResponseBody; 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 public static Reader createReaderFromResponse(HttpResponse theResponse) throws IllegalStateException, IOException { 481 HttpEntity entity = theResponse.getEntity(); 482 if (entity == null) { 483 return new StringReader(""); 484 } 485 Charset charset = null; 486 if (entity.getContentType() != null && entity.getContentType().getElements() != null && entity.getContentType().getElements().length > 0) { 487 ContentType ct = ContentType.get(entity); 488 charset = ct.getCharset(); 489 } 490 if (charset == null) { 491 if (Constants.STATUS_HTTP_204_NO_CONTENT != theResponse.getStatusLine().getStatusCode()) { 492 ourLog.warn("Response did not specify a charset."); 493 } 494 charset = Charset.forName("UTF-8"); 495 } 496 497 Reader reader = new InputStreamReader(theResponse.getEntity().getContent(), charset); 498 return reader; 499 } 500 501 @Override 502 public <T extends IBaseResource> T fetchResourceFromUrl(Class<T> theResourceType, String theUrl) { 503 BaseHttpClientInvocation clientInvocation = new HttpGetClientInvocation(theUrl); 504 ResourceResponseHandler<T> binding = new ResourceResponseHandler<T>(theResourceType, null, false); 505 return invokeClient(getFhirContext(), binding, clientInvocation, null, false, false, null, null); 506 } 507 508 protected final class ResourceResponseHandler<T extends IBaseResource> implements IClientResponseHandler<T> { 509 510 private boolean myAllowHtmlResponse; 511 private IIdType myId; 512 private Class<T> myType; 513 514 public ResourceResponseHandler(Class<T> theType, IIdType theId) { 515 myType = theType; 516 myId = theId; 517 } 518 519 public ResourceResponseHandler(Class<T> theType, IIdType theId, boolean theAllowHtmlResponse) { 520 myType = theType; 521 myId = theId; 522 myAllowHtmlResponse = theAllowHtmlResponse; 523 } 524 525 @Override 526 public T invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map<String, List<String>> theHeaders) throws BaseServerResponseException { 527 EncodingEnum respType = EncodingEnum.forContentType(theResponseMimeType); 528 if (respType == null) { 529 if (myAllowHtmlResponse && theResponseMimeType.toLowerCase().contains(Constants.CT_HTML) && myType != null) { 530 return readHtmlResponse(theResponseReader); 531 } 532 throw NonFhirResponseException.newInstance(theResponseStatusCode, theResponseMimeType, theResponseReader); 533 } 534 IParser parser = respType.newParser(getFhirContext()); 535 T retVal = parser.parseResource(myType, theResponseReader); 536 537 MethodUtil.parseClientRequestResourceHeaders(myId, theHeaders, retVal); 538 539 return retVal; 540 } 541 542 @SuppressWarnings("unchecked") 543 private T readHtmlResponse(Reader theResponseReader) { 544 RuntimeResourceDefinition resDef = getFhirContext().getResourceDefinition(myType); 545 IBaseResource instance = resDef.newInstance(); 546 BaseRuntimeChildDefinition textChild = resDef.getChildByName("text"); 547 BaseRuntimeElementCompositeDefinition<?> textElement = (BaseRuntimeElementCompositeDefinition<?>) textChild.getChildByName("text"); 548 IBase textInstance = textElement.newInstance(); 549 textChild.getMutator().addValue(instance, textInstance); 550 551 BaseRuntimeChildDefinition divChild = textElement.getChildByName("div"); 552 BaseRuntimeElementDefinition<?> divElement = divChild.getChildByName("div"); 553 IPrimitiveType<?> divInstance = (IPrimitiveType<?>) divElement.newInstance(); 554 try { 555 divInstance.setValueAsString(IOUtils.toString(theResponseReader)); 556 } catch (Exception e) { 557 throw new InvalidResponseException(400, "Failed to process HTML response from server: " + e.getMessage(), e); 558 } 559 divChild.getMutator().addValue(textInstance, divInstance); 560 return (T) instance; 561 } 562 } 563 564}