Implement handleException on the server interceptor framework, as well

as some site and documentation enhancements
This commit is contained in:
James Agnew 2014-11-07 14:26:59 -05:00
parent 8f704030ed
commit d22a35788f
26 changed files with 528 additions and 179 deletions

View File

@ -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

View File

@ -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

View File

@ -3,6 +3,9 @@ package example;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.ExtensionDt; import ca.uhn.fhir.model.api.ExtensionDt;
import ca.uhn.fhir.model.dstu.composite.HumanNameDt; 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.DateTimeDt;
import ca.uhn.fhir.model.primitive.StringDt; import ca.uhn.fhir.model.primitive.StringDt;
import ca.uhn.fhir.parser.DataFormatException; import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.rest.server.interceptor.InterceptorAdapter;
public class ServerInterceptors { public class ServerInterceptors {
@SuppressWarnings("unused") @SuppressWarnings("unused")
public static void main(String[] args) throws DataFormatException, IOException { public static void main(String[] args) throws DataFormatException, IOException {
// START SNIPPET: resourceExtension // START SNIPPET: resourceExtension
// Create an example patient // Create an example patient

View File

@ -19,9 +19,11 @@
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-project-info-reports-plugin</artifactId> <artifactId>maven-project-info-reports-plugin</artifactId>
<version>2.7</version> <version>2.7</version>
<inherited>false</inherited>
<reportSets> <reportSets>
<reportSet> <reportSet>
<reports> <reports>
<report>scm</report>
</reports> </reports>
</reportSet> </reportSet>
</reportSets> </reportSets>
@ -39,6 +41,7 @@
<configuration> <configuration>
<links> <links>
<link>http://docs.oracle.com/javaee/7/api</link> <link>http://docs.oracle.com/javaee/7/api</link>
<link>http://jamesagnew.github.io/hapi-fhir/apidocs</link>
</links> </links>
</configuration> </configuration>
</reportSet> </reportSet>

View File

@ -15,13 +15,6 @@
<name>HAPI FHIR - Core Library</name> <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> <dependencies>
<!-- JSON --> <!-- JSON -->
@ -214,6 +207,8 @@
<groupId>org.codehaus.mojo</groupId> <groupId>org.codehaus.mojo</groupId>
<artifactId>findbugs-maven-plugin</artifactId> <artifactId>findbugs-maven-plugin</artifactId>
<version>3.0.0</version> <version>3.0.0</version>
<configuration>
</configuration>
</plugin> </plugin>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>

View File

@ -207,10 +207,11 @@ public class ParameterUtil {
case ',': case ',':
case '|': case '|':
b.append('\\'); b.append('\\');
// fall through break;
default: default:
b.append(next); break;
} }
b.append(next);
} }
return b.toString(); return b.toString();

View File

@ -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;
}

View File

@ -92,13 +92,12 @@ public class RestfulServer extends HttpServlet {
private AddProfileTagEnum myAddProfileTag; private AddProfileTagEnum myAddProfileTag;
private FhirContext myFhirContext; private FhirContext myFhirContext;
private String myImplementationDescription; private String myImplementationDescription;
private List<IServerInterceptor> myInterceptors = new ArrayList<IServerInterceptor>(); private final List<IServerInterceptor> myInterceptors = new ArrayList<IServerInterceptor>();
private ResourceBinding myNullResourceBinding = new ResourceBinding(); private ResourceBinding myNullResourceBinding = new ResourceBinding();
private IPagingProvider myPagingProvider; private IPagingProvider myPagingProvider;
private Collection<Object> myPlainProviders; private Collection<Object> myPlainProviders;
private Map<String, ResourceBinding> myResourceNameToProvider = new HashMap<String, ResourceBinding>(); private Map<String, ResourceBinding> myResourceNameToProvider = new HashMap<String, ResourceBinding>();
private Collection<IResourceProvider> myResourceProviders; private Collection<IResourceProvider> myResourceProviders;
private ISecurityManager mySecurityManager;
private IServerAddressStrategy myServerAddressStrategy = new IncomingRequestAddressStrategy(); private IServerAddressStrategy myServerAddressStrategy = new IncomingRequestAddressStrategy();
private BaseMethodBinding<?> myServerConformanceMethod; private BaseMethodBinding<?> myServerConformanceMethod;
private Object myServerConformanceProvider; 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. * This method is called prior to sending a response to incoming requests. It is used to add custom headers.
* <p> * <p>
* Use caution if overriding this method: it is recommended to call <code>super.addHeadersToResponse</code> to avoid * Use caution if overriding this method: it is recommended to call <code>super.addHeadersToResponse</code> to avoid inadvertantly disabling functionality.
* inadvertantly disabling functionality.
* </p> * </p>
*/ */
public void addHeadersToResponse(HttpServletResponse theHttpResponse) { public void addHeadersToResponse(HttpServletResponse theHttpResponse) {
@ -236,14 +234,13 @@ public class RestfulServer extends HttpServlet {
Package pack = annotation.annotationType().getPackage(); Package pack = annotation.annotationType().getPackage();
if (pack.equals(IdParam.class.getPackage())) { if (pack.equals(IdParam.class.getPackage())) {
if (!allowableParams.contains(annotation.annotationType())) { 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); resourceBinding.addMethod(foundMethodBinding);
ourLog.debug(" * Method: {}#{} is a handler", theProvider.getClass(), m.getName()); 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 * 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
* providers should generally use this context if one is needed, as opposed to creating their own. * creating their own.
*/ */
public FhirContext getFhirContext() { public FhirContext getFhirContext() {
return myFhirContext; return myFhirContext;
@ -336,26 +333,16 @@ public class RestfulServer extends HttpServlet {
} }
/** /**
* Provides the security manager, or <code>null</code> if none * 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 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}
*/ */
public IServerAddressStrategy getServerAddressStrategy() { public IServerAddressStrategy getServerAddressStrategy() {
return myServerAddressStrategy; return myServerAddressStrategy;
} }
/** /**
* Returns the server conformance provider, which is the provider that is used to generate the server's conformance * Returns the server conformance provider, which is the provider that is used to generate the server's conformance (metadata) statement.
* (metadata) statement.
* <p> * <p>
* By default, the {@link ServerConformanceProvider} is used, but this can be changed, or set to <code>null</code> * 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.
* if you do not wish to export a conformance statement.
* </p> * </p>
*/ */
public Object getServerConformanceProvider() { 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, * 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.
* but can be helpful to set with something appropriate.
* *
* @see RestfulServer#setServerName(String) * @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 * 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.
* only, but can be helpful to set with something appropriate.
*/ */
public String getServerVersion() { public String getServerVersion() {
return myServerVersion; return myServerVersion;
@ -417,12 +402,14 @@ public class RestfulServer extends HttpServlet {
NarrativeModeEnum narrativeMode = determineNarrativeMode(theRequest); NarrativeModeEnum narrativeMode = determineNarrativeMode(theRequest);
boolean respondGzip = theRequest.isRespondGzip(); 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--) { for (int i = getInterceptors().size() - 1; i >= 0; i--) {
IServerInterceptor next = getInterceptors().get(i); IServerInterceptor next = getInterceptors().get(i);
boolean continueProcessing = next.outgoingResponse(theRequest, bundle, theRequest.getServletRequest(), theRequest.getServletResponse()); boolean continueProcessing = next.outgoingResponse(theRequest, bundle, theRequest.getServletRequest(), theRequest.getServletResponse());
if (!continueProcessing) { if (!continueProcessing) {
ourLog.debug("Interceptor {} returned false, not continuing processing");
return; return;
} }
} }
@ -435,31 +422,29 @@ public class RestfulServer extends HttpServlet {
for (IServerInterceptor next : myInterceptors) { for (IServerInterceptor next : myInterceptors) {
boolean continueProcessing = next.incomingRequestPreProcessed(theRequest, theResponse); boolean continueProcessing = next.incomingRequestPreProcessed(theRequest, theResponse);
if (!continueProcessing) { if (!continueProcessing) {
ourLog.debug("Interceptor {} returned false, not continuing processing");
return; return;
} }
} }
String fhirServerBase = null; String fhirServerBase = null;
boolean requestIsBrowser = requestIsBrowser(theRequest); boolean requestIsBrowser = requestIsBrowser(theRequest);
RequestDetails requestDetails=null;
try { try {
if (null != mySecurityManager) {
mySecurityManager.authenticate(theRequest);
}
String resourceName = null; String resourceName = null;
String requestFullPath = StringUtils.defaultString(theRequest.getRequestURI()); String requestFullPath = StringUtils.defaultString(theRequest.getRequestURI());
String servletPath = StringUtils.defaultString(theRequest.getServletPath()); String servletPath = StringUtils.defaultString(theRequest.getServletPath());
StringBuffer requestUrl = theRequest.getRequestURL(); StringBuffer requestUrl = theRequest.getRequestURL();
String servletContextPath = ""; String servletContextPath = "";
// if (getServletContext().getMajorVersion() >= 3) { // if (getServletContext().getMajorVersion() >= 3) {
// // getServletContext is only supported in version 3+ of servlet-api // // getServletContext is only supported in version 3+ of servlet-api
if (getServletContext() != null) { if (getServletContext() != null) {
servletContextPath = StringUtils.defaultString(getServletContext().getContextPath()); servletContextPath = StringUtils.defaultString(getServletContext().getContextPath());
} }
// } // }
if (ourLog.isTraceEnabled()) { if (ourLog.isTraceEnabled()) {
ourLog.trace("Request FullPath: {}", requestFullPath); ourLog.trace("Request FullPath: {}", requestFullPath);
ourLog.trace("Servlet Path: {}", servletPath); ourLog.trace("Servlet Path: {}", servletPath);
@ -609,7 +594,7 @@ public class RestfulServer extends HttpServlet {
throw new InvalidRequestException(b.toString()); throw new InvalidRequestException(b.toString());
} }
RequestDetails requestDetails = r; requestDetails = r;
requestDetails.setResourceOperationType(resourceMethod.getResourceOperationType()); requestDetails.setResourceOperationType(resourceMethod.getResourceOperationType());
requestDetails.setSystemOperationType(resourceMethod.getSystemOperationType()); requestDetails.setSystemOperationType(resourceMethod.getSystemOperationType());
requestDetails.setOtherOperationType(resourceMethod.getOtherOperationType()); requestDetails.setOtherOperationType(resourceMethod.getOtherOperationType());
@ -617,6 +602,7 @@ public class RestfulServer extends HttpServlet {
for (IServerInterceptor next : myInterceptors) { for (IServerInterceptor next : myInterceptors) {
boolean continueProcessing = next.incomingRequestPostProcessed(requestDetails, theRequest, theResponse); boolean continueProcessing = next.incomingRequestPostProcessed(requestDetails, theRequest, theResponse);
if (!continueProcessing) { if (!continueProcessing) {
ourLog.debug("Interceptor {} returned false, not continuing processing");
return; return;
} }
} }
@ -624,6 +610,15 @@ public class RestfulServer extends HttpServlet {
resourceMethod.invokeServer(this, r); resourceMethod.invokeServer(this, r);
} catch (AuthenticationException e) { } catch (AuthenticationException e) {
for (int i = getInterceptors().size() - 1; i >= 0; i--) {
IServerInterceptor next = getInterceptors().get(i);
if (!next.handleException(requestDetails, e, theRequest, theResponse)) {
ourLog.debug("Interceptor {} returned false, not continuing processing");
return;
}
}
if (requestIsBrowser) { if (requestIsBrowser) {
// if request is coming from a browser, prompt the user to enter login credentials // if request is coming from a browser, prompt the user to enter login credentials
theResponse.setHeader("WWW-Authenticate", "BASIC realm=\"FHIR\""); theResponse.setHeader("WWW-Authenticate", "BASIC realm=\"FHIR\"");
@ -636,14 +631,30 @@ public class RestfulServer extends HttpServlet {
} catch (Throwable 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(requestDetails, e, theRequest, theResponse)) {
ourLog.debug("Interceptor {} returned false, not continuing processing");
return;
}
}
BaseOperationOutcome oo = null; BaseOperationOutcome oo = null;
int statusCode = 500; int statusCode = Constants.STATUS_HTTP_500_INTERNAL_ERROR;
if (e instanceof BaseServerResponseException) { if (e instanceof BaseServerResponseException) {
oo = ((BaseServerResponseException) e).getOperationOutcome(); oo = ((BaseServerResponseException) e).getOperationOutcome();
statusCode = ((BaseServerResponseException) e).getStatusCode(); statusCode = ((BaseServerResponseException) e).getStatusCode();
} }
/*
* Generate an OperationOutcome to return, unless the exception throw by
* the resource provider had one
*/
if (oo == null) { if (oo == null) {
try { try {
oo = (BaseOperationOutcome) myFhirContext.getResourceDefinition("OperationOutcome").getImplementingClass().newInstance(); 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); ourLog.error("Failed to instantiate OperationOutcome resource instance", e1);
throw new ServletException("Failed to instantiate OperationOutcome resource instance", e1); throw new ServletException("Failed to instantiate OperationOutcome resource instance", e1);
} }
BaseIssue issue = oo.addIssue(); BaseIssue issue = oo.addIssue();
issue.getSeverityElement().setValue("error"); issue.getSeverityElement().setValue("error");
if (e instanceof InternalErrorException) { if (e instanceof InternalErrorException) {
ourLog.error("Failure during REST processing", e); ourLog.error("Failure during REST processing", e);
issue.getDetailsElement().setValue(e.toString() + "\n\n" + ExceptionUtils.getStackTrace(e)); issue.getDetailsElement().setValue(e.toString() + "\n\n" + ExceptionUtils.getStackTrace(e));
statusCode = ((InternalErrorException) e).getStatusCode();
} else if (e instanceof BaseServerResponseException) { } else if (e instanceof BaseServerResponseException) {
ourLog.warn("Failure during REST processing: {}", e.toString()); ourLog.warn("Failure during REST processing: {}", e.toString());
statusCode = ((BaseServerResponseException) e).getStatusCode(); BaseServerResponseException baseServerResponseException = (BaseServerResponseException) e;
statusCode = baseServerResponseException.getStatusCode();
issue.getDetailsElement().setValue(e.getMessage()); 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 { } 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)); 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, * 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
* but subclasses may put initialization code in {@link #initialize()}, which is called immediately before beginning * called immediately before beginning initialization of the restful server's internal init.
* initialization of the restful server's internal init.
*/ */
@Override @Override
public final void init() throws ServletException { public final void init() throws ServletException {
@ -691,11 +711,6 @@ public class RestfulServer extends HttpServlet {
try { try {
ourLog.info("Initializing HAPI FHIR restful server"); 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 providedResourceScanner = new ProvidedResourceScanner(getFhirContext());
providedResourceScanner.scanForProvidedResources(this); providedResourceScanner.scanForProvidedResources(this);
@ -735,14 +750,13 @@ public class RestfulServer extends HttpServlet {
ourLog.error("An error occurred while loading request handlers!", ex); ourLog.error("An error occurred while loading request handlers!", ex);
throw new ServletException("Failed to initialize FHIR Restful server", ex); throw new ServletException("Failed to initialize FHIR Restful server", ex);
} }
myStarted = true; myStarted = true;
ourLog.info("A FHIR has been lit on this server"); 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 * This method may be overridden by subclasses to do perform initialization that needs to be performed prior to the server being used.
* server being used.
*/ */
protected void initialize() throws ServletException { protected void initialize() throws ServletException {
// nothing by default // 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} * 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
* (which is the default), the server will automatically add a profile tag based on the class of the resource(s) * on the class of the resource(s) being returned.
* being returned.
* *
* @param theAddProfileTag * @param theAddProfileTag
* The behaviour enum (must not be null) * The behaviour enum (must not be null)
@ -784,7 +797,6 @@ public class RestfulServer extends HttpServlet {
myImplementationDescription = theImplementationDescription; myImplementationDescription = theImplementationDescription;
} }
/** /**
* Sets (or clears) the list of interceptors * 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 * 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 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}
*/ */
public void setServerAddressStrategy(IServerAddressStrategy theServerAddressStrategy) { public void setServerAddressStrategy(IServerAddressStrategy theServerAddressStrategy) {
Validate.notNull(theServerAddressStrategy, "Server address strategy can not be null"); 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 * Returns the server conformance provider, which is the provider that is used to generate the server's conformance (metadata) statement.
* (metadata) statement.
* <p> * <p>
* By default, the {@link ServerConformanceProvider} is used, but this can be changed, or set to <code>null</code> * 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.
* if you do not wish to export a conformance statement.
* </p> * </p>
* Note that this method can only be called before the server is initialized. * Note that this method can only be called before the server is initialized.
* *
* @throws IllegalStateException * @throws IllegalStateException
* Note that this method can only be called prior to {@link #init() initialization} and will throw an * Note that this method can only be called prior to {@link #init() initialization} and will throw an {@link IllegalStateException} if called after that.
* {@link IllegalStateException} if called after that.
*/ */
public void setServerConformanceProvider(Object theServerConformanceProvider) { public void setServerConformanceProvider(Object theServerConformanceProvider) {
if (myStarted) { 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, * 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.
* but can be helpful to set with something appropriate.
*/ */
public void setServerName(String theServerName) { public void setServerName(String theServerName) {
myServerName = theServerName; myServerName = theServerName;
} }
/** /**
* Gets the server's version, as exported in conformance profiles exported by the server. This is informational * 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.
* only, but can be helpful to set with something appropriate.
*/ */
public void setServerVersion(String theServerVersion) { public void setServerVersion(String theServerVersion) {
myServerVersion = theServerVersion; myServerVersion = theServerVersion;
} }
/** /**
* If set to <code>true</code> (default is false), the server will use browser friendly content-types (instead of * 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
* standard FHIR ones) when it detects that the request is coming from a browser instead of a FHIR * instead of a FHIR
*/ */
public void setUseBrowserFriendlyContentTypes(boolean theUseBrowserFriendlyContentTypes) { public void setUseBrowserFriendlyContentTypes(boolean theUseBrowserFriendlyContentTypes) {
myUseBrowserFriendlyContentTypes = 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, public static Bundle createBundleFromBundleProvider(RestfulServer theServer, HttpServletResponse theHttpResponse, IBundleProvider theResult, EncodingEnum theResponseEncoding,
NarrativeModeEnum theNarrativeMode, int theOffset, Integer theLimit, String theSearchId) { String theServerBase, String theCompleteUrl, boolean thePrettyPrint, boolean theRequestIsBrowser, NarrativeModeEnum theNarrativeMode, int theOffset, Integer theLimit, String theSearchId) {
theHttpResponse.setStatus(200); theHttpResponse.setStatus(200);
if (theRequestIsBrowser && theServer.isUseBrowserFriendlyContentTypes()) { if (theRequestIsBrowser && theServer.isUseBrowserFriendlyContentTypes()) {
@ -962,7 +961,7 @@ public class RestfulServer extends HttpServlet {
numToReturn = theResult.size(); numToReturn = theResult.size();
resourceList = theResult.getResources(0, numToReturn); resourceList = theResult.getResources(0, numToReturn);
validateResourceListNotNull(resourceList); validateResourceListNotNull(resourceList);
} else { } else {
IPagingProvider pagingProvider = theServer.getPagingProvider(); IPagingProvider pagingProvider = theServer.getPagingProvider();
if (theLimit == null) { if (theLimit == null) {
@ -1045,11 +1044,11 @@ public class RestfulServer extends HttpServlet {
Set<String> containedIds = new HashSet<String>(); Set<String> containedIds = new HashSet<String>();
for (IResource nextContained : next.getContained().getContainedResources()) { for (IResource nextContained : next.getContained().getContainedResources()) {
if (nextContained.getId().isEmpty()==false) { if (nextContained.getId().isEmpty() == false) {
containedIds.add(nextContained.getId().getValue()); containedIds.add(nextContained.getId().getValue());
} }
} }
if (theContext.getNarrativeGenerator() != null) { if (theContext.getNarrativeGenerator() != null) {
String title = theContext.getNarrativeGenerator().generateTitle(next); String title = theContext.getNarrativeGenerator().generateTitle(next);
ourLog.trace("Narrative generator created title: {}", title); 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 // Don't add contained IDs as top level resources
continue; continue;
} }
IdDt id = nextRes.getId().toVersionless(); IdDt id = nextRes.getId().toVersionless();
if (id.hasResourceType() == false) { if (id.hasResourceType() == false) {
String resName = theContext.getResourceDefinition(nextRes).getName(); 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 * 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.
* incoming HttpServletRequest's <code>"_format"</code> parameter and <code>"Accept:"</code>
* HTTP header.
*/ */
public static EncodingEnum determineResponseEncoding(HttpServletRequest theReq) { public static EncodingEnum determineResponseEncoding(HttpServletRequest theReq) {
String[] format = theReq.getParameterValues(Constants.PARAM_FORMAT); String[] format = theReq.getParameterValues(Constants.PARAM_FORMAT);
@ -1283,8 +1280,8 @@ public class RestfulServer extends HttpServlet {
return prettyPrint; return prettyPrint;
} }
public static void streamResponseAsBundle(RestfulServer theServer, HttpServletResponse theHttpResponse, Bundle bundle, EncodingEnum theResponseEncoding, String theServerBase, boolean thePrettyPrint, NarrativeModeEnum theNarrativeMode, boolean theRespondGzip) public static void streamResponseAsBundle(RestfulServer theServer, HttpServletResponse theHttpResponse, Bundle bundle, EncodingEnum theResponseEncoding, String theServerBase,
throws IOException { boolean thePrettyPrint, NarrativeModeEnum theNarrativeMode, boolean theRespondGzip) throws IOException {
assert !theServerBase.endsWith("/"); assert !theServerBase.endsWith("/");
Writer writer = getWriter(theHttpResponse, theRespondGzip); 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, public static void streamResponseAsResource(RestfulServer theServer, HttpServletResponse theHttpResponse, IResource theResource, EncodingEnum theResponseEncoding, boolean thePrettyPrint,
String theServerBase) throws IOException { boolean theRequestIsBrowser, NarrativeModeEnum theNarrativeMode, boolean theRespondGzip, String theServerBase) throws IOException {
int stausCode = 200; int stausCode = 200;
streamResponseAsResource(theServer, theHttpResponse, theResource, theResponseEncoding, thePrettyPrint, theRequestIsBrowser, theNarrativeMode, stausCode, theRespondGzip, theServerBase); 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, private static void streamResponseAsResource(RestfulServer theServer, HttpServletResponse theHttpResponse, IResource theResource, EncodingEnum theResponseEncoding, boolean thePrettyPrint,
boolean theRespondGzip, String theServerBase) throws IOException { boolean theRequestIsBrowser, NarrativeModeEnum theNarrativeMode, int stausCode, boolean theRespondGzip, String theServerBase) throws IOException {
theHttpResponse.setStatus(stausCode); theHttpResponse.setStatus(stausCode);
if (theResource.getId() != null && theResource.getId().hasIdPart() && isNotBlank(theServerBase)) { if (theResource.getId() != null && theResource.getId().hasIdPart() && isNotBlank(theServerBase)) {

View File

@ -1,7 +1,9 @@
package ca.uhn.fhir.rest.server.exceptions; package ca.uhn.fhir.rest.server.exceptions;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import ca.uhn.fhir.model.base.resource.BaseOperationOutcome; 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 * 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, and RESTful server methods should also only call subclasses of this exception * subclasses of this exception type.
* type.
*/ */
public abstract class BaseServerResponseException extends RuntimeException { public abstract class BaseServerResponseException extends RuntimeException {
@ -49,11 +50,12 @@ public abstract class BaseServerResponseException extends RuntimeException {
registerExceptionType(NotImplementedOperationException.STATUS_CODE, NotImplementedOperationException.class); registerExceptionType(NotImplementedOperationException.STATUS_CODE, NotImplementedOperationException.class);
} }
private List<String> myAdditionalMessages = null;
private BaseOperationOutcome myBaseOperationOutcome; private BaseOperationOutcome myBaseOperationOutcome;
private String myResponseBody; private String myResponseBody;
private String myResponseMimeType; private String myResponseMimeType;
private int myStatusCode; private int myStatusCode;
/** /**
* Constructor * Constructor
* *
@ -67,7 +69,24 @@ public abstract class BaseServerResponseException extends RuntimeException {
myStatusCode = theStatusCode; myStatusCode = theStatusCode;
myBaseOperationOutcome = null; 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 * Constructor
* *
@ -76,8 +95,7 @@ public abstract class BaseServerResponseException extends RuntimeException {
* @param theMessage * @param theMessage
* The message * The message
* @param theBaseOperationOutcome * @param theBaseOperationOutcome
* An BaseOperationOutcome resource to return to the calling client (in a server) or the BaseOperationOutcome * An BaseOperationOutcome resource to return to the calling client (in a server) or the BaseOperationOutcome that was returned from the server (in a client)
* that was returned from the server (in a client)
*/ */
public BaseServerResponseException(int theStatusCode, String theMessage, BaseOperationOutcome theBaseOperationOutcome) { public BaseServerResponseException(int theStatusCode, String theMessage, BaseOperationOutcome theBaseOperationOutcome) {
super(theMessage); super(theMessage);
@ -111,8 +129,7 @@ public abstract class BaseServerResponseException extends RuntimeException {
* @param theCause * @param theCause
* The underlying cause exception * The underlying cause exception
* @param theBaseOperationOutcome * @param theBaseOperationOutcome
* An BaseOperationOutcome resource to return to the calling client (in a server) or the BaseOperationOutcome * An BaseOperationOutcome resource to return to the calling client (in a server) or the BaseOperationOutcome that was returned from the server (in a client)
* that was returned from the server (in a client)
*/ */
public BaseServerResponseException(int theStatusCode, String theMessage, Throwable theCause, BaseOperationOutcome theBaseOperationOutcome) { public BaseServerResponseException(int theStatusCode, String theMessage, Throwable theCause, BaseOperationOutcome theBaseOperationOutcome) {
super(theMessage, theCause); super(theMessage, theCause);
@ -142,8 +159,7 @@ public abstract class BaseServerResponseException extends RuntimeException {
* @param theCause * @param theCause
* The underlying cause exception * The underlying cause exception
* @param theBaseOperationOutcome * @param theBaseOperationOutcome
* An BaseOperationOutcome resource to return to the calling client (in a server) or the BaseOperationOutcome * An BaseOperationOutcome resource to return to the calling client (in a server) or the BaseOperationOutcome that was returned from the server (in a client)
* that was returned from the server (in a client)
*/ */
public BaseServerResponseException(int theStatusCode, Throwable theCause, BaseOperationOutcome theBaseOperationOutcome) { public BaseServerResponseException(int theStatusCode, Throwable theCause, BaseOperationOutcome theBaseOperationOutcome) {
super(theCause.toString(), theCause); super(theCause.toString(), theCause);
@ -151,6 +167,10 @@ public abstract class BaseServerResponseException extends RuntimeException {
myBaseOperationOutcome = theBaseOperationOutcome; 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> * 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 * 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.
* server, or <code>null</code> otherwise.
* <p> * <p>
* In a restful server, this method is currently ignored. * In a restful server, this method is currently ignored.
* </p> * </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 * In a RESTful client, this method will be populated with the HTTP status code that was returned with the HTTP response.
* response.
* <p> * <p>
* In a restful server, this method is currently ignored. * In a restful server, this method is currently ignored.
* </p> * </p>
@ -188,11 +206,11 @@ public abstract class BaseServerResponseException extends RuntimeException {
} }
/** /**
* Sets the BaseOperationOutcome resource associated with this exception. In server * 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, this is the OperartionOutcome resource to include with the HTTP response. In * implementations you should not call this method.
* client implementations you should not call this method.
* *
* @param theBaseOperationOutcome The BaseOperationOutcome resource * @param theBaseOperationOutcome
* The BaseOperationOutcome resource
*/ */
public void setOperationOutcome(BaseOperationOutcome theBaseOperationOutcome) { public void setOperationOutcome(BaseOperationOutcome theBaseOperationOutcome) {
myBaseOperationOutcome = theBaseOperationOutcome; myBaseOperationOutcome = theBaseOperationOutcome;

View File

@ -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. * 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) { public UnprocessableEntityException(String... theMessage) {
super(STATUS_CODE, theMessage[0]); // TODO: this used to generate an OperationOutcome - why? super(STATUS_CODE, theMessage);
} }
} }

View File

@ -64,7 +64,8 @@ public interface IServerInterceptor {
* </p> * </p>
* *
* @param theRequestDetails * @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 * @param theRequest
* The incoming request * The incoming request
* @param theResponse * @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 * 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 * @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 * @param theResponseObject
* The actual object which is being streamed to the client as a response * The actual object which is being streamed to the client as a response
* @param theRequest * @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 * 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 * @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 * @param theResponseObject
* The actual object which is being streamed to the client as a response * The actual object which is being streamed to the client as a response
* @param theRequest * @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 * 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 * @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 * @param theResponseObject
* The actual object which is being streamed to the client as a response * The actual object which is being streamed to the client as a response
* @param theRequest * @param theRequest
@ -171,7 +175,9 @@ public interface IServerInterceptor {
* </p> * </p>
* *
* @param theRequestDetails * @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 * @param theResponseObject
* The actual object which is being streamed to the client as a response * The actual object which is being streamed to the client as a response
* @param theRequest * @param theRequest

View File

@ -357,6 +357,8 @@ public class XmlUtil {
case '"': case '"':
case '&': case '&':
hasEscapable = true; hasEscapable = true;
default:
break;
} }
} }

View File

@ -33,6 +33,7 @@ import ca.uhn.fhir.rest.annotation.Search;
import ca.uhn.fhir.rest.param.StringParam; import ca.uhn.fhir.rest.param.StringParam;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.fhir.util.PortUtil; import ca.uhn.fhir.util.PortUtil;
/** /**
@ -54,6 +55,21 @@ public class ExceptionTest {
ourExceptionType=null; 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 @Test
public void testInternalError() throws Exception { public void testInternalError() throws Exception {
{ {
@ -162,10 +178,15 @@ public class ExceptionTest {
@Search @Search
public List<Patient> findPatient(@RequiredParam(name = "throwInternalError") StringParam theParam) { public List<Patient> throwInternalError(@RequiredParam(name = "throwInternalError") StringParam theParam) {
throw new InternalErrorException("Exception Text"); throw new InternalErrorException("Exception Text");
} }
@Search
public List<Patient> throwUnprocessableEntityWithMultipleMessages(@RequiredParam(name = "throwUnprocessableEntityWithMultipleMessages") StringParam theParam) {
throw new UnprocessableEntityException("message1", "message2", "message3");
}
@Override @Override
public Class<? extends IResource> getResourceType() { public Class<? extends IResource> getResourceType() {
return Patient.class; return Patient.class;

View File

@ -1378,11 +1378,6 @@ public class ResfulServerMethodTest {
return myResourceProviders; return myResourceProviders;
} }
@Override
public ISecurityManager getSecurityManager() {
return null;
}
} }
} }

View File

@ -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;
}
}
}

View File

@ -221,7 +221,7 @@ public class TinderStructuresMojo extends AbstractMojo {
String dtOutputDir = "target/generated-sources/tinder/ca/uhn/fhir/model/dev/composite"; String dtOutputDir = "target/generated-sources/tinder/ca/uhn/fhir/model/dev/composite";
ResourceGeneratorUsingSpreadsheet rp = new ResourceGeneratorUsingSpreadsheet("dev", "."); 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")); "organization","location","relatedperson","appointment","slot","order","availability","device", "valueset"));
rp.parse(); rp.parse();
rp.bindValueSets(vsp); rp.bindValueSets(vsp);

1
list_releases.sh Normal file
View File

@ -0,0 +1 @@
curl https://api.github.com/repos/jamesagnew/hapi-fhir/releases

28
pom.xml
View File

@ -28,6 +28,13 @@
<url>https://github.com/jamesagnew/hapi-fhir/issues/</url> <url>https://github.com/jamesagnew/hapi-fhir/issues/</url>
</issueManagement> </issueManagement>
<distributionManagement>
<site>
<id>git.server</id>
<url>scm:git:git@github.com:jamesagnew/hapi-fhir.git</url>
</site>
</distributionManagement>
<scm> <scm>
<connection>scm:git:git@github.com:jamesagnew/hapi-fhir.git</connection> <connection>scm:git:git@github.com:jamesagnew/hapi-fhir.git</connection>
<url>scm:git:git@github.com:jamesagnew/hapi-fhir.git</url> <url>scm:git:git@github.com:jamesagnew/hapi-fhir.git</url>
@ -502,12 +509,33 @@
</reportSet> </reportSet>
</reportSets> </reportSets>
</plugin> </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> <!-- <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-linkcheck-plugin</artifactId>
<version>1.1</version> </plugin> --> <version>1.1</version> </plugin> -->
</plugins> </plugins>
</reporting> </reporting>
<profiles> <profiles>
<profile>
<id>ROOT</id>
<modules>
</modules>
</profile>
<profile> <profile>
<id>SIGN_ARTIFACTS</id> <id>SIGN_ARTIFACTS</id>
<activation> <activation>

View File

@ -7,11 +7,19 @@
</properties> </properties>
<body> <body>
<release version="0.8" date="TBD"> <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 Profile generation on the server was not working due to IdDt being
incorrectly used. Thanks to Bill de Beaubien for the pull request! incorrectly used. Thanks to Bill de Beaubien for the pull request!
</action> </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 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! defined extension which was of a composite type. Thanks to Bill de Beaubien for the pull request!
</action> </action>
@ -19,7 +27,7 @@
Remove unnessecary IOException from narrative generator API. Thanks to Remove unnessecary IOException from narrative generator API. Thanks to
Petro Mykhailysyn for the pull request! Petro Mykhailysyn for the pull request!
</action> </action>
<action type="add" issue="48"> <action type="add" issue="48" dev="wdebeau1">
Introduced a new Introduced a new
<![CDATA[<code>@ProvidesResources</code>]]> annotation which can be added to <![CDATA[<code>@ProvidesResources</code>]]> annotation which can be added to
resource provider and servers to allow them to declare additional resource 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 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). returns a default Patient, but sometimes uses a custom subclass).
Thanks to Bill de Beaubien for the pull request! 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> </action>
</release> </release>
<release version="0.7" date="2014-Oct-23"> <release version="0.7" date="2014-Oct-23">

View File

@ -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&#xa;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&#xa;Provider&#xa;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

View File

@ -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

View File

@ -69,6 +69,32 @@
of the operation being invoked (resource, bundle, etc.) of the operation being invoked (resource, bundle, etc.)
</li> </li>
</ul> </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"/> <br clear="all"/>
<subsection name="Registering Interceptors"> <subsection name="Registering Interceptors">
@ -129,6 +155,29 @@
</section> </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> </body>
</document> </document>

View File

@ -28,21 +28,21 @@
</p> </p>
<ul> <ul>
<li> <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>
<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). for DSTU1 (all contents of this JAR were previously found in hapi-fhir-base).
</li> </li>
<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 <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 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 resources, or both depending on your needs. Note that using DEV resources may introduce
incompatibilities with other frameworks however.<br/><br/> incompatibilities with other frameworks however. If you are including this JAR,
<b>You must also include hapi-fhir-structures-dstu-[version].jar</b> if you include <b>you must also include hapi-fhir-structures-dstu-[version].jar</b>.
the dev structures JAR at this time. Hopefully at some point soon this requirement Hopefully by the time 0.8 is final this requirement will be relaxed, but for now it
will be relaxed. is mandatory.
</li> </li>
</ul> </ul>