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