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