
001/* 002 * #%L 003 * HAPI FHIR - Server Framework 004 * %% 005 * Copyright (C) 2014 - 2025 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.server; 021 022import ca.uhn.fhir.context.ConfigurationException; 023import ca.uhn.fhir.context.FhirContext; 024import ca.uhn.fhir.context.RuntimeResourceDefinition; 025import ca.uhn.fhir.context.api.AddProfileTagEnum; 026import ca.uhn.fhir.context.api.BundleInclusionRule; 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.model.primitive.InstantDt; 033import ca.uhn.fhir.parser.IParser; 034import ca.uhn.fhir.rest.annotation.Destroy; 035import ca.uhn.fhir.rest.annotation.IdParam; 036import ca.uhn.fhir.rest.annotation.Initialize; 037import ca.uhn.fhir.rest.api.Constants; 038import ca.uhn.fhir.rest.api.EncodingEnum; 039import ca.uhn.fhir.rest.api.MethodOutcome; 040import ca.uhn.fhir.rest.api.PreferReturnEnum; 041import ca.uhn.fhir.rest.api.RequestTypeEnum; 042import ca.uhn.fhir.rest.api.RestOperationTypeEnum; 043import ca.uhn.fhir.rest.api.server.BaseParseAction; 044import ca.uhn.fhir.rest.api.server.IFhirVersionServer; 045import ca.uhn.fhir.rest.api.server.IRestfulServer; 046import ca.uhn.fhir.rest.api.server.RequestDetails; 047import ca.uhn.fhir.rest.server.RestfulServerUtils.ResponseEncoding; 048import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; 049import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 050import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException; 051import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; 052import ca.uhn.fhir.rest.server.interceptor.ExceptionHandlingInterceptor; 053import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; 054import ca.uhn.fhir.rest.server.method.BaseMethodBinding; 055import ca.uhn.fhir.rest.server.method.ConformanceMethodBinding; 056import ca.uhn.fhir.rest.server.method.MethodMatchEnum; 057import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; 058import ca.uhn.fhir.rest.server.tenant.ITenantIdentificationStrategy; 059import ca.uhn.fhir.util.CoverageIgnore; 060import ca.uhn.fhir.util.OperationOutcomeUtil; 061import ca.uhn.fhir.util.ReflectionUtil; 062import ca.uhn.fhir.util.UrlPathTokenizer; 063import ca.uhn.fhir.util.UrlUtil; 064import ca.uhn.fhir.util.VersionUtil; 065import com.google.common.collect.Lists; 066import jakarta.annotation.Nonnull; 067import jakarta.annotation.Nullable; 068import jakarta.servlet.ServletException; 069import jakarta.servlet.UnavailableException; 070import jakarta.servlet.http.HttpServlet; 071import jakarta.servlet.http.HttpServletRequest; 072import jakarta.servlet.http.HttpServletResponse; 073import org.apache.commons.lang3.RandomStringUtils; 074import org.apache.commons.lang3.StringUtils; 075import org.apache.commons.lang3.Validate; 076import org.hl7.fhir.instance.model.api.IBaseConformance; 077import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; 078import org.hl7.fhir.instance.model.api.IBaseResource; 079import org.hl7.fhir.instance.model.api.IIdType; 080import org.slf4j.Logger; 081import org.slf4j.LoggerFactory; 082 083import java.io.IOException; 084import java.io.InputStream; 085import java.io.Writer; 086import java.lang.annotation.Annotation; 087import java.lang.reflect.Method; 088import java.lang.reflect.Modifier; 089import java.util.ArrayList; 090import java.util.Arrays; 091import java.util.Collection; 092import java.util.Collections; 093import java.util.Date; 094import java.util.HashMap; 095import java.util.HashSet; 096import java.util.Iterator; 097import java.util.List; 098import java.util.ListIterator; 099import java.util.Map; 100import java.util.Set; 101import java.util.concurrent.locks.Lock; 102import java.util.concurrent.locks.ReentrantLock; 103import java.util.jar.Manifest; 104 105import static ca.uhn.fhir.util.StringUtil.toUtf8String; 106import static java.util.stream.Collectors.toList; 107import static org.apache.commons.lang3.StringUtils.isBlank; 108import static org.apache.commons.lang3.StringUtils.isNotBlank; 109 110/** 111 * This class is the central class for the HAPI FHIR Plain Server framework. 112 * <p> 113 * See <a href="https://hapifhir.io/hapi-fhir/docs/server_plain/">HAPI FHIR Plain Server</a> 114 * for information on how to use this framework. 115 */ 116@SuppressWarnings("WeakerAccess") 117public class RestfulServer extends HttpServlet implements IRestfulServer<ServletRequestDetails> { 118 119 /** 120 * All incoming requests will have an attribute added to {@link HttpServletRequest#getAttribute(String)} 121 * with this key. The value will be a Java {@link Date} with the time that request processing began. 122 */ 123 public static final String REQUEST_START_TIME = RestfulServer.class.getName() + "REQUEST_START_TIME"; 124 /** 125 * Default setting for {@link #setETagSupport(ETagSupportEnum) ETag Support}: {@link ETagSupportEnum#ENABLED} 126 */ 127 public static final ETagSupportEnum DEFAULT_ETAG_SUPPORT = ETagSupportEnum.ENABLED; 128 /** 129 * Requests will have an HttpServletRequest attribute set with this name, containing the servlet 130 * context, in order to avoid a dependency on Servlet-API 3.0+ 131 */ 132 public static final String SERVLET_CONTEXT_ATTRIBUTE = "ca.uhn.fhir.rest.server.RestfulServer.servlet_context"; 133 /** 134 * Default value for {@link #setDefaultPreferReturn(PreferReturnEnum)} 135 */ 136 public static final PreferReturnEnum DEFAULT_PREFER_RETURN = PreferReturnEnum.REPRESENTATION; 137 138 private static final ExceptionHandlingInterceptor DEFAULT_EXCEPTION_HANDLER = new ExceptionHandlingInterceptor(); 139 private static final Logger ourLog = LoggerFactory.getLogger(RestfulServer.class); 140 private static final long serialVersionUID = 1L; 141 private final List<Object> myPlainProviders = new ArrayList<>(); 142 private final List<IResourceProvider> myResourceProviders = new ArrayList<>(); 143 private IInterceptorService myInterceptorService; 144 private BundleInclusionRule myBundleInclusionRule = BundleInclusionRule.BASED_ON_INCLUDES; 145 private boolean myDefaultPrettyPrint = false; 146 private EncodingEnum myDefaultResponseEncoding = EncodingEnum.JSON; 147 private ETagSupportEnum myETagSupport = DEFAULT_ETAG_SUPPORT; 148 private FhirContext myFhirContext; 149 private boolean myIgnoreServerParsedRequestParameters = true; 150 private String myImplementationDescription; 151 private String myCopyright; 152 private IPagingProvider myPagingProvider; 153 private Integer myDefaultPageSize; 154 private Integer myMaximumPageSize; 155 private boolean myStatelessPagingDefault = false; 156 private Lock myProviderRegistrationMutex = new ReentrantLock(); 157 private Map<String, ResourceBinding> myResourceNameToBinding = new HashMap<>(); 158 private IServerAddressStrategy myServerAddressStrategy = new IncomingRequestAddressStrategy(); 159 private ResourceBinding myServerBinding = new ResourceBinding(); 160 private ResourceBinding myGlobalBinding = new ResourceBinding(); 161 private ConformanceMethodBinding myServerConformanceMethod; 162 private Object myServerConformanceProvider; 163 private String myServerName = "HAPI FHIR Server"; 164 /** 165 * This is configurable but by default we just use HAPI version 166 */ 167 private String myServerVersion = createPoweredByHeaderProductVersion(); 168 169 private boolean myStarted; 170 private boolean myUncompressIncomingContents = true; 171 private ITenantIdentificationStrategy myTenantIdentificationStrategy; 172 private PreferReturnEnum myDefaultPreferReturn = DEFAULT_PREFER_RETURN; 173 private ElementsSupportEnum myElementsSupport = ElementsSupportEnum.EXTENDED; 174 175 /** 176 * Constructor. Note that if no {@link FhirContext} is passed in to the server (either through the constructor, or 177 * through {@link #setFhirContext(FhirContext)}) the server will determine which 178 * version of FHIR to support through classpath scanning. This is brittle, and it is highly recommended to explicitly 179 * specify a FHIR version. 180 */ 181 public RestfulServer() { 182 this(null); 183 } 184 185 /** 186 * Constructor 187 */ 188 public RestfulServer(FhirContext theCtx) { 189 this(theCtx, new InterceptorService()); 190 } 191 192 public RestfulServer(FhirContext theCtx, IInterceptorService theInterceptorService) { 193 myFhirContext = theCtx; 194 setInterceptorService(theInterceptorService); 195 } 196 197 /** 198 * @since 5.5.0 199 */ 200 public ConformanceMethodBinding getServerConformanceMethod() { 201 return myServerConformanceMethod; 202 } 203 204 private void addContentLocationHeaders( 205 RequestDetails theRequest, 206 HttpServletResponse servletResponse, 207 MethodOutcome response, 208 String resourceName) { 209 if (response != null && response.getId() != null) { 210 addLocationHeader(theRequest, servletResponse, response, Constants.HEADER_LOCATION, resourceName); 211 addLocationHeader(theRequest, servletResponse, response, Constants.HEADER_CONTENT_LOCATION, resourceName); 212 } 213 } 214 215 /** 216 * This method is called prior to sending a response to incoming requests. It is used to add custom headers. 217 * <p> 218 * Use caution if overriding this method: it is recommended to call <code>super.addHeadersToResponse</code> to avoid 219 * inadvertently disabling functionality. 220 * </p> 221 */ 222 public void addHeadersToResponse(HttpServletResponse theHttpResponse) { 223 String poweredByHeader = createPoweredByHeader(); 224 if (isNotBlank(poweredByHeader)) { 225 theHttpResponse.addHeader(Constants.HEADER_POWERED_BY, poweredByHeader); 226 } 227 } 228 229 private void addLocationHeader( 230 RequestDetails theRequest, 231 HttpServletResponse theResponse, 232 MethodOutcome response, 233 String headerLocation, 234 String resourceName) { 235 StringBuilder b = new StringBuilder(); 236 b.append(theRequest.getFhirServerBase()); 237 b.append('/'); 238 b.append(resourceName); 239 b.append('/'); 240 b.append(response.getId().getIdPart()); 241 if (response.getId().hasVersionIdPart()) { 242 b.append("/" + Constants.PARAM_HISTORY + "/"); 243 b.append(response.getId().getVersionIdPart()); 244 } 245 theResponse.addHeader(headerLocation, b.toString()); 246 } 247 248 public RestfulServerConfiguration createConfiguration() { 249 RestfulServerConfiguration result = new RestfulServerConfiguration(); 250 result.setResourceBindings(getResourceBindings()); 251 result.setServerBindings(getServerBindings()); 252 result.setGlobalBindings(getGlobalBindings()); 253 result.setImplementationDescription(getImplementationDescription()); 254 result.setServerVersion(getServerVersion()); 255 result.setServerName(getServerName()); 256 result.setFhirContext(getFhirContext()); 257 result.setServerAddressStrategy(myServerAddressStrategy); 258 try (InputStream inputStream = getClass().getResourceAsStream("/META-INF/MANIFEST.MF")) { 259 if (inputStream != null) { 260 Manifest manifest = new Manifest(inputStream); 261 String value = manifest.getMainAttributes().getValue("Build-Time"); 262 result.setConformanceDate(new InstantDt(value)); 263 } 264 } catch (Exception e) { 265 // fall through 266 } 267 result.computeSharedSupertypeForResourcePerName(getResourceProviders()); 268 return result; 269 } 270 271 private List<BaseMethodBinding> getGlobalBindings() { 272 return myGlobalBinding.getMethodBindings(); 273 } 274 275 protected List<String> createPoweredByAttributes() { 276 return Lists.newArrayList( 277 "FHIR Server", 278 "FHIR " + myFhirContext.getVersion().getVersion().getFhirVersionString() + "/" 279 + myFhirContext.getVersion().getVersion().name()); 280 } 281 282 /** 283 * Subclasses may override to provide their own powered by 284 * header. Note that if you want to be nice and still credit HAPI 285 * FHIR you could consider overriding 286 * {@link #createPoweredByAttributes()} instead and adding your own 287 * fragments to the list. 288 */ 289 protected String createPoweredByHeader() { 290 StringBuilder b = new StringBuilder(); 291 b.append(createPoweredByHeaderProductName()); 292 b.append(" "); 293 b.append(createPoweredByHeaderProductVersion()); 294 b.append(" "); 295 b.append(createPoweredByHeaderComponentName()); 296 b.append(" ("); 297 298 List<String> poweredByAttributes = createPoweredByAttributes(); 299 for (ListIterator<String> iter = poweredByAttributes.listIterator(); iter.hasNext(); ) { 300 if (iter.nextIndex() > 0) { 301 b.append("; "); 302 } 303 b.append(iter.next()); 304 } 305 306 b.append(")"); 307 return b.toString(); 308 } 309 310 /** 311 * Subclasses my override 312 * 313 * @see #createPoweredByHeader() 314 */ 315 protected String createPoweredByHeaderComponentName() { 316 return "REST Server"; 317 } 318 319 /** 320 * Subclasses my override 321 * 322 * @see #createPoweredByHeader() 323 */ 324 protected String createPoweredByHeaderProductName() { 325 return "HAPI FHIR"; 326 } 327 328 /** 329 * Subclasses my override 330 * 331 * @see #createPoweredByHeader() 332 */ 333 protected String createPoweredByHeaderProductVersion() { 334 String version = VersionUtil.getVersion(); 335 if (VersionUtil.isSnapshot()) { 336 version = version + "/" + VersionUtil.getBuildNumber() + "/" + VersionUtil.getBuildDate(); 337 } 338 return version; 339 } 340 341 @Override 342 public void destroy() { 343 if (getResourceProviders() != null) { 344 for (IResourceProvider iResourceProvider : getResourceProviders()) { 345 invokeDestroy(iResourceProvider); 346 } 347 } 348 349 if (myServerConformanceProvider != null) { 350 invokeDestroy(myServerConformanceProvider); 351 } 352 353 if (getPlainProviders() != null) { 354 for (Object next : getPlainProviders()) { 355 invokeDestroy(next); 356 } 357 } 358 359 if (myServerConformanceMethod != null) { 360 myServerConformanceMethod.close(); 361 } 362 myResourceNameToBinding.values().stream() 363 .flatMap(t -> t.getMethodBindings().stream()) 364 .forEach(t -> t.close()); 365 myGlobalBinding.getMethodBindings().forEach(t -> t.close()); 366 myServerBinding.getMethodBindings().forEach(t -> t.close()); 367 368 myResourceNameToBinding.clear(); 369 myGlobalBinding.getMethodBindings().clear(); 370 myServerBinding.getMethodBindings().clear(); 371 } 372 373 /** 374 * Figure out and return whichever method binding is appropriate for 375 * the given request 376 */ 377 public BaseMethodBinding determineResourceMethod(RequestDetails requestDetails, String requestPath) { 378 RequestTypeEnum requestType = requestDetails.getRequestType(); 379 380 ResourceBinding resourceBinding = null; 381 BaseMethodBinding resourceMethod = null; 382 String resourceName = requestDetails.getResourceName(); 383 if (myServerConformanceMethod.incomingServerRequestMatchesMethod(requestDetails) != MethodMatchEnum.NONE) { 384 resourceMethod = myServerConformanceMethod; 385 } else if (resourceName == null) { 386 resourceBinding = myServerBinding; 387 } else { 388 resourceBinding = myResourceNameToBinding.get(resourceName); 389 if (resourceBinding == null) { 390 throwUnknownResourceTypeException(resourceName); 391 } 392 } 393 394 if (resourceMethod == null) { 395 if (resourceBinding != null) { 396 resourceMethod = resourceBinding.getMethod(requestDetails); 397 } 398 if (resourceMethod == null) { 399 resourceMethod = myGlobalBinding.getMethod(requestDetails); 400 } 401 } 402 if (resourceMethod == null) { 403 if (requestDetails.getParameters().containsKey(Constants.PARAM_PAGINGACTION)) { 404 // request for _getpages when no PageProvider registered 405 throwUnknownFhirOperationException(requestDetails, requestPath, requestType); 406 } 407 408 if (isBlank(requestPath)) { 409 throw new InvalidRequestException( 410 Msg.code(287) + myFhirContext.getLocalizer().getMessage(RestfulServer.class, "rootRequest")); 411 } 412 throwUnknownFhirOperationException(requestDetails, requestPath, requestType); 413 } 414 return resourceMethod; 415 } 416 417 @Override 418 protected void doDelete(HttpServletRequest request, HttpServletResponse response) 419 throws ServletException, IOException { 420 handleRequest(RequestTypeEnum.DELETE, request, response); 421 } 422 423 @Override 424 protected void doGet(HttpServletRequest request, HttpServletResponse response) 425 throws ServletException, IOException { 426 handleRequest(RequestTypeEnum.GET, request, response); 427 } 428 429 @Override 430 protected void doOptions(HttpServletRequest theReq, HttpServletResponse theResp) 431 throws ServletException, IOException { 432 handleRequest(RequestTypeEnum.OPTIONS, theReq, theResp); 433 } 434 435 @Override 436 protected void doPost(HttpServletRequest request, HttpServletResponse response) 437 throws ServletException, IOException { 438 handleRequest(RequestTypeEnum.POST, request, response); 439 } 440 441 @Override 442 protected void doPut(HttpServletRequest request, HttpServletResponse response) 443 throws ServletException, IOException { 444 handleRequest(RequestTypeEnum.PUT, request, response); 445 } 446 447 private void findResourceMethods(Object theProvider) { 448 449 ourLog.debug("Scanning type for RESTful methods: {}", theProvider.getClass()); 450 int count = 0; 451 452 Class<?> clazz = theProvider.getClass(); 453 Class<?> supertype = clazz.getSuperclass(); 454 while (!Object.class.equals(supertype)) { 455 count += findResourceMethodsOnInterfaces(theProvider, supertype.getInterfaces()); 456 count += findResourceMethods(theProvider, supertype); 457 supertype = supertype.getSuperclass(); 458 } 459 460 try { 461 count += findResourceMethodsOnInterfaces(theProvider, clazz.getInterfaces()); 462 count += findResourceMethods(theProvider, clazz); 463 } catch (ConfigurationException e) { 464 throw new ConfigurationException( 465 Msg.code(288) + "Failure scanning class " + clazz.getSimpleName() + ": " + e.getMessage(), e); 466 } 467 if (count == 0) { 468 throw new ConfigurationException( 469 Msg.code(289) + "Did not find any annotated RESTful methods on provider class " 470 + theProvider.getClass().getName()); 471 } 472 } 473 474 private int findResourceMethodsOnInterfaces(Object theProvider, Class<?>[] interfaces) { 475 int count = 0; 476 for (Class<?> anInterface : interfaces) { 477 count += findResourceMethodsOnInterfaces(theProvider, anInterface.getInterfaces()); 478 count += findResourceMethods(theProvider, anInterface); 479 } 480 return count; 481 } 482 483 private int findResourceMethods(Object theProvider, Class<?> clazz) throws ConfigurationException { 484 int count = 0; 485 486 for (Method m : ReflectionUtil.getDeclaredMethods(clazz)) { 487 BaseMethodBinding foundMethodBinding = BaseMethodBinding.bindMethod(m, getFhirContext(), theProvider); 488 if (foundMethodBinding == null) { 489 continue; 490 } 491 492 count++; 493 494 if (foundMethodBinding instanceof ConformanceMethodBinding) { 495 myServerConformanceMethod = (ConformanceMethodBinding) foundMethodBinding; 496 if (myServerConformanceProvider == null) { 497 myServerConformanceProvider = theProvider; 498 } 499 continue; 500 } 501 502 if (!Modifier.isPublic(m.getModifiers())) { 503 throw new ConfigurationException(Msg.code(290) + "Method '" + m.getName() 504 + "' is not public, FHIR RESTful methods must be public"); 505 } 506 if (Modifier.isStatic(m.getModifiers())) { 507 throw new ConfigurationException(Msg.code(291) + "Method '" + m.getName() 508 + "' is static, FHIR RESTful methods must not be static"); 509 } 510 ourLog.trace("Scanning public method: {}#{}", theProvider.getClass(), m.getName()); 511 512 // Interceptor call: SERVER_PROVIDER_METHOD_BOUND 513 if (myInterceptorService.hasHooks(Pointcut.SERVER_PROVIDER_METHOD_BOUND)) { 514 HookParams params = new HookParams().add(BaseMethodBinding.class, foundMethodBinding); 515 BaseMethodBinding newMethodBinding = (BaseMethodBinding) 516 myInterceptorService.callHooksAndReturnObject(Pointcut.SERVER_PROVIDER_METHOD_BOUND, params); 517 if (newMethodBinding == null) { 518 ourLog.info( 519 "Method binding {} was discarded by interceptor and will not be registered", 520 foundMethodBinding); 521 continue; 522 } 523 foundMethodBinding = newMethodBinding; 524 } 525 526 String resourceName = foundMethodBinding.getResourceName(); 527 ResourceBinding resourceBinding; 528 if (resourceName == null) { 529 if (foundMethodBinding.isGlobalMethod()) { 530 resourceBinding = myGlobalBinding; 531 } else { 532 resourceBinding = myServerBinding; 533 } 534 } else { 535 RuntimeResourceDefinition definition = getFhirContext().getResourceDefinition(resourceName); 536 if (myResourceNameToBinding.containsKey(definition.getName())) { 537 resourceBinding = myResourceNameToBinding.get(definition.getName()); 538 } else { 539 resourceBinding = new ResourceBinding(); 540 resourceBinding.setResourceName(resourceName); 541 myResourceNameToBinding.put(resourceName, resourceBinding); 542 } 543 } 544 545 List<Class<?>> allowableParams = foundMethodBinding.getAllowableParamAnnotations(); 546 if (allowableParams != null) { 547 for (Annotation[] nextParamAnnotations : m.getParameterAnnotations()) { 548 for (Annotation annotation : nextParamAnnotations) { 549 Package pack = annotation.annotationType().getPackage(); 550 if (pack.equals(IdParam.class.getPackage())) { 551 if (!allowableParams.contains(annotation.annotationType())) { 552 throw new ConfigurationException(Msg.code(292) + "Method[" + m 553 + "] is not allowed to have a parameter annotated with " + annotation); 554 } 555 } 556 } 557 } 558 } 559 560 resourceBinding.addMethod(foundMethodBinding); 561 ourLog.trace(" * Method: {}#{} is a handler", theProvider.getClass(), m.getName()); 562 } 563 564 return count; 565 } 566 567 /** 568 * @deprecated As of HAPI FHIR 1.5, this property has been moved to 569 * {@link FhirContext#setAddProfileTagWhenEncoding(AddProfileTagEnum)} 570 */ 571 @Override 572 @Deprecated 573 public AddProfileTagEnum getAddProfileTag() { 574 return myFhirContext.getAddProfileTagWhenEncoding(); 575 } 576 577 /** 578 * Sets the profile tagging behaviour for the server. When set to a value other than {@link AddProfileTagEnum#NEVER} 579 * (which is the default), the server will automatically add a profile tag based on 580 * the class of the resource(s) being returned. 581 * 582 * @param theAddProfileTag The behaviour enum (must not be null) 583 * @deprecated As of HAPI FHIR 1.5, this property has been moved to 584 * {@link FhirContext#setAddProfileTagWhenEncoding(AddProfileTagEnum)} 585 */ 586 @Deprecated 587 @CoverageIgnore 588 public void setAddProfileTag(AddProfileTagEnum theAddProfileTag) { 589 Validate.notNull(theAddProfileTag, "theAddProfileTag must not be null"); 590 myFhirContext.setAddProfileTagWhenEncoding(theAddProfileTag); 591 } 592 593 @Override 594 public BundleInclusionRule getBundleInclusionRule() { 595 return myBundleInclusionRule; 596 } 597 598 /** 599 * Set how bundle factory should decide whether referenced resources should be included in bundles 600 * 601 * @param theBundleInclusionRule - inclusion rule (@see BundleInclusionRule for behaviors) 602 */ 603 public void setBundleInclusionRule(BundleInclusionRule theBundleInclusionRule) { 604 myBundleInclusionRule = theBundleInclusionRule; 605 } 606 607 /** 608 * Returns the default encoding to return (XML/JSON) if an incoming request does not specify a preference (either 609 * with the <code>_format</code> URL parameter, or with an <code>Accept</code> header 610 * in the request. The default is {@link EncodingEnum#XML}. Will not return null. 611 */ 612 @Override 613 public EncodingEnum getDefaultResponseEncoding() { 614 return myDefaultResponseEncoding; 615 } 616 617 /** 618 * Sets the default encoding to return (XML/JSON) if an incoming request does not specify a preference (either with 619 * the <code>_format</code> URL parameter, or with an <code>Accept</code> header in 620 * the request. The default is {@link EncodingEnum#XML}. 621 * <p> 622 * Note when testing this feature: Some browsers will include "application/xml" in their Accept header, which means 623 * that the 624 * </p> 625 */ 626 public void setDefaultResponseEncoding(EncodingEnum theDefaultResponseEncoding) { 627 Validate.notNull(theDefaultResponseEncoding, "theDefaultResponseEncoding can not be null"); 628 myDefaultResponseEncoding = theDefaultResponseEncoding; 629 } 630 631 @Override 632 public ETagSupportEnum getETagSupport() { 633 return myETagSupport; 634 } 635 636 /** 637 * Sets (enables/disables) the server support for ETags. Must not be <code>null</code>. Default is 638 * {@link #DEFAULT_ETAG_SUPPORT} 639 * 640 * @param theETagSupport The ETag support mode 641 */ 642 public void setETagSupport(ETagSupportEnum theETagSupport) { 643 if (theETagSupport == null) { 644 throw new NullPointerException(Msg.code(293) + "theETagSupport can not be null"); 645 } 646 myETagSupport = theETagSupport; 647 } 648 649 @Override 650 public ElementsSupportEnum getElementsSupport() { 651 return myElementsSupport; 652 } 653 654 /** 655 * Sets the elements support mode. 656 * 657 * @see <a href="http://hapifhir.io/doc_rest_server.html#extended_elements_support">Extended Elements Support</a> 658 */ 659 public void setElementsSupport(ElementsSupportEnum theElementsSupport) { 660 Validate.notNull(theElementsSupport, "theElementsSupport must not be null"); 661 myElementsSupport = theElementsSupport; 662 } 663 664 /** 665 * Gets the {@link FhirContext} associated with this server. For efficient processing, resource providers and plain 666 * providers should generally use this context if one is needed, as opposed to 667 * creating their own. 668 */ 669 @Override 670 public FhirContext getFhirContext() { 671 if (myFhirContext == null) { 672 // TODO: Use of a deprecated method should be resolved. 673 myFhirContext = new FhirContext(); 674 } 675 return myFhirContext; 676 } 677 678 public void setFhirContext(FhirContext theFhirContext) { 679 Validate.notNull(theFhirContext, "FhirContext must not be null"); 680 myFhirContext = theFhirContext; 681 } 682 683 public String getImplementationDescription() { 684 return myImplementationDescription; 685 } 686 687 public void setImplementationDescription(String theImplementationDescription) { 688 myImplementationDescription = theImplementationDescription; 689 } 690 691 /** 692 * Returns the server copyright (will be added to the CapabilityStatement). Note that FHIR allows Markdown in this string. 693 */ 694 public String getCopyright() { 695 return myCopyright; 696 } 697 698 /** 699 * Sets the server copyright (will be added to the CapabilityStatement). Note that FHIR allows Markdown in this string. 700 */ 701 public void setCopyright(String theCopyright) { 702 myCopyright = theCopyright; 703 } 704 705 /** 706 * Returns a list of all registered server interceptors 707 * 708 * @deprecated As of HAPI FHIR 3.8.0, use {@link #getInterceptorService()} to access the interceptor service. You can register and unregister interceptors using this service. 709 */ 710 @Deprecated 711 @Override 712 public List<IServerInterceptor> getInterceptors_() { 713 List<IServerInterceptor> retVal = getInterceptorService().getAllRegisteredInterceptors().stream() 714 .filter(t -> t instanceof IServerInterceptor) 715 .map(t -> (IServerInterceptor) t) 716 .collect(toList()); 717 return Collections.unmodifiableList(retVal); 718 } 719 720 /** 721 * Returns the interceptor registry for this service. Use this registry to register and unregister 722 * 723 * @since 3.8.0 724 */ 725 @Override 726 public IInterceptorService getInterceptorService() { 727 return myInterceptorService; 728 } 729 730 /** 731 * Sets the interceptor registry for this service. Use this registry to register and unregister 732 * 733 * @since 3.8.0 734 */ 735 public void setInterceptorService(@Nonnull IInterceptorService theInterceptorService) { 736 Validate.notNull(theInterceptorService, "theInterceptorService must not be null"); 737 myInterceptorService = theInterceptorService; 738 } 739 740 /** 741 * Sets (or clears) the list of interceptors 742 * 743 * @param theList The list of interceptors (may be null) 744 * @deprecated As of HAPI FHIR 3.8.0, use {@link #getInterceptorService()} to access the interceptor service. You can register and unregister interceptors using this service. 745 */ 746 @Deprecated 747 public void setInterceptors(@Nonnull List<?> theList) { 748 myInterceptorService.unregisterAllInterceptors(); 749 myInterceptorService.registerInterceptors(theList); 750 } 751 752 /** 753 * Sets (or clears) the list of interceptors 754 * 755 * @param theInterceptors The list of interceptors (may be null) 756 * @deprecated As of HAPI FHIR 3.8.0, use {@link #getInterceptorService()} to access the interceptor service. You can register and unregister interceptors using this service. 757 */ 758 @Deprecated 759 public void setInterceptors(IServerInterceptor... theInterceptors) { 760 Validate.noNullElements(theInterceptors, "theInterceptors must not contain any null elements"); 761 setInterceptors(Arrays.asList(theInterceptors)); 762 } 763 764 @Override 765 public IPagingProvider getPagingProvider() { 766 return myPagingProvider; 767 } 768 769 /** 770 * Sets the paging provider to use, or <code>null</code> to use no paging (which is the default). 771 * This will set defaultPageSize and maximumPageSize from the paging provider. 772 */ 773 public void setPagingProvider(IPagingProvider thePagingProvider) { 774 myPagingProvider = thePagingProvider; 775 if (myPagingProvider != null) { 776 setDefaultPageSize(myPagingProvider.getDefaultPageSize()); 777 setMaximumPageSize(myPagingProvider.getMaximumPageSize()); 778 } 779 } 780 781 @Override 782 public Integer getDefaultPageSize() { 783 return myDefaultPageSize; 784 } 785 786 /** 787 * Sets the default page size to use, or <code>null</code> if no default page size 788 */ 789 public void setDefaultPageSize(Integer thePageSize) { 790 myDefaultPageSize = thePageSize; 791 } 792 793 @Override 794 public Integer getMaximumPageSize() { 795 return myMaximumPageSize; 796 } 797 798 /** 799 * Sets the maximum page size to use, or <code>null</code> if no maximum page size 800 */ 801 public void setMaximumPageSize(Integer theMaximumPageSize) { 802 myMaximumPageSize = theMaximumPageSize; 803 } 804 805 /** 806 * Provides the non-resource specific providers which implement method calls on this server 807 * 808 * @see #getResourceProviders() 809 */ 810 public Collection<Object> getPlainProviders() { 811 return myPlainProviders; 812 } 813 814 /** 815 * Sets the non-resource specific providers which implement method calls on this server. 816 * 817 * @see #setResourceProviders(Collection) 818 * @deprecated This method causes inconsistent behaviour depending on the order it is called in. Use {@link #registerProviders(Object...)} instead. 819 */ 820 @Deprecated 821 public void setPlainProviders(Object... theProv) { 822 setPlainProviders(Arrays.asList(theProv)); 823 } 824 825 /** 826 * Sets the non-resource specific providers which implement method calls on this server. 827 * 828 * @see #setResourceProviders(Collection) 829 * @deprecated This method causes inconsistent behaviour depending on the order it is called in. Use {@link #registerProviders(Object...)} instead. 830 */ 831 @Deprecated 832 public void setPlainProviders(Collection<Object> theProviders) { 833 Validate.noNullElements(theProviders, "theProviders must not contain any null elements"); 834 835 myPlainProviders.clear(); 836 myPlainProviders.addAll(theProviders); 837 } 838 839 /** 840 * Allows users of RestfulServer to override the getRequestPath method to let them build their custom request path 841 * implementation 842 * 843 * @param requestFullPath the full request path 844 * @param servletContextPath the servelet context path 845 * @param servletPath the servelet path 846 * @return created resource path 847 */ 848 // NOTE: Don't make this a static method!! People want to override it 849 protected String getRequestPath(String requestFullPath, String servletContextPath, String servletPath) { 850 return requestFullPath.substring(escapedLength(servletContextPath) + escapedLength(servletPath)); 851 } 852 853 public Collection<ResourceBinding> getResourceBindings() { 854 return myResourceNameToBinding.values(); 855 } 856 857 public Collection<BaseMethodBinding> getProviderMethodBindings(Object theProvider) { 858 Set<BaseMethodBinding> retVal = new HashSet<>(); 859 for (ResourceBinding resourceBinding : getResourceBindings()) { 860 for (BaseMethodBinding methodBinding : resourceBinding.getMethodBindings()) { 861 if (theProvider.equals(methodBinding.getProvider())) { 862 retVal.add(methodBinding); 863 } 864 } 865 } 866 867 return retVal; 868 } 869 870 /** 871 * Provides the resource providers for this server 872 */ 873 public List<IResourceProvider> getResourceProviders() { 874 return Collections.unmodifiableList(myResourceProviders); 875 } 876 877 /** 878 * Sets the resource providers for this server 879 */ 880 public void setResourceProviders(IResourceProvider... theResourceProviders) { 881 myResourceProviders.clear(); 882 if (theResourceProviders != null) { 883 myResourceProviders.addAll(Arrays.asList(theResourceProviders)); 884 } 885 } 886 887 /** 888 * Sets the resource providers for this server 889 */ 890 public void setResourceProviders(Collection<IResourceProvider> theProviders) { 891 Validate.noNullElements(theProviders, "theProviders must not contain any null elements"); 892 893 myResourceProviders.clear(); 894 myResourceProviders.addAll(theProviders); 895 } 896 897 /** 898 * Get the server address strategy, which is used to determine what base URL to provide clients to refer to this 899 * server. Defaults to an instance of {@link IncomingRequestAddressStrategy} 900 */ 901 public IServerAddressStrategy getServerAddressStrategy() { 902 return myServerAddressStrategy; 903 } 904 905 /** 906 * Provide a server address strategy, which is used to determine what base URL to provide clients to refer to this 907 * server. Defaults to an instance of {@link IncomingRequestAddressStrategy} 908 */ 909 public void setServerAddressStrategy(IServerAddressStrategy theServerAddressStrategy) { 910 Validate.notNull(theServerAddressStrategy, "Server address strategy can not be null"); 911 myServerAddressStrategy = theServerAddressStrategy; 912 } 913 914 /** 915 * Returns the server base URL (with no trailing '/') for a given request 916 */ 917 public String getServerBaseForRequest(ServletRequestDetails theRequest) { 918 String fhirServerBase; 919 fhirServerBase = 920 myServerAddressStrategy.determineServerBase(getServletContext(), theRequest.getServletRequest()); 921 assert isNotBlank(fhirServerBase) : "Server Address Strategy did not return a value"; 922 923 if (fhirServerBase.endsWith("/")) { 924 fhirServerBase = fhirServerBase.substring(0, fhirServerBase.length() - 1); 925 } 926 927 if (myTenantIdentificationStrategy != null) { 928 fhirServerBase = myTenantIdentificationStrategy.massageServerBaseUrl(fhirServerBase, theRequest); 929 } 930 931 return fhirServerBase; 932 } 933 934 /** 935 * Returns the method bindings for this server which are not specific to any particular resource type. This method is 936 * internal to HAPI and developers generally do not need to interact with it. Use 937 * with caution, as it may change. 938 */ 939 public List<BaseMethodBinding> getServerBindings() { 940 return myServerBinding.getMethodBindings(); 941 } 942 943 /** 944 * Returns the server conformance provider, which is the provider that is used to generate the server's conformance 945 * (metadata) statement if one has been explicitly defined. 946 * <p> 947 * By default, the ServerConformanceProvider for the declared version of FHIR is used, but this can be changed, or 948 * set to <code>null</code> to use the appropriate one for the given FHIR version. 949 * </p> 950 */ 951 public Object getServerConformanceProvider() { 952 return myServerConformanceProvider; 953 } 954 955 /** 956 * Returns the server conformance provider, which is the provider that is used to generate the server's conformance 957 * (metadata) statement. 958 * <p> 959 * By default, the ServerConformanceProvider implementation for the declared version of FHIR is used, but this can be 960 * changed, or set to <code>null</code> if you do not wish to export a conformance 961 * statement. 962 * </p> 963 * This method should only be called before the server is initialized. 964 * Calling it after the server has started is allowed, but you should be 965 * very careful in this case that you only call it while no traffic is 966 * hitting the server. 967 * 968 * @throws IllegalStateException Note that this method can only be called prior to {@link #init() initialization} and will throw an 969 * {@link IllegalStateException} if called after that. 970 */ 971 public void setServerConformanceProvider(@Nonnull Object theServerConformanceProvider) { 972 Validate.notNull(theServerConformanceProvider, "theServerConformanceProvider must not be null"); 973 974 if (myServerConformanceProvider != null) { 975 unregisterProvider(myServerConformanceProvider); 976 } 977 978 // call the setRestfulServer() method to point the Conformance 979 // Provider to this server instance. This is done to avoid 980 // passing the server into the constructor. Having that sort 981 // of cross linkage causes reference cycles in Spring wiring 982 try { 983 Method setRestfulServer = 984 theServerConformanceProvider.getClass().getMethod("setRestfulServer", RestfulServer.class); 985 if (setRestfulServer != null) { 986 setRestfulServer.invoke(theServerConformanceProvider, this); 987 } 988 } catch (Exception e) { 989 ourLog.warn("Error calling IServerConformanceProvider.setRestfulServer", e); 990 } 991 myServerConformanceProvider = theServerConformanceProvider; 992 993 findResourceMethods(myServerConformanceProvider); 994 } 995 996 /** 997 * Gets the server's name, as exported in conformance profiles exported by the server. This is informational only, 998 * but can be helpful to set with something appropriate. 999 * 1000 * @see RestfulServer#setServerName(String) 1001 */ 1002 public String getServerName() { 1003 return myServerName; 1004 } 1005 1006 /** 1007 * Sets the server's name, as exported in conformance profiles exported by the server. This is informational only, 1008 * but can be helpful to set with something appropriate. 1009 */ 1010 public void setServerName(String theServerName) { 1011 myServerName = theServerName; 1012 } 1013 1014 /** 1015 * Gets the server's version, as exported in conformance profiles exported by the server. This is informational only, 1016 * but can be helpful to set with something appropriate. 1017 */ 1018 public String getServerVersion() { 1019 return myServerVersion; 1020 } 1021 1022 /** 1023 * Gets the server's version, as exported in conformance profiles exported by the server. This is informational only, 1024 * but can be helpful to set with something appropriate. 1025 */ 1026 public void setServerVersion(String theServerVersion) { 1027 myServerVersion = theServerVersion; 1028 } 1029 1030 @SuppressWarnings("WeakerAccess") 1031 protected void handleRequest( 1032 RequestTypeEnum theRequestType, HttpServletRequest theRequest, HttpServletResponse theResponse) 1033 throws ServletException, IOException { 1034 String fhirServerBase; 1035 ServletRequestDetails requestDetails = newRequestDetails(theRequestType, theRequest, theResponse); 1036 1037 String requestId = getOrCreateRequestId(theRequest); 1038 requestDetails.setRequestId(requestId); 1039 addRequestIdToResponse(requestDetails, requestId); 1040 1041 theRequest.setAttribute(SERVLET_CONTEXT_ATTRIBUTE, getServletContext()); 1042 1043 // keep track of any unhandled exceptions in case the exception handler throws another exception 1044 Throwable unhandledException = null; 1045 1046 try { 1047 1048 /* *********************************** 1049 * Parse out the request parameters 1050 * ***********************************/ 1051 1052 String requestFullPath = StringUtils.defaultString(theRequest.getRequestURI()); 1053 String servletPath = StringUtils.defaultString(theRequest.getServletPath()); 1054 StringBuffer requestUrl = theRequest.getRequestURL(); 1055 String servletContextPath = myServerAddressStrategy.determineServletContextPath(theRequest, this); 1056 1057 /* 1058 * Just for debugging.. 1059 */ 1060 if (ourLog.isTraceEnabled()) { 1061 ourLog.trace("Request FullPath: {}", requestFullPath); 1062 ourLog.trace("Servlet Path: {}", servletPath); 1063 ourLog.trace("Request Url: {}", requestUrl); 1064 ourLog.trace("Context Path: {}", servletContextPath); 1065 } 1066 1067 String completeUrl; 1068 Map<String, String[]> params = null; 1069 if (isNotBlank(theRequest.getQueryString())) { 1070 completeUrl = requestUrl + "?" + theRequest.getQueryString(); 1071 /* 1072 * By default, we manually parse the request params (the URL params, or the body for 1073 * POST form queries) since Java containers can't be trusted to use UTF-8 encoding 1074 * when parsing. Specifically Tomcat 7 and Glassfish 4.0 use 8859-1 for some dumb 1075 * reason.... grr..... 1076 */ 1077 if (isIgnoreServerParsedRequestParameters()) { 1078 String contentType = theRequest.getHeader(Constants.HEADER_CONTENT_TYPE); 1079 if (theRequestType == RequestTypeEnum.POST 1080 && isNotBlank(contentType) 1081 && contentType.startsWith(Constants.CT_X_FORM_URLENCODED)) { 1082 String requestBody = toUtf8String(requestDetails.loadRequestContents()); 1083 params = UrlUtil.parseQueryStrings(theRequest.getQueryString(), requestBody); 1084 } else if (theRequestType == RequestTypeEnum.GET) { 1085 params = UrlUtil.parseQueryString(theRequest.getQueryString()); 1086 } 1087 } 1088 } else { 1089 completeUrl = requestUrl.toString(); 1090 } 1091 1092 if (params == null) { 1093 1094 // If the request is coming in with a content-encoding, don't try to 1095 // load the params from the content. 1096 if (isNotBlank(theRequest.getHeader(Constants.HEADER_CONTENT_ENCODING))) { 1097 if (isNotBlank(theRequest.getQueryString())) { 1098 params = UrlUtil.parseQueryString(theRequest.getQueryString()); 1099 } else { 1100 params = Collections.emptyMap(); 1101 } 1102 } 1103 1104 if (params == null) { 1105 params = new HashMap<>(theRequest.getParameterMap()); 1106 } 1107 } 1108 1109 requestDetails.setParameters(params); 1110 1111 /* ************************* 1112 * Notify interceptors about the incoming request 1113 * *************************/ 1114 1115 // Interceptor: SERVER_INCOMING_REQUEST_PRE_PROCESSED 1116 if (myInterceptorService.hasHooks(Pointcut.SERVER_INCOMING_REQUEST_PRE_PROCESSED)) { 1117 HookParams preProcessedParams = new HookParams(); 1118 preProcessedParams.add(HttpServletRequest.class, theRequest); 1119 preProcessedParams.add(HttpServletResponse.class, theResponse); 1120 if (!myInterceptorService.callHooks( 1121 Pointcut.SERVER_INCOMING_REQUEST_PRE_PROCESSED, preProcessedParams)) { 1122 return; 1123 } 1124 } 1125 1126 String requestPath = getRequestPath(requestFullPath, servletContextPath, servletPath); 1127 1128 if (requestPath.length() > 0 && requestPath.charAt(0) == '/') { 1129 requestPath = requestPath.substring(1); 1130 } 1131 1132 IIdType id; 1133 populateRequestDetailsFromRequestPath(requestDetails, requestPath); 1134 1135 fhirServerBase = getServerBaseForRequest(requestDetails); 1136 1137 if (theRequestType == RequestTypeEnum.PUT) { 1138 String contentLocation = theRequest.getHeader(Constants.HEADER_CONTENT_LOCATION); 1139 if (contentLocation != null) { 1140 id = myFhirContext.getVersion().newIdType(); 1141 id.setValue(contentLocation); 1142 requestDetails.setId(id); 1143 } 1144 } 1145 1146 String acceptEncoding = theRequest.getHeader(Constants.HEADER_ACCEPT_ENCODING); 1147 boolean respondGzip = false; 1148 if (acceptEncoding != null) { 1149 String[] parts = acceptEncoding.trim().split("\\s*,\\s*"); 1150 for (String string : parts) { 1151 if (string.equals("gzip")) { 1152 respondGzip = true; 1153 break; 1154 } 1155 } 1156 } 1157 requestDetails.setRespondGzip(respondGzip); 1158 requestDetails.setRequestPath(requestPath); 1159 requestDetails.setFhirServerBase(fhirServerBase); 1160 requestDetails.setCompleteUrl(completeUrl); 1161 1162 // Interceptor: SERVER_INCOMING_REQUEST_PRE_HANDLER_SELECTED 1163 if (myInterceptorService.hasHooks(Pointcut.SERVER_INCOMING_REQUEST_PRE_HANDLER_SELECTED)) { 1164 HookParams preProcessedParams = new HookParams(); 1165 preProcessedParams.add(HttpServletRequest.class, theRequest); 1166 preProcessedParams.add(HttpServletResponse.class, theResponse); 1167 preProcessedParams.add(RequestDetails.class, requestDetails); 1168 preProcessedParams.add(ServletRequestDetails.class, requestDetails); 1169 if (!myInterceptorService.callHooks( 1170 Pointcut.SERVER_INCOMING_REQUEST_PRE_HANDLER_SELECTED, preProcessedParams)) { 1171 return; 1172 } 1173 } 1174 1175 validateRequest(requestDetails); 1176 1177 BaseMethodBinding resourceMethod = determineResourceMethod(requestDetails, requestPath); 1178 1179 RestOperationTypeEnum operation = resourceMethod.getRestOperationType(requestDetails); 1180 requestDetails.setRestOperationType(operation); 1181 1182 // Interceptor: SERVER_INCOMING_REQUEST_POST_PROCESSED 1183 if (myInterceptorService.hasHooks(Pointcut.SERVER_INCOMING_REQUEST_POST_PROCESSED)) { 1184 HookParams postProcessedParams = new HookParams(); 1185 postProcessedParams.add(RequestDetails.class, requestDetails); 1186 postProcessedParams.add(ServletRequestDetails.class, requestDetails); 1187 postProcessedParams.add(HttpServletRequest.class, theRequest); 1188 postProcessedParams.add(HttpServletResponse.class, theResponse); 1189 if (!myInterceptorService.callHooks( 1190 Pointcut.SERVER_INCOMING_REQUEST_POST_PROCESSED, postProcessedParams)) { 1191 return; 1192 } 1193 } 1194 1195 /* 1196 * Actually invoke the server method. This call is to a HAPI method binding, which 1197 * is an object that wraps a specific implementing (user-supplied) method, but 1198 * handles its input and provides its output back to the client. 1199 * 1200 * This is basically the end of processing for a successful request, since the 1201 * method binding replies to the client and closes the response. 1202 */ 1203 resourceMethod.invokeServer(this, requestDetails); 1204 1205 // Invoke interceptors 1206 HookParams hookParams = new HookParams(); 1207 hookParams.add(RequestDetails.class, requestDetails); 1208 hookParams.add(ServletRequestDetails.class, requestDetails); 1209 myInterceptorService.callHooks(Pointcut.SERVER_PROCESSING_COMPLETED_NORMALLY, hookParams); 1210 1211 } catch (Throwable e) { 1212 1213 unhandledException = e; 1214 1215 /* 1216 * We have caught an exception during request processing. This might be because a handling method threw 1217 * something they wanted to throw (e.g. UnprocessableEntityException because the request 1218 * had business requirement problems) or it could be due to bugs (e.g. NullPointerException). 1219 * 1220 * First we let the interceptors have a crack at converting the exception into something HAPI can use 1221 * (BaseServerResponseException) 1222 */ 1223 HookParams preProcessParams = new HookParams(); 1224 preProcessParams.add(RequestDetails.class, requestDetails); 1225 preProcessParams.add(ServletRequestDetails.class, requestDetails); 1226 preProcessParams.add(HttpServletRequest.class, theRequest); 1227 preProcessParams.add(HttpServletResponse.class, theResponse); 1228 preProcessParams.add(Throwable.class, e); 1229 BaseServerResponseException exception = 1230 (BaseServerResponseException) myInterceptorService.callHooksAndReturnObject( 1231 Pointcut.SERVER_PRE_PROCESS_OUTGOING_EXCEPTION, preProcessParams); 1232 1233 /* 1234 * If none of the interceptors converted the exception, default behaviour is to keep the exception as-is if it 1235 * extends BaseServerResponseException, otherwise wrap it in an 1236 * InternalErrorException. 1237 */ 1238 if (exception == null) { 1239 exception = DEFAULT_EXCEPTION_HANDLER.preProcessOutgoingException(requestDetails, e, theRequest); 1240 } 1241 1242 /* 1243 * If it's a 410 Gone, we want to include a location header in the response 1244 * if we can, since that can include the resource version which is nice 1245 * for the user. 1246 */ 1247 if (exception instanceof ResourceGoneException) { 1248 IIdType resourceId = ((ResourceGoneException) exception).getResourceId(); 1249 if (resourceId != null && resourceId.hasResourceType() && resourceId.hasIdPart()) { 1250 String baseUrl = 1251 myServerAddressStrategy.determineServerBase(theRequest.getServletContext(), theRequest); 1252 resourceId = resourceId.withServerBase(baseUrl, resourceId.getResourceType()); 1253 requestDetails.getResponse().addHeader(Constants.HEADER_LOCATION, resourceId.getValue()); 1254 } 1255 } 1256 1257 /* 1258 * Next, interceptors get a shot at handling the exception 1259 */ 1260 HookParams handleExceptionParams = new HookParams(); 1261 handleExceptionParams.add(RequestDetails.class, requestDetails); 1262 handleExceptionParams.add(ServletRequestDetails.class, requestDetails); 1263 handleExceptionParams.add(HttpServletRequest.class, theRequest); 1264 handleExceptionParams.add(HttpServletResponse.class, theResponse); 1265 handleExceptionParams.add(BaseServerResponseException.class, exception); 1266 if (!myInterceptorService.callHooks(Pointcut.SERVER_HANDLE_EXCEPTION, handleExceptionParams)) { 1267 return; 1268 } 1269 1270 /* 1271 * If we're handling an exception, no summary mode should be applied 1272 */ 1273 requestDetails.removeParameter(Constants.PARAM_SUMMARY); 1274 requestDetails.removeParameter(Constants.PARAM_ELEMENTS); 1275 requestDetails.removeParameter(Constants.PARAM_ELEMENTS + Constants.PARAM_ELEMENTS_EXCLUDE_MODIFIER); 1276 1277 /* 1278 * If nobody handles it, default behaviour is to stream back the OperationOutcome to the client. 1279 */ 1280 DEFAULT_EXCEPTION_HANDLER.handleException(requestDetails, exception, theRequest, theResponse); 1281 unhandledException = null; 1282 1283 } finally { 1284 1285 if (unhandledException != null) { 1286 ourLog.error( 1287 Msg.code(2544) + "Exception handling threw an exception. Initial exception was: {}", 1288 unhandledException.getMessage(), 1289 unhandledException); 1290 unhandledException = null; 1291 } 1292 1293 HookParams params = new HookParams(); 1294 params.add(RequestDetails.class, requestDetails); 1295 params.addIfMatchesType(ServletRequestDetails.class, requestDetails); 1296 myInterceptorService.callHooks(Pointcut.SERVER_PROCESSING_COMPLETED, params); 1297 } 1298 } 1299 1300 /** 1301 * Subclasses may override this to customize the way that the RequestDetails object is created. Generally speaking, the 1302 * right way to do this is to override this method, but call the super-implementation (<code>super.newRequestDetails</code>) 1303 * and then customize the returned object before returning it. 1304 * 1305 * @param theRequestType The HTTP request verb 1306 * @param theRequest The servlet request 1307 * @param theResponse The servlet response 1308 * @return A ServletRequestDetails instance to be passed to any resource providers, interceptors, etc. that are invoked as a part of serving this request. 1309 */ 1310 @Nonnull 1311 protected ServletRequestDetails newRequestDetails( 1312 RequestTypeEnum theRequestType, HttpServletRequest theRequest, HttpServletResponse theResponse) { 1313 ServletRequestDetails requestDetails = newRequestDetails(); 1314 requestDetails.setServer(this); 1315 requestDetails.setRequestType(theRequestType); 1316 requestDetails.setServletRequest(theRequest); 1317 requestDetails.setServletResponse(theResponse); 1318 return requestDetails; 1319 } 1320 1321 /** 1322 * @deprecated Deprecated in HAPI FHIR 4.1.0 - Users wishing to override this method should override {@link #newRequestDetails(RequestTypeEnum, HttpServletRequest, HttpServletResponse)} instead 1323 */ 1324 @Deprecated 1325 protected ServletRequestDetails newRequestDetails() { 1326 return new ServletRequestDetails(getInterceptorService()); 1327 } 1328 1329 protected void addRequestIdToResponse(ServletRequestDetails theRequestDetails, String theRequestId) { 1330 String caseSensitiveRequestIdKey = Constants.HEADER_REQUEST_ID; 1331 for (String key : theRequestDetails.getHeaders().keySet()) { 1332 if (Constants.HEADER_REQUEST_ID.equalsIgnoreCase(key)) { 1333 caseSensitiveRequestIdKey = key; 1334 break; 1335 } 1336 } 1337 theRequestDetails.getResponse().addHeader(caseSensitiveRequestIdKey, theRequestId); 1338 } 1339 1340 /** 1341 * Reads a request ID from the request headers via the {@link Constants#HEADER_REQUEST_ID} 1342 * header, or generates one if none is supplied. 1343 * <p> 1344 * Note that the generated request ID is a random 64-bit long integer encoded as 1345 * hexadecimal. It is not generated using any cryptographic algorithms or a secure 1346 * PRNG, so it should not be used for anything other than troubleshooting purposes. 1347 * </p> 1348 */ 1349 protected String getOrCreateRequestId(HttpServletRequest theRequest) { 1350 String requestId = ServletRequestTracing.maybeGetRequestId(theRequest); 1351 1352 // TODO can we delete this and newRequestId() 1353 // and use ServletRequestTracing.getOrGenerateRequestId() instead? 1354 // newRequestId() is protected. Do you think anyone actually overrode it? 1355 if (isBlank(requestId)) { 1356 int requestIdLength = Constants.REQUEST_ID_LENGTH; 1357 requestId = newRequestId(requestIdLength); 1358 } 1359 1360 return requestId; 1361 } 1362 1363 /** 1364 * Generate a new request ID string. Subclasses may ovrride. 1365 */ 1366 protected String newRequestId(int theRequestIdLength) { 1367 String requestId; 1368 requestId = RandomStringUtils.randomAlphanumeric(theRequestIdLength); 1369 return requestId; 1370 } 1371 1372 protected void validateRequest(ServletRequestDetails theRequestDetails) { 1373 String[] elements = theRequestDetails.getParameters().get(Constants.PARAM_ELEMENTS); 1374 if (elements != null) { 1375 for (String next : elements) { 1376 if (next.indexOf(':') != -1) { 1377 throw new InvalidRequestException(Msg.code(295) + "Invalid _elements value: \"" + next + "\""); 1378 } 1379 } 1380 } 1381 1382 elements = theRequestDetails 1383 .getParameters() 1384 .get(Constants.PARAM_ELEMENTS + Constants.PARAM_ELEMENTS_EXCLUDE_MODIFIER); 1385 if (elements != null) { 1386 for (String next : elements) { 1387 if (next.indexOf(':') != -1) { 1388 throw new InvalidRequestException(Msg.code(296) + "Invalid _elements value: \"" + next + "\""); 1389 } 1390 } 1391 } 1392 } 1393 1394 /** 1395 * Initializes the server. Note that this method is final to avoid accidentally introducing bugs in implementations, 1396 * but subclasses may put initialization code in {@link #initialize()}, which is 1397 * called immediately before beginning initialization of the restful server's internal init. 1398 */ 1399 @Override 1400 public final void init() throws ServletException { 1401 myProviderRegistrationMutex.lock(); 1402 try { 1403 initialize(); 1404 1405 Object confProvider; 1406 try { 1407 ourLog.info( 1408 "Initializing HAPI FHIR restful server running in {} mode", 1409 getFhirContext().getVersion().getVersion().name()); 1410 1411 Collection<IResourceProvider> resourceProvider = getResourceProviders(); 1412 // 'true' tells registerProviders() that 1413 // this call is part of initialization 1414 registerProviders(resourceProvider, true); 1415 1416 Collection<Object> providers = getPlainProviders(); 1417 // 'true' tells registerProviders() that 1418 // this call is part of initialization 1419 registerProviders(providers, true); 1420 1421 confProvider = getServerConformanceProvider(); 1422 if (confProvider == null) { 1423 IFhirVersionServer versionServer = 1424 (IFhirVersionServer) getFhirContext().getVersion().getServerVersion(); 1425 confProvider = versionServer.createServerConformanceProvider(this); 1426 } 1427 setServerConformanceProvider(confProvider); 1428 1429 ourLog.trace("Invoking provider initialize methods"); 1430 if (getResourceProviders() != null) { 1431 for (IResourceProvider iResourceProvider : getResourceProviders()) { 1432 invokeInitialize(iResourceProvider); 1433 } 1434 } 1435 1436 invokeInitialize(confProvider); 1437 if (getPlainProviders() != null) { 1438 for (Object next : getPlainProviders()) { 1439 invokeInitialize(next); 1440 } 1441 } 1442 1443 /* 1444 * This is a bit odd, but we have a placeholder @GetPage method for now 1445 * that gets the server to bind for the paging request. At some point 1446 * it would be nice to set things up so that client code could provide 1447 * an alternate implementation, but this isn't currently possible.. 1448 */ 1449 findResourceMethods(new PageProvider()); 1450 1451 } catch (Exception e) { 1452 ourLog.error("An error occurred while loading request handlers!", e); 1453 throw new ServletException( 1454 Msg.code(297) + "Failed to initialize FHIR Restful server: " + e.getMessage(), e); 1455 } 1456 1457 myStarted = true; 1458 ourLog.info("A FHIR has been lit on this server"); 1459 } finally { 1460 myProviderRegistrationMutex.unlock(); 1461 } 1462 } 1463 1464 /** 1465 * This method may be overridden by subclasses to do perform initialization that needs to be performed prior to the 1466 * server being used. 1467 * 1468 * @throws ServletException If the initialization failed. Note that you should consider throwing {@link UnavailableException} 1469 * (which extends {@link ServletException}), as this is a flag to the servlet container 1470 * that the servlet is not usable. 1471 */ 1472 protected void initialize() throws ServletException { 1473 // nothing by default 1474 } 1475 1476 private void invokeDestroy(Object theProvider) { 1477 invokeDestroy(theProvider, theProvider.getClass()); 1478 } 1479 1480 private void invokeDestroy(Object theProvider, Class<?> clazz) { 1481 for (Method m : ReflectionUtil.getDeclaredMethods(clazz)) { 1482 Destroy destroy = m.getAnnotation(Destroy.class); 1483 if (destroy != null) { 1484 invokeInitializeOrDestroyMethod(theProvider, m, "destroy"); 1485 } 1486 } 1487 1488 Class<?> supertype = clazz.getSuperclass(); 1489 if (!Object.class.equals(supertype)) { 1490 invokeDestroy(theProvider, supertype); 1491 } 1492 } 1493 1494 private void invokeInitialize(Object theProvider) { 1495 invokeInitialize(theProvider, theProvider.getClass()); 1496 } 1497 1498 private void invokeInitialize(Object theProvider, Class<?> clazz) { 1499 for (Method m : ReflectionUtil.getDeclaredMethods(clazz)) { 1500 Initialize initialize = m.getAnnotation(Initialize.class); 1501 if (initialize != null) { 1502 invokeInitializeOrDestroyMethod(theProvider, m, "initialize"); 1503 } 1504 } 1505 1506 Class<?> supertype = clazz.getSuperclass(); 1507 if (!Object.class.equals(supertype)) { 1508 invokeInitialize(theProvider, supertype); 1509 } 1510 } 1511 1512 private void invokeInitializeOrDestroyMethod(Object theProvider, Method m, String theMethodDescription) { 1513 1514 Class<?>[] paramTypes = m.getParameterTypes(); 1515 Object[] params = new Object[paramTypes.length]; 1516 1517 int index = 0; 1518 for (Class<?> nextParamType : paramTypes) { 1519 1520 if (RestfulServer.class.equals(nextParamType) || IRestfulServerDefaults.class.equals(nextParamType)) { 1521 params[index] = this; 1522 } 1523 1524 index++; 1525 } 1526 1527 try { 1528 m.invoke(theProvider, params); 1529 } catch (Exception e) { 1530 ourLog.error("Exception occurred in " + theMethodDescription + " method '" + m.getName() + "'", e); 1531 } 1532 } 1533 1534 /** 1535 * Should the server "pretty print" responses by default (requesting clients can always override this default by 1536 * supplying an <code>Accept</code> header in the request, or a <code>_pretty</code> 1537 * parameter in the request URL. 1538 * <p> 1539 * The default is <code>false</code> 1540 * </p> 1541 * <p> 1542 * Note that this setting is ignored by {@link ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor} 1543 * when streaming HTML, although even when that interceptor it used this setting will 1544 * still be honoured when streaming raw FHIR. 1545 * </p> 1546 * 1547 * @return Returns the default pretty print setting 1548 */ 1549 @Override 1550 public boolean isDefaultPrettyPrint() { 1551 return myDefaultPrettyPrint; 1552 } 1553 1554 /** 1555 * Should the server "pretty print" responses by default (requesting clients can always override this default by 1556 * supplying an <code>Accept</code> header in the request, or a <code>_pretty</code> 1557 * parameter in the request URL. 1558 * <p> 1559 * The default is <code>false</code> 1560 * </p> 1561 * <p> 1562 * Note that this setting is ignored by {@link ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor} 1563 * when streaming HTML, although even when that interceptor it used this setting will 1564 * still be honoured when streaming raw FHIR. 1565 * </p> 1566 * 1567 * @param theDefaultPrettyPrint The default pretty print setting 1568 */ 1569 public void setDefaultPrettyPrint(boolean theDefaultPrettyPrint) { 1570 myDefaultPrettyPrint = theDefaultPrettyPrint; 1571 } 1572 1573 /** 1574 * If set to <code>true</code> (the default is <code>true</code>) this server will not 1575 * use the parsed request parameters (URL parameters and HTTP POST form contents) but 1576 * will instead parse these values manually from the request URL and request body. 1577 * <p> 1578 * This is useful because many servlet containers (e.g. Tomcat, Glassfish) will use 1579 * ISO-8859-1 encoding to parse escaped URL characters instead of using UTF-8 1580 * as is specified by FHIR. 1581 * </p> 1582 */ 1583 public boolean isIgnoreServerParsedRequestParameters() { 1584 return myIgnoreServerParsedRequestParameters; 1585 } 1586 1587 /** 1588 * If set to <code>true</code> (the default is <code>true</code>) this server will not 1589 * use the parsed request parameters (URL parameters and HTTP POST form contents) but 1590 * will instead parse these values manually from the request URL and request body. 1591 * <p> 1592 * This is useful because many servlet containers (e.g. Tomcat, Glassfish) will use 1593 * ISO-8859-1 encoding to parse escaped URL characters instead of using UTF-8 1594 * as is specified by FHIR. 1595 * </p> 1596 */ 1597 public void setIgnoreServerParsedRequestParameters(boolean theIgnoreServerParsedRequestParameters) { 1598 myIgnoreServerParsedRequestParameters = theIgnoreServerParsedRequestParameters; 1599 } 1600 1601 /** 1602 * Should the server attempt to decompress incoming request contents (default is <code>true</code>). Typically this 1603 * should be set to <code>true</code> unless the server has other configuration to 1604 * deal with decompressing request bodies (e.g. a filter applied to the whole server). 1605 */ 1606 public boolean isUncompressIncomingContents() { 1607 return myUncompressIncomingContents; 1608 } 1609 1610 /** 1611 * Should the server attempt to decompress incoming request contents (default is <code>true</code>). Typically this 1612 * should be set to <code>true</code> unless the server has other configuration to 1613 * deal with decompressing request bodies (e.g. a filter applied to the whole server). 1614 */ 1615 public void setUncompressIncomingContents(boolean theUncompressIncomingContents) { 1616 myUncompressIncomingContents = theUncompressIncomingContents; 1617 } 1618 1619 private String resolveRequestPath(RequestDetails theRequestDetails, String theRequestPath) { 1620 if (myTenantIdentificationStrategy != null) { 1621 theRequestPath = myTenantIdentificationStrategy.resolveRelativeUrl(theRequestPath, theRequestDetails); 1622 } 1623 return theRequestPath; 1624 } 1625 1626 public void populateRequestDetailsFromRequestPath(RequestDetails theRequestDetails, String theRequestPath) { 1627 String resolvedRequestPath = resolveRequestPath(theRequestDetails, theRequestPath); 1628 UrlPathTokenizer tok = new UrlPathTokenizer(resolvedRequestPath); 1629 1630 if (myTenantIdentificationStrategy != null) { 1631 myTenantIdentificationStrategy.extractTenant(tok, theRequestDetails); 1632 } 1633 1634 IIdType id = null; 1635 String operation = null; 1636 String compartment = null; 1637 String resourceName = null; 1638 if (tok.hasMoreTokens()) { 1639 resourceName = tok.nextTokenUnescapedAndSanitized(); 1640 if (partIsOperation(resourceName)) { 1641 operation = resourceName; 1642 resourceName = null; 1643 } 1644 } 1645 theRequestDetails.setResourceName(resourceName); 1646 1647 if (tok.hasMoreTokens()) { 1648 String nextString = tok.nextTokenUnescapedAndSanitized(); 1649 if (partIsOperation(nextString)) { 1650 operation = nextString; 1651 } else { 1652 id = myFhirContext.getVersion().newIdType(); 1653 id.setParts(null, resourceName, UrlUtil.unescape(nextString), null); 1654 } 1655 } 1656 1657 if (tok.hasMoreTokens()) { 1658 String nextString = tok.nextTokenUnescapedAndSanitized(); 1659 if (nextString.equals(Constants.PARAM_HISTORY)) { 1660 if (tok.hasMoreTokens()) { 1661 String versionString = tok.nextTokenUnescapedAndSanitized(); 1662 if (id == null) { 1663 throw new InvalidRequestException( 1664 Msg.code(298) + "Don't know how to handle request path: " + resolvedRequestPath); 1665 } 1666 id.setParts(null, resourceName, id.getIdPart(), UrlUtil.unescape(versionString)); 1667 } else { 1668 operation = Constants.PARAM_HISTORY; 1669 } 1670 } else if (partIsOperation(nextString)) { 1671 if (operation != null) { 1672 throw new InvalidRequestException( 1673 Msg.code(299) + "URL Path contains two operations: " + resolvedRequestPath); 1674 } 1675 operation = nextString; 1676 } else { 1677 compartment = nextString; 1678 } 1679 } 1680 1681 // Secondary is for things like ..../_tags/_delete 1682 String secondaryOperation = null; 1683 1684 while (tok.hasMoreTokens()) { 1685 String nextString = tok.nextTokenUnescapedAndSanitized(); 1686 if (operation == null) { 1687 operation = nextString; 1688 } else if (secondaryOperation == null) { 1689 secondaryOperation = nextString; 1690 } else { 1691 throw new InvalidRequestException(Msg.code(300) + "URL path has unexpected token '" + nextString 1692 + "' at the end: " + resolvedRequestPath); 1693 } 1694 } 1695 1696 theRequestDetails.setId(id); 1697 theRequestDetails.setOperation(operation); 1698 theRequestDetails.setSecondaryOperation(secondaryOperation); 1699 theRequestDetails.setCompartmentName(compartment); 1700 } 1701 1702 /** 1703 * Registers an interceptor. This method is a convenience method which calls 1704 * <code>getInterceptorService().registerInterceptor(theInterceptor);</code> 1705 * 1706 * @param theInterceptor The interceptor, must not be null 1707 */ 1708 public void registerInterceptor(Object theInterceptor) { 1709 Validate.notNull(theInterceptor, "Interceptor can not be null"); 1710 getInterceptorService().registerInterceptor(theInterceptor); 1711 } 1712 1713 /** 1714 * Register a single provider. This could be a Resource Provider or a "plain" provider not associated with any 1715 * resource. 1716 */ 1717 public void registerProvider(Object provider) { 1718 if (provider != null) { 1719 Collection<Object> providerList = new ArrayList<>(1); 1720 providerList.add(provider); 1721 registerProviders(providerList); 1722 } 1723 } 1724 1725 /** 1726 * Register a group of providers. These could be Resource Providers (classes implementing {@link IResourceProvider}) or "plain" providers, or a mixture of the two. 1727 * 1728 * @param theProviders a {@code Collection} of theProviders. The parameter could be null or an empty {@code Collection} 1729 */ 1730 public void registerProviders(Object... theProviders) { 1731 Validate.noNullElements(theProviders); 1732 registerProviders(Arrays.asList(theProviders)); 1733 } 1734 1735 /** 1736 * Register a group of theProviders. These could be Resource Providers, "plain" theProviders or a mixture of the two. 1737 * 1738 * @param theProviders a {@code Collection} of theProviders. The parameter could be null or an empty {@code Collection} 1739 */ 1740 public void registerProviders(Collection<?> theProviders) { 1741 Validate.noNullElements(theProviders, "theProviders must not contain any null elements"); 1742 1743 myProviderRegistrationMutex.lock(); 1744 try { 1745 if (!myStarted) { 1746 for (Object provider : theProviders) { 1747 ourLog.debug("Registration of provider [" 1748 + provider.getClass().getName() + "] will be delayed until FHIR server startup"); 1749 if (provider instanceof IResourceProvider) { 1750 myResourceProviders.add((IResourceProvider) provider); 1751 } else { 1752 myPlainProviders.add(provider); 1753 } 1754 } 1755 return; 1756 } 1757 } finally { 1758 myProviderRegistrationMutex.unlock(); 1759 } 1760 registerProviders(theProviders, false); 1761 } 1762 1763 /* 1764 * Inner method to actually register theProviders 1765 */ 1766 protected void registerProviders(@Nullable Collection<?> theProviders, boolean inInit) { 1767 Validate.noNullElements(theProviders, "theProviders must not contain any null elements"); 1768 1769 List<IResourceProvider> newResourceProviders = new ArrayList<>(); 1770 List<Object> newPlainProviders = new ArrayList<>(); 1771 1772 if (theProviders != null) { 1773 for (Object provider : theProviders) { 1774 if (provider instanceof IResourceProvider) { 1775 IResourceProvider rsrcProvider = (IResourceProvider) provider; 1776 Class<? extends IBaseResource> resourceType = rsrcProvider.getResourceType(); 1777 if (resourceType == null) { 1778 throw new NullPointerException(Msg.code(301) + "getResourceType() on class '" 1779 + rsrcProvider.getClass().getCanonicalName() + "' returned null"); 1780 } 1781 if (!inInit) { 1782 myResourceProviders.add(rsrcProvider); 1783 } 1784 newResourceProviders.add(rsrcProvider); 1785 } else { 1786 if (!inInit) { 1787 myPlainProviders.add(provider); 1788 } 1789 newPlainProviders.add(provider); 1790 } 1791 } 1792 if (!newResourceProviders.isEmpty()) { 1793 ourLog.info( 1794 "Added {} resource provider(s). Total {}", 1795 newResourceProviders.size(), 1796 myResourceProviders.size()); 1797 for (IResourceProvider provider : newResourceProviders) { 1798 findResourceMethods(provider); 1799 } 1800 } 1801 if (!newPlainProviders.isEmpty()) { 1802 ourLog.info("Added {} plain provider(s). Total {}", newPlainProviders.size(), myPlainProviders.size()); 1803 for (Object provider : newPlainProviders) { 1804 findResourceMethods(provider); 1805 } 1806 } 1807 if (!inInit) { 1808 ourLog.trace("Invoking provider initialize methods"); 1809 if (!newResourceProviders.isEmpty()) { 1810 for (IResourceProvider provider : newResourceProviders) { 1811 invokeInitialize(provider); 1812 } 1813 } 1814 if (!newPlainProviders.isEmpty()) { 1815 for (Object provider : newPlainProviders) { 1816 invokeInitialize(provider); 1817 } 1818 } 1819 } 1820 } 1821 } 1822 1823 /* 1824 * Remove registered RESTful methods for a Provider (and all superclasses) when it is being unregistered 1825 */ 1826 private void removeResourceMethods(Object theProvider) { 1827 ourLog.info("Removing RESTful methods for: {}", theProvider.getClass()); 1828 Class<?> clazz = theProvider.getClass(); 1829 Class<?> supertype = clazz.getSuperclass(); 1830 Collection<String> resourceNames = new ArrayList<>(); 1831 while (!Object.class.equals(supertype)) { 1832 removeResourceMethods(theProvider, supertype, resourceNames); 1833 removeResourceMethodsOnInterfaces(theProvider, supertype.getInterfaces(), resourceNames); 1834 supertype = supertype.getSuperclass(); 1835 } 1836 removeResourceMethods(theProvider, clazz, resourceNames); 1837 removeResourceMethodsOnInterfaces(theProvider, clazz.getInterfaces(), resourceNames); 1838 removeResourceNameBindings(resourceNames, theProvider); 1839 } 1840 1841 private void removeResourceNameBindings(Collection<String> resourceNames, Object theProvider) { 1842 for (String resourceName : resourceNames) { 1843 ResourceBinding resourceBinding = myResourceNameToBinding.get(resourceName); 1844 if (resourceBinding == null) { 1845 continue; 1846 } 1847 1848 for (Iterator<BaseMethodBinding> it = 1849 resourceBinding.getMethodBindings().iterator(); 1850 it.hasNext(); ) { 1851 BaseMethodBinding binding = it.next(); 1852 if (theProvider.equals(binding.getProvider())) { 1853 it.remove(); 1854 ourLog.info("{} binding of {} was removed", resourceName, binding); 1855 } 1856 } 1857 1858 if (resourceBinding.getMethodBindings().isEmpty()) { 1859 myResourceNameToBinding.remove(resourceName); 1860 } 1861 } 1862 } 1863 1864 private void removeResourceMethodsOnInterfaces( 1865 Object theProvider, Class<?>[] interfaces, Collection<String> resourceNames) { 1866 for (Class<?> anInterface : interfaces) { 1867 removeResourceMethods(theProvider, anInterface, resourceNames); 1868 removeResourceMethodsOnInterfaces(theProvider, anInterface.getInterfaces(), resourceNames); 1869 } 1870 } 1871 1872 /* 1873 * Collect the set of RESTful methods for a single class when it is being unregistered 1874 */ 1875 private void removeResourceMethods(Object theProvider, Class<?> clazz, Collection<String> resourceNames) 1876 throws ConfigurationException { 1877 for (Method m : ReflectionUtil.getDeclaredMethods(clazz)) { 1878 BaseMethodBinding foundMethodBinding = BaseMethodBinding.bindMethod(m, getFhirContext(), theProvider); 1879 if (foundMethodBinding == null) { 1880 continue; // not a bound method 1881 } 1882 if (foundMethodBinding instanceof ConformanceMethodBinding) { 1883 myServerConformanceMethod = null; 1884 continue; 1885 } 1886 String resourceName = foundMethodBinding.getResourceName(); 1887 if (!resourceNames.contains(resourceName)) { 1888 resourceNames.add(resourceName); 1889 } 1890 } 1891 } 1892 1893 public Object returnResponse( 1894 ServletRequestDetails theRequest, 1895 BaseParseAction<?> outcome, 1896 int operationStatus, 1897 boolean allowPrefer, 1898 MethodOutcome response, 1899 String resourceName) 1900 throws IOException { 1901 HttpServletResponse servletResponse = theRequest.getServletResponse(); 1902 servletResponse.setStatus(operationStatus); 1903 servletResponse.setCharacterEncoding(Constants.CHARSET_NAME_UTF8); 1904 addHeadersToResponse(servletResponse); 1905 if (allowPrefer) { 1906 addContentLocationHeaders(theRequest, servletResponse, response, resourceName); 1907 } 1908 Writer writer; 1909 if (outcome != null) { 1910 ResponseEncoding encoding = RestfulServerUtils.determineResponseEncodingWithDefault(theRequest); 1911 servletResponse.setContentType(encoding.getResourceContentType()); 1912 writer = servletResponse.getWriter(); 1913 IParser parser = encoding.getEncoding().newParser(getFhirContext()); 1914 parser.setPrettyPrint(RestfulServerUtils.prettyPrintResponse(this, theRequest)); 1915 outcome.execute(parser, writer); 1916 } else { 1917 servletResponse.setContentType(Constants.CT_TEXT_WITH_UTF8); 1918 writer = servletResponse.getWriter(); 1919 } 1920 return writer; 1921 } 1922 1923 @Override 1924 protected void service(HttpServletRequest theReq, HttpServletResponse theResp) 1925 throws ServletException, IOException { 1926 theReq.setAttribute(REQUEST_START_TIME, new Date()); 1927 1928 RequestTypeEnum method; 1929 try { 1930 method = RequestTypeEnum.valueOf(theReq.getMethod()); 1931 } catch (IllegalArgumentException e) { 1932 super.service(theReq, theResp); 1933 return; 1934 } 1935 1936 switch (method) { 1937 case DELETE: 1938 doDelete(theReq, theResp); 1939 break; 1940 case GET: 1941 doGet(theReq, theResp); 1942 break; 1943 case OPTIONS: 1944 doOptions(theReq, theResp); 1945 break; 1946 case POST: 1947 doPost(theReq, theResp); 1948 break; 1949 case PUT: 1950 doPut(theReq, theResp); 1951 break; 1952 case PATCH: 1953 case TRACE: 1954 case TRACK: 1955 case HEAD: 1956 case CONNECT: 1957 default: 1958 handleRequest(method, theReq, theResp); 1959 break; 1960 } 1961 } 1962 1963 /** 1964 * Sets the non-resource specific providers which implement method calls on this server 1965 * 1966 * @see #setResourceProviders(Collection) 1967 */ 1968 public void setProviders(Object... theProviders) { 1969 Validate.noNullElements(theProviders, "theProviders must not contain any null elements"); 1970 1971 myPlainProviders.clear(); 1972 if (theProviders != null) { 1973 myPlainProviders.addAll(Arrays.asList(theProviders)); 1974 } 1975 } 1976 1977 /** 1978 * If provided (default is <code>null</code>), the tenant identification 1979 * strategy provides a mechanism for a multitenant server to identify which tenant 1980 * a given request corresponds to. 1981 */ 1982 public void setTenantIdentificationStrategy(ITenantIdentificationStrategy theTenantIdentificationStrategy) { 1983 myTenantIdentificationStrategy = theTenantIdentificationStrategy; 1984 } 1985 1986 protected void throwUnknownFhirOperationException( 1987 RequestDetails requestDetails, String requestPath, RequestTypeEnum theRequestType) { 1988 FhirContext fhirContext = myFhirContext; 1989 throwUnknownFhirOperationException(requestDetails, requestPath, theRequestType, fhirContext); 1990 } 1991 1992 protected void throwUnknownResourceTypeException(String theResourceName) { 1993 /* perform a 'distinct' in case there are multiple concrete IResourceProviders declared for the same FHIR-Resource. (A concrete IResourceProvider for Patient@Read and a separate concrete for Patient@Search for example */ 1994 /* perform a 'sort' to provide an easier to read alphabetized list (vs how the different FHIR-resource IResourceProviders happened to be registered */ 1995 List<String> knownDistinctAndSortedResourceTypes = myResourceProviders.stream() 1996 .map(t -> 1997 myFhirContext.getResourceDefinition(t.getResourceType()).getName()) 1998 .distinct() 1999 .sorted() 2000 .collect(toList()); 2001 throw new ResourceNotFoundException(Msg.code(302) + "Unknown resource type '" + theResourceName 2002 + "' - Server knows how to handle: " + knownDistinctAndSortedResourceTypes); 2003 } 2004 2005 /** 2006 * Unregisters an interceptor. This method is a convenience method which calls 2007 * <code>getInterceptorService().unregisterInterceptor(theInterceptor);</code> 2008 * 2009 * @param theInterceptor The interceptor, must not be null 2010 */ 2011 public void unregisterInterceptor(Object theInterceptor) { 2012 Validate.notNull(theInterceptor, "Interceptor can not be null"); 2013 getInterceptorService().unregisterInterceptor(theInterceptor); 2014 } 2015 2016 /** 2017 * Unregister one provider (either a Resource provider or a plain provider) 2018 */ 2019 public void unregisterProvider(Object provider) { 2020 if (provider != null) { 2021 Collection<Object> providerList = new ArrayList<>(1); 2022 providerList.add(provider); 2023 unregisterProviders(providerList); 2024 } 2025 } 2026 2027 /** 2028 * Unregister a {@code Collection} of providers 2029 */ 2030 public void unregisterProviders(Collection<?> providers) { 2031 if (providers != null) { 2032 for (Object provider : providers) { 2033 removeResourceMethods(provider); 2034 if (provider instanceof IResourceProvider) { 2035 myResourceProviders.remove(provider); 2036 } else { 2037 myPlainProviders.remove(provider); 2038 } 2039 invokeDestroy(provider); 2040 } 2041 } 2042 } 2043 2044 /** 2045 * Unregisters all plain and resource providers (but not the conformance provider). 2046 */ 2047 public void unregisterAllProviders() { 2048 unregisterAllProviders(myPlainProviders); 2049 unregisterAllProviders(myResourceProviders); 2050 } 2051 2052 private void unregisterAllProviders(List<?> theProviders) { 2053 while (theProviders.size() > 0) { 2054 unregisterProvider(theProviders.get(0)); 2055 } 2056 } 2057 2058 /** 2059 * By default, server create/update/patch/transaction methods return a copy of the resource 2060 * as it was stored. This may be overridden by the client using the 2061 * <code>Prefer</code> header. 2062 * <p> 2063 * This setting changes the default behaviour if no Prefer header is supplied by the client. 2064 * The default is {@link PreferReturnEnum#REPRESENTATION} 2065 * </p> 2066 * 2067 * @see <a href="http://hl7.org/fhir/http.html#ops">HL7 FHIR Specification</a> section on the Prefer header 2068 */ 2069 @Override 2070 public PreferReturnEnum getDefaultPreferReturn() { 2071 return myDefaultPreferReturn; 2072 } 2073 2074 /** 2075 * By default, server create/update/patch/transaction methods return a copy of the resource 2076 * as it was stored. This may be overridden by the client using the 2077 * <code>Prefer</code> header. 2078 * <p> 2079 * This setting changes the default behaviour if no Prefer header is supplied by the client. 2080 * The default is {@link PreferReturnEnum#REPRESENTATION} 2081 * </p> 2082 * 2083 * @see <a href="http://hl7.org/fhir/http.html#ops">HL7 FHIR Specification</a> section on the Prefer header 2084 */ 2085 public void setDefaultPreferReturn(PreferReturnEnum theDefaultPreferReturn) { 2086 Validate.notNull(theDefaultPreferReturn, "theDefaultPreferReturn must not be null"); 2087 myDefaultPreferReturn = theDefaultPreferReturn; 2088 } 2089 2090 /** 2091 * Create a CapabilityStatement based on the given request 2092 */ 2093 public IBaseConformance getCapabilityStatement(ServletRequestDetails theRequestDetails) { 2094 // Create a cloned request details so we can make it indicate that this is a capabilities request 2095 ServletRequestDetails requestDetails = new ServletRequestDetails(theRequestDetails); 2096 requestDetails.setRestOperationType(RestOperationTypeEnum.METADATA); 2097 2098 return myServerConformanceMethod.provideCapabilityStatement(this, requestDetails); 2099 } 2100 2101 /** 2102 * Count length of URL string, but treating unescaped sequences (e.g. ' ') as their unescaped equivalent (%20) 2103 */ 2104 protected static int escapedLength(String theServletPath) { 2105 int delta = 0; 2106 for (int i = 0; i < theServletPath.length(); i++) { 2107 char next = theServletPath.charAt(i); 2108 if (next == ' ') { 2109 delta = delta + 2; 2110 } 2111 } 2112 return theServletPath.length() + delta; 2113 } 2114 2115 public static void throwUnknownFhirOperationException( 2116 RequestDetails requestDetails, 2117 String requestPath, 2118 RequestTypeEnum theRequestType, 2119 FhirContext theFhirContext) { 2120 String message = theFhirContext 2121 .getLocalizer() 2122 .getMessage( 2123 RestfulServer.class, 2124 "unknownMethod", 2125 theRequestType.name(), 2126 requestPath, 2127 requestDetails.getParameters().keySet()); 2128 2129 IBaseOperationOutcome oo = OperationOutcomeUtil.newInstance(theFhirContext); 2130 OperationOutcomeUtil.addIssue(theFhirContext, oo, "error", message, null, "not-supported"); 2131 2132 throw new InvalidRequestException(Msg.code(303) + message, oo); 2133 } 2134 2135 private static boolean partIsOperation(String nextString) { 2136 return nextString.length() > 0 2137 && (nextString.charAt(0) == '_' 2138 || nextString.charAt(0) == '$' 2139 || nextString.equals(Constants.URL_TOKEN_METADATA)); 2140 } 2141 2142 // /** 2143 // * Returns the read method binding for the given resource type, or 2144 // * returns <code>null</code> if not 2145 // * @param theResourceType The resource type, e.g. "Patient" 2146 // * @return The read method binding, or null 2147 // */ 2148 // public ReadMethodBinding findReadMethodBinding(String theResourceType) { 2149 // ReadMethodBinding retVal = null; 2150 // 2151 // ResourceBinding type = myResourceNameToBinding.get(theResourceType); 2152 // if (type != null) { 2153 // for (BaseMethodBinding<?> next : type.getMethodBindings()) { 2154 // if (next instanceof ReadMethodBinding) { 2155 // retVal = (ReadMethodBinding) next; 2156 // } 2157 // } 2158 // } 2159 // 2160 // return retVal; 2161 // } 2162}