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