001/* 002 * #%L 003 * HAPI FHIR - Client Framework 004 * %% 005 * Copyright (C) 2014 - 2024 Smile CDR, Inc. 006 * %% 007 * Licensed under the Apache License, Version 2.0 (the "License"); 008 * you may not use this file except in compliance with the License. 009 * You may obtain a copy of the License at 010 * 011 * http://www.apache.org/licenses/LICENSE-2.0 012 * 013 * Unless required by applicable law or agreed to in writing, software 014 * distributed under the License is distributed on an "AS IS" BASIS, 015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 016 * See the License for the specific language governing permissions and 017 * limitations under the License. 018 * #L% 019 */ 020package ca.uhn.fhir.rest.client.impl; 021 022import ca.uhn.fhir.context.BaseRuntimeChildDefinition; 023import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition; 024import ca.uhn.fhir.context.BaseRuntimeElementDefinition; 025import ca.uhn.fhir.context.FhirContext; 026import ca.uhn.fhir.context.RuntimeResourceDefinition; 027import ca.uhn.fhir.i18n.Msg; 028import ca.uhn.fhir.interceptor.api.HookParams; 029import ca.uhn.fhir.interceptor.api.IInterceptorService; 030import ca.uhn.fhir.interceptor.api.Pointcut; 031import ca.uhn.fhir.interceptor.executor.InterceptorService; 032import ca.uhn.fhir.parser.DataFormatException; 033import ca.uhn.fhir.parser.IParser; 034import ca.uhn.fhir.rest.api.CacheControlDirective; 035import ca.uhn.fhir.rest.api.Constants; 036import ca.uhn.fhir.rest.api.EncodingEnum; 037import ca.uhn.fhir.rest.api.RequestFormatParamStyleEnum; 038import ca.uhn.fhir.rest.api.SummaryEnum; 039import ca.uhn.fhir.rest.client.api.ClientResponseContext; 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.api.IRestfulClientFactory; 045import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum; 046import ca.uhn.fhir.rest.client.exceptions.FhirClientConnectionException; 047import ca.uhn.fhir.rest.client.exceptions.InvalidResponseException; 048import ca.uhn.fhir.rest.client.exceptions.NonFhirResponseException; 049import ca.uhn.fhir.rest.client.interceptor.AdditionalRequestHeadersInterceptor; 050import ca.uhn.fhir.rest.client.method.HttpGetClientInvocation; 051import ca.uhn.fhir.rest.client.method.IClientResponseHandler; 052import ca.uhn.fhir.rest.client.method.IClientResponseHandlerHandlesBinary; 053import ca.uhn.fhir.rest.client.method.MethodUtil; 054import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; 055import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 056import ca.uhn.fhir.system.HapiSystemProperties; 057import ca.uhn.fhir.util.BinaryUtil; 058import ca.uhn.fhir.util.OperationOutcomeUtil; 059import ca.uhn.fhir.util.XmlDetectionUtil; 060import com.google.common.base.Charsets; 061import jakarta.annotation.Nonnull; 062import org.apache.commons.io.IOUtils; 063import org.apache.commons.lang3.StringUtils; 064import org.apache.commons.lang3.Validate; 065import org.hl7.fhir.instance.model.api.IBase; 066import org.hl7.fhir.instance.model.api.IBaseBinary; 067import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; 068import org.hl7.fhir.instance.model.api.IBaseResource; 069import org.hl7.fhir.instance.model.api.IIdType; 070import org.hl7.fhir.instance.model.api.IPrimitiveType; 071 072import java.io.ByteArrayInputStream; 073import java.io.IOException; 074import java.io.InputStream; 075import java.io.Reader; 076import java.util.ArrayList; 077import java.util.Collections; 078import java.util.HashMap; 079import java.util.LinkedHashMap; 080import java.util.List; 081import java.util.Map; 082import java.util.Set; 083 084import static org.apache.commons.lang3.StringUtils.isBlank; 085import static org.apache.commons.lang3.StringUtils.isNotBlank; 086 087public abstract class BaseClient implements IRestfulClient { 088 089 private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseClient.class); 090 091 private final IHttpClient myClient; 092 private final RestfulClientFactory myFactory; 093 private final String myUrlBase; 094 private boolean myDontValidateConformance; 095 private EncodingEnum myEncoding = null; // default unspecified (will be JSON) 096 private boolean myKeepResponses = false; 097 private IHttpResponse myLastResponse; 098 private String myLastResponseBody; 099 private Boolean myPrettyPrint = false; 100 private SummaryEnum mySummary; 101 private RequestFormatParamStyleEnum myRequestFormatParamStyle = RequestFormatParamStyleEnum.SHORT; 102 private IInterceptorService myInterceptorService; 103 104 BaseClient(IHttpClient theClient, String theUrlBase, RestfulClientFactory theFactory) { 105 super(); 106 myClient = theClient; 107 myUrlBase = theUrlBase; 108 myFactory = theFactory; 109 110 /* 111 * This property is used by unit tests - do not rely on it in production code 112 * as it may change at any time. If you want to capture responses in a reliable 113 * way in your own code, just use client interceptors 114 */ 115 if (HapiSystemProperties.isHapiClientKeepResponsesEnabled()) { 116 setKeepResponses(true); 117 } 118 119 if (XmlDetectionUtil.isStaxPresent() == false) { 120 myEncoding = EncodingEnum.JSON; 121 } 122 123 setInterceptorService(new InterceptorService()); 124 } 125 126 @Override 127 public IInterceptorService getInterceptorService() { 128 return myInterceptorService; 129 } 130 131 @Override 132 public void setInterceptorService(@Nonnull IInterceptorService theInterceptorService) { 133 Validate.notNull(theInterceptorService, "theInterceptorService must not be null"); 134 myInterceptorService = theInterceptorService; 135 } 136 137 protected Map<String, List<String>> createExtraParams(String theCustomAcceptHeader) { 138 HashMap<String, List<String>> retVal = new LinkedHashMap<>(); 139 140 if (isBlank(theCustomAcceptHeader)) { 141 if (myRequestFormatParamStyle == RequestFormatParamStyleEnum.SHORT) { 142 if (getEncoding() == EncodingEnum.XML) { 143 retVal.put(Constants.PARAM_FORMAT, Collections.singletonList("xml")); 144 } else if (getEncoding() == EncodingEnum.JSON) { 145 retVal.put(Constants.PARAM_FORMAT, Collections.singletonList("json")); 146 } 147 } 148 } 149 150 if (isPrettyPrint()) { 151 retVal.put(Constants.PARAM_PRETTY, Collections.singletonList(Constants.PARAM_PRETTY_VALUE_TRUE)); 152 } 153 154 return retVal; 155 } 156 157 @Override 158 public <T extends IBaseResource> T fetchResourceFromUrl(Class<T> theResourceType, String theUrl) { 159 BaseHttpClientInvocation clientInvocation = new HttpGetClientInvocation(getFhirContext(), theUrl); 160 ResourceResponseHandler<T> binding = new ResourceResponseHandler<>(theResourceType); 161 return invokeClient( 162 getFhirContext(), binding, clientInvocation, null, false, false, null, null, null, null, null); 163 } 164 165 void forceConformanceCheck() { 166 myFactory.validateServerBase(myUrlBase, myClient, this); 167 } 168 169 @Override 170 public EncodingEnum getEncoding() { 171 return myEncoding; 172 } 173 174 /** 175 * Sets the encoding that will be used on requests. Default is <code>null</code>, which means the client will not 176 * explicitly request an encoding. (This is perfectly acceptable behaviour according to the FHIR specification. In 177 * this case, the server will choose which encoding to return, and the client can handle either XML or JSON) 178 */ 179 @Override 180 public void setEncoding(EncodingEnum theEncoding) { 181 myEncoding = theEncoding; 182 // return this; 183 } 184 185 /** 186 * {@inheritDoc} 187 */ 188 @Override 189 public IHttpClient getHttpClient() { 190 return myClient; 191 } 192 193 /** 194 * For now, this is a part of the internal API of HAPI - Use with caution as this method may change! 195 */ 196 public IHttpResponse getLastResponse() { 197 return myLastResponse; 198 } 199 200 /** 201 * For now, this is a part of the internal API of HAPI - Use with caution as this method may change! 202 */ 203 public String getLastResponseBody() { 204 return myLastResponseBody; 205 } 206 207 /** 208 * {@inheritDoc} 209 */ 210 @Override 211 public String getServerBase() { 212 return myUrlBase; 213 } 214 215 public SummaryEnum getSummary() { 216 return mySummary; 217 } 218 219 @Override 220 public void setSummary(SummaryEnum theSummary) { 221 mySummary = theSummary; 222 } 223 224 public String getUrlBase() { 225 return myUrlBase; 226 } 227 228 @Override 229 public void setFormatParamStyle(RequestFormatParamStyleEnum theRequestFormatParamStyle) { 230 Validate.notNull(theRequestFormatParamStyle, "theRequestFormatParamStyle must not be null"); 231 myRequestFormatParamStyle = theRequestFormatParamStyle; 232 } 233 234 protected <T> T invokeClient( 235 FhirContext theContext, IClientResponseHandler<T> binding, BaseHttpClientInvocation clientInvocation) { 236 return invokeClient(theContext, binding, clientInvocation, false); 237 } 238 239 protected <T> T invokeClient( 240 FhirContext theContext, 241 IClientResponseHandler<T> binding, 242 BaseHttpClientInvocation clientInvocation, 243 boolean theLogRequestAndResponse) { 244 return invokeClient( 245 theContext, 246 binding, 247 clientInvocation, 248 null, 249 null, 250 theLogRequestAndResponse, 251 null, 252 null, 253 null, 254 null, 255 null); 256 } 257 258 protected <T> T invokeClient( 259 FhirContext theContext, 260 IClientResponseHandler<T> binding, 261 BaseHttpClientInvocation clientInvocation, 262 EncodingEnum theEncoding, 263 Boolean thePrettyPrint, 264 boolean theLogRequestAndResponse, 265 SummaryEnum theSummaryMode, 266 Set<String> theSubsetElements, 267 CacheControlDirective theCacheControlDirective, 268 String theCustomAcceptHeader, 269 Map<String, List<String>> theCustomHeaders) { 270 271 if (!myDontValidateConformance) { 272 myFactory.validateServerBaseIfConfiguredToDoSo(myUrlBase, myClient, this); 273 } 274 275 // TODO: handle non 2xx status codes by throwing the correct exception, 276 // and ensure it's passed upwards 277 IHttpRequest httpRequest = null; 278 IHttpResponse response = null; 279 try { 280 Map<String, List<String>> params = createExtraParams(theCustomAcceptHeader); 281 282 if (clientInvocation instanceof HttpGetClientInvocation) { 283 if (myRequestFormatParamStyle == RequestFormatParamStyleEnum.SHORT && isBlank(theCustomAcceptHeader)) { 284 if (theEncoding == EncodingEnum.XML) { 285 params.put(Constants.PARAM_FORMAT, Collections.singletonList("xml")); 286 } else if (theEncoding == EncodingEnum.JSON) { 287 params.put(Constants.PARAM_FORMAT, Collections.singletonList("json")); 288 } 289 } 290 } 291 292 if (theSummaryMode != null) { 293 params.put(Constants.PARAM_SUMMARY, Collections.singletonList(theSummaryMode.getCode())); 294 } else if (mySummary != null) { 295 params.put(Constants.PARAM_SUMMARY, Collections.singletonList(mySummary.getCode())); 296 } 297 298 if (thePrettyPrint == Boolean.TRUE) { 299 params.put(Constants.PARAM_PRETTY, Collections.singletonList(Constants.PARAM_PRETTY_VALUE_TRUE)); 300 } 301 302 if (theSubsetElements != null && theSubsetElements.isEmpty() == false) { 303 params.put( 304 Constants.PARAM_ELEMENTS, Collections.singletonList(StringUtils.join(theSubsetElements, ','))); 305 } 306 307 EncodingEnum encoding = getEncoding(); 308 if (theEncoding != null) { 309 encoding = theEncoding; 310 } 311 312 httpRequest = clientInvocation.asHttpRequest(myUrlBase, params, encoding, thePrettyPrint); 313 314 if (isNotBlank(theCustomAcceptHeader)) { 315 httpRequest.removeHeaders(Constants.HEADER_ACCEPT); 316 httpRequest.addHeader(Constants.HEADER_ACCEPT, theCustomAcceptHeader); 317 } 318 319 if (theCacheControlDirective != null) { 320 StringBuilder b = new StringBuilder(); 321 addToCacheControlHeader(b, Constants.CACHE_CONTROL_NO_CACHE, theCacheControlDirective.isNoCache()); 322 addToCacheControlHeader(b, Constants.CACHE_CONTROL_NO_STORE, theCacheControlDirective.isNoStore()); 323 if (theCacheControlDirective.getMaxResults() != null) { 324 addToCacheControlHeader( 325 b, 326 Constants.CACHE_CONTROL_MAX_RESULTS + "=" 327 + theCacheControlDirective.getMaxResults().intValue(), 328 true); 329 } 330 if (b.length() > 0) { 331 httpRequest.addHeader(Constants.HEADER_CACHE_CONTROL, b.toString()); 332 } 333 } 334 335 if (theLogRequestAndResponse) { 336 ourLog.info("Client invoking: {}", httpRequest); 337 String body = httpRequest.getRequestBodyFromStream(); 338 if (body != null) { 339 ourLog.info("Client request body: {}", body); 340 } 341 } 342 343 if (theCustomHeaders != null) { 344 AdditionalRequestHeadersInterceptor interceptor = 345 new AdditionalRequestHeadersInterceptor(theCustomHeaders); 346 interceptor.interceptRequest(httpRequest); 347 } 348 349 HookParams requestParams = new HookParams(); 350 requestParams.add(IHttpRequest.class, httpRequest); 351 requestParams.add(IRestfulClient.class, this); 352 getInterceptorService().callHooks(Pointcut.CLIENT_REQUEST, requestParams); 353 354 response = httpRequest.execute(); 355 356 final Class<? extends IBaseResource> returnType = (binding instanceof ResourceResponseHandler) 357 ? ((ResourceResponseHandler<? extends IBaseResource>) binding).getReturnType() 358 : null; 359 360 final ClientResponseContext clientResponseContext = 361 new ClientResponseContext(httpRequest, response, this, getFhirContext(), returnType); 362 HookParams responseParams = new HookParams(); 363 responseParams.add(IHttpRequest.class, httpRequest); 364 responseParams.add(IHttpResponse.class, response); 365 responseParams.add(IRestfulClient.class, this); 366 responseParams.add(ClientResponseContext.class, clientResponseContext); 367 368 getInterceptorService().callHooks(Pointcut.CLIENT_RESPONSE, responseParams); 369 370 // Replace the contents of the response with whatever the hook returned, or the same response as before if 371 // it no-op'd 372 response = clientResponseContext.getHttpResponse(); 373 374 String mimeType; 375 if (Constants.STATUS_HTTP_204_NO_CONTENT == response.getStatus()) { 376 mimeType = null; 377 } else { 378 mimeType = response.getMimeType(); 379 } 380 381 Map<String, List<String>> headers = response.getAllHeaders(); 382 383 if (response.getStatus() < 200 || response.getStatus() > 299) { 384 String body = null; 385 try (Reader reader = response.createReader()) { 386 body = IOUtils.toString(reader); 387 } catch (Exception e) { 388 ourLog.debug("Failed to read input stream", e); 389 } 390 391 String message = "HTTP " + response.getStatus() + " " + response.getStatusInfo(); 392 IBaseOperationOutcome oo = null; 393 if (Constants.CT_TEXT.equals(mimeType)) { 394 message = message + ": " + body; 395 } else { 396 EncodingEnum enc = EncodingEnum.forContentType(mimeType); 397 if (enc != null) { 398 IParser p = enc.newParser(theContext); 399 try { 400 // TODO: handle if something other than OO comes back 401 oo = (IBaseOperationOutcome) p.parseResource(body); 402 String details = OperationOutcomeUtil.getFirstIssueDetails(getFhirContext(), oo); 403 if (isNotBlank(details)) { 404 message = message + ": " + details; 405 } 406 } catch (Exception e) { 407 ourLog.debug("Failed to process OperationOutcome response"); 408 } 409 } 410 } 411 412 keepResponseAndLogIt(theLogRequestAndResponse, response, body); 413 414 BaseServerResponseException exception = 415 BaseServerResponseException.newInstance(response.getStatus(), message); 416 exception.setOperationOutcome(oo); 417 418 if (body != null) { 419 exception.setResponseBody(body); 420 } 421 422 throw exception; 423 } 424 if (binding instanceof IClientResponseHandlerHandlesBinary) { 425 IClientResponseHandlerHandlesBinary<T> handlesBinary = (IClientResponseHandlerHandlesBinary<T>) binding; 426 if (handlesBinary.isBinary()) { 427 try (InputStream reader = response.readEntity()) { 428 return handlesBinary.invokeClientForBinary(mimeType, reader, response.getStatus(), headers); 429 } 430 } 431 } 432 433 try (InputStream inputStream = response.readEntity()) { 434 InputStream inputStreamToReturn = inputStream; 435 436 if (ourLog.isTraceEnabled() || myKeepResponses || theLogRequestAndResponse) { 437 if (inputStream != null) { 438 String responseString = IOUtils.toString(inputStream, Charsets.UTF_8); 439 keepResponseAndLogIt(theLogRequestAndResponse, response, responseString); 440 inputStreamToReturn = new ByteArrayInputStream(responseString.getBytes(Charsets.UTF_8)); 441 } 442 } 443 444 if (inputStreamToReturn == null) { 445 inputStreamToReturn = new ByteArrayInputStream(new byte[] {}); 446 } 447 448 return binding.invokeClient(mimeType, inputStreamToReturn, response.getStatus(), headers); 449 } 450 451 } catch (DataFormatException e) { 452 String msg; 453 if (httpRequest != null) { 454 msg = getFhirContext() 455 .getLocalizer() 456 .getMessage( 457 BaseClient.class, 458 "failedToParseResponse", 459 httpRequest.getHttpVerbName(), 460 httpRequest.getUri(), 461 e.toString()); 462 } else { 463 msg = getFhirContext() 464 .getLocalizer() 465 .getMessage(BaseClient.class, "failedToParseResponse", "UNKNOWN", "UNKNOWN", e.toString()); 466 } 467 throw new FhirClientConnectionException(Msg.code(1359) + msg, e); 468 } catch (IllegalStateException e) { 469 throw new FhirClientConnectionException(Msg.code(1360) + e); 470 } catch (IOException e) { 471 String msg; 472 msg = getFhirContext() 473 .getLocalizer() 474 .getMessage( 475 BaseClient.class, 476 "failedToParseResponse", 477 httpRequest.getHttpVerbName(), 478 httpRequest.getUri(), 479 e.toString()); 480 throw new FhirClientConnectionException(Msg.code(1361) + msg, e); 481 } catch (RuntimeException e) { 482 throw e; 483 } catch (Exception e) { 484 throw new FhirClientConnectionException(Msg.code(1362) + e); 485 } finally { 486 if (response != null) { 487 response.close(); 488 } 489 } 490 } 491 492 private void addToCacheControlHeader(StringBuilder theBuilder, String theDirective, boolean theActive) { 493 if (theActive) { 494 if (theBuilder.length() > 0) { 495 theBuilder.append(", "); 496 } 497 theBuilder.append(theDirective); 498 } 499 } 500 501 /** 502 * For now, this is a part of the internal API of HAPI - Use with caution as this method may change! 503 */ 504 public boolean isKeepResponses() { 505 return myKeepResponses; 506 } 507 508 /** 509 * For now, this is a part of the internal API of HAPI - Use with caution as this method may change! 510 */ 511 public void setKeepResponses(boolean theKeepResponses) { 512 myKeepResponses = theKeepResponses; 513 } 514 515 /** 516 * Returns the pretty print flag, which is a request to the server for it to return "pretty printed" responses. Note 517 * that this is currently a non-standard flag (_pretty) which is supported only by HAPI based servers (and any other 518 * servers which might implement it). 519 */ 520 public boolean isPrettyPrint() { 521 return Boolean.TRUE.equals(myPrettyPrint); 522 } 523 524 /** 525 * Sets the pretty print flag, which is a request to the server for it to return "pretty printed" responses. Note 526 * that this is currently a non-standard flag (_pretty) which is supported only by HAPI based servers (and any other 527 * servers which might implement it). 528 */ 529 @Override 530 public void setPrettyPrint(Boolean thePrettyPrint) { 531 myPrettyPrint = thePrettyPrint; 532 // return this; 533 } 534 535 private void keepResponseAndLogIt(boolean theLogRequestAndResponse, IHttpResponse response, String responseString) { 536 if (myKeepResponses) { 537 myLastResponse = response; 538 myLastResponseBody = responseString; 539 } 540 if (theLogRequestAndResponse) { 541 String message = "HTTP " + response.getStatus() + " " + response.getStatusInfo(); 542 if (StringUtils.isNotBlank(responseString)) { 543 ourLog.info("Client response: {}\n{}", message, responseString); 544 } else { 545 ourLog.info("Client response: {}", message); 546 } 547 } else { 548 ourLog.trace("FHIR response:\n{}\n{}", response, responseString); 549 } 550 } 551 552 @Override 553 public void registerInterceptor(Object theInterceptor) { 554 Validate.notNull(theInterceptor, "Interceptor can not be null"); 555 getInterceptorService().registerInterceptor(theInterceptor); 556 } 557 558 /** 559 * This method is an internal part of the HAPI API and may change, use with caution. If you want to disable the 560 * loading of conformance statements, use 561 * {@link IRestfulClientFactory#setServerValidationMode(ServerValidationModeEnum)} 562 */ 563 public void setDontValidateConformance(boolean theDontValidateConformance) { 564 myDontValidateConformance = theDontValidateConformance; 565 } 566 567 @Override 568 public void unregisterInterceptor(Object theInterceptor) { 569 Validate.notNull(theInterceptor, "Interceptor can not be null"); 570 getInterceptorService().unregisterInterceptor(theInterceptor); 571 } 572 573 protected final class ResourceOrBinaryResponseHandler extends ResourceResponseHandler<IBaseResource> { 574 575 @Override 576 public IBaseResource invokeClient( 577 String theResponseMimeType, 578 InputStream theResponseInputStream, 579 int theResponseStatusCode, 580 Map<String, List<String>> theHeaders) 581 throws BaseServerResponseException { 582 583 /* 584 * For operation responses, if the response content type is a FHIR content-type 585 * (which is will probably almost always be) we just handle it normally. However, 586 * if we get back a successful (2xx) response from an operation, and the content 587 * type is something other than FHIR, we'll return it as a Binary wrapped in 588 * a Parameters resource. 589 */ 590 EncodingEnum respType = EncodingEnum.forContentType(theResponseMimeType); 591 if (respType != null || theResponseStatusCode < 200 || theResponseStatusCode >= 300) { 592 return super.invokeClient( 593 theResponseMimeType, theResponseInputStream, theResponseStatusCode, theHeaders); 594 } 595 596 // Create a Binary resource to return 597 IBaseBinary responseBinary = BinaryUtil.newBinary(getFhirContext()); 598 599 // Fetch the content type 600 String contentType = null; 601 List<String> contentTypeHeaders = theHeaders.get(Constants.HEADER_CONTENT_TYPE_LC); 602 if (contentTypeHeaders != null && contentTypeHeaders.size() > 0) { 603 contentType = contentTypeHeaders.get(0); 604 } 605 responseBinary.setContentType(contentType); 606 607 // Fetch the content itself 608 try { 609 responseBinary.setContent(IOUtils.toByteArray(theResponseInputStream)); 610 } catch (IOException e) { 611 throw new InternalErrorException(Msg.code(1363) + "IO failure parsing response", e); 612 } 613 614 return responseBinary; 615 } 616 } 617 618 protected class ResourceResponseHandler<T extends IBaseResource> implements IClientResponseHandler<T> { 619 620 private boolean myAllowHtmlResponse; 621 private IIdType myId; 622 private List<Class<? extends IBaseResource>> myPreferResponseTypes; 623 private Class<T> myReturnType; 624 625 public ResourceResponseHandler() { 626 this(null); 627 } 628 629 public ResourceResponseHandler(Class<T> theReturnType) { 630 this(theReturnType, null, null); 631 } 632 633 public ResourceResponseHandler( 634 Class<T> theReturnType, Class<? extends IBaseResource> thePreferResponseType, IIdType theId) { 635 this(theReturnType, thePreferResponseType, theId, false); 636 } 637 638 public ResourceResponseHandler( 639 Class<T> theReturnType, 640 Class<? extends IBaseResource> thePreferResponseType, 641 IIdType theId, 642 boolean theAllowHtmlResponse) { 643 this(theReturnType, toTypeList(thePreferResponseType), theId, theAllowHtmlResponse); 644 } 645 646 public ResourceResponseHandler(Class<T> theClass, List<Class<? extends IBaseResource>> thePreferResponseTypes) { 647 this(theClass, thePreferResponseTypes, null, false); 648 } 649 650 public ResourceResponseHandler( 651 Class<T> theReturnType, 652 List<Class<? extends IBaseResource>> thePreferResponseTypes, 653 IIdType theId, 654 boolean theAllowHtmlResponse) { 655 myReturnType = theReturnType; 656 myId = theId; 657 myPreferResponseTypes = thePreferResponseTypes; 658 myAllowHtmlResponse = theAllowHtmlResponse; 659 } 660 661 public Class<T> getReturnType() { 662 return myReturnType; 663 } 664 665 @Override 666 public T invokeClient( 667 String theResponseMimeType, 668 InputStream theResponseInputStream, 669 int theResponseStatusCode, 670 Map<String, List<String>> theHeaders) 671 throws BaseServerResponseException { 672 if (theResponseStatusCode == Constants.STATUS_HTTP_204_NO_CONTENT) { 673 return null; 674 } 675 676 EncodingEnum respType = EncodingEnum.forContentType(theResponseMimeType); 677 if (respType == null) { 678 if (myAllowHtmlResponse 679 && theResponseMimeType.toLowerCase().contains(Constants.CT_HTML) 680 && myReturnType != null) { 681 return readHtmlResponse(theResponseInputStream); 682 } 683 throw NonFhirResponseException.newInstance( 684 theResponseStatusCode, theResponseMimeType, theResponseInputStream); 685 } 686 IParser parser = respType.newParser(getFhirContext()); 687 parser.setServerBaseUrl(getUrlBase()); 688 if (myPreferResponseTypes != null) { 689 parser.setPreferTypes(myPreferResponseTypes); 690 } 691 T retVal = parser.parseResource(myReturnType, theResponseInputStream); 692 693 MethodUtil.parseClientRequestResourceHeaders(myId, theHeaders, retVal); 694 695 return retVal; 696 } 697 698 @SuppressWarnings("unchecked") 699 private T readHtmlResponse(InputStream theResponseInputStream) { 700 RuntimeResourceDefinition resDef = getFhirContext().getResourceDefinition(myReturnType); 701 IBaseResource instance = resDef.newInstance(); 702 BaseRuntimeChildDefinition textChild = resDef.getChildByName("text"); 703 BaseRuntimeElementCompositeDefinition<?> textElement = 704 (BaseRuntimeElementCompositeDefinition<?>) textChild.getChildByName("text"); 705 IBase textInstance = textElement.newInstance(); 706 textChild.getMutator().addValue(instance, textInstance); 707 708 BaseRuntimeChildDefinition divChild = textElement.getChildByName("div"); 709 BaseRuntimeElementDefinition<?> divElement = divChild.getChildByName("div"); 710 IPrimitiveType<?> divInstance = (IPrimitiveType<?>) divElement.newInstance(); 711 try { 712 divInstance.setValueAsString(IOUtils.toString(theResponseInputStream, Charsets.UTF_8)); 713 } catch (Exception e) { 714 throw new InvalidResponseException( 715 Msg.code(1364) + "Failed to process HTML response from server: " + e.getMessage(), 400, e); 716 } 717 divChild.getMutator().addValue(textInstance, divInstance); 718 return (T) instance; 719 } 720 721 public ResourceResponseHandler<T> setPreferResponseTypes( 722 List<Class<? extends IBaseResource>> thePreferResponseTypes) { 723 myPreferResponseTypes = thePreferResponseTypes; 724 return this; 725 } 726 } 727 728 static ArrayList<Class<? extends IBaseResource>> toTypeList(Class<? extends IBaseResource> thePreferResponseType) { 729 ArrayList<Class<? extends IBaseResource>> preferResponseTypes = null; 730 if (thePreferResponseType != null) { 731 preferResponseTypes = new ArrayList<>(1); 732 preferResponseTypes.add(thePreferResponseType); 733 } 734 return preferResponseTypes; 735 } 736}