All Downloads are FREE. Search and download functionalities are using the official Maven repository.

ca.uhn.fhir.rest.server.RestfulServer Maven / Gradle / Ivy

There is a newer version: 7.6.1
Show newest version
package ca.uhn.fhir.rest.server;

/*
 * #%L
 * HAPI FHIR - Core Library
 * %%
 * Copyright (C) 2014 - 2016 University Health Network
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;

import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.Writer;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.*;
import java.util.Map.Entry;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.jar.Manifest;

import javax.servlet.ServletException;
import javax.servlet.UnavailableException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.io.Charsets;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;

import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.ProvidedResourceScanner;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.parser.IParser;
import ca.uhn.fhir.rest.annotation.Destroy;
import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.Initialize;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.api.RequestTypeEnum;
import ca.uhn.fhir.rest.method.BaseMethodBinding;
import ca.uhn.fhir.rest.method.ConformanceMethodBinding;
import ca.uhn.fhir.rest.method.ParseAction;
import ca.uhn.fhir.rest.method.RequestDetails;
import ca.uhn.fhir.rest.server.RestfulServerUtils.ResponseEncoding;
import ca.uhn.fhir.rest.server.exceptions.AuthenticationException;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.NotModifiedException;
import ca.uhn.fhir.rest.server.interceptor.ExceptionHandlingInterceptor;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.util.*;

public class RestfulServer extends HttpServlet implements IRestfulServer {

	/**
	 * All incoming requests will have an attribute added to {@link HttpServletRequest#getAttribute(String)}
	 * with this key. The value will be a Java {@link Date} with the time that request processing began.
	 */
	public static final String REQUEST_START_TIME = RestfulServer.class.getName() + "REQUEST_START_TIME";

	/**
	 * Default setting for {@link #setETagSupport(ETagSupportEnum) ETag Support}: {@link ETagSupportEnum#ENABLED}
	 */
	public static final ETagSupportEnum DEFAULT_ETAG_SUPPORT = ETagSupportEnum.ENABLED;

	private static final ExceptionHandlingInterceptor DEFAULT_EXCEPTION_HANDLER = new ExceptionHandlingInterceptor();

	private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(RestfulServer.class);

	private static final long serialVersionUID = 1L;
	/**
	 * Requests will have an HttpServletRequest attribute set with this name, containing the servlet
	 * context, in order to avoid a dependency on Servlet-API 3.0+
	 */
	public static final String SERVLET_CONTEXT_ATTRIBUTE = "ca.uhn.fhir.rest.server.RestfulServer.servlet_context";
	private BundleInclusionRule myBundleInclusionRule = BundleInclusionRule.BASED_ON_INCLUDES;
	private boolean myDefaultPrettyPrint = false;
	private EncodingEnum myDefaultResponseEncoding = EncodingEnum.XML;
	private ETagSupportEnum myETagSupport = DEFAULT_ETAG_SUPPORT;
	private FhirContext myFhirContext;
	private boolean myIgnoreServerParsedRequestParameters = true;
	private String myImplementationDescription;
	private final List myInterceptors = new ArrayList();
	private IPagingProvider myPagingProvider;
	private final List myPlainProviders = new ArrayList();
	private Lock myProviderRegistrationMutex = new ReentrantLock();
	private Map myResourceNameToBinding = new HashMap();
	private final List myResourceProviders = new ArrayList();
	private IServerAddressStrategy myServerAddressStrategy = new IncomingRequestAddressStrategy();
	private ResourceBinding myServerBinding = new ResourceBinding();
	private BaseMethodBinding myServerConformanceMethod;
	private Object myServerConformanceProvider;
	private String myServerName = "HAPI FHIR Server";
	/** This is configurable but by default we just use HAPI version */
	private String myServerVersion = VersionUtil.getVersion();
	private boolean myStarted;
	private Map myTypeToProvider = new HashMap();
	private boolean myUncompressIncomingContents = true;
	private boolean myUseBrowserFriendlyContentTypes;

	/**
	 * 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
	 * version of FHIR to support through classpath scanning. This is brittle, and it is highly recommended to explicitly
	 * specify a FHIR version.
	 */
	public RestfulServer() {
		this(null);
	}

	/**
	 * Constructor
	 */
	public RestfulServer(FhirContext theCtx) {
		myFhirContext = theCtx;
	}

	private void addContentLocationHeaders(RequestDetails theRequest, HttpServletResponse servletResponse, MethodOutcome response, String resourceName) {
		if (response != null && response.getId() != null) {
			addLocationHeader(theRequest, servletResponse, response, Constants.HEADER_LOCATION, resourceName);
			addLocationHeader(theRequest, servletResponse, response, Constants.HEADER_CONTENT_LOCATION, resourceName);
		}
	}

	/**
	 * This method is called prior to sending a response to incoming requests. It is used to add custom headers.
	 * 

* Use caution if overriding this method: it is recommended to call super.addHeadersToResponse to avoid * inadvertantly disabling functionality. *

*/ public void addHeadersToResponse(HttpServletResponse theHttpResponse) { StringBuilder b = new StringBuilder(); b.append("HAPI FHIR "); b.append(VersionUtil.getVersion()); b.append(" REST Server (FHIR Server; FHIR "); b.append(myFhirContext.getVersion().getVersion().getFhirVersionString()); b.append('/'); b.append(myFhirContext.getVersion().getVersion().name()); b.append(")"); theHttpResponse.addHeader("X-Powered-By", b.toString()); } private void addLocationHeader(RequestDetails theRequest, HttpServletResponse theResponse, MethodOutcome response, String headerLocation, String resourceName) { StringBuilder b = new StringBuilder(); b.append(theRequest.getFhirServerBase()); b.append('/'); b.append(resourceName); b.append('/'); b.append(response.getId().getIdPart()); if (response.getId().hasVersionIdPart()) { b.append("/" + Constants.PARAM_HISTORY + "/"); b.append(response.getId().getVersionIdPart()); } theResponse.addHeader(headerLocation, b.toString()); } private void assertProviderIsValid(Object theNext) throws ConfigurationException { if (Modifier.isPublic(theNext.getClass().getModifiers()) == false) { throw new ConfigurationException("Can not use provider '" + theNext.getClass() + "' - Class must be public"); } } public RestulfulServerConfiguration createConfiguration() { RestulfulServerConfiguration result = new RestulfulServerConfiguration(); result.setResourceBindings(getResourceBindings()); result.setServerBindings(getServerBindings()); result.setImplementationDescription(getImplementationDescription()); result.setServerVersion(getServerVersion()); result.setServerName(getServerName()); result.setFhirContext(getFhirContext()); result.setServerAddressStrategy(myServerAddressStrategy); InputStream inputStream = null; try { inputStream = getClass().getResourceAsStream("/META-INF/MANIFEST.MF"); if (inputStream != null) { Manifest manifest = new Manifest(inputStream); result.setConformanceDate(manifest.getMainAttributes().getValue("Build-Time")); } } catch (IOException e) { // fall through } finally { if (inputStream != null) { IOUtils.closeQuietly(inputStream); } } return result; } @Override public void destroy() { if (getResourceProviders() != null) { for (IResourceProvider iResourceProvider : getResourceProviders()) { invokeDestroy(iResourceProvider); } } if (myServerConformanceProvider != null) { invokeDestroy(myServerConformanceProvider); } if (getPlainProviders() != null) { for (Object next : getPlainProviders()) { invokeDestroy(next); } } } public BaseMethodBinding determineResourceMethod(RequestDetails requestDetails, String requestPath) { RequestTypeEnum requestType = requestDetails.getRequestType(); ResourceBinding resourceBinding = null; BaseMethodBinding resourceMethod = null; String resourceName = requestDetails.getResourceName(); if (myServerConformanceMethod.incomingServerRequestMatchesMethod(requestDetails)) { resourceMethod = myServerConformanceMethod; } else if (resourceName == null) { resourceBinding = myServerBinding; } else { resourceBinding = myResourceNameToBinding.get(resourceName); if (resourceBinding == null) { throw new InvalidRequestException("Unknown resource type '" + resourceName + "' - Server knows how to handle: " + myResourceNameToBinding.keySet()); } } if (resourceMethod == null) { if (resourceBinding != null) { resourceMethod = resourceBinding.getMethod(requestDetails); } } if (resourceMethod == null) { if (isBlank(requestPath)) { throw new InvalidRequestException(myFhirContext.getLocalizer().getMessage(RestfulServer.class, "rootRequest")); } else { throw new InvalidRequestException(myFhirContext.getLocalizer().getMessage(RestfulServer.class, "unknownMethod", requestType.name(), requestPath, requestDetails.getParameters().keySet())); } } return resourceMethod; } /** * Count length of URL string, but treating unescaped sequences (e.g. ' ') as their unescaped equivalent (%20) */ protected int escapedLength(String theServletPath) { int delta = 0; for (int i = 0; i < theServletPath.length(); i++) { char next = theServletPath.charAt(i); if (next == ' ') { delta = delta + 2; } } return theServletPath.length() + delta; } private void findResourceMethods(Object theProvider) { ourLog.info("Scanning type for RESTful methods: {}", theProvider.getClass()); int count = 0; Class clazz = theProvider.getClass(); Class supertype = clazz.getSuperclass(); while (!Object.class.equals(supertype)) { count += findResourceMethods(theProvider, supertype); supertype = supertype.getSuperclass(); } count += findResourceMethods(theProvider, clazz); if (count == 0) { throw new ConfigurationException("Did not find any annotated RESTful methods on provider class " + theProvider.getClass().getCanonicalName()); } } private int findResourceMethods(Object theProvider, Class clazz) throws ConfigurationException { int count = 0; for (Method m : ReflectionUtil.getDeclaredMethods(clazz)) { BaseMethodBinding foundMethodBinding = BaseMethodBinding.bindMethod(m, getFhirContext(), theProvider); if (foundMethodBinding == null) { continue; } count++; if (foundMethodBinding instanceof ConformanceMethodBinding) { myServerConformanceMethod = foundMethodBinding; continue; } if (!Modifier.isPublic(m.getModifiers())) { throw new ConfigurationException("Method '" + m.getName() + "' is not public, FHIR RESTful methods must be public"); } else { if (Modifier.isStatic(m.getModifiers())) { throw new ConfigurationException("Method '" + m.getName() + "' is static, FHIR RESTful methods must not be static"); } else { ourLog.debug("Scanning public method: {}#{}", theProvider.getClass(), m.getName()); String resourceName = foundMethodBinding.getResourceName(); ResourceBinding resourceBinding; if (resourceName == null) { resourceBinding = myServerBinding; } else { RuntimeResourceDefinition definition = getFhirContext().getResourceDefinition(resourceName); if (myResourceNameToBinding.containsKey(definition.getName())) { resourceBinding = myResourceNameToBinding.get(definition.getName()); } else { resourceBinding = new ResourceBinding(); resourceBinding.setResourceName(resourceName); myResourceNameToBinding.put(resourceName, resourceBinding); } } List> allowableParams = foundMethodBinding.getAllowableParamAnnotations(); if (allowableParams != null) { for (Annotation[] nextParamAnnotations : m.getParameterAnnotations()) { for (Annotation annotation : nextParamAnnotations) { Package pack = annotation.annotationType().getPackage(); if (pack.equals(IdParam.class.getPackage())) { if (!allowableParams.contains(annotation.annotationType())) { throw new ConfigurationException("Method[" + m.toString() + "] is not allowed to have a parameter annotated with " + annotation); } } } } } resourceBinding.addMethod(foundMethodBinding); ourLog.debug(" * Method: {}#{} is a handler", theProvider.getClass(), m.getName()); } } } return count; } /** * @deprecated As of HAPI FHIR 1.5, this property has been moved to * {@link FhirContext#setAddProfileTagWhenEncoding(AddProfileTagEnum)} */ @Override @Deprecated public AddProfileTagEnum getAddProfileTag() { return myFhirContext.getAddProfileTagWhenEncoding(); } @Override public BundleInclusionRule getBundleInclusionRule() { return myBundleInclusionRule; } /** * Returns the default encoding to return (XML/JSON) if an incoming request does not specify a preference (either * with the _format URL parameter, or with an Accept header * in the request. The default is {@link EncodingEnum#XML}. Will not return null. */ @Override public EncodingEnum getDefaultResponseEncoding() { return myDefaultResponseEncoding; } @Override public ETagSupportEnum getETagSupport() { return myETagSupport; } /** * 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 * creating their own. */ @Override public FhirContext getFhirContext() { if (myFhirContext == null) { myFhirContext = new FhirContext(); } return myFhirContext; } public String getImplementationDescription() { return myImplementationDescription; } /** * Returns a ist of all registered server interceptors */ @Override public List getInterceptors() { return Collections.unmodifiableList(myInterceptors); } @Override public IPagingProvider getPagingProvider() { return myPagingProvider; } /** * Provides the non-resource specific providers which implement method calls on this server * * @see #getResourceProviders() */ public Collection getPlainProviders() { return myPlainProviders; } /** * Allows users of RestfulServer to override the getRequestPath method to let them build their custom request path * implementation * * @param requestFullPath * the full request path * @param servletContextPath * the servelet context path * @param servletPath * the servelet path * @return created resource path */ protected String getRequestPath(String requestFullPath, String servletContextPath, String servletPath) { return requestFullPath.substring(escapedLength(servletContextPath) + escapedLength(servletPath)); } public Collection getResourceBindings() { return myResourceNameToBinding.values(); } /** * Provides the resource providers for this server */ public Collection getResourceProviders() { return myResourceProviders; } /** * 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} */ public IServerAddressStrategy getServerAddressStrategy() { return myServerAddressStrategy; } /** * Returns the server base URL (with no trailing '/') for a given request */ public String getServerBaseForRequest(HttpServletRequest theRequest) { String fhirServerBase; fhirServerBase = myServerAddressStrategy.determineServerBase(getServletContext(), theRequest); if (fhirServerBase.endsWith("/")) { fhirServerBase = fhirServerBase.substring(0, fhirServerBase.length() - 1); } return fhirServerBase; } /** * 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 * with caution, as it may change. */ public List> getServerBindings() { return myServerBinding.getMethodBindings(); } /** * 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. *

* By default, the ServerConformanceProvider for the declared version of FHIR is used, but this can be changed, or * set to null to use the appropriate one for the given FHIR version. *

*/ public Object getServerConformanceProvider() { return myServerConformanceProvider; } /** * 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. * * @see RestfulServer#setServerName(String) */ public String getServerName() { return myServerName; } public IResourceProvider getServerProfilesProvider() { return getFhirContext().getVersion().createServerProfilesProvider(this); } /** * 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. */ public String getServerVersion() { return myServerVersion; } protected void handleRequest(RequestTypeEnum theRequestType, HttpServletRequest theRequest, HttpServletResponse theResponse) throws ServletException, IOException { String fhirServerBase = null; ServletRequestDetails requestDetails = new ServletRequestDetails(); requestDetails.setServer(this); requestDetails.setRequestType(theRequestType); requestDetails.setServletRequest(theRequest); requestDetails.setServletResponse(theResponse); theRequest.setAttribute(SERVLET_CONTEXT_ATTRIBUTE, getServletContext()); try { /* *********************************** * Parse out the request parameters * ***********************************/ String requestFullPath = StringUtils.defaultString(theRequest.getRequestURI()); String servletPath = StringUtils.defaultString(theRequest.getServletPath()); StringBuffer requestUrl = theRequest.getRequestURL(); String servletContextPath = IncomingRequestAddressStrategy.determineServletContextPath(theRequest, this); /* * Just for debugging.. */ if (ourLog.isTraceEnabled()) { ourLog.trace("Request FullPath: {}", requestFullPath); ourLog.trace("Servlet Path: {}", servletPath); ourLog.trace("Request Url: {}", requestUrl); ourLog.trace("Context Path: {}", servletContextPath); } String completeUrl; Map params = null; if (StringUtils.isNotBlank(theRequest.getQueryString())) { completeUrl = requestUrl + "?" + theRequest.getQueryString(); /* * By default, we manually parse the request params (the URL params, or the body for * POST form queries) since Java containers can't be trusted to use UTF-8 encoding * when parsing. Specifically Tomcat 7 and Glassfish 4.0 use 8859-1 for some dumb * reason.... grr..... */ if (isIgnoreServerParsedRequestParameters()) { String contentType = theRequest.getHeader(Constants.HEADER_CONTENT_TYPE); if (theRequestType == RequestTypeEnum.POST && isNotBlank(contentType) && contentType.startsWith(Constants.CT_X_FORM_URLENCODED)) { String requestBody = new String(requestDetails.loadRequestContents(), Constants.CHARSET_UTF8); params = UrlUtil.parseQueryStrings(theRequest.getQueryString(), requestBody); } else if (theRequestType == RequestTypeEnum.GET) { params = UrlUtil.parseQueryString(theRequest.getQueryString()); } } } else { completeUrl = requestUrl.toString(); } if (params == null) { params = new HashMap(theRequest.getParameterMap()); } requestDetails.setParameters(params); /* ************************* * Notify interceptors about the incoming request * *************************/ for (IServerInterceptor next : myInterceptors) { boolean continueProcessing = next.incomingRequestPreProcessed(theRequest, theResponse); if (!continueProcessing) { ourLog.debug("Interceptor {} returned false, not continuing processing"); return; } } String requestPath = getRequestPath(requestFullPath, servletContextPath, servletPath); if (requestPath.length() > 0 && requestPath.charAt(0) == '/') { requestPath = requestPath.substring(1); } fhirServerBase = getServerBaseForRequest(theRequest); IIdType id; populateRequestDetailsFromRequestPath(requestDetails, requestPath); if (theRequestType == RequestTypeEnum.PUT) { String contentLocation = theRequest.getHeader(Constants.HEADER_CONTENT_LOCATION); if (contentLocation != null) { id = myFhirContext.getVersion().newIdType(); id.setValue(contentLocation); requestDetails.setId(id); } } String acceptEncoding = theRequest.getHeader(Constants.HEADER_ACCEPT_ENCODING); boolean respondGzip = false; if (acceptEncoding != null) { String[] parts = acceptEncoding.trim().split("\\s*,\\s*"); for (String string : parts) { if (string.equals("gzip")) { respondGzip = true; } } } requestDetails.setRespondGzip(respondGzip); requestDetails.setRequestPath(requestPath); requestDetails.setFhirServerBase(fhirServerBase); requestDetails.setCompleteUrl(completeUrl); // String pagingAction = theRequest.getParameter(Constants.PARAM_PAGINGACTION); // if (getPagingProvider() != null && isNotBlank(pagingAction)) { // requestDetails.setRestOperationType(RestOperationTypeEnum.GET_PAGE); // if (theRequestType != RequestTypeEnum.GET) { // /* // * We reconstruct the link-self URL using the request parameters, and this would break if the parameters came // in using a POST. We could probably work around that but why bother unless // * someone comes up with a reason for needing it. // */ // throw new InvalidRequestException(getFhirContext().getLocalizer().getMessage(RestfulServer.class, // "getPagesNonHttpGet")); // } // handlePagingRequest(requestDetails, theResponse, pagingAction); // return; // } BaseMethodBinding resourceMethod = determineResourceMethod(requestDetails, requestPath); requestDetails.setRestOperationType(resourceMethod.getRestOperationType()); // Handle server interceptors for (IServerInterceptor next : myInterceptors) { boolean continueProcessing = next.incomingRequestPostProcessed(requestDetails, theRequest, theResponse); if (!continueProcessing) { ourLog.debug("Interceptor {} returned false, not continuing processing"); return; } } /* * 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 its output back to the client. * * This is basically the end of processing for a successful request, since the * method binding replies to the client and closes the response. */ Closeable outputStreamOrWriter = (Closeable) resourceMethod.invokeServer(this, requestDetails); for (int i = getInterceptors().size() - 1; i >= 0; i--) { IServerInterceptor next = getInterceptors().get(i); next.processingCompletedNormally(requestDetails); } if (outputStreamOrWriter != null) { outputStreamOrWriter.close(); } } catch (NotModifiedException e) { for (int i = getInterceptors().size() - 1; i >= 0; i--) { IServerInterceptor next = getInterceptors().get(i); if (!next.handleException(requestDetails, e, theRequest, theResponse)) { ourLog.debug("Interceptor {} returned false, not continuing processing"); return; } } writeExceptionToResponse(theResponse, e); } catch (AuthenticationException e) { for (int i = getInterceptors().size() - 1; i >= 0; i--) { IServerInterceptor next = getInterceptors().get(i); if (!next.handleException(requestDetails, e, theRequest, theResponse)) { ourLog.debug("Interceptor {} returned false, not continuing processing"); return; } } writeExceptionToResponse(theResponse, e); } catch (Throwable e) { /* * 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 * had business requirement problems) or it could be due to bugs (e.g. NullPointerException). * * First we let the interceptors have a crack at converting the exception into something HAPI can use * (BaseServerResponseException) */ BaseServerResponseException exception = null; for (int i = getInterceptors().size() - 1; i >= 0; i--) { IServerInterceptor next = getInterceptors().get(i); exception = next.preProcessOutgoingException(requestDetails, e, theRequest); if (exception != null) { ourLog.debug("Interceptor {} returned false, not continuing processing"); break; } } /* * 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 * InternalErrorException. */ if (exception == null) { exception = DEFAULT_EXCEPTION_HANDLER.preProcessOutgoingException(requestDetails, e, theRequest); } /* * Next, interceptors get a shot at handling the exception */ for (int i = getInterceptors().size() - 1; i >= 0; i--) { IServerInterceptor next = getInterceptors().get(i); if (!next.handleException(requestDetails, exception, theRequest, theResponse)) { ourLog.debug("Interceptor {} returned false, not continuing processing"); return; } } /* * If we're handling an exception, no summary mode should be applied */ requestDetails.getParameters().remove(Constants.PARAM_SUMMARY); requestDetails.getParameters().remove(Constants.PARAM_ELEMENTS); /* * If nobody handles it, default behaviour is to stream back the OperationOutcome to the client. */ DEFAULT_EXCEPTION_HANDLER.handleException(requestDetails, exception, theRequest, theResponse); } } /** * 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 * called immediately before beginning initialization of the restful server's internal init. */ @Override public final void init() throws ServletException { myProviderRegistrationMutex.lock(); try { initialize(); Object confProvider; try { ourLog.info("Initializing HAPI FHIR restful server running in " + getFhirContext().getVersion().getVersion().name() + " mode"); ProvidedResourceScanner providedResourceScanner = new ProvidedResourceScanner(getFhirContext()); providedResourceScanner.scanForProvidedResources(this); Collection resourceProvider = getResourceProviders(); // 'true' tells registerProviders() that // this call is part of initialization registerProviders(resourceProvider, true); Collection providers = getPlainProviders(); // 'true' tells registerProviders() that // this call is part of initialization registerProviders(providers, true); findResourceMethods(getServerProfilesProvider()); confProvider = getServerConformanceProvider(); if (confProvider == null) { confProvider = getFhirContext().getVersion().createServerConformanceProvider(this); } // findSystemMethods(confProvider); findResourceMethods(confProvider); ourLog.trace("Invoking provider initialize methods"); if (getResourceProviders() != null) { for (IResourceProvider iResourceProvider : getResourceProviders()) { invokeInitialize(iResourceProvider); } } if (confProvider != null) { invokeInitialize(confProvider); } if (getPlainProviders() != null) { for (Object next : getPlainProviders()) { invokeInitialize(next); } } /* * This is a bit odd, but we have a placeholder @GetPage method for now * that gets the server to bind for the paging request. At some point * it would be nice to set things up so that client code could provide * an alternate implementation, but this isn't currently possible.. */ findResourceMethods(new PageProvider()); } catch (Exception ex) { ourLog.error("An error occurred while loading request handlers!", ex); throw new ServletException("Failed to initialize FHIR Restful server", ex); } myStarted = true; ourLog.info("A FHIR has been lit on this server"); } finally { myProviderRegistrationMutex.unlock(); } } /** * This method may be overridden by subclasses to do perform initialization that needs to be performed prior to the * server being used. * * @throws ServletException * 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 * that the servlet is not usable. */ protected void initialize() throws ServletException { // nothing by default } private void invokeDestroy(Object theProvider) { invokeDestroy(theProvider, theProvider.getClass()); } private void invokeDestroy(Object theProvider, Class clazz) { for (Method m : ReflectionUtil.getDeclaredMethods(clazz)) { Destroy destroy = m.getAnnotation(Destroy.class); if (destroy != null) { try { m.invoke(theProvider); } catch (IllegalAccessException e) { ourLog.error("Exception occurred in destroy ", e); } catch (InvocationTargetException e) { ourLog.error("Exception occurred in destroy ", e); } return; } } Class supertype = clazz.getSuperclass(); if (!Object.class.equals(supertype)) { invokeDestroy(theProvider, supertype); } } private void invokeInitialize(Object theProvider) { invokeInitialize(theProvider, theProvider.getClass()); } private void invokeInitialize(Object theProvider, Class clazz) { for (Method m : ReflectionUtil.getDeclaredMethods(clazz)) { Initialize initialize = m.getAnnotation(Initialize.class); if (initialize != null) { try { m.invoke(theProvider); } catch (IllegalAccessException e) { ourLog.error("Exception occurred in destroy ", e); } catch (InvocationTargetException e) { ourLog.error("Exception occurred in destroy ", e); } return; } } Class supertype = clazz.getSuperclass(); if (!Object.class.equals(supertype)) { invokeInitialize(theProvider, supertype); } } /** * Should the server "pretty print" responses by default (requesting clients can always override this default by * supplying an Accept header in the request, or a _pretty * parameter in the request URL. *

* The default is false *

* * @return Returns the default pretty print setting */ @Override public boolean isDefaultPrettyPrint() { return myDefaultPrettyPrint; } /** * If set to true (the default is true) this server will not * use the parsed request parameters (URL parameters and HTTP POST form contents) but * will instead parse these values manually from the request URL and request body. *

* This is useful because many servlet containers (e.g. Tomcat, Glassfish) will use * ISO-8859-1 encoding to parse escaped URL characters instead of using UTF-8 * as is specified by FHIR. *

*/ public boolean isIgnoreServerParsedRequestParameters() { return myIgnoreServerParsedRequestParameters; } /** * Should the server attempt to decompress incoming request contents (default is true). Typically this * should be set to true unless the server has other configuration to * deal with decompressing request bodies (e.g. a filter applied to the whole server). */ public boolean isUncompressIncomingContents() { return myUncompressIncomingContents; } /** * @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 * highlighting. Deprocated in 1.4 */ @Deprecated @Override public boolean isUseBrowserFriendlyContentTypes() { return myUseBrowserFriendlyContentTypes; } public void populateRequestDetailsFromRequestPath(RequestDetails theRequestDetails, String theRequestPath) { StringTokenizer tok = new UrlPathTokenizer(theRequestPath); String resourceName = null; IIdType id = null; String operation = null; String compartment = null; if (tok.hasMoreTokens()) { resourceName = tok.nextToken(); if (partIsOperation(resourceName)) { operation = resourceName; resourceName = null; } } theRequestDetails.setResourceName(resourceName); if (tok.hasMoreTokens()) { String nextString = tok.nextToken(); if (partIsOperation(nextString)) { operation = nextString; } else { id = myFhirContext.getVersion().newIdType(); id.setParts(null, resourceName, UrlUtil.unescape(nextString), null); } } if (tok.hasMoreTokens()) { String nextString = tok.nextToken(); if (nextString.equals(Constants.PARAM_HISTORY)) { if (tok.hasMoreTokens()) { String versionString = tok.nextToken(); if (id == null) { throw new InvalidRequestException("Don't know how to handle request path: " + theRequestPath); } id.setParts(null, resourceName, id.getIdPart(), UrlUtil.unescape(versionString)); } else { operation = Constants.PARAM_HISTORY; } } else if (partIsOperation(nextString)) { if (operation != null) { throw new InvalidRequestException("URL Path contains two operations: " + theRequestPath); } operation = nextString; } else { compartment = nextString; } } // Secondary is for things like ..../_tags/_delete String secondaryOperation = null; while (tok.hasMoreTokens()) { String nextString = tok.nextToken(); if (operation == null) { operation = nextString; } else if (secondaryOperation == null) { secondaryOperation = nextString; } else { throw new InvalidRequestException("URL path has unexpected token '" + nextString + "' at the end: " + theRequestPath); } } theRequestDetails.setId(id); theRequestDetails.setOperation(operation); theRequestDetails.setSecondaryOperation(secondaryOperation); theRequestDetails.setCompartmentName(compartment); } public void registerInterceptor(IServerInterceptor theInterceptor) { Validate.notNull(theInterceptor, "Interceptor can not be null"); myInterceptors.add(theInterceptor); } /** * Register a single provider. This could be a Resource Provider or a "plain" provider not associated with any * resource. */ public void registerProvider(Object provider) { if (provider != null) { Collection providerList = new ArrayList(1); providerList.add(provider); registerProviders(providerList); } } /** * Register a group of providers. These could be Resource Providers, "plain" providers or a mixture of the two. * * @param providers * a {@code Collection} of providers. The parameter could be null or an empty {@code Collection} */ public void registerProviders(Collection providers) { myProviderRegistrationMutex.lock(); try { if (!myStarted) { for (Object provider : providers) { ourLog.info("Registration of provider [" + provider.getClass().getName() + "] will be delayed until FHIR server startup"); if (provider instanceof IResourceProvider) { myResourceProviders.add((IResourceProvider) provider); } else { myPlainProviders.add(provider); } } return; } } finally { myProviderRegistrationMutex.unlock(); } registerProviders(providers, false); } /* * Inner method to actually register providers */ protected void registerProviders(Collection providers, boolean inInit) { List newResourceProviders = new ArrayList(); List newPlainProviders = new ArrayList(); ProvidedResourceScanner providedResourceScanner = new ProvidedResourceScanner(getFhirContext()); if (providers != null) { for (Object provider : providers) { if (provider instanceof IResourceProvider) { IResourceProvider rsrcProvider = (IResourceProvider) provider; Class resourceType = rsrcProvider.getResourceType(); if (resourceType == null) { throw new NullPointerException("getResourceType() on class '" + rsrcProvider.getClass().getCanonicalName() + "' returned null"); } String resourceName = getFhirContext().getResourceDefinition(resourceType).getName(); if (myTypeToProvider.containsKey(resourceName)) { throw new ConfigurationException("Multiple resource providers return resource type[" + resourceName + "]: First[" + myTypeToProvider.get(resourceName).getClass().getCanonicalName() + "] and Second[" + rsrcProvider.getClass().getCanonicalName() + "]"); } if (!inInit) { myResourceProviders.add(rsrcProvider); } myTypeToProvider.put(resourceName, rsrcProvider); providedResourceScanner.scanForProvidedResources(rsrcProvider); newResourceProviders.add(rsrcProvider); } else { if (!inInit) { myPlainProviders.add(provider); } newPlainProviders.add(provider); } } if (!newResourceProviders.isEmpty()) { ourLog.info("Added {} resource provider(s). Total {}", newResourceProviders.size(), myTypeToProvider.size()); for (IResourceProvider provider : newResourceProviders) { assertProviderIsValid(provider); findResourceMethods(provider); } } if (!newPlainProviders.isEmpty()) { ourLog.info("Added {} plain provider(s). Total {}", newPlainProviders.size()); for (Object provider : newPlainProviders) { assertProviderIsValid(provider); findResourceMethods(provider); } } if (!inInit) { ourLog.trace("Invoking provider initialize methods"); if (!newResourceProviders.isEmpty()) { for (IResourceProvider provider : newResourceProviders) { invokeInitialize(provider); } } if (!newPlainProviders.isEmpty()) { for (Object provider : newPlainProviders) { invokeInitialize(provider); } } } } } /* * Remove registered RESTful methods for a Provider (and all superclasses) when it is being unregistered */ private void removeResourceMethods(Object theProvider) throws Exception { ourLog.info("Removing RESTful methods for: {}", theProvider.getClass()); Class clazz = theProvider.getClass(); Class supertype = clazz.getSuperclass(); Collection resourceNames = new ArrayList(); while (!Object.class.equals(supertype)) { removeResourceMethods(theProvider, supertype, resourceNames); supertype = supertype.getSuperclass(); } removeResourceMethods(theProvider, clazz, resourceNames); for (String resourceName : resourceNames) { myResourceNameToBinding.remove(resourceName); } } /* * Collect the set of RESTful methods for a single class when it is being unregistered */ private void removeResourceMethods(Object theProvider, Class clazz, Collection resourceNames) throws ConfigurationException { for (Method m : ReflectionUtil.getDeclaredMethods(clazz)) { BaseMethodBinding foundMethodBinding = BaseMethodBinding.bindMethod(m, getFhirContext(), theProvider); if (foundMethodBinding == null) { continue; // not a bound method } if (foundMethodBinding instanceof ConformanceMethodBinding) { myServerConformanceMethod = null; continue; } String resourceName = foundMethodBinding.getResourceName(); if (!resourceNames.contains(resourceName)) { resourceNames.add(resourceName); } } } public Object returnResponse(ServletRequestDetails theRequest, ParseAction outcome, int operationStatus, boolean allowPrefer, MethodOutcome response, String resourceName) throws IOException { HttpServletResponse servletResponse = theRequest.getServletResponse(); servletResponse.setStatus(operationStatus); servletResponse.setCharacterEncoding(Constants.CHARSET_NAME_UTF8); addHeadersToResponse(servletResponse); if (allowPrefer) { addContentLocationHeaders(theRequest, servletResponse, response, resourceName); } Writer writer; if (outcome != null) { ResponseEncoding encoding = RestfulServerUtils.determineResponseEncodingWithDefault(theRequest); servletResponse.setContentType(encoding.getResourceContentType()); writer = servletResponse.getWriter(); IParser parser = encoding.getEncoding().newParser(getFhirContext()); parser.setPrettyPrint(RestfulServerUtils.prettyPrintResponse(this, theRequest)); outcome.execute(parser, writer); } else { servletResponse.setContentType(Constants.CT_TEXT_WITH_UTF8); writer = servletResponse.getWriter(); } return writer; } @Override protected void service(HttpServletRequest theReq, HttpServletResponse theResp) throws ServletException, IOException { theReq.setAttribute(REQUEST_START_TIME, new Date()); RequestTypeEnum method; try { method = RequestTypeEnum.valueOf(theReq.getMethod()); } catch (IllegalArgumentException e) { super.service(theReq, theResp); return; } switch (method) { case DELETE: doDelete(theReq, theResp); break; case GET: doGet(theReq, theResp); break; case OPTIONS: doOptions(theReq, theResp); break; case POST: doPost(theReq, theResp); break; case PUT: doPut(theReq, theResp); break; default: handleRequest(method, theReq, theResp); break; } } @Override protected void doDelete(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { handleRequest(RequestTypeEnum.DELETE, request, response); } @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { handleRequest(RequestTypeEnum.GET, request, response); } @Override protected void doOptions(HttpServletRequest theReq, HttpServletResponse theResp) throws ServletException, IOException { handleRequest(RequestTypeEnum.OPTIONS, theReq, theResp); } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { handleRequest(RequestTypeEnum.POST, request, response); } @Override protected void doPut(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { handleRequest(RequestTypeEnum.PUT, request, response); } /** * 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 * the class of the resource(s) being returned. * * @param theAddProfileTag * The behaviour enum (must not be null) * @deprecated As of HAPI FHIR 1.5, this property has been moved to * {@link FhirContext#setAddProfileTagWhenEncoding(AddProfileTagEnum)} */ @Deprecated @CoverageIgnore public void setAddProfileTag(AddProfileTagEnum theAddProfileTag) { Validate.notNull(theAddProfileTag, "theAddProfileTag must not be null"); myFhirContext.setAddProfileTagWhenEncoding(theAddProfileTag); } /** * Set how bundle factory should decide whether referenced resources should be included in bundles * * @param theBundleInclusionRule * - inclusion rule (@see BundleInclusionRule for behaviors) */ public void setBundleInclusionRule(BundleInclusionRule theBundleInclusionRule) { myBundleInclusionRule = theBundleInclusionRule; } /** * Should the server "pretty print" responses by default (requesting clients can always override this default by * supplying an Accept header in the request, or a _pretty * parameter in the request URL. *

* The default is false *

* * @param theDefaultPrettyPrint * The default pretty print setting */ public void setDefaultPrettyPrint(boolean theDefaultPrettyPrint) { myDefaultPrettyPrint = theDefaultPrettyPrint; } /** * Sets the default encoding to return (XML/JSON) if an incoming request does not specify a preference (either with * the _format URL parameter, or with an Accept header in * the request. The default is {@link EncodingEnum#XML}. *

* Note when testing this feature: Some browsers will include "application/xml" in their Accept header, which means * that the *

*/ public void setDefaultResponseEncoding(EncodingEnum theDefaultResponseEncoding) { Validate.notNull(theDefaultResponseEncoding, "theDefaultResponseEncoding can not be null"); myDefaultResponseEncoding = theDefaultResponseEncoding; } /** * Sets (enables/disables) the server support for ETags. Must not be null. Default is * {@link #DEFAULT_ETAG_SUPPORT} * * @param theETagSupport * The ETag support mode */ public void setETagSupport(ETagSupportEnum theETagSupport) { if (theETagSupport == null) { throw new NullPointerException("theETagSupport can not be null"); } myETagSupport = theETagSupport; } public void setFhirContext(FhirContext theFhirContext) { Validate.notNull(theFhirContext, "FhirContext must not be null"); myFhirContext = theFhirContext; } /** * If set to true (the default is true) this server will not * use the parsed request parameters (URL parameters and HTTP POST form contents) but * will instead parse these values manually from the request URL and request body. *

* This is useful because many servlet containers (e.g. Tomcat, Glassfish) will use * ISO-8859-1 encoding to parse escaped URL characters instead of using UTF-8 * as is specified by FHIR. *

*/ public void setIgnoreServerParsedRequestParameters(boolean theIgnoreServerParsedRequestParameters) { myIgnoreServerParsedRequestParameters = theIgnoreServerParsedRequestParameters; } public void setImplementationDescription(String theImplementationDescription) { myImplementationDescription = theImplementationDescription; } /** * Sets (or clears) the list of interceptors * * @param theList * The list of interceptors (may be null) */ public void setInterceptors(IServerInterceptor... theList) { myInterceptors.clear(); if (theList != null) { myInterceptors.addAll(Arrays.asList(theList)); } } /** * Sets (or clears) the list of interceptors * * @param theList * The list of interceptors (may be null) */ public void setInterceptors(List theList) { myInterceptors.clear(); if (theList != null) { myInterceptors.addAll(theList); } } /** * Sets the paging provider to use, or null to use no paging (which is the default) */ public void setPagingProvider(IPagingProvider thePagingProvider) { myPagingProvider = thePagingProvider; } /** * Sets the non-resource specific providers which implement method calls on this server. * * @see #setResourceProviders(Collection) */ public void setPlainProviders(Collection theProviders) { myPlainProviders.clear(); if (theProviders != null) { myPlainProviders.addAll(theProviders); } } /** * Sets the non-resource specific providers which implement method calls on this server. * * @see #setResourceProviders(Collection) */ public void setPlainProviders(Object... theProv) { setPlainProviders(Arrays.asList(theProv)); } /** * Sets the non-resource specific providers which implement method calls on this server * * @see #setResourceProviders(Collection) */ public void setProviders(Object... theProviders) { myPlainProviders.clear(); if (theProviders != null) { myPlainProviders.addAll(Arrays.asList(theProviders)); } } /** * Sets the resource providers for this server */ public void setResourceProviders(Collection theResourceProviders) { myResourceProviders.clear(); if (theResourceProviders != null) { myResourceProviders.addAll(theResourceProviders); } } /** * Sets the resource providers for this server */ public void setResourceProviders(IResourceProvider... theResourceProviders) { myResourceProviders.clear(); if (theResourceProviders != null) { myResourceProviders.addAll(Arrays.asList(theResourceProviders)); } } /** * 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} */ public void setServerAddressStrategy(IServerAddressStrategy theServerAddressStrategy) { Validate.notNull(theServerAddressStrategy, "Server address strategy can not be null"); myServerAddressStrategy = theServerAddressStrategy; } /** * Returns the server conformance provider, which is the provider that is used to generate the server's conformance * (metadata) statement. *

* By default, the ServerConformanceProvider implementation for the declared version of FHIR is used, but this can be * changed, or set to null if you do not wish to export a conformance * statement. *

* Note that this method can only be called before the server is initialized. * * @throws IllegalStateException * Note that this method can only be called prior to {@link #init() initialization} and will throw an * {@link IllegalStateException} if called after that. */ public void setServerConformanceProvider(Object theServerConformanceProvider) { if (myStarted) { throw new IllegalStateException("Server is already started"); } // call the setRestfulServer() method to point the Conformance // Provider to this server instance. This is done to avoid // passing the server into the constructor. Having that sort // of cross linkage causes reference cycles in Spring wiring try { Method setRestfulServer = theServerConformanceProvider.getClass().getMethod("setRestfulServer", new Class[] { RestfulServer.class }); if (setRestfulServer != null) { setRestfulServer.invoke(theServerConformanceProvider, new Object[] { this }); } } catch (Exception e) { ourLog.warn("Error calling IServerConformanceProvider.setRestfulServer", e); } myServerConformanceProvider = theServerConformanceProvider; } /** * 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. */ public void setServerName(String theServerName) { myServerName = theServerName; } /** * 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. */ public void setServerVersion(String theServerVersion) { myServerVersion = theServerVersion; } /** * Should the server attempt to decompress incoming request contents (default is true). Typically this * should be set to true unless the server has other configuration to * deal with decompressing request bodies (e.g. a filter applied to the whole server). */ public void setUncompressIncomingContents(boolean theUncompressIncomingContents) { myUncompressIncomingContents = theUncompressIncomingContents; } /** * @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 * highlighting. Deprocated in 1.4 */ @Deprecated public void setUseBrowserFriendlyContentTypes(boolean theUseBrowserFriendlyContentTypes) { myUseBrowserFriendlyContentTypes = theUseBrowserFriendlyContentTypes; } public void unregisterInterceptor(IServerInterceptor theInterceptor) { Validate.notNull(theInterceptor, "Interceptor can not be null"); myInterceptors.remove(theInterceptor); } /** * Unregister one provider (either a Resource provider or a plain provider) * * @param provider * @throws Exception */ public void unregisterProvider(Object provider) throws Exception { if (provider != null) { Collection providerList = new ArrayList(1); providerList.add(provider); unregisterProviders(providerList); } } /** * Unregister a {@code Collection} of providers * * @param providers * @throws Exception */ public void unregisterProviders(Collection providers) throws Exception { ProvidedResourceScanner providedResourceScanner = new ProvidedResourceScanner(getFhirContext()); if (providers != null) { for (Object provider : providers) { removeResourceMethods(provider); if (provider instanceof IResourceProvider) { myResourceProviders.remove(provider); IResourceProvider rsrcProvider = (IResourceProvider) provider; Class resourceType = rsrcProvider.getResourceType(); String resourceName = getFhirContext().getResourceDefinition(resourceType).getName(); myTypeToProvider.remove(resourceName); providedResourceScanner.removeProvidedResources(rsrcProvider); } else { myPlainProviders.remove(provider); } invokeDestroy(provider); } } } private void writeExceptionToResponse(HttpServletResponse theResponse, BaseServerResponseException theException) throws IOException { theResponse.setStatus(theException.getStatusCode()); addHeadersToResponse(theResponse); if (theException.hasResponseHeaders()) { for (Entry> nextEntry : theException.getResponseHeaders().entrySet()) { for (String nextValue : nextEntry.getValue()) { if (isNotBlank(nextValue)) { theResponse.addHeader(nextEntry.getKey(), nextValue); } } } } theResponse.setContentType("text/plain"); theResponse.setCharacterEncoding("UTF-8"); theResponse.getWriter().write(theException.getMessage()); } private static boolean partIsOperation(String nextString) { return nextString.length() > 0 && (nextString.charAt(0) == '_' || nextString.charAt(0) == '$' || nextString.equals(Constants.URL_TOKEN_METADATA)); } }