Implement handleException on the server interceptor framework, as well
as some site and documentation enhancements
This commit is contained in:
parent
8f704030ed
commit
d22a35788f
|
@ -0,0 +1,29 @@
|
|||
package example;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import ca.uhn.fhir.rest.server.interceptor.InterceptorAdapter;
|
||||
|
||||
//START SNIPPET: interceptor
|
||||
public class RequestCounterInterceptor extends InterceptorAdapter
|
||||
{
|
||||
|
||||
private int myRequestCount;
|
||||
|
||||
public int getRequestCount() {
|
||||
return myRequestCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override the incomingRequestPreProcessed method, which is called
|
||||
* for each incoming request before any processing is done
|
||||
*/
|
||||
@Override
|
||||
public boolean incomingRequestPreProcessed(HttpServletRequest theRequest, HttpServletResponse theResponse) {
|
||||
myRequestCount++;
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
//END SNIPPET: interceptor
|
|
@ -0,0 +1,42 @@
|
|||
package example;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import ca.uhn.fhir.rest.method.RequestDetails;
|
||||
import ca.uhn.fhir.rest.server.Constants;
|
||||
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
|
||||
import ca.uhn.fhir.rest.server.interceptor.InterceptorAdapter;
|
||||
|
||||
//START SNIPPET: interceptor
|
||||
public class RequestExceptionInterceptor extends InterceptorAdapter
|
||||
{
|
||||
|
||||
@Override
|
||||
public boolean handleException(RequestDetails theRequestDetails, Throwable theException, HttpServletRequest theServletRequest,
|
||||
HttpServletResponse theServletResponse) throws ServletException, IOException {
|
||||
|
||||
// If the exception is a built-in type, it defines the correct status
|
||||
// code to return. Otherwise default to 500.
|
||||
if (theException instanceof BaseServerResponseException) {
|
||||
theServletResponse.setStatus(((BaseServerResponseException) theException).getStatusCode());
|
||||
} else {
|
||||
theServletResponse.setStatus(Constants.STATUS_HTTP_500_INTERNAL_ERROR);
|
||||
}
|
||||
|
||||
// Provide a response ourself
|
||||
theServletResponse.setContentType("text/plain");
|
||||
theServletResponse.getWriter().append("Failed to process!");
|
||||
theServletResponse.getWriter().close();
|
||||
|
||||
// Since we handled this response in the interceptor, we must return false
|
||||
// to stop processing immediately
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
//END SNIPPET: interceptor
|
|
@ -3,6 +3,9 @@ package example;
|
|||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.model.api.ExtensionDt;
|
||||
import ca.uhn.fhir.model.dstu.composite.HumanNameDt;
|
||||
|
@ -11,12 +14,13 @@ import ca.uhn.fhir.model.dstu.valueset.IdentifierUseEnum;
|
|||
import ca.uhn.fhir.model.primitive.DateTimeDt;
|
||||
import ca.uhn.fhir.model.primitive.StringDt;
|
||||
import ca.uhn.fhir.parser.DataFormatException;
|
||||
import ca.uhn.fhir.rest.server.interceptor.InterceptorAdapter;
|
||||
|
||||
public class ServerInterceptors {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public static void main(String[] args) throws DataFormatException, IOException {
|
||||
|
||||
|
||||
|
||||
// START SNIPPET: resourceExtension
|
||||
// Create an example patient
|
||||
|
|
|
@ -19,9 +19,11 @@
|
|||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-project-info-reports-plugin</artifactId>
|
||||
<version>2.7</version>
|
||||
<inherited>false</inherited>
|
||||
<reportSets>
|
||||
<reportSet>
|
||||
<reports>
|
||||
<report>scm</report>
|
||||
</reports>
|
||||
</reportSet>
|
||||
</reportSets>
|
||||
|
@ -39,6 +41,7 @@
|
|||
<configuration>
|
||||
<links>
|
||||
<link>http://docs.oracle.com/javaee/7/api</link>
|
||||
<link>http://jamesagnew.github.io/hapi-fhir/apidocs</link>
|
||||
</links>
|
||||
</configuration>
|
||||
</reportSet>
|
||||
|
|
|
@ -15,13 +15,6 @@
|
|||
|
||||
<name>HAPI FHIR - Core Library</name>
|
||||
|
||||
<distributionManagement>
|
||||
<site>
|
||||
<id>git.server</id>
|
||||
<url>scm:git:git@github.com:jamesagnew/hapi-fhir.git</url>
|
||||
</site>
|
||||
</distributionManagement>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<!-- JSON -->
|
||||
|
@ -214,6 +207,8 @@
|
|||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>findbugs-maven-plugin</artifactId>
|
||||
<version>3.0.0</version>
|
||||
<configuration>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
|
|
|
@ -207,10 +207,11 @@ public class ParameterUtil {
|
|||
case ',':
|
||||
case '|':
|
||||
b.append('\\');
|
||||
// fall through
|
||||
break;
|
||||
default:
|
||||
b.append(next);
|
||||
break;
|
||||
}
|
||||
b.append(next);
|
||||
}
|
||||
|
||||
return b.toString();
|
||||
|
|
|
@ -1,32 +0,0 @@
|
|||
package ca.uhn.fhir.rest.server;
|
||||
|
||||
/*
|
||||
* #%L
|
||||
* HAPI FHIR - Core Library
|
||||
* %%
|
||||
* Copyright (C) 2014 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 javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import ca.uhn.fhir.rest.server.exceptions.AuthenticationException;
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link ca.uhn.fhir.rest.server.security.ISecurityManager} instead
|
||||
*/
|
||||
public interface ISecurityManager {
|
||||
public void authenticate(HttpServletRequest request) throws AuthenticationException;
|
||||
}
|
|
@ -92,13 +92,12 @@ public class RestfulServer extends HttpServlet {
|
|||
private AddProfileTagEnum myAddProfileTag;
|
||||
private FhirContext myFhirContext;
|
||||
private String myImplementationDescription;
|
||||
private List<IServerInterceptor> myInterceptors = new ArrayList<IServerInterceptor>();
|
||||
private final List<IServerInterceptor> myInterceptors = new ArrayList<IServerInterceptor>();
|
||||
private ResourceBinding myNullResourceBinding = new ResourceBinding();
|
||||
private IPagingProvider myPagingProvider;
|
||||
private Collection<Object> myPlainProviders;
|
||||
private Map<String, ResourceBinding> myResourceNameToProvider = new HashMap<String, ResourceBinding>();
|
||||
private Collection<IResourceProvider> myResourceProviders;
|
||||
private ISecurityManager mySecurityManager;
|
||||
private IServerAddressStrategy myServerAddressStrategy = new IncomingRequestAddressStrategy();
|
||||
private BaseMethodBinding<?> myServerConformanceMethod;
|
||||
private Object myServerConformanceProvider;
|
||||
|
@ -123,8 +122,7 @@ public class RestfulServer extends HttpServlet {
|
|||
/**
|
||||
* This method is called prior to sending a response to incoming requests. It is used to add custom headers.
|
||||
* <p>
|
||||
* Use caution if overriding this method: it is recommended to call <code>super.addHeadersToResponse</code> to avoid
|
||||
* inadvertantly disabling functionality.
|
||||
* Use caution if overriding this method: it is recommended to call <code>super.addHeadersToResponse</code> to avoid inadvertantly disabling functionality.
|
||||
* </p>
|
||||
*/
|
||||
public void addHeadersToResponse(HttpServletResponse theHttpResponse) {
|
||||
|
@ -236,14 +234,13 @@ public class RestfulServer extends HttpServlet {
|
|||
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);
|
||||
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());
|
||||
}
|
||||
|
@ -293,8 +290,8 @@ public class RestfulServer extends HttpServlet {
|
|||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* 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;
|
||||
|
@ -336,26 +333,16 @@ public class RestfulServer extends HttpServlet {
|
|||
}
|
||||
|
||||
/**
|
||||
* Provides the security manager, or <code>null</code> if none
|
||||
*/
|
||||
public ISecurityManager getSecurityManager() {
|
||||
return mySecurityManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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}
|
||||
* 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 conformance provider, which is the provider that is used to generate the server's conformance
|
||||
* (metadata) statement.
|
||||
* Returns the server conformance provider, which is the provider that is used to generate the server's conformance (metadata) statement.
|
||||
* <p>
|
||||
* By default, the {@link ServerConformanceProvider} is used, but this can be changed, or set to <code>null</code>
|
||||
* if you do not wish to export a conformance statement.
|
||||
* By default, the {@link ServerConformanceProvider} is used, but this can be changed, or set to <code>null</code> if you do not wish to export a conformance statement.
|
||||
* </p>
|
||||
*/
|
||||
public Object getServerConformanceProvider() {
|
||||
|
@ -363,8 +350,7 @@ public class RestfulServer extends HttpServlet {
|
|||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* 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)
|
||||
*/
|
||||
|
@ -377,8 +363,7 @@ public class RestfulServer extends HttpServlet {
|
|||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* 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;
|
||||
|
@ -417,12 +402,14 @@ public class RestfulServer extends HttpServlet {
|
|||
NarrativeModeEnum narrativeMode = determineNarrativeMode(theRequest);
|
||||
boolean respondGzip = theRequest.isRespondGzip();
|
||||
|
||||
Bundle bundle = createBundleFromBundleProvider(this, theResponse, resultList, responseEncoding, theRequest.getFhirServerBase(), theRequest.getCompleteUrl(), prettyPrint, requestIsBrowser, narrativeMode, start, count, thePagingAction);
|
||||
Bundle bundle = createBundleFromBundleProvider(this, theResponse, resultList, responseEncoding, theRequest.getFhirServerBase(), theRequest.getCompleteUrl(), prettyPrint, requestIsBrowser,
|
||||
narrativeMode, start, count, thePagingAction);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -435,31 +422,29 @@ public class RestfulServer extends HttpServlet {
|
|||
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);
|
||||
RequestDetails requestDetails=null;
|
||||
try {
|
||||
|
||||
if (null != mySecurityManager) {
|
||||
mySecurityManager.authenticate(theRequest);
|
||||
}
|
||||
|
||||
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 (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);
|
||||
|
@ -609,7 +594,7 @@ public class RestfulServer extends HttpServlet {
|
|||
throw new InvalidRequestException(b.toString());
|
||||
}
|
||||
|
||||
RequestDetails requestDetails = r;
|
||||
requestDetails = r;
|
||||
requestDetails.setResourceOperationType(resourceMethod.getResourceOperationType());
|
||||
requestDetails.setSystemOperationType(resourceMethod.getSystemOperationType());
|
||||
requestDetails.setOtherOperationType(resourceMethod.getOtherOperationType());
|
||||
|
@ -617,6 +602,7 @@ public class RestfulServer extends HttpServlet {
|
|||
for (IServerInterceptor next : myInterceptors) {
|
||||
boolean continueProcessing = next.incomingRequestPostProcessed(requestDetails, theRequest, theResponse);
|
||||
if (!continueProcessing) {
|
||||
ourLog.debug("Interceptor {} returned false, not continuing processing");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -624,6 +610,15 @@ public class RestfulServer extends HttpServlet {
|
|||
resourceMethod.invokeServer(this, r);
|
||||
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
||||
if (requestIsBrowser) {
|
||||
// if request is coming from a browser, prompt the user to enter login credentials
|
||||
theResponse.setHeader("WWW-Authenticate", "BASIC realm=\"FHIR\"");
|
||||
|
@ -636,14 +631,30 @@ public class RestfulServer extends HttpServlet {
|
|||
|
||||
} 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(requestDetails, e, theRequest, theResponse)) {
|
||||
ourLog.debug("Interceptor {} returned false, not continuing processing");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
BaseOperationOutcome oo = null;
|
||||
int statusCode = 500;
|
||||
int statusCode = Constants.STATUS_HTTP_500_INTERNAL_ERROR;
|
||||
|
||||
if (e instanceof BaseServerResponseException) {
|
||||
oo = ((BaseServerResponseException) e).getOperationOutcome();
|
||||
statusCode = ((BaseServerResponseException) e).getStatusCode();
|
||||
}
|
||||
|
||||
/*
|
||||
* Generate an OperationOutcome to return, unless the exception throw by
|
||||
* the resource provider had one
|
||||
*/
|
||||
if (oo == null) {
|
||||
try {
|
||||
oo = (BaseOperationOutcome) myFhirContext.getResourceDefinition("OperationOutcome").getImplementingClass().newInstance();
|
||||
|
@ -651,19 +662,29 @@ public class RestfulServer extends HttpServlet {
|
|||
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 (e instanceof InternalErrorException) {
|
||||
ourLog.error("Failure during REST processing", e);
|
||||
issue.getDetailsElement().setValue(e.toString() + "\n\n" + ExceptionUtils.getStackTrace(e));
|
||||
statusCode = ((InternalErrorException) e).getStatusCode();
|
||||
} else if (e instanceof BaseServerResponseException) {
|
||||
ourLog.warn("Failure during REST processing: {}", e.toString());
|
||||
statusCode = ((BaseServerResponseException) e).getStatusCode();
|
||||
BaseServerResponseException baseServerResponseException = (BaseServerResponseException) e;
|
||||
statusCode = baseServerResponseException.getStatusCode();
|
||||
issue.getDetailsElement().setValue(e.getMessage());
|
||||
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", e);
|
||||
ourLog.error("Failure during REST processing: " + e.toString(), e);
|
||||
issue.getDetailsElement().setValue(e.toString() + "\n\n" + ExceptionUtils.getStackTrace(e));
|
||||
statusCode = Constants.STATUS_HTTP_500_INTERNAL_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -681,9 +702,8 @@ public class RestfulServer extends HttpServlet {
|
|||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* 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 {
|
||||
|
@ -691,11 +711,6 @@ public class RestfulServer extends HttpServlet {
|
|||
try {
|
||||
ourLog.info("Initializing HAPI FHIR restful server");
|
||||
|
||||
mySecurityManager = getSecurityManager();
|
||||
if (null == mySecurityManager) {
|
||||
ourLog.trace("No security manager has been provided");
|
||||
}
|
||||
|
||||
ProvidedResourceScanner providedResourceScanner = new ProvidedResourceScanner(getFhirContext());
|
||||
providedResourceScanner.scanForProvidedResources(this);
|
||||
|
||||
|
@ -735,14 +750,13 @@ public class RestfulServer extends HttpServlet {
|
|||
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.
|
||||
* This method may be overridden by subclasses to do perform initialization that needs to be performed prior to the server being used.
|
||||
*/
|
||||
protected void initialize() throws ServletException {
|
||||
// nothing by default
|
||||
|
@ -763,9 +777,8 @@ public class RestfulServer extends HttpServlet {
|
|||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* 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)
|
||||
|
@ -784,7 +797,6 @@ public class RestfulServer extends HttpServlet {
|
|||
myImplementationDescription = theImplementationDescription;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets (or clears) the list of interceptors
|
||||
*
|
||||
|
@ -860,15 +872,7 @@ public class RestfulServer extends HttpServlet {
|
|||
}
|
||||
|
||||
/**
|
||||
* Sets the security manager, or <code>null</code> if none
|
||||
*/
|
||||
public void setSecurityManager(ISecurityManager theSecurityManager) {
|
||||
mySecurityManager = theSecurityManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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}
|
||||
* 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");
|
||||
|
@ -876,17 +880,14 @@ public class RestfulServer extends HttpServlet {
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns the server conformance provider, which is the provider that is used to generate the server's conformance
|
||||
* (metadata) statement.
|
||||
* Returns the server conformance provider, which is the provider that is used to generate the server's conformance (metadata) statement.
|
||||
* <p>
|
||||
* By default, the {@link ServerConformanceProvider} is used, but this can be changed, or set to <code>null</code>
|
||||
* if you do not wish to export a conformance statement.
|
||||
* By default, the {@link ServerConformanceProvider} is used, but this can be changed, or set to <code>null</code> if you do not wish to export a conformance statement.
|
||||
* </p>
|
||||
* 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.
|
||||
* 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) {
|
||||
|
@ -896,24 +897,22 @@ public class RestfulServer extends HttpServlet {
|
|||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* 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.
|
||||
* 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 <code>true</code> (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
|
||||
* If set to <code>true</code> (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;
|
||||
|
@ -939,8 +938,8 @@ public class RestfulServer extends HttpServlet {
|
|||
}
|
||||
}
|
||||
|
||||
public static Bundle createBundleFromBundleProvider(RestfulServer theServer, HttpServletResponse theHttpResponse, IBundleProvider theResult, EncodingEnum theResponseEncoding, String theServerBase, String theCompleteUrl, boolean thePrettyPrint, boolean theRequestIsBrowser,
|
||||
NarrativeModeEnum theNarrativeMode, int theOffset, Integer theLimit, String theSearchId) {
|
||||
public static Bundle createBundleFromBundleProvider(RestfulServer theServer, HttpServletResponse theHttpResponse, IBundleProvider theResult, EncodingEnum theResponseEncoding,
|
||||
String theServerBase, String theCompleteUrl, boolean thePrettyPrint, boolean theRequestIsBrowser, NarrativeModeEnum theNarrativeMode, int theOffset, Integer theLimit, String theSearchId) {
|
||||
theHttpResponse.setStatus(200);
|
||||
|
||||
if (theRequestIsBrowser && theServer.isUseBrowserFriendlyContentTypes()) {
|
||||
|
@ -962,7 +961,7 @@ public class RestfulServer extends HttpServlet {
|
|||
numToReturn = theResult.size();
|
||||
resourceList = theResult.getResources(0, numToReturn);
|
||||
validateResourceListNotNull(resourceList);
|
||||
|
||||
|
||||
} else {
|
||||
IPagingProvider pagingProvider = theServer.getPagingProvider();
|
||||
if (theLimit == null) {
|
||||
|
@ -1045,11 +1044,11 @@ public class RestfulServer extends HttpServlet {
|
|||
|
||||
Set<String> containedIds = new HashSet<String>();
|
||||
for (IResource nextContained : next.getContained().getContainedResources()) {
|
||||
if (nextContained.getId().isEmpty()==false) {
|
||||
if (nextContained.getId().isEmpty() == false) {
|
||||
containedIds.add(nextContained.getId().getValue());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (theContext.getNarrativeGenerator() != null) {
|
||||
String title = theContext.getNarrativeGenerator().generateTitle(next);
|
||||
ourLog.trace("Narrative generator created title: {}", title);
|
||||
|
@ -1072,7 +1071,7 @@ public class RestfulServer extends HttpServlet {
|
|||
// Don't add contained IDs as top level resources
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
IdDt id = nextRes.getId().toVersionless();
|
||||
if (id.hasResourceType() == false) {
|
||||
String resName = theContext.getResourceDefinition(nextRes).getName();
|
||||
|
@ -1188,9 +1187,7 @@ public class RestfulServer extends HttpServlet {
|
|||
}
|
||||
|
||||
/**
|
||||
* Determine whether a response should be given in JSON or XML format based on the
|
||||
* incoming HttpServletRequest's <code>"_format"</code> parameter and <code>"Accept:"</code>
|
||||
* HTTP header.
|
||||
* Determine whether a response should be given in JSON or XML format based on the incoming HttpServletRequest's <code>"_format"</code> parameter and <code>"Accept:"</code> HTTP header.
|
||||
*/
|
||||
public static EncodingEnum determineResponseEncoding(HttpServletRequest theReq) {
|
||||
String[] format = theReq.getParameterValues(Constants.PARAM_FORMAT);
|
||||
|
@ -1283,8 +1280,8 @@ public class RestfulServer extends HttpServlet {
|
|||
return prettyPrint;
|
||||
}
|
||||
|
||||
public static void streamResponseAsBundle(RestfulServer theServer, HttpServletResponse theHttpResponse, Bundle bundle, EncodingEnum theResponseEncoding, String theServerBase, boolean thePrettyPrint, NarrativeModeEnum theNarrativeMode, boolean theRespondGzip)
|
||||
throws IOException {
|
||||
public static void streamResponseAsBundle(RestfulServer theServer, HttpServletResponse theHttpResponse, Bundle bundle, EncodingEnum theResponseEncoding, String theServerBase,
|
||||
boolean thePrettyPrint, NarrativeModeEnum theNarrativeMode, boolean theRespondGzip) throws IOException {
|
||||
assert !theServerBase.endsWith("/");
|
||||
|
||||
Writer writer = getWriter(theHttpResponse, theRespondGzip);
|
||||
|
@ -1302,14 +1299,14 @@ public class RestfulServer extends HttpServlet {
|
|||
}
|
||||
}
|
||||
|
||||
public static void streamResponseAsResource(RestfulServer theServer, HttpServletResponse theHttpResponse, IResource theResource, EncodingEnum theResponseEncoding, boolean thePrettyPrint, boolean theRequestIsBrowser, NarrativeModeEnum theNarrativeMode, boolean theRespondGzip,
|
||||
String theServerBase) throws IOException {
|
||||
public static void streamResponseAsResource(RestfulServer theServer, HttpServletResponse theHttpResponse, IResource theResource, EncodingEnum theResponseEncoding, boolean thePrettyPrint,
|
||||
boolean theRequestIsBrowser, NarrativeModeEnum theNarrativeMode, boolean theRespondGzip, String theServerBase) throws IOException {
|
||||
int stausCode = 200;
|
||||
streamResponseAsResource(theServer, theHttpResponse, theResource, theResponseEncoding, thePrettyPrint, theRequestIsBrowser, theNarrativeMode, stausCode, theRespondGzip, theServerBase);
|
||||
}
|
||||
|
||||
private static void streamResponseAsResource(RestfulServer theServer, HttpServletResponse theHttpResponse, IResource theResource, EncodingEnum theResponseEncoding, boolean thePrettyPrint, boolean theRequestIsBrowser, NarrativeModeEnum theNarrativeMode, int stausCode,
|
||||
boolean theRespondGzip, String theServerBase) throws IOException {
|
||||
private static void streamResponseAsResource(RestfulServer theServer, HttpServletResponse theHttpResponse, IResource theResource, EncodingEnum theResponseEncoding, boolean thePrettyPrint,
|
||||
boolean theRequestIsBrowser, NarrativeModeEnum theNarrativeMode, int stausCode, boolean theRespondGzip, String theServerBase) throws IOException {
|
||||
theHttpResponse.setStatus(stausCode);
|
||||
|
||||
if (theResource.getId() != null && theResource.getId().hasIdPart() && isNotBlank(theServerBase)) {
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
package ca.uhn.fhir.rest.server.exceptions;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import ca.uhn.fhir.model.base.resource.BaseOperationOutcome;
|
||||
|
@ -27,9 +29,8 @@ import ca.uhn.fhir.model.base.resource.BaseOperationOutcome;
|
|||
*/
|
||||
|
||||
/**
|
||||
* Base class for RESTful client and server exceptions. RESTful client methods will only throw exceptions which are
|
||||
* subclasses of this exception type, and RESTful server methods should also only call subclasses of this exception
|
||||
* type.
|
||||
* Base class for RESTful client and server exceptions. RESTful client methods will only throw exceptions which are subclasses of this exception type, and RESTful server methods should also only call
|
||||
* subclasses of this exception type.
|
||||
*/
|
||||
public abstract class BaseServerResponseException extends RuntimeException {
|
||||
|
||||
|
@ -49,11 +50,12 @@ public abstract class BaseServerResponseException extends RuntimeException {
|
|||
registerExceptionType(NotImplementedOperationException.STATUS_CODE, NotImplementedOperationException.class);
|
||||
}
|
||||
|
||||
private List<String> myAdditionalMessages = null;
|
||||
private BaseOperationOutcome myBaseOperationOutcome;
|
||||
private String myResponseBody;
|
||||
private String myResponseMimeType;
|
||||
private int myStatusCode;
|
||||
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
|
@ -67,7 +69,24 @@ public abstract class BaseServerResponseException extends RuntimeException {
|
|||
myStatusCode = theStatusCode;
|
||||
myBaseOperationOutcome = null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param theStatusCode
|
||||
* The HTTP status code corresponding to this problem
|
||||
* @param theMessage
|
||||
* The message
|
||||
*/
|
||||
public BaseServerResponseException(int theStatusCode, String... theMessages) {
|
||||
super(theMessages != null && theMessages.length > 0 ? theMessages[0] : null);
|
||||
myStatusCode = theStatusCode;
|
||||
myBaseOperationOutcome = null;
|
||||
if (theMessages != null && theMessages.length > 1) {
|
||||
myAdditionalMessages = Arrays.asList(Arrays.copyOfRange(theMessages, 1, theMessages.length, String[].class));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
|
@ -76,8 +95,7 @@ public abstract class BaseServerResponseException extends RuntimeException {
|
|||
* @param theMessage
|
||||
* The message
|
||||
* @param theBaseOperationOutcome
|
||||
* An BaseOperationOutcome resource to return to the calling client (in a server) or the BaseOperationOutcome
|
||||
* that was returned from the server (in a client)
|
||||
* An BaseOperationOutcome resource to return to the calling client (in a server) or the BaseOperationOutcome that was returned from the server (in a client)
|
||||
*/
|
||||
public BaseServerResponseException(int theStatusCode, String theMessage, BaseOperationOutcome theBaseOperationOutcome) {
|
||||
super(theMessage);
|
||||
|
@ -111,8 +129,7 @@ public abstract class BaseServerResponseException extends RuntimeException {
|
|||
* @param theCause
|
||||
* The underlying cause exception
|
||||
* @param theBaseOperationOutcome
|
||||
* An BaseOperationOutcome resource to return to the calling client (in a server) or the BaseOperationOutcome
|
||||
* that was returned from the server (in a client)
|
||||
* An BaseOperationOutcome resource to return to the calling client (in a server) or the BaseOperationOutcome that was returned from the server (in a client)
|
||||
*/
|
||||
public BaseServerResponseException(int theStatusCode, String theMessage, Throwable theCause, BaseOperationOutcome theBaseOperationOutcome) {
|
||||
super(theMessage, theCause);
|
||||
|
@ -142,8 +159,7 @@ public abstract class BaseServerResponseException extends RuntimeException {
|
|||
* @param theCause
|
||||
* The underlying cause exception
|
||||
* @param theBaseOperationOutcome
|
||||
* An BaseOperationOutcome resource to return to the calling client (in a server) or the BaseOperationOutcome
|
||||
* that was returned from the server (in a client)
|
||||
* An BaseOperationOutcome resource to return to the calling client (in a server) or the BaseOperationOutcome that was returned from the server (in a client)
|
||||
*/
|
||||
public BaseServerResponseException(int theStatusCode, Throwable theCause, BaseOperationOutcome theBaseOperationOutcome) {
|
||||
super(theCause.toString(), theCause);
|
||||
|
@ -151,6 +167,10 @@ public abstract class BaseServerResponseException extends RuntimeException {
|
|||
myBaseOperationOutcome = theBaseOperationOutcome;
|
||||
}
|
||||
|
||||
public List<String> getAdditionalMessages() {
|
||||
return myAdditionalMessages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link BaseOperationOutcome} resource if any which was supplied in the response, or <code>null</code>
|
||||
*/
|
||||
|
@ -159,8 +179,7 @@ public abstract class BaseServerResponseException extends RuntimeException {
|
|||
}
|
||||
|
||||
/**
|
||||
* In a RESTful client, this method will be populated with the body of the HTTP respone if one was provided by the
|
||||
* server, or <code>null</code> otherwise.
|
||||
* In a RESTful client, this method will be populated with the body of the HTTP respone if one was provided by the server, or <code>null</code> otherwise.
|
||||
* <p>
|
||||
* In a restful server, this method is currently ignored.
|
||||
* </p>
|
||||
|
@ -170,8 +189,7 @@ public abstract class BaseServerResponseException extends RuntimeException {
|
|||
}
|
||||
|
||||
/**
|
||||
* In a RESTful client, this method will be populated with the HTTP status code that was returned with the HTTP
|
||||
* response.
|
||||
* In a RESTful client, this method will be populated with the HTTP status code that was returned with the HTTP response.
|
||||
* <p>
|
||||
* In a restful server, this method is currently ignored.
|
||||
* </p>
|
||||
|
@ -188,11 +206,11 @@ public abstract class BaseServerResponseException extends RuntimeException {
|
|||
}
|
||||
|
||||
/**
|
||||
* Sets the BaseOperationOutcome resource associated with this exception. In server
|
||||
* implementations, this is the OperartionOutcome resource to include with the HTTP response. In
|
||||
* client implementations you should not call this method.
|
||||
* Sets the BaseOperationOutcome resource associated with this exception. In server implementations, this is the OperartionOutcome resource to include with the HTTP response. In client
|
||||
* implementations you should not call this method.
|
||||
*
|
||||
* @param theBaseOperationOutcome The BaseOperationOutcome resource
|
||||
* @param theBaseOperationOutcome
|
||||
* The BaseOperationOutcome resource
|
||||
*/
|
||||
public void setOperationOutcome(BaseOperationOutcome theBaseOperationOutcome) {
|
||||
myBaseOperationOutcome = theBaseOperationOutcome;
|
||||
|
|
|
@ -77,7 +77,7 @@ public class UnprocessableEntityException extends BaseServerResponseException {
|
|||
* Constructor which accepts an array of Strings describing the issue. This strings will be translated into an {@link BaseOperationOutcome} resource which will be supplied in the response.
|
||||
*/
|
||||
public UnprocessableEntityException(String... theMessage) {
|
||||
super(STATUS_CODE, theMessage[0]); // TODO: this used to generate an OperationOutcome - why?
|
||||
super(STATUS_CODE, theMessage);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -64,7 +64,8 @@ public interface IServerInterceptor {
|
|||
* </p>
|
||||
*
|
||||
* @param theRequestDetails
|
||||
* A bean containing details about the request that is about to be processed, including
|
||||
* 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 HttpServletRequest servlet request}.
|
||||
* @param theRequest
|
||||
* The incoming request
|
||||
* @param theResponse
|
||||
|
@ -82,7 +83,8 @@ public interface IServerInterceptor {
|
|||
* This method is called after the server implementation method has been called, but before any attempt to stream the response back to the client
|
||||
*
|
||||
* @param theRequestDetails
|
||||
* A bean containing details about the request that is about to be processed, including
|
||||
* 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 HttpServletRequest servlet request}.
|
||||
* @param theResponseObject
|
||||
* The actual object which is being streamed to the client as a response
|
||||
* @param theRequest
|
||||
|
@ -124,7 +126,8 @@ public interface IServerInterceptor {
|
|||
* This method is called after the server implementation method has been called, but before any attempt to stream the response back to the client
|
||||
*
|
||||
* @param theRequestDetails
|
||||
* A bean containing details about the request that is about to be processed, including
|
||||
* 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 HttpServletRequest servlet request}.
|
||||
* @param theResponseObject
|
||||
* The actual object which is being streamed to the client as a response
|
||||
* @param theRequest
|
||||
|
@ -145,7 +148,8 @@ public interface IServerInterceptor {
|
|||
* This method is called after the server implementation method has been called, but before any attempt to stream the response back to the client
|
||||
*
|
||||
* @param theRequestDetails
|
||||
* A bean containing details about the request that is about to be processed, including
|
||||
* 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 HttpServletRequest servlet request}.
|
||||
* @param theResponseObject
|
||||
* The actual object which is being streamed to the client as a response
|
||||
* @param theRequest
|
||||
|
@ -171,7 +175,9 @@ public interface IServerInterceptor {
|
|||
* </p>
|
||||
*
|
||||
* @param theRequestDetails
|
||||
* A bean containing details about the request that is about to be processed, including
|
||||
* Contains either <code>null</code>, 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 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.
|
||||
* @param theResponseObject
|
||||
* The actual object which is being streamed to the client as a response
|
||||
* @param theRequest
|
||||
|
|
|
@ -357,6 +357,8 @@ public class XmlUtil {
|
|||
case '"':
|
||||
case '&':
|
||||
hasEscapable = true;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -33,6 +33,7 @@ import ca.uhn.fhir.rest.annotation.Search;
|
|||
import ca.uhn.fhir.rest.param.StringParam;
|
||||
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
|
||||
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
|
||||
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
|
||||
import ca.uhn.fhir.util.PortUtil;
|
||||
|
||||
/**
|
||||
|
@ -54,6 +55,21 @@ public class ExceptionTest {
|
|||
ourExceptionType=null;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testThrowUnprocessableEntityWithMultipleMessages() throws Exception {
|
||||
{
|
||||
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?throwUnprocessableEntityWithMultipleMessages=aaa");
|
||||
HttpResponse status = ourClient.execute(httpGet);
|
||||
String responseContent = IOUtils.toString(status.getEntity().getContent());
|
||||
IOUtils.closeQuietly(status.getEntity().getContent());
|
||||
ourLog.info(responseContent);
|
||||
assertEquals(422, status.getStatusLine().getStatusCode());
|
||||
OperationOutcome oo = (OperationOutcome) servlet.getFhirContext().newXmlParser().parseResource(responseContent);
|
||||
assertThat(oo.getIssueFirstRep().getDetails().getValue(), StringContains.containsString("message1"));
|
||||
assertEquals(3, oo.getIssue().size());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInternalError() throws Exception {
|
||||
{
|
||||
|
@ -162,10 +178,15 @@ public class ExceptionTest {
|
|||
|
||||
|
||||
@Search
|
||||
public List<Patient> findPatient(@RequiredParam(name = "throwInternalError") StringParam theParam) {
|
||||
public List<Patient> throwInternalError(@RequiredParam(name = "throwInternalError") StringParam theParam) {
|
||||
throw new InternalErrorException("Exception Text");
|
||||
}
|
||||
|
||||
@Search
|
||||
public List<Patient> throwUnprocessableEntityWithMultipleMessages(@RequiredParam(name = "throwUnprocessableEntityWithMultipleMessages") StringParam theParam) {
|
||||
throw new UnprocessableEntityException("message1", "message2", "message3");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends IResource> getResourceType() {
|
||||
return Patient.class;
|
||||
|
|
|
@ -1378,11 +1378,6 @@ public class ResfulServerMethodTest {
|
|||
return myResourceProviders;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ISecurityManager getSecurityManager() {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,165 @@
|
|||
package ca.uhn.fhir.rest.server.interceptor;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.http.HttpResponse;
|
||||
import org.apache.http.client.methods.HttpGet;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.apache.http.impl.client.HttpClientBuilder;
|
||||
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.servlet.ServletHandler;
|
||||
import org.eclipse.jetty.servlet.ServletHolder;
|
||||
import org.hamcrest.core.StringContains;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
import org.mockito.stubbing.Answer;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import ca.uhn.fhir.model.dstu.composite.HumanNameDt;
|
||||
import ca.uhn.fhir.model.dstu.composite.IdentifierDt;
|
||||
import ca.uhn.fhir.model.dstu.resource.Patient;
|
||||
import ca.uhn.fhir.model.dstu.valueset.IdentifierUseEnum;
|
||||
import ca.uhn.fhir.model.primitive.IdDt;
|
||||
import ca.uhn.fhir.model.primitive.UriDt;
|
||||
import ca.uhn.fhir.rest.annotation.IdParam;
|
||||
import ca.uhn.fhir.rest.annotation.Read;
|
||||
import ca.uhn.fhir.rest.annotation.RequiredParam;
|
||||
import ca.uhn.fhir.rest.annotation.Search;
|
||||
import ca.uhn.fhir.rest.method.RequestDetails;
|
||||
import ca.uhn.fhir.rest.server.IResourceProvider;
|
||||
import ca.uhn.fhir.rest.server.RestfulServer;
|
||||
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
|
||||
import ca.uhn.fhir.util.PortUtil;
|
||||
|
||||
public class ExceptionHandlingInterceptorTest {
|
||||
|
||||
private static CloseableHttpClient ourClient;
|
||||
private static int ourPort;
|
||||
private static Server ourServer;
|
||||
private static RestfulServer servlet;
|
||||
private IServerInterceptor myInterceptor;
|
||||
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ExceptionHandlingInterceptorTest.class);
|
||||
|
||||
@Test
|
||||
public void testThrowUnprocessableEntityException() throws Exception {
|
||||
|
||||
when(myInterceptor.incomingRequestPreProcessed(any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true);
|
||||
when(myInterceptor.incomingRequestPostProcessed(any(RequestDetails.class), any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true);
|
||||
when(myInterceptor.handleException(any(RequestDetails.class), any(Throwable.class), any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true);
|
||||
|
||||
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?_query=throwUnprocessableEntityException");
|
||||
HttpResponse status = ourClient.execute(httpGet);
|
||||
ourLog.info(IOUtils.toString(status.getEntity().getContent()));
|
||||
assertEquals(422, status.getStatusLine().getStatusCode());
|
||||
IOUtils.closeQuietly(status.getEntity().getContent());
|
||||
|
||||
ArgumentCaptor<Throwable> captor = ArgumentCaptor.forClass(Throwable.class);
|
||||
verify(myInterceptor, times(1)).handleException(any(RequestDetails.class), captor.capture(), any(HttpServletRequest.class), any(HttpServletResponse.class));
|
||||
|
||||
assertEquals(UnprocessableEntityException.class, captor.getValue().getClass());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testThrowUnprocessableEntityExceptionAndOverrideResponse() throws Exception {
|
||||
|
||||
when(myInterceptor.incomingRequestPreProcessed(any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true);
|
||||
when(myInterceptor.incomingRequestPostProcessed(any(RequestDetails.class), any(HttpServletRequest.class), any(HttpServletResponse.class))).thenReturn(true);
|
||||
|
||||
when(myInterceptor.handleException(any(RequestDetails.class), any(Throwable.class), any(HttpServletRequest.class), any(HttpServletResponse.class))).thenAnswer(new Answer<Boolean>() {
|
||||
@Override
|
||||
public Boolean answer(InvocationOnMock theInvocation) throws Throwable {
|
||||
HttpServletResponse resp = (HttpServletResponse) theInvocation.getArguments()[3];
|
||||
resp.setStatus(405);
|
||||
resp.setContentType("text/plain");
|
||||
resp.getWriter().write("HELP IM A BUG");
|
||||
resp.getWriter().close();
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?_query=throwUnprocessableEntityException");
|
||||
HttpResponse status = ourClient.execute(httpGet);
|
||||
String responseContent = IOUtils.toString(status.getEntity().getContent());
|
||||
ourLog.info(responseContent);
|
||||
assertEquals(405, status.getStatusLine().getStatusCode());
|
||||
IOUtils.closeQuietly(status.getEntity().getContent());
|
||||
|
||||
assertEquals("HELP IM A BUG", responseContent);
|
||||
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void afterClass() throws Exception {
|
||||
ourServer.stop();
|
||||
}
|
||||
|
||||
@Before
|
||||
public void before() {
|
||||
myInterceptor = mock(IServerInterceptor.class);
|
||||
servlet.setInterceptors(Collections.singletonList(myInterceptor));
|
||||
}
|
||||
|
||||
@BeforeClass
|
||||
public static void beforeClass() throws Exception {
|
||||
ourPort = PortUtil.findFreePort();
|
||||
ourServer = new Server(ourPort);
|
||||
|
||||
DummyPatientResourceProvider patientProvider = new DummyPatientResourceProvider();
|
||||
|
||||
ServletHandler proxyHandler = new ServletHandler();
|
||||
servlet = new RestfulServer();
|
||||
servlet.setResourceProviders(patientProvider);
|
||||
ServletHolder servletHolder = new ServletHolder(servlet);
|
||||
proxyHandler.addServletWithMapping(servletHolder, "/*");
|
||||
ourServer.setHandler(proxyHandler);
|
||||
ourServer.start();
|
||||
|
||||
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS);
|
||||
HttpClientBuilder builder = HttpClientBuilder.create();
|
||||
builder.setConnectionManager(connectionManager);
|
||||
ourClient = builder.build();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Created by dsotnikov on 2/25/2014.
|
||||
*/
|
||||
public static class DummyPatientResourceProvider implements IResourceProvider {
|
||||
|
||||
/**
|
||||
* Retrieve the resource by its identifier
|
||||
*
|
||||
* @param theId
|
||||
* The resource identity
|
||||
* @return The resource
|
||||
*/
|
||||
@Search(queryName = "throwUnprocessableEntityException")
|
||||
public List<Patient> throwUnprocessableEntityException() {
|
||||
throw new UnprocessableEntityException("Unprocessable!");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<Patient> getResourceType() {
|
||||
return Patient.class;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -221,7 +221,7 @@ public class TinderStructuresMojo extends AbstractMojo {
|
|||
String dtOutputDir = "target/generated-sources/tinder/ca/uhn/fhir/model/dev/composite";
|
||||
|
||||
ResourceGeneratorUsingSpreadsheet rp = new ResourceGeneratorUsingSpreadsheet("dev", ".");
|
||||
rp.setBaseResourceNames(Arrays.asList("referralrequest", "patient","practitioner","encounter",
|
||||
rp.setBaseResourceNames(Arrays.asList("conformance", "referralrequest", "patient","practitioner","encounter",
|
||||
"organization","location","relatedperson","appointment","slot","order","availability","device", "valueset"));
|
||||
rp.parse();
|
||||
rp.bindValueSets(vsp);
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
curl https://api.github.com/repos/jamesagnew/hapi-fhir/releases
|
28
pom.xml
28
pom.xml
|
@ -28,6 +28,13 @@
|
|||
<url>https://github.com/jamesagnew/hapi-fhir/issues/</url>
|
||||
</issueManagement>
|
||||
|
||||
<distributionManagement>
|
||||
<site>
|
||||
<id>git.server</id>
|
||||
<url>scm:git:git@github.com:jamesagnew/hapi-fhir.git</url>
|
||||
</site>
|
||||
</distributionManagement>
|
||||
|
||||
<scm>
|
||||
<connection>scm:git:git@github.com:jamesagnew/hapi-fhir.git</connection>
|
||||
<url>scm:git:git@github.com:jamesagnew/hapi-fhir.git</url>
|
||||
|
@ -502,12 +509,33 @@
|
|||
</reportSet>
|
||||
</reportSets>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>findbugs-maven-plugin</artifactId>
|
||||
<version>3.0.0</version>
|
||||
<configuration>
|
||||
<classFilesDirectory>./hapi-fhir-base/target/classes</classFilesDirectory>
|
||||
</configuration>
|
||||
<reportSets>
|
||||
<reportSet>
|
||||
<reports>
|
||||
<report>findbugs</report>
|
||||
</reports>
|
||||
</reportSet>
|
||||
</reportSets>
|
||||
</plugin>
|
||||
|
||||
<!-- <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-linkcheck-plugin</artifactId>
|
||||
<version>1.1</version> </plugin> -->
|
||||
</plugins>
|
||||
</reporting>
|
||||
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>ROOT</id>
|
||||
<modules>
|
||||
</modules>
|
||||
</profile>
|
||||
<profile>
|
||||
<id>SIGN_ARTIFACTS</id>
|
||||
<activation>
|
||||
|
|
|
@ -7,11 +7,19 @@
|
|||
</properties>
|
||||
<body>
|
||||
<release version="0.8" date="TBD">
|
||||
<action type="add" issue="38">
|
||||
<action>
|
||||
<![CDATA[<b>API CHANGE:</b>]]> The "FHIR structures" for DSTU1 (the classes which model the
|
||||
resources and composite datatypes) have been moved out of the core JAR into their
|
||||
own JAR, in order to allow support for DEV resources, and DSTU2 resources when thast
|
||||
version is finalized. See
|
||||
<![CDATA[<a href="./doc_upgrading.html">upgrading</a>]]>
|
||||
for more information.
|
||||
</action>
|
||||
<action type="add" issue="38" dev="wdebeau1">
|
||||
Profile generation on the server was not working due to IdDt being
|
||||
incorrectly used. Thanks to Bill de Beaubien for the pull request!
|
||||
</action>
|
||||
<action type="add" issue="42">
|
||||
<action type="add" issue="42" dev="wdebeau1">
|
||||
Profiles did not generate correctly if a resource definition class had a
|
||||
defined extension which was of a composite type. Thanks to Bill de Beaubien for the pull request!
|
||||
</action>
|
||||
|
@ -19,7 +27,7 @@
|
|||
Remove unnessecary IOException from narrative generator API. Thanks to
|
||||
Petro Mykhailysyn for the pull request!
|
||||
</action>
|
||||
<action type="add" issue="48">
|
||||
<action type="add" issue="48" dev="wdebeau1">
|
||||
Introduced a new
|
||||
<![CDATA[<code>@ProvidesResources</code>]]> annotation which can be added to
|
||||
resource provider and servers to allow them to declare additional resource
|
||||
|
@ -27,6 +35,16 @@
|
|||
serve up multiple classes for the same resource type (e.g. a server that sometimes
|
||||
returns a default Patient, but sometimes uses a custom subclass).
|
||||
Thanks to Bill de Beaubien for the pull request!
|
||||
</action>
|
||||
<action>
|
||||
Add a new method <![CDATA[handleException]]> to the server interceptor
|
||||
framework which allows interceptors to be notified of any exceptions and
|
||||
runtime errors within server methods. Interceptors may optionally also
|
||||
override the default error handling behaviour of the RestfulServer.
|
||||
</action>
|
||||
<action dev="wdebeau1">
|
||||
Add constants to BaseResource for the "_id" search parameter which all resources
|
||||
should support.
|
||||
</action>
|
||||
</release>
|
||||
<release version="0.7" date="2014-Oct-23">
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
<mxGraphModel dx="940" dy="431" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" fold="1" page="1" pageScale="1" pageWidth="826" pageHeight="1169" style="default-style2" math="0"><root><mxCell id="0"/><mxCell id="1" parent="0"/><mxCell id="2" value="RESTful
Server" style="shape=umlLifeline;perimeter=lifelinePerimeter;size=46;strokeColor=#0000FF;strokeWidth=2;fillColor=#99FFFF" parent="1" vertex="1"><mxGeometry x="150" y="20" width="100" height="280" as="geometry"/></mxCell><mxCell id="3" value="Interceptor" style="shape=umlLifeline;perimeter=lifelinePerimeter;size=46;strokeColor=#0000FF;strokeWidth=2;fillColor=#99FFFF" parent="1" vertex="1"><mxGeometry x="280" y="20" width="100" height="280" as="geometry"/></mxCell><mxCell id="4" value="Resource/Plain
Provider
Method" style="shape=umlLifeline;perimeter=lifelinePerimeter;size=46;strokeColor=#0000FF;strokeWidth=2;fillColor=#99FFFF" parent="1" vertex="1"><mxGeometry x="400" y="20" width="100" height="280" as="geometry"/></mxCell><mxCell id="7" value="" style="ellipse;shape=startState;fillColor=#000000;strokeColor=#ff0000;" parent="1" vertex="1"><mxGeometry x="70" y="80" width="30" height="30" as="geometry"/></mxCell><mxCell id="8" value="Incoming Request" style="edgeStyle=elbowEdgeStyle;elbow=horizontal;verticalAlign=bottom;endArrow=open;endSize=8;strokeColor=#ff0000;entryX=0.5;entryY=0.3;entryPerimeter=0" parent="1" source="7" edge="1"><mxGeometry x="70" y="80" as="geometry"><mxPoint x="200" y="95" as="targetPoint"/></mxGeometry></mxCell><mxCell id="21" value="Request is handled" style="rounded=1;whiteSpace=wrap" parent="1" vertex="1"><mxGeometry x="390" y="150" width="120" height="30" as="geometry"/></mxCell><mxCell id="22" value="" style="edgeStyle=none;align=left;entryX=0.5;entryY=0.632;entryPerimeter=0;exitX=0.5;exitY=0.632;exitPerimeter=0" parent="1" edge="1"><mxGeometry width="100" height="100" relative="1" as="geometry"><mxPoint x="200" y="140.15999999999997" as="sourcePoint"/><mxPoint x="450" y="140.15999999999997" as="targetPoint"/></mxGeometry></mxCell><mxCell id="24" value="" parent="1" vertex="1"><mxGeometry x="320" y="233" width="20" height="27" as="geometry"/></mxCell><mxCell id="25" value="handleException" style="edgeStyle=elbowEdgeStyle;elbow=vertical;verticalAlign=bottom;endArrow=block;exitX=0.5;exitY=0.478;exitPerimeter=0;align=left" parent="1" target="24" edge="1"><mxGeometry x="221.75" y="233.25" as="geometry"><mxPoint x="200" y="233.39999999999998" as="sourcePoint"/></mxGeometry></mxCell><mxCell id="26" value="return true;" style="edgeStyle=elbowEdgeStyle;elbow=vertical;verticalAlign=bottom;dashed=1;endArrow=open;endSize=8;" parent="1" source="24" edge="1"><mxGeometry x="221.75" y="233.25" as="geometry"><mxPoint x="200" y="260" as="targetPoint"/></mxGeometry></mxCell><mxCell id="27" value="" style="ellipse;shape=endState;fillColor=#000000;strokeColor=#ff0000;align=left" parent="1" vertex="1"><mxGeometry x="70" y="270" width="30" height="30" as="geometry"/></mxCell><mxCell id="28" value="Response" style="edgeStyle=orthogonalEdgeStyle;endArrow=block;dashed=0;dashPattern=1 4;align=left;entryX=1;entryY=0.5;exitX=0.5;exitY=0.954;exitPerimeter=0;endFill=1;strokeWidth=1;strokeColor=#FF0000" parent="1" target="27" edge="1"><mxGeometry x="0.500299820107935" y="-13" width="100" height="100" relative="1" as="geometry"><mxPoint x="200" y="285.05999999999995" as="sourcePoint"/><mxPoint x="650" y="213" as="targetPoint"/><mxPoint x="-8" y="-12" as="offset"/></mxGeometry></mxCell><mxCell id="29" value="" style="ellipse;shape=cloud;whiteSpace=wrap;html=1;" vertex="1" parent="1"><mxGeometry x="175" y="100" width="50" height="30" as="geometry"/></mxCell><mxCell id="30" value="throw exception" style="edgeStyle=elbowEdgeStyle;elbow=vertical;verticalAlign=bottom;dashed=1;endArrow=open;endSize=8;" edge="1" parent="1"><mxGeometry x="351.75" y="173.25" as="geometry"><mxPoint x="200" y="200" as="targetPoint"/><mxPoint x="450" y="200" as="sourcePoint"/></mxGeometry></mxCell></root></mxGraphModel>
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,4 @@
|
|||
In this directory:
|
||||
|
||||
* XCF files are opened in The GIMP
|
||||
* XML files are opened in draw.io
|
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 5.3 KiB |
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 7.7 KiB |
|
@ -69,6 +69,32 @@
|
|||
of the operation being invoked (resource, bundle, etc.)
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<br clear="all"/>
|
||||
<subsection name="Exception Handling">
|
||||
|
||||
<img src="svg/restful-server-interceptors-exception.svg" alt="Interceptors" align="right"/>
|
||||
|
||||
<p>
|
||||
In the event of an exception being thrown within the server, the interceptor
|
||||
method
|
||||
<code><a href="./apidocs/ca/uhn/fhir/rest/server/interceptor/IServerInterceptor.html#handleException(ca.uhn.fhir.rest.method.RequestDetails,%20java.lang.Throwable,%20javax.servlet.http.HttpServletRequest,%20javax.servlet.http.HttpServletResponse)">handleException</a></code>
|
||||
will be called. This applies both to HAPI-FHIR defined exceptions thrown within resource provider methods
|
||||
you have created as well as unexpected exceptions such as NullPointerException thrown
|
||||
at any point in the handling chain.
|
||||
</p>
|
||||
<p>
|
||||
In general, you will want to return <code>true</code> from the <code>handleException</code>
|
||||
method, which means that processing continues normally (RestfulServer will return an
|
||||
HTTP 4xx or 5xx response automatically depending on the specific exception which was thrown).
|
||||
</p>
|
||||
<p>
|
||||
However, you may override the server's built-in exception handling by returning
|
||||
<code>false</code>. In this case, you must provide your own response by
|
||||
interacting with the <code>HttpServletResponse</code> object which is
|
||||
passed in.
|
||||
</p>
|
||||
</subsection>
|
||||
|
||||
<br clear="all"/>
|
||||
<subsection name="Registering Interceptors">
|
||||
|
@ -129,6 +155,29 @@
|
|||
|
||||
</section>
|
||||
|
||||
<section name="Creating Interceptors">
|
||||
|
||||
<p>
|
||||
Creating your own interceptors is easy. HAPI-FHIR provides a class called
|
||||
<code>InterceptorAdapter</code> which you can extend and then override any
|
||||
methods you wish. The following example shows a simple request counter.
|
||||
</p>
|
||||
<macro name="snippet">
|
||||
<param name="id" value="interceptor" />
|
||||
<param name="file" value="examples/src/main/java/example/RequestCounterInterceptor.java" />
|
||||
</macro>
|
||||
|
||||
<p>
|
||||
The following example shows an exception handling interceptor which
|
||||
overrides the built-in exception handling by providing a custom response.
|
||||
</p>
|
||||
<macro name="snippet">
|
||||
<param name="id" value="interceptor" />
|
||||
<param name="file" value="examples/src/main/java/example/RequestExceptionInterceptor.java" />
|
||||
</macro>
|
||||
|
||||
</section>
|
||||
|
||||
</body>
|
||||
|
||||
</document>
|
||||
|
|
|
@ -28,21 +28,21 @@
|
|||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
The <code>hapi-fhir-base-[version].jar</code> file containing the core library.
|
||||
<i>Required:</i> The <code>hapi-fhir-base-[version].jar</code> file containing the core library.
|
||||
</li>
|
||||
<li>
|
||||
The <code>hapi-fhir-structures-dstu-[version].jar</code> file containing the FHIR model classes
|
||||
<i>Required:</i> The <code>hapi-fhir-structures-dstu-[version].jar</code> file containing the FHIR model classes
|
||||
for DSTU1 (all contents of this JAR were previously found in hapi-fhir-base).
|
||||
</li>
|
||||
<li>
|
||||
<i>Optionally: </i>You may also choose to include the
|
||||
<i>Optional:</i> You may also choose to include the
|
||||
<code>hapi-fhir-structures-dev-[version].jar</code>. This JAR contains structures for the
|
||||
latest DEV version of FHIR. You may create a client/server which supports either DSTU1 or DEV
|
||||
resources, or both depending on your needs. Note that using DEV resources may introduce
|
||||
incompatibilities with other frameworks however.<br/><br/>
|
||||
<b>You must also include hapi-fhir-structures-dstu-[version].jar</b> if you include
|
||||
the dev structures JAR at this time. Hopefully at some point soon this requirement
|
||||
will be relaxed.
|
||||
incompatibilities with other frameworks however. If you are including this JAR,
|
||||
<b>you must also include hapi-fhir-structures-dstu-[version].jar</b>.
|
||||
Hopefully by the time 0.8 is final this requirement will be relaxed, but for now it
|
||||
is mandatory.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
|
|
Loading…
Reference in New Issue