From 44260803748d099aeac5c62844d4a2c9f55f14ec Mon Sep 17 00:00:00 2001 From: James Agnew Date: Sat, 21 Mar 2015 09:12:41 +0100 Subject: [PATCH] Documentaiton on idempotence --- .../main/java/example/ServerOperations.java | 4 +- .../fhir/rest/server/RestfulServer.java.orig | 1070 +++++++++++++++++ .../ExceptionHandlingInterceptor.java.orig | 172 +++ .../interceptor/IServerInterceptor.java | 25 +- .../interceptor/InterceptorAdapter.java | 2 +- src/site/xdoc/doc_rest_operations.xml | 25 + 6 files changed, 1282 insertions(+), 16 deletions(-) create mode 100644 hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java.orig create mode 100644 hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/ExceptionHandlingInterceptor.java.orig diff --git a/examples/src/main/java/example/ServerOperations.java b/examples/src/main/java/example/ServerOperations.java index f70b16e01b8..9bfb9993d7d 100644 --- a/examples/src/main/java/example/ServerOperations.java +++ b/examples/src/main/java/example/ServerOperations.java @@ -16,7 +16,7 @@ import ca.uhn.fhir.rest.annotation.OperationParam; public class ServerOperations { //START SNIPPET: patientTypeOperation - @Operation(name="$everything") + @Operation(name="$everything", idempotent=true) public Bundle patientTypeOperation( @OperationParam(name="start") DateDt theStart, @OperationParam(name="end") DateDt theEnd) { @@ -28,7 +28,7 @@ public class ServerOperations { //END SNIPPET: patientTypeOperation //START SNIPPET: patientInstanceOperation - @Operation(name="$everything") + @Operation(name="$everything", idempotent=true) public Bundle patientInstanceOperation( @IdParam IdDt thePatientId, @OperationParam(name="start") DateDt theStart, diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java.orig b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java.orig new file mode 100644 index 00000000000..821eb2cc3ab --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java.orig @@ -0,0 +1,1070 @@ +package ca.uhn.fhir.rest.server; + +/* + * #%L + * HAPI FHIR - Core Library + * %% + * Copyright (C) 2014 - 2015 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.isNotBlank; + +import java.io.IOException; +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.*; + +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.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.hl7.fhir.instance.model.IBaseResource; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.ProvidedResourceScanner; +import ca.uhn.fhir.context.RuntimeResourceDefinition; +import ca.uhn.fhir.model.api.Bundle; +import ca.uhn.fhir.model.api.IResource; +import ca.uhn.fhir.model.primitive.IdDt; +import ca.uhn.fhir.rest.annotation.Destroy; +import ca.uhn.fhir.rest.annotation.IdParam; +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.OtherOperationTypeEnum; +import ca.uhn.fhir.rest.method.Request; +<<<<<<< HEAD +import ca.uhn.fhir.rest.method.RequestDetails; +======= +import ca.uhn.fhir.rest.method.SearchMethodBinding; +import ca.uhn.fhir.rest.method.SearchMethodBinding.RequestType; +>>>>>>> 5956ab75fd9186ccc8b9836b60bc421b19d3d288 +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.util.ReflectionUtil; +import ca.uhn.fhir.util.UrlUtil; +import ca.uhn.fhir.util.VersionUtil; + +public class RestfulServer extends HttpServlet { + + /** + * Default setting for {@link #setETagSupport(ETagSupportEnum) ETag Support}: {@link ETagSupportEnum#ENABLED} + */ + public static final ETagSupportEnum DEFAULT_ETAG_SUPPORT = ETagSupportEnum.ENABLED; + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(RestfulServer.class); + + private static final long serialVersionUID = 1L; + private AddProfileTagEnum myAddProfileTag; + 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 String myImplementationDescription; + private final List myInterceptors = new ArrayList(); + private IPagingProvider myPagingProvider; + private Collection myPlainProviders; + private Map myResourceNameToProvider = new HashMap(); + private Collection myResourceProviders; + 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 boolean myUseBrowserFriendlyContentTypes; + + /** + * Constructor + */ + public RestfulServer() { + this(new FhirContext()); + } + + public RestfulServer(FhirContext theCtx) { + myFhirContext = theCtx; + } + + /** + * 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) { + theHttpResponse.addHeader("X-Powered-By", "HAPI FHIR " + VersionUtil.getVersion() + " RESTful Server"); + } + + 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"); + } + } + + @Override + public void destroy() { + if (getResourceProviders() != null) { + for (IResourceProvider iResourceProvider : getResourceProviders()) { + invokeDestroy(iResourceProvider); + } + } + } + + @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); + } + + /** + * 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) throws Exception { + + 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, myFhirContext, theProvider); + if (foundMethodBinding == null) { + continue; + } + + count++; + + 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 = myFhirContext.getResourceDefinition(resourceName); + if (myResourceNameToProvider.containsKey(definition.getName())) { + resourceBinding = myResourceNameToProvider.get(definition.getName()); + } else { + resourceBinding = new ResourceBinding(); + resourceBinding.setResourceName(resourceName); + myResourceNameToProvider.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; + } + + private void findSystemMethods(Object theSystemProvider) { + Class clazz = theSystemProvider.getClass(); + + findSystemMethods(theSystemProvider, clazz); + + } + + private void findSystemMethods(Object theSystemProvider, Class clazz) { + Class supertype = clazz.getSuperclass(); + if (!Object.class.equals(supertype)) { + findSystemMethods(theSystemProvider, supertype); + } + + for (Method m : ReflectionUtil.getDeclaredMethods(clazz)) { + if (Modifier.isPublic(m.getModifiers())) { + ourLog.debug("Scanning public method: {}#{}", theSystemProvider.getClass(), m.getName()); + + BaseMethodBinding foundMethodBinding = BaseMethodBinding.bindMethod(m, myFhirContext, theSystemProvider); + if (foundMethodBinding != null) { + if (foundMethodBinding instanceof ConformanceMethodBinding) { + myServerConformanceMethod = foundMethodBinding; + } else { + myServerBinding.addMethod(foundMethodBinding); + } + ourLog.info(" * Method: {}#{} is a handler", theSystemProvider.getClass(), m.getName()); + } else { + ourLog.debug(" * Method: {}#{} is not a handler", theSystemProvider.getClass(), m.getName()); + } + } + } + } + + /** + * Returns the setting for automatically adding profile tags + * + * @see #setAddProfileTag(AddProfileTagEnum) + */ + public AddProfileTagEnum getAddProfileTag() { + return myAddProfileTag; + } + + 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}. + */ + public EncodingEnum getDefaultResponseEncoding() { + return myDefaultResponseEncoding; + } + + /** + * Returns the server support for ETags (will not be null). Default is {@link #DEFAULT_ETAG_SUPPORT} + */ + 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. + */ + public FhirContext getFhirContext() { + return myFhirContext; + } + + public String getImplementationDescription() { + return myImplementationDescription; + } + + /** + * Returns a ist of all registered server interceptors + */ + public List getInterceptors() { + return Collections.unmodifiableList(myInterceptors); + } + + 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 myResourceNameToProvider.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; + } + + 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 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 myFhirContext.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; + } + + private void handlePagingRequest(Request theRequest, HttpServletResponse theResponse, String thePagingAction) throws IOException { + IBundleProvider resultList = getPagingProvider().retrieveResultList(thePagingAction); + if (resultList == null) { + ourLog.info("Client requested unknown paging ID[{}]", thePagingAction); + theResponse.setStatus(Constants.STATUS_HTTP_410_GONE); + addHeadersToResponse(theResponse); + theResponse.setContentType("text/plain"); + theResponse.setCharacterEncoding("UTF-8"); + theResponse.getWriter().append("Search ID[" + thePagingAction + "] does not exist and may have expired."); + theResponse.getWriter().close(); + return; + } + + Integer count = RestfulServerUtils.extractCountParameter(theRequest.getServletRequest()); + if (count == null) { + count = getPagingProvider().getDefaultPageSize(); + } else if (count > getPagingProvider().getMaximumPageSize()) { + count = getPagingProvider().getMaximumPageSize(); + } + + Integer offsetI = RestfulServerUtils.tryToExtractNamedParameter(theRequest.getServletRequest(), Constants.PARAM_PAGINGOFFSET); + if (offsetI == null || offsetI < 0) { + offsetI = 0; + } + + int start = Math.min(offsetI, resultList.size() - 1); + + EncodingEnum responseEncoding = RestfulServerUtils.determineResponseEncodingNoDefault(theRequest.getServletRequest()); + boolean prettyPrint = RestfulServerUtils.prettyPrintResponse(this, theRequest); + boolean requestIsBrowser = requestIsBrowser(theRequest.getServletRequest()); + NarrativeModeEnum narrativeMode = RestfulServerUtils.determineNarrativeMode(theRequest); + boolean respondGzip = theRequest.isRespondGzip(); + + IVersionSpecificBundleFactory bundleFactory = myFhirContext.newBundleFactory(); + bundleFactory.initializeBundleFromBundleProvider(this, resultList, responseEncoding, theRequest.getFhirServerBase(), theRequest.getCompleteUrl(), prettyPrint, start, count, thePagingAction, + null, IResource.WILDCARD_ALL_SET); + + Bundle bundle = bundleFactory.getDstu1Bundle(); + if (bundle != null) { + for (int i = getInterceptors().size() - 1; i >= 0; i--) { + IServerInterceptor next = getInterceptors().get(i); + boolean continueProcessing = next.outgoingResponse(theRequest, bundle, theRequest.getServletRequest(), theRequest.getServletResponse()); + if (!continueProcessing) { + ourLog.debug("Interceptor {} returned false, not continuing processing"); + return; + } + } + RestfulServerUtils.streamResponseAsBundle(this, theResponse, bundle, responseEncoding, theRequest.getFhirServerBase(), prettyPrint, narrativeMode, respondGzip, requestIsBrowser); + } else { + IBaseResource resBundle = bundleFactory.getResourceBundle(); + for (int i = getInterceptors().size() - 1; i >= 0; i--) { + IServerInterceptor next = getInterceptors().get(i); + boolean continueProcessing = next.outgoingResponse(theRequest, resBundle, theRequest.getServletRequest(), theRequest.getServletResponse()); + if (!continueProcessing) { + ourLog.debug("Interceptor {} returned false, not continuing processing"); + return; + } + } + RestfulServerUtils.streamResponseAsResource(this, theResponse, (IResource) resBundle, responseEncoding, prettyPrint, requestIsBrowser, narrativeMode, Constants.STATUS_HTTP_200_OK, theRequest.isRespondGzip(), theRequest.getFhirServerBase()); + } + } + + protected void handleRequest(RequestTypeEnum theRequestType, HttpServletRequest theRequest, HttpServletResponse theResponse) throws ServletException, IOException { + for (IServerInterceptor next : myInterceptors) { + boolean continueProcessing = next.incomingRequestPreProcessed(theRequest, theResponse); + if (!continueProcessing) { + ourLog.debug("Interceptor {} returned false, not continuing processing"); + return; + } + } + + String fhirServerBase = null; + boolean requestIsBrowser = requestIsBrowser(theRequest); + Request requestDetails = new Request(); + requestDetails.setServer(this); + + try { + + String resourceName = null; + String requestFullPath = StringUtils.defaultString(theRequest.getRequestURI()); + String servletPath = StringUtils.defaultString(theRequest.getServletPath()); + StringBuffer requestUrl = theRequest.getRequestURL(); + String servletContextPath = ""; + + // if (getServletContext().getMajorVersion() >= 3) { + // // getServletContext is only supported in version 3+ of servlet-api + if (getServletContext() != null) { + servletContextPath = StringUtils.defaultString(getServletContext().getContextPath()); + } + // } + + if (ourLog.isTraceEnabled()) { + ourLog.trace("Request FullPath: {}", requestFullPath); + ourLog.trace("Servlet Path: {}", servletPath); + ourLog.trace("Request Url: {}", requestUrl); + ourLog.trace("Context Path: {}", servletContextPath); + } + + IdDt id = null; + String operation = null; + String compartment = null; + + String requestPath = getRequestPath(requestFullPath, servletContextPath, servletPath); + if (requestPath.length() > 0 && requestPath.charAt(0) == '/') { + requestPath = requestPath.substring(1); + } + + fhirServerBase = getServerBaseForRequest(theRequest); + + String completeUrl = StringUtils.isNotBlank(theRequest.getQueryString()) ? requestUrl + "?" + theRequest.getQueryString() : requestUrl.toString(); + + Map params = new HashMap(theRequest.getParameterMap()); + requestDetails.setParameters(params); + + StringTokenizer tok = new StringTokenizer(requestPath, "/"); + if (tok.hasMoreTokens()) { + resourceName = tok.nextToken(); + if (partIsOperation(resourceName)) { + operation = resourceName; + resourceName = null; + } + } + requestDetails.setResourceName(resourceName); + + ResourceBinding resourceBinding = null; + BaseMethodBinding resourceMethod = null; + if (Constants.URL_TOKEN_METADATA.equals(resourceName) || theRequestType == RequestTypeEnum.OPTIONS) { + resourceMethod = myServerConformanceMethod; + } else if (resourceName == null) { + resourceBinding = myServerBinding; + } else { + resourceBinding = myResourceNameToProvider.get(resourceName); + if (resourceBinding == null) { + throw new InvalidRequestException("Unknown resource type '" + resourceName + "' - Server knows how to handle: " + myResourceNameToProvider.keySet()); + } + } + + if (tok.hasMoreTokens()) { + String nextString = tok.nextToken(); + if (partIsOperation(nextString)) { + operation = nextString; + } else { + id = new IdDt(resourceName, UrlUtil.unescape(nextString)); + } + } + + 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: " + requestPath); + } + id = new IdDt(resourceName, id.getIdPart(), UrlUtil.unescape(versionString)); + } else { + operation = Constants.PARAM_HISTORY; + } + } else if (partIsOperation(nextString)) { + // FIXME: this would be untrue for _meta/_delete + if (operation != null) { + throw new InvalidRequestException("URL Path contains two operations: " + requestPath); + } + 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: " + requestPath); + } + } + + if (theRequestType == RequestTypeEnum.PUT) { + String contentLocation = theRequest.getHeader(Constants.HEADER_CONTENT_LOCATION); + if (contentLocation != null) { + id = new IdDt(contentLocation); + } + } + requestDetails.setId(id); + requestDetails.setOperation(operation); + requestDetails.setSecondaryOperation(secondaryOperation); + requestDetails.setCompartmentName(compartment); + + // TODO: look for more tokens for version, compartments, etc... + + 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.setRequestType(theRequestType); + requestDetails.setFhirServerBase(fhirServerBase); + requestDetails.setCompleteUrl(completeUrl); + requestDetails.setServletRequest(theRequest); + requestDetails.setServletResponse(theResponse); + + String pagingAction = theRequest.getParameter(Constants.PARAM_PAGINGACTION); + if (getPagingProvider() != null && isNotBlank(pagingAction)) { + requestDetails.setOtherOperationType(OtherOperationTypeEnum.GET_PAGE); + handlePagingRequest(requestDetails, theResponse, pagingAction); + return; + } + + if (resourceMethod == null) { + if (resourceBinding != null) { + resourceMethod = resourceBinding.getMethod(requestDetails); + } + } + if (resourceMethod == null) { + StringBuilder b = new StringBuilder(); + b.append("No resource method available for "); + b.append(theRequestType.name()); + b.append(" operation["); + b.append(requestPath); + b.append("]"); + b.append(" with parameters "); + b.append(params.keySet()); + throw new InvalidRequestException(b.toString()); + } + requestDetails.setResourceOperationType(resourceMethod.getResourceOperationType()); + requestDetails.setSystemOperationType(resourceMethod.getSystemOperationType()); + requestDetails.setOtherOperationType(resourceMethod.getOtherOperationType()); + + for (IServerInterceptor next : myInterceptors) { + boolean continueProcessing = next.incomingRequestPostProcessed(requestDetails, theRequest, theResponse); + if (!continueProcessing) { + ourLog.debug("Interceptor {} returned false, not continuing processing"); + return; + } + } + + resourceMethod.invokeServer(this, requestDetails); + + } catch (NotModifiedException e) { + + for (int i = getInterceptors().size() - 1; i >= 0; i--) { + IServerInterceptor next = getInterceptors().get(i); + if (!next.handleException(this, 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(this, requestDetails, e, theRequest, theResponse)) { + ourLog.debug("Interceptor {} returned false, not continuing processing"); + return; + } + } + + if (requestIsBrowser) { + // if request is coming from a browser, prompt the user to enter login credentials + theResponse.setHeader("WWW-Authenticate", "BASIC realm=\"FHIR\""); + } + writeExceptionToResponse(theResponse, e); + + } catch (Throwable e) { + + /* + * We have caught an exception while handling an incoming server request. Start by notifying the interceptors.. + */ + for (int i = getInterceptors().size() - 1; i >= 0; i--) { + IServerInterceptor next = getInterceptors().get(i); + if (!next.handleException(this, requestDetails, e, theRequest, theResponse)) { + ourLog.debug("Interceptor {} returned false, not continuing processing"); + return; + } + } + + new ExceptionHandlingInterceptor().handleException(this, requestDetails, e, 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 { + initialize(); + try { + ourLog.info("Initializing HAPI FHIR restful server"); + + ProvidedResourceScanner providedResourceScanner = new ProvidedResourceScanner(getFhirContext()); + providedResourceScanner.scanForProvidedResources(this); + + Collection resourceProvider = getResourceProviders(); + if (resourceProvider != null) { + Map typeToProvider = new HashMap(); + for (IResourceProvider nextProvider : resourceProvider) { + + Class resourceType = nextProvider.getResourceType(); + if (resourceType == null) { + throw new NullPointerException("getResourceType() on class '" + nextProvider.getClass().getCanonicalName() + "' returned null"); + } + + String resourceName = myFhirContext.getResourceDefinition(resourceType).getName(); + if (typeToProvider.containsKey(resourceName)) { + throw new ServletException("Multiple resource providers return resource type[" + resourceName + "]: First[" + typeToProvider.get(resourceName).getClass().getCanonicalName() + "] and Second[" + nextProvider.getClass().getCanonicalName() + "]"); + } + typeToProvider.put(resourceName, nextProvider); + providedResourceScanner.scanForProvidedResources(nextProvider); + } + ourLog.info("Got {} resource providers", typeToProvider.size()); + for (IResourceProvider provider : typeToProvider.values()) { + assertProviderIsValid(provider); + findResourceMethods(provider); + } + } + + Collection providers = getPlainProviders(); + if (providers != null) { + for (Object next : providers) { + assertProviderIsValid(next); + findResourceMethods(next); + } + } + + findResourceMethods(getServerProfilesProvider()); + + Object confProvider = getServerConformanceProvider(); + if (confProvider == null) { + confProvider = myFhirContext.getVersion().createServerConformanceProvider(this); + } + findSystemMethods(confProvider); + + } 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"); + } + + /** + * 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) { + Class clazz = theProvider.getClass(); + invokeDestroy(theProvider, clazz); + } + + 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); + } + } + + /** + * 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 + */ + public boolean isDefaultPrettyPrint() { + return myDefaultPrettyPrint; + } + + public boolean isUseBrowserFriendlyContentTypes() { + return myUseBrowserFriendlyContentTypes; + } + + public void registerInterceptor(IServerInterceptor theInterceptor) { + Validate.notNull(theInterceptor, "Interceptor can not be null"); + myInterceptors.add(theInterceptor); + } + + public static boolean requestIsBrowser(HttpServletRequest theRequest) { + String userAgent = theRequest.getHeader("User-Agent"); + return userAgent != null && userAgent.contains("Mozilla"); + } + + /** + * 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) + */ + public void setAddProfileTag(AddProfileTagEnum theAddProfileTag) { + Validate.notNull(theAddProfileTag, "theAddProfileTag must not be null"); + myAddProfileTag = 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}. + */ + 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; + } + + 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 = 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 = Arrays.asList(theProviders); + } + + /** + * Sets the resource providers for this server + */ + public void setResourceProviders(Collection theResourceProviders) { + myResourceProviders = theResourceProviders; + } + + /** + * Sets the resource providers for this server + */ + public void setResourceProviders(IResourceProvider... theResourceProviders) { + myResourceProviders = 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"); + } + 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; + } + + /** + * If set to true (default is false), the server will use browser friendly content-types (instead of + * standard FHIR ones) when it detects that the request is coming from a browser instead of a FHIR + */ + public void setUseBrowserFriendlyContentTypes(boolean theUseBrowserFriendlyContentTypes) { + myUseBrowserFriendlyContentTypes = theUseBrowserFriendlyContentTypes; + } + + public void unregisterInterceptor(IServerInterceptor theInterceptor) { + Validate.notNull(theInterceptor, "Interceptor can not be null"); + myInterceptors.remove(theInterceptor); + } + + private void writeExceptionToResponse(HttpServletResponse theResponse, BaseServerResponseException theException) throws IOException { + theResponse.setStatus(theException.getStatusCode()); + addHeadersToResponse(theResponse); + 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) == '$'); + } + + public enum NarrativeModeEnum { + NORMAL, ONLY, SUPPRESS; + + public static NarrativeModeEnum valueOfCaseInsensitive(String theCode) { + return valueOf(NarrativeModeEnum.class, theCode.toUpperCase()); + } + } +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/ExceptionHandlingInterceptor.java.orig b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/ExceptionHandlingInterceptor.java.orig new file mode 100644 index 00000000000..9e81a04ae44 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/ExceptionHandlingInterceptor.java.orig @@ -0,0 +1,172 @@ +package ca.uhn.fhir.rest.server.interceptor; + +/* + * #%L + * HAPI FHIR - Core Library + * %% + * Copyright (C) 2014 - 2015 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.isNotBlank; + +import java.io.IOException; +import java.util.Map; +import java.util.Map.Entry; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.lang3.exception.ExceptionUtils; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.model.base.resource.BaseOperationOutcome; +import ca.uhn.fhir.model.base.resource.BaseOperationOutcome.BaseIssue; +import ca.uhn.fhir.rest.method.Request; +import ca.uhn.fhir.rest.method.RequestDetails; +import ca.uhn.fhir.rest.server.Constants; +import ca.uhn.fhir.rest.server.RestfulServer; +import ca.uhn.fhir.rest.server.RestfulServer.NarrativeModeEnum; +import ca.uhn.fhir.rest.server.RestfulServerUtils; +import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; + +public class ExceptionHandlingInterceptor extends InterceptorAdapter { + + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ExceptionHandlingInterceptor.class); + private Class[] myReturnStackTracesForExceptionTypes; + + /** + * If any server methods throw an exception which extends any of the given exception types, the exception + * stack trace will be returned to the user. This can be useful for helping to diagnose issues, but may + * not be desirable for production situations. + * + * @param theExceptionTypes The exception types for which to return the stack trace to the user. + * @return Returns an instance of this interceptor, to allow for easy method chaining. + */ + public ExceptionHandlingInterceptor setReturnStackTracesForExceptionTypes(Class... theExceptionTypes) { + myReturnStackTracesForExceptionTypes = theExceptionTypes; + return this; + } + + @Override +<<<<<<< HEAD + public boolean handleException(RequestDetails theRequestDetails, Throwable theException, HttpServletRequest theRequest, HttpServletResponse theResponse) throws ServletException, IOException { +======= + public boolean handleException(RestfulServer theRestfulServer, RequestDetails theRequestDetails, Throwable theException, HttpServletRequest theRequest, HttpServletResponse theResponse) throws ServletException, IOException { + ourLog.error("AA", theException); +>>>>>>> 5956ab75fd9186ccc8b9836b60bc421b19d3d288 + BaseOperationOutcome oo = null; + int statusCode = Constants.STATUS_HTTP_500_INTERNAL_ERROR; + + FhirContext ctx = theRestfulServer.getFhirContext(); + + if (theException instanceof BaseServerResponseException) { + oo = ((BaseServerResponseException) theException).getOperationOutcome(); + statusCode = ((BaseServerResponseException) theException).getStatusCode(); + } + + /* + * Generate an OperationOutcome to return, unless the exception throw by the resource provider had one + */ + if (oo == null) { + try { + oo = (BaseOperationOutcome) ctx.getResourceDefinition("OperationOutcome").getImplementingClass().newInstance(); + } catch (Exception e1) { + ourLog.error("Failed to instantiate OperationOutcome resource instance", e1); + throw new ServletException("Failed to instantiate OperationOutcome resource instance", e1); + } + + BaseIssue issue = oo.addIssue(); + issue.getSeverityElement().setValue("error"); + if (theException instanceof InternalErrorException) { + ourLog.error("Failure during REST processing", theException); + populateDetails(theException, issue); + } else if (theException instanceof BaseServerResponseException) { + ourLog.warn("Failure during REST processing: {}", theException); + BaseServerResponseException baseServerResponseException = (BaseServerResponseException) theException; + statusCode = baseServerResponseException.getStatusCode(); + populateDetails(theException, issue); + if (baseServerResponseException.getAdditionalMessages() != null) { + for (String next : baseServerResponseException.getAdditionalMessages()) { + BaseIssue issue2 = oo.addIssue(); + issue2.getSeverityElement().setValue("error"); + issue2.setDetails(next); + } + } + } else { + ourLog.error("Failure during REST processing: " + theException.toString(), theException); + populateDetails(theException, issue); + statusCode = Constants.STATUS_HTTP_500_INTERNAL_ERROR; + } + } else { + ourLog.error("Unknown error during processing", theException); + } + + // Add headers associated with the specific error code + if (theException instanceof BaseServerResponseException) { + Map additional = ((BaseServerResponseException) theException).getAssociatedHeaders(); + if (additional != null) { + for (Entry next : additional.entrySet()) { + if (isNotBlank(next.getKey()) && next.getValue() != null) { + String nextKey = next.getKey(); + for (String nextValue : next.getValue()) { + theResponse.addHeader(nextKey, nextValue); + } + } + } + } + } + + boolean requestIsBrowser = RestfulServer.requestIsBrowser(theRequest); + String fhirServerBase = theRestfulServer.getServerBaseForRequest(theRequest); + + RestfulServerUtils.streamResponseAsResource(theRestfulServer, theResponse, oo, RestfulServerUtils.determineResponseEncodingNoDefault(theRequest), true, requestIsBrowser, + NarrativeModeEnum.NORMAL, statusCode, false, fhirServerBase); + +<<<<<<< HEAD +// theResponse.setStatus(statusCode); +// theRequestDetails.getServer().addHeadersToResponse(theResponse); +// theResponse.setContentType("text/plain"); +// theResponse.setCharacterEncoding("UTF-8"); +// theResponse.getWriter().append(theException.getMessage()); +// theResponse.getWriter().close(); +======= + theResponse.setStatus(statusCode); + theRestfulServer.addHeadersToResponse(theResponse); + theResponse.setContentType("text/plain"); + theResponse.setCharacterEncoding("UTF-8"); + theResponse.getWriter().append(theException.getMessage()); + theResponse.getWriter().close(); +>>>>>>> 5956ab75fd9186ccc8b9836b60bc421b19d3d288 + + return false; + } + + private void populateDetails(Throwable theException, BaseIssue issue) { + if (myReturnStackTracesForExceptionTypes != null) { + for (Class next : myReturnStackTracesForExceptionTypes) { + if (next.isAssignableFrom(theException.getClass())) { + issue.getDetailsElement().setValue(theException.getMessage() + "\n\n" + ExceptionUtils.getStackTrace(theException)); + return; + } + } + } + + issue.getDetailsElement().setValue(theException.getMessage()); + } + +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/IServerInterceptor.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/IServerInterceptor.java index d822f6b84bc..7ba1c7377c2 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/IServerInterceptor.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/IServerInterceptor.java @@ -26,7 +26,6 @@ import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import ca.uhn.fhir.rest.server.RestfulServer; import org.hl7.fhir.instance.model.IBaseResource; import ca.uhn.fhir.model.api.Bundle; @@ -58,7 +57,7 @@ public interface IServerInterceptor { * The incoming request * @param theResponse * The response. Note that interceptors may choose to provide a response (i.e. by calling {@link HttpServletResponse#getWriter()}) but in that case it is important to return - * false + * false to indicate that the server itself should not also provide a response. * @return Return true if processing should continue normally. This is generally the right thing to do. If your interceptor is providing a response rather than letting HAPI handle the * response normally, you must return false. In this case, no further processing will occur and no further interceptors will be called. */ @@ -77,7 +76,7 @@ public interface IServerInterceptor { * The incoming request * @param theResponse * The response. Note that interceptors may choose to provide a response (i.e. by calling {@link HttpServletResponse#getWriter()}) but in that case it is important to return - * false + * false to indicate that the server itself should not also provide a response. * @return Return true if processing should continue normally. This is generally the right thing to do. If your interceptor is providing a response rather than letting HAPI handle the * response normally, you must return false. In this case, no further processing will occur and no further interceptors will be called. * @throws AuthenticationException @@ -98,7 +97,7 @@ public interface IServerInterceptor { * The incoming request * @param theServletResponse * The response. Note that interceptors may choose to provide a response (i.e. by calling {@link HttpServletResponse#getWriter()}) but in that case it is important to return - * false + * false to indicate that the server itself should not also provide a response. * @return Return true if processing should continue normally. This is generally the right thing to do. If your interceptor is providing a response rather than letting HAPI handle the * response normally, you must return false. In this case, no further processing will occur and no further interceptors will be called. * @throws AuthenticationException @@ -119,7 +118,7 @@ public interface IServerInterceptor { * The incoming request * @param theServletResponse * The response. Note that interceptors may choose to provide a response (i.e. by calling {@link HttpServletResponse#getWriter()}) but in that case it is important to return - * false + * false to indicate that the server itself should not also provide a response. * @return Return true if processing should continue normally. This is generally the right thing to do. If your interceptor is providing a response rather than letting HAPI handle the * response normally, you must return false. In this case, no further processing will occur and no further interceptors will be called. * @throws AuthenticationException @@ -141,7 +140,7 @@ public interface IServerInterceptor { * The incoming request * @param theServletResponse * The response. Note that interceptors may choose to provide a response (i.e. by calling {@link HttpServletResponse#getWriter()}) but in that case it is important to return - * false + * false to indicate that the server itself should not also provide a response. * @return Return true if processing should continue normally. This is generally the right thing to do. If your interceptor is providing a response rather than letting HAPI handle the * response normally, you must return false. In this case, no further processing will occur and no further interceptors will be called. * @throws AuthenticationException @@ -161,7 +160,7 @@ public interface IServerInterceptor { * The incoming request * @param theServletResponse * The response. Note that interceptors may choose to provide a response (i.e. by calling {@link HttpServletResponse#getWriter()}) but in that case it is important to return - * false + * false to indicate that the server itself should not also provide a response. * @return Return true if processing should continue normally. This is generally the right thing to do. If your interceptor is providing a response rather than letting HAPI handle the * response normally, you must return false. In this case, no further processing will occur and no further interceptors will be called. * @throws AuthenticationException @@ -180,16 +179,16 @@ public interface IServerInterceptor { *

* * - * @param server * @param theRequestDetails - * Contains either null, or a bean containing details about the request that is about to be processed, including details such as the resource type and logical ID (if any) and other - * FHIR-specific aspects of the request which have been pulled out of the {@link javax.servlet.http.HttpServletRequest servlet request}. This parameter may be - * null if the request processing did not successfully parse the incoming request, but will generally not be null. + * A bean containing details about the request that is about to be processed, including details such as the resource type and logical ID (if any) and other + * FHIR-specific aspects of the request which have been pulled out of the {@link javax.servlet.http.HttpServletRequest servlet request}. Note that the + * bean properties are not all guaranteed to be populated, depending on how early + * during processing the exception occurred. * @param theServletRequest * The incoming request * @param theServletResponse * The response. Note that interceptors may choose to provide a response (i.e. by calling {@link javax.servlet.http.HttpServletResponse#getWriter()}) but in that case it is important to return - * false + * false to indicate that the server itself should not also provide a response. * @return Return true if processing should continue normally. This is generally the right thing to do. If your interceptor is providing a response rather than letting HAPI handle the * response normally, you must return false. In this case, no further processing will occur and no further interceptors will be called. * @throws ServletException @@ -197,7 +196,7 @@ public interface IServerInterceptor { * @throws IOException * If this exception is thrown, it will be re-thrown up to the container for handling. */ - public boolean handleException(RestfulServer server, RequestDetails theRequestDetails, Throwable theException, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse) throws ServletException, + public boolean handleException(RequestDetails theRequestDetails, Throwable theException, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse) throws ServletException, IOException; } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/InterceptorAdapter.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/InterceptorAdapter.java index 920a245b4d7..40a62fa9425 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/InterceptorAdapter.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/InterceptorAdapter.java @@ -71,7 +71,7 @@ public class InterceptorAdapter implements IServerInterceptor { } @Override - public boolean handleException(RestfulServer server, RequestDetails theRequestDetails, Throwable theException, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse) throws ServletException, + public boolean handleException(RequestDetails theRequestDetails, Throwable theException, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse) throws ServletException, IOException { return true; } diff --git a/src/site/xdoc/doc_rest_operations.xml b/src/site/xdoc/doc_rest_operations.xml index f1903041439..50c1be96c93 100644 --- a/src/site/xdoc/doc_rest_operations.xml +++ b/src/site/xdoc/doc_rest_operations.xml @@ -1574,6 +1574,31 @@ + + +

+ The FHIR specification notes that if an operation is + idempotent + (which means roughly that it does not modity any data or state + on the server) then it may be invoked with an HTTP GET + instead of an HTTP POST. +

+

+ If you are implementing an operation which is idempotent, + you should mark your operation with + idempotent=true, + as shown in some of the examples above. The default value + for this flag is false, meaning that operations + will not support HTTP GET by default. +

+

+ Note that the HTTP GET form is only supported if the operation + has only primitive parameters (no complex parameters or resource parameters). + If a client makes a request containing a complex parameter, the + server will respond with an HTTP 405 Method Not Supported. +

+
+