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