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