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