Indicate support for conditional create/update/delete in DSTU2 server conformance statement

This commit is contained in:
jamesagnew 2015-06-25 22:22:19 -04:00
parent 2d05b48bdc
commit 8b65a9aedf
6 changed files with 430 additions and 226 deletions

View File

@ -85,10 +85,15 @@ import ca.uhn.fhir.util.ReflectionUtil;
public abstract class BaseMethodBinding<T> implements IClientResponseHandler<T> { public abstract class BaseMethodBinding<T> implements IClientResponseHandler<T> {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseMethodBinding.class); private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseMethodBinding.class);
/**
* @see BaseMethodBinding#loadRequestContents(RequestDetails)
*/
private static volatile IRequestReader ourRequestReader;
private FhirContext myContext; private FhirContext myContext;
private Method myMethod; private Method myMethod;
private List<IParameter> myParameters; private List<IParameter> myParameters;
private Object myProvider; private Object myProvider;
private boolean mySupportsConditional;
public BaseMethodBinding(Method theMethod, FhirContext theContext, Object theProvider) { public BaseMethodBinding(Method theMethod, FhirContext theContext, Object theProvider) {
assert theMethod != null; assert theMethod != null;
@ -98,6 +103,14 @@ public abstract class BaseMethodBinding<T> implements IClientResponseHandler<T>
myContext = theContext; myContext = theContext;
myProvider = theProvider; myProvider = theProvider;
myParameters = MethodUtil.getResourceParameters(theContext, theMethod, theProvider, getResourceOperationType()); myParameters = MethodUtil.getResourceParameters(theContext, theMethod, theProvider, getResourceOperationType());
for (IParameter next : myParameters) {
if (next instanceof ConditionalParamBinder) {
mySupportsConditional = true;
break;
}
}
} }
protected IParser createAppropriateParserForParsingResponse(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode) { protected IParser createAppropriateParserForParsingResponse(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode) {
@ -223,7 +236,8 @@ public abstract class BaseMethodBinding<T> implements IClientResponseHandler<T>
public abstract RestfulOperationTypeEnum getResourceOperationType(); public abstract RestfulOperationTypeEnum getResourceOperationType();
/** /**
* Returns the value of {@link #getResourceOperationType()} or {@link #getSystemOperationType()} or {@link #getOtherOperationType()} * Returns the value of {@link #getResourceOperationType()} or {@link #getSystemOperationType()} or
* {@link #getOtherOperationType()}
*/ */
public String getResourceOrSystemOperationType() { public String getResourceOrSystemOperationType() {
Enum<?> retVal = getResourceOperationType(); Enum<?> retVal = getResourceOperationType();
@ -264,11 +278,20 @@ public abstract class BaseMethodBinding<T> implements IClientResponseHandler<T>
} }
} }
/**
* Does this method have a parameter annotated with {@link ConditionalParamBinder}. Note that many operations don't
* actually support this paramter, so this will only return true occasionally.
*/
public boolean isSupportsConditional() {
return mySupportsConditional;
}
protected byte[] loadRequestContents(RequestDetails theRequest) throws IOException { protected byte[] loadRequestContents(RequestDetails theRequest) throws IOException {
/* /*
* This is weird, but this class is used both in clients and in servers, and we want to avoid needing to depend on servlet-api in clients since there is no point. So we dynamically load a class * This is weird, but this class is used both in clients and in servers, and we want to avoid needing to depend on
* that does the servlet processing in servers. Down the road it may make sense to just split the method binding classes into server and client versions, but this isn't actually a huge deal I * servlet-api in clients since there is no point. So we dynamically load a class that does the servlet processing
* don't think. * in servers. Down the road it may make sense to just split the method binding classes into server and client
* versions, but this isn't actually a huge deal I don't think.
*/ */
IRequestReader reader = ourRequestReader; IRequestReader reader = ourRequestReader;
if (reader == null) { if (reader == null) {
@ -385,16 +408,14 @@ public abstract class BaseMethodBinding<T> implements IClientResponseHandler<T>
if (theProvider instanceof IResourceProvider) { if (theProvider instanceof IResourceProvider) {
returnTypeFromRp = ((IResourceProvider) theProvider).getResourceType(); returnTypeFromRp = ((IResourceProvider) theProvider).getResourceType();
if (!verifyIsValidResourceReturnType(returnTypeFromRp)) { if (!verifyIsValidResourceReturnType(returnTypeFromRp)) {
throw new ConfigurationException("getResourceType() from " + IResourceProvider.class.getSimpleName() + " type " + theMethod.getDeclaringClass().getCanonicalName() + " returned " throw new ConfigurationException("getResourceType() from " + IResourceProvider.class.getSimpleName() + " type " + theMethod.getDeclaringClass().getCanonicalName() + " returned " + toLogString(returnTypeFromRp) + " - Must return a resource type");
+ toLogString(returnTypeFromRp) + " - Must return a resource type");
} }
} }
Class<?> returnTypeFromMethod = theMethod.getReturnType(); Class<?> returnTypeFromMethod = theMethod.getReturnType();
if (getTags != null) { if (getTags != null) {
if (!TagList.class.equals(returnTypeFromMethod)) { if (!TagList.class.equals(returnTypeFromMethod)) {
throw new ConfigurationException("Method '" + theMethod.getName() + "' from type " + theMethod.getDeclaringClass().getCanonicalName() + " is annotated with @" throw new ConfigurationException("Method '" + theMethod.getName() + "' from type " + theMethod.getDeclaringClass().getCanonicalName() + " is annotated with @" + GetTags.class.getSimpleName() + " but does not return type " + TagList.class.getName());
+ GetTags.class.getSimpleName() + " but does not return type " + TagList.class.getName());
} }
} else if (MethodOutcome.class.equals(returnTypeFromMethod)) { } else if (MethodOutcome.class.equals(returnTypeFromMethod)) {
// returns a method outcome // returns a method outcome
@ -407,15 +428,13 @@ public abstract class BaseMethodBinding<T> implements IClientResponseHandler<T>
} else if (Collection.class.isAssignableFrom(returnTypeFromMethod)) { } else if (Collection.class.isAssignableFrom(returnTypeFromMethod)) {
returnTypeFromMethod = ReflectionUtil.getGenericCollectionTypeOfMethodReturnType(theMethod); returnTypeFromMethod = ReflectionUtil.getGenericCollectionTypeOfMethodReturnType(theMethod);
if (!verifyIsValidResourceReturnType(returnTypeFromMethod) && !IResource.class.equals(returnTypeFromMethod)) { if (!verifyIsValidResourceReturnType(returnTypeFromMethod) && !IResource.class.equals(returnTypeFromMethod)) {
throw new ConfigurationException("Method '" + theMethod.getName() + "' from " + IResourceProvider.class.getSimpleName() + " type " + theMethod.getDeclaringClass().getCanonicalName() throw new ConfigurationException("Method '" + theMethod.getName() + "' from " + IResourceProvider.class.getSimpleName() + " type " + theMethod.getDeclaringClass().getCanonicalName() + " returns a collection with generic type " + toLogString(returnTypeFromMethod)
+ " returns a collection with generic type " + toLogString(returnTypeFromMethod)
+ " - Must return a resource type or a collection (List, Set) with a resource type parameter (e.g. List<Patient> or List<IResource> )"); + " - Must return a resource type or a collection (List, Set) with a resource type parameter (e.g. List<Patient> or List<IResource> )");
} }
} else { } else {
if (!IResource.class.equals(returnTypeFromMethod) && !verifyIsValidResourceReturnType(returnTypeFromMethod)) { if (!IResource.class.equals(returnTypeFromMethod) && !verifyIsValidResourceReturnType(returnTypeFromMethod)) {
throw new ConfigurationException("Method '" + theMethod.getName() + "' from " + IResourceProvider.class.getSimpleName() + " type " + theMethod.getDeclaringClass().getCanonicalName() throw new ConfigurationException("Method '" + theMethod.getName() + "' from " + IResourceProvider.class.getSimpleName() + " type " + theMethod.getDeclaringClass().getCanonicalName() + " returns " + toLogString(returnTypeFromMethod) + " - Must return a resource type (eg Patient, "
+ " returns " + toLogString(returnTypeFromMethod) + " - Must return a resource type (eg Patient, " + Bundle.class.getSimpleName() + ", " + IBundleProvider.class.getSimpleName() + Bundle.class.getSimpleName() + ", " + IBundleProvider.class.getSimpleName() + ", etc., see the documentation for more details)");
+ ", etc., see the documentation for more details)");
} }
} }
@ -445,13 +464,12 @@ public abstract class BaseMethodBinding<T> implements IClientResponseHandler<T>
if (returnTypeFromRp != null) { if (returnTypeFromRp != null) {
if (returnTypeFromAnnotation != null && returnTypeFromAnnotation != IResource.class) { if (returnTypeFromAnnotation != null && returnTypeFromAnnotation != IResource.class) {
if (!returnTypeFromRp.isAssignableFrom(returnTypeFromAnnotation)) { if (!returnTypeFromRp.isAssignableFrom(returnTypeFromAnnotation)) {
throw new ConfigurationException("Method '" + theMethod.getName() + "' in type " + theMethod.getDeclaringClass().getCanonicalName() + " returns type " throw new ConfigurationException("Method '" + theMethod.getName() + "' in type " + theMethod.getDeclaringClass().getCanonicalName() + " returns type " + returnTypeFromMethod.getCanonicalName() + " - Must return " + returnTypeFromRp.getCanonicalName()
+ returnTypeFromMethod.getCanonicalName() + " - Must return " + returnTypeFromRp.getCanonicalName() + " (or a subclass of it) per IResourceProvider contract"); + " (or a subclass of it) per IResourceProvider contract");
} }
if (!returnTypeFromRp.isAssignableFrom(returnTypeFromAnnotation)) { if (!returnTypeFromRp.isAssignableFrom(returnTypeFromAnnotation)) {
throw new ConfigurationException("Method '" + theMethod.getName() + "' in type " + theMethod.getDeclaringClass().getCanonicalName() + " claims to return type " throw new ConfigurationException("Method '" + theMethod.getName() + "' in type " + theMethod.getDeclaringClass().getCanonicalName() + " claims to return type " + returnTypeFromAnnotation.getCanonicalName() + " per method annotation - Must return "
+ returnTypeFromAnnotation.getCanonicalName() + " per method annotation - Must return " + returnTypeFromRp.getCanonicalName() + returnTypeFromRp.getCanonicalName() + " (or a subclass of it) per IResourceProvider contract");
+ " (or a subclass of it) per IResourceProvider contract");
} }
returnType = returnTypeFromAnnotation; returnType = returnTypeFromAnnotation;
} else { } else {
@ -460,8 +478,8 @@ public abstract class BaseMethodBinding<T> implements IClientResponseHandler<T>
} else { } else {
if (returnTypeFromAnnotation != IResource.class) { if (returnTypeFromAnnotation != IResource.class) {
if (!verifyIsValidResourceReturnType(returnTypeFromAnnotation)) { if (!verifyIsValidResourceReturnType(returnTypeFromAnnotation)) {
throw new ConfigurationException("Method '" + theMethod.getName() + "' from " + IResourceProvider.class.getSimpleName() + " type " + theMethod.getDeclaringClass().getCanonicalName() throw new ConfigurationException("Method '" + theMethod.getName() + "' from " + IResourceProvider.class.getSimpleName() + " type " + theMethod.getDeclaringClass().getCanonicalName() + " returns " + toLogString(returnTypeFromAnnotation)
+ " returns " + toLogString(returnTypeFromAnnotation) + " according to annotation - Must return a resource type"); + " according to annotation - Must return a resource type");
} }
returnType = returnTypeFromAnnotation; returnType = returnTypeFromAnnotation;
} else { } else {
@ -572,8 +590,7 @@ public abstract class BaseMethodBinding<T> implements IClientResponseHandler<T>
if (obj1 == null) { if (obj1 == null) {
obj1 = object; obj1 = object;
} else { } else {
throw new ConfigurationException("Method " + theNextMethod.getName() + " on type '" + theNextMethod.getDeclaringClass().getSimpleName() + " has annotations @" throw new ConfigurationException("Method " + theNextMethod.getName() + " on type '" + theNextMethod.getDeclaringClass().getSimpleName() + " has annotations @" + obj1.getClass().getSimpleName() + " and @" + object.getClass().getSimpleName() + ". Can not have both.");
+ obj1.getClass().getSimpleName() + " and @" + object.getClass().getSimpleName() + ". Can not have both.");
} }
} }
@ -588,18 +605,6 @@ public abstract class BaseMethodBinding<T> implements IClientResponseHandler<T>
return true; return true;
} }
/**
* @see BaseMethodBinding#loadRequestContents(RequestDetails)
*/
private static volatile IRequestReader ourRequestReader;
/**
* @see BaseMethodBinding#loadRequestContents(RequestDetails)
*/
private static interface IRequestReader {
InputStream getInputStream(RequestDetails theRequestDetails) throws IOException;
}
/** /**
* @see BaseMethodBinding#loadRequestContents(RequestDetails) * @see BaseMethodBinding#loadRequestContents(RequestDetails)
*/ */
@ -620,4 +625,11 @@ public abstract class BaseMethodBinding<T> implements IClientResponseHandler<T>
} }
} }
/**
* @see BaseMethodBinding#loadRequestContents(RequestDetails)
*/
private static interface IRequestReader {
InputStream getInputStream(RequestDetails theRequestDetails) throws IOException;
}
} }

View File

@ -35,10 +35,10 @@ import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
abstract class BaseOutcomeReturningMethodBindingWithResourceParam extends BaseOutcomeReturningMethodBinding { abstract class BaseOutcomeReturningMethodBindingWithResourceParam extends BaseOutcomeReturningMethodBinding {
private int myResourceParameterIndex;
private String myResourceName;
private Class<? extends IBaseResource> myResourceType;
private Integer myIdParamIndex; private Integer myIdParamIndex;
private String myResourceName;
private int myResourceParameterIndex;
private Class<? extends IBaseResource> myResourceType;
public BaseOutcomeReturningMethodBindingWithResourceParam(Method theMethod, FhirContext theContext, Class<?> theMethodAnnotation, Object theProvider) { public BaseOutcomeReturningMethodBindingWithResourceParam(Method theMethod, FhirContext theContext, Class<?> theMethodAnnotation, Object theProvider) {
super(theMethod, theContext, theMethodAnnotation, theProvider); super(theMethod, theContext, theMethodAnnotation, theProvider);

View File

@ -99,7 +99,7 @@ abstract class BaseResourceReturningMethodBinding extends BaseMethodBinding<Obje
myMethodReturnType = MethodReturnTypeEnum.LIST_OF_RESOURCES; myMethodReturnType = MethodReturnTypeEnum.LIST_OF_RESOURCES;
Class<?> collectionType = ReflectionUtil.getGenericCollectionTypeOfMethodReturnType(theMethod); Class<?> collectionType = ReflectionUtil.getGenericCollectionTypeOfMethodReturnType(theMethod);
if (collectionType != null) { if (collectionType != null) {
if (!Object.class.equals(collectionType) && !IResource.class.isAssignableFrom(collectionType)) { if (!Object.class.equals(collectionType) && !IBaseResource.class.isAssignableFrom(collectionType)) {
throw new ConfigurationException("Method " + theMethod.getDeclaringClass().getSimpleName() + "#" + theMethod.getName() + " returns an invalid collection generic type: " throw new ConfigurationException("Method " + theMethod.getDeclaringClass().getSimpleName() + "#" + theMethod.getName() + " returns an invalid collection generic type: "
+ collectionType); + collectionType);
} }

View File

@ -71,8 +71,10 @@ import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
* Server FHIR Provider which serves the conformance statement for a RESTful server implementation * Server FHIR Provider which serves the conformance statement for a RESTful server implementation
* *
* <p> * <p>
* Note: This class is safe to extend, but it is important to note that the same instance of {@link Conformance} is always returned unless {@link #setCache(boolean)} is called with a value of * Note: This class is safe to extend, but it is important to note that the same instance of {@link Conformance} is
* <code>false</code>. This means that if you are adding anything to the returned conformance instance on each call you should call <code>setCache(false)</code> in your provider constructor. * always returned unless {@link #setCache(boolean)} is called with a value of <code>false</code>. This means that if
* you are adding anything to the returned conformance instance on each call you should call
* <code>setCache(false)</code> in your provider constructor.
* </p> * </p>
*/ */
public class ServerConformanceProvider implements IServerConformanceProvider<Conformance> { public class ServerConformanceProvider implements IServerConformanceProvider<Conformance> {
@ -87,8 +89,9 @@ public class ServerConformanceProvider implements IServerConformanceProvider<Con
} }
/** /**
* Gets the value of the "publisher" that will be placed in the generated conformance statement. As this is a mandatory element, the value should not be null (although this is not enforced). The * Gets the value of the "publisher" that will be placed in the generated conformance statement. As this is a
* value defaults to "Not provided" but may be set to null, which will cause this element to be omitted. * mandatory element, the value should not be null (although this is not enforced). The value defaults to
* "Not provided" but may be set to null, which will cause this element to be omitted.
*/ */
public String getPublisher() { public String getPublisher() {
return myPublisher; return myPublisher;
@ -107,7 +110,7 @@ public class ServerConformanceProvider implements IServerConformanceProvider<Con
retVal.setDate(DateTimeDt.withCurrentTime()); retVal.setDate(DateTimeDt.withCurrentTime());
retVal.setFhirVersion("0.5.0"); // TODO: pull from model retVal.setFhirVersion("0.5.0"); // TODO: pull from model
retVal.setAcceptUnknown(false); // TODO: make this configurable - this is a fairly big effort since the parser retVal.setAcceptUnknown(false); // TODO: make this configurable - this is a fairly big effort since the parser
// needs to be modified to actually allow it // needs to be modified to actually allow it
retVal.getImplementation().setDescription(myRestfulServer.getImplementationDescription()); retVal.getImplementation().setDescription(myRestfulServer.getImplementationDescription());
retVal.getSoftware().setName(myRestfulServer.getServerName()); retVal.getSoftware().setName(myRestfulServer.getServerName());
@ -172,6 +175,22 @@ public class ServerConformanceProvider implements IServerConformanceProvider<Con
resource.addInteraction().setCode(resOp); resource.addInteraction().setCode(resOp);
} }
} }
if (nextMethodBinding.isSupportsConditional()) {
switch (resOp) {
case CREATE:
resource.setConditionalCreate(true);
break;
case DELETE:
resource.setConditionalDelete(true);
break;
case UPDATE:
resource.setConditionalUpdate(true);
break;
default:
break;
}
}
} }
} }
@ -386,8 +405,9 @@ public class ServerConformanceProvider implements IServerConformanceProvider<Con
} }
/** /**
* Sets the value of the "publisher" that will be placed in the generated conformance statement. As this is a mandatory element, the value should not be null (although this is not enforced). The * Sets the value of the "publisher" that will be placed in the generated conformance statement. As this is a
* value defaults to "Not provided" but may be set to null, which will cause this element to be omitted. * mandatory element, the value should not be null (although this is not enforced). The value defaults to
* "Not provided" but may be set to null, which will cause this element to be omitted.
*/ */
public void setPublisher(String thePublisher) { public void setPublisher(String thePublisher) {
myPublisher = thePublisher; myPublisher = thePublisher;

View File

@ -1,11 +1,8 @@
package ca.uhn.fhir.rest.server; package ca.uhn.fhir.rest.server;
import static org.hamcrest.Matchers.*; import static org.hamcrest.Matchers.*;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.*;
import static org.junit.Assert.assertThat; import static org.mockito.Mockito.*;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
@ -26,9 +23,15 @@ import ca.uhn.fhir.model.dstu2.resource.Conformance.Rest;
import ca.uhn.fhir.model.dstu2.resource.Conformance.RestResource; import ca.uhn.fhir.model.dstu2.resource.Conformance.RestResource;
import ca.uhn.fhir.model.dstu2.resource.DiagnosticReport; import ca.uhn.fhir.model.dstu2.resource.DiagnosticReport;
import ca.uhn.fhir.model.dstu2.resource.Patient; import ca.uhn.fhir.model.dstu2.resource.Patient;
import ca.uhn.fhir.model.dstu2.valueset.SystemRestfulInteractionEnum;
import ca.uhn.fhir.model.dstu2.valueset.TypeRestfulInteractionEnum;
import ca.uhn.fhir.model.primitive.DateDt; import ca.uhn.fhir.model.primitive.DateDt;
import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.model.primitive.StringDt; import ca.uhn.fhir.model.primitive.StringDt;
import ca.uhn.fhir.rest.annotation.ConditionalUrlParam;
import ca.uhn.fhir.rest.annotation.Create;
import ca.uhn.fhir.rest.annotation.Delete;
import ca.uhn.fhir.rest.annotation.History;
import ca.uhn.fhir.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.IncludeParam; import ca.uhn.fhir.rest.annotation.IncludeParam;
import ca.uhn.fhir.rest.annotation.Operation; import ca.uhn.fhir.rest.annotation.Operation;
@ -36,84 +39,35 @@ import ca.uhn.fhir.rest.annotation.OperationParam;
import ca.uhn.fhir.rest.annotation.OptionalParam; import ca.uhn.fhir.rest.annotation.OptionalParam;
import ca.uhn.fhir.rest.annotation.Read; import ca.uhn.fhir.rest.annotation.Read;
import ca.uhn.fhir.rest.annotation.RequiredParam; import ca.uhn.fhir.rest.annotation.RequiredParam;
import ca.uhn.fhir.rest.annotation.ResourceParam;
import ca.uhn.fhir.rest.annotation.Search; import ca.uhn.fhir.rest.annotation.Search;
import ca.uhn.fhir.rest.annotation.Update;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.method.BaseMethodBinding; import ca.uhn.fhir.rest.method.BaseMethodBinding;
import ca.uhn.fhir.rest.method.SearchMethodBinding; import ca.uhn.fhir.rest.method.SearchMethodBinding;
import ca.uhn.fhir.rest.method.SearchParameter; import ca.uhn.fhir.rest.method.SearchParameter;
import ca.uhn.fhir.rest.param.DateParam;
import ca.uhn.fhir.rest.param.DateRangeParam; import ca.uhn.fhir.rest.param.DateRangeParam;
import ca.uhn.fhir.rest.param.TokenOrListParam; import ca.uhn.fhir.rest.param.TokenOrListParam;
import ca.uhn.fhir.rest.server.provider.dstu2.ServerConformanceProvider; import ca.uhn.fhir.rest.server.provider.dstu2.ServerConformanceProvider;
public class ServerConformanceProviderDstu2Test { public class ServerConformanceProviderDstu2Test {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ServerConformanceProviderDstu2Test.class);
private static FhirContext ourCtx = FhirContext.forDstu2(); private static FhirContext ourCtx = FhirContext.forDstu2();
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ServerConformanceProviderDstu2Test.class);
@Test private HttpServletRequest createHttpServletRequest() {
public void testSearchParameterDocumentation() throws Exception { HttpServletRequest req = mock(HttpServletRequest.class);
when(req.getRequestURI()).thenReturn("/FhirStorm/fhir/Patient/_search");
RestfulServer rs = new RestfulServer(ourCtx); when(req.getServletPath()).thenReturn("/fhir");
rs.setProviders(new SearchProvider()); when(req.getRequestURL()).thenReturn(new StringBuffer().append("http://fhirstorm.dyndns.org:8080/FhirStorm/fhir/Patient/_search"));
when(req.getContextPath()).thenReturn("/FhirStorm");
ServerConformanceProvider sc = new ServerConformanceProvider(rs); return req;
rs.setServerConformanceProvider(sc);
rs.init(createServletConfig());
boolean found=false;
Collection<ResourceBinding> resourceBindings = rs.getResourceBindings();
for (ResourceBinding resourceBinding : resourceBindings) {
if (resourceBinding.getResourceName().equals("Patient")) {
List<BaseMethodBinding<?>> methodBindings = resourceBinding.getMethodBindings();
SearchMethodBinding binding = (SearchMethodBinding) methodBindings.get(0);
SearchParameter param = (SearchParameter) binding.getParameters().iterator().next();
assertEquals("The patient's identifier (MRN or other card number)", param.getDescription());
found=true;
}
}
assertTrue(found);
Conformance conformance = sc.getServerConformance(createHttpServletRequest());
String conf = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance);
ourLog.info(conf);
assertThat(conf, containsString("<documentation value=\"The patient's identifier (MRN or other card number)\"/>"));
assertThat(conf, containsString("<type value=\"token\"/>"));
} }
@Test private ServletConfig createServletConfig() {
public void testOperationDocumentation() throws Exception { ServletConfig sc = mock(ServletConfig.class);
when(sc.getServletContext()).thenReturn(null);
RestfulServer rs = new RestfulServer(ourCtx); return sc;
rs.setProviders(new SearchProvider());
ServerConformanceProvider sc = new ServerConformanceProvider(rs);
rs.setServerConformanceProvider(sc);
rs.init(createServletConfig());
boolean found=false;
Collection<ResourceBinding> resourceBindings = rs.getResourceBindings();
for (ResourceBinding resourceBinding : resourceBindings) {
if (resourceBinding.getResourceName().equals("Patient")) {
List<BaseMethodBinding<?>> methodBindings = resourceBinding.getMethodBindings();
SearchMethodBinding binding = (SearchMethodBinding) methodBindings.get(0);
SearchParameter param = (SearchParameter) binding.getParameters().iterator().next();
assertEquals("The patient's identifier (MRN or other card number)", param.getDescription());
found=true;
}
}
assertTrue(found);
Conformance conformance = sc.getServerConformance(createHttpServletRequest());
String conf = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance);
ourLog.info(conf);
assertThat(conf, containsString("<documentation value=\"The patient's identifier (MRN or other card number)\"/>"));
assertThat(conf, containsString("<type value=\"token\"/>"));
} }
@Test @Test
@ -135,10 +89,10 @@ public class ServerConformanceProviderDstu2Test {
} }
@Test @Test
public void testValidateGeneratedStatement() throws Exception { public void testInstanceHistorySupported() throws Exception {
RestfulServer rs = new RestfulServer(ourCtx); RestfulServer rs = new RestfulServer(ourCtx);
rs.setProviders(new MultiOptionalProvider()); rs.setProviders(new InstanceHistoryProvider());
ServerConformanceProvider sc = new ServerConformanceProvider(rs); ServerConformanceProvider sc = new ServerConformanceProvider(rs);
rs.setServerConformanceProvider(sc); rs.setServerConformanceProvider(sc);
@ -146,12 +100,13 @@ public class ServerConformanceProviderDstu2Test {
rs.init(createServletConfig()); rs.init(createServletConfig());
Conformance conformance = sc.getServerConformance(createHttpServletRequest()); Conformance conformance = sc.getServerConformance(createHttpServletRequest());
String conf = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance);
ourLog.info(conf);
assertTrue(ourCtx.newValidator().validateWithResult(conformance).isSuccessful()); conf = ourCtx.newXmlParser().setPrettyPrint(false).encodeResourceToString(conformance);
assertThat(conf, containsString("<interaction><code value=\"" + TypeRestfulInteractionEnum.HISTORY_INSTANCE.getCode() + "\"/></interaction>"));
} }
@Test @Test
public void testMultiOptionalDocumentation() throws Exception { public void testMultiOptionalDocumentation() throws Exception {
@ -163,7 +118,7 @@ public class ServerConformanceProviderDstu2Test {
rs.init(createServletConfig()); rs.init(createServletConfig());
boolean found=false; boolean found = false;
Collection<ResourceBinding> resourceBindings = rs.getResourceBindings(); Collection<ResourceBinding> resourceBindings = rs.getResourceBindings();
for (ResourceBinding resourceBinding : resourceBindings) { for (ResourceBinding resourceBinding : resourceBindings) {
if (resourceBinding.getResourceName().equals("Patient")) { if (resourceBinding.getResourceName().equals("Patient")) {
@ -171,7 +126,7 @@ public class ServerConformanceProviderDstu2Test {
SearchMethodBinding binding = (SearchMethodBinding) methodBindings.get(0); SearchMethodBinding binding = (SearchMethodBinding) methodBindings.get(0);
SearchParameter param = (SearchParameter) binding.getParameters().iterator().next(); SearchParameter param = (SearchParameter) binding.getParameters().iterator().next();
assertEquals("The patient's identifier", param.getDescription()); assertEquals("The patient's identifier", param.getDescription());
found=true; found = true;
} }
} }
@ -185,6 +140,69 @@ public class ServerConformanceProviderDstu2Test {
assertThat(conf, containsString("<type value=\"token\"/>")); assertThat(conf, containsString("<type value=\"token\"/>"));
} }
@Test
public void testOperationDocumentation() throws Exception {
RestfulServer rs = new RestfulServer(ourCtx);
rs.setProviders(new SearchProvider());
ServerConformanceProvider sc = new ServerConformanceProvider(rs);
rs.setServerConformanceProvider(sc);
rs.init(createServletConfig());
boolean found = false;
Collection<ResourceBinding> resourceBindings = rs.getResourceBindings();
for (ResourceBinding resourceBinding : resourceBindings) {
if (resourceBinding.getResourceName().equals("Patient")) {
List<BaseMethodBinding<?>> methodBindings = resourceBinding.getMethodBindings();
SearchMethodBinding binding = (SearchMethodBinding) methodBindings.get(0);
SearchParameter param = (SearchParameter) binding.getParameters().iterator().next();
assertEquals("The patient's identifier (MRN or other card number)", param.getDescription());
found = true;
}
}
assertTrue(found);
Conformance conformance = sc.getServerConformance(createHttpServletRequest());
String conf = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance);
ourLog.info(conf);
assertThat(conf, containsString("<documentation value=\"The patient's identifier (MRN or other card number)\"/>"));
assertThat(conf, containsString("<type value=\"token\"/>"));
}
@Test
public void testProviderWithRequiredAndOptional() throws Exception {
RestfulServer rs = new RestfulServer(ourCtx);
rs.setProviders(new ProviderWithRequiredAndOptional());
ServerConformanceProvider sc = new ServerConformanceProvider(rs);
rs.setServerConformanceProvider(sc);
rs.init(createServletConfig());
Conformance conformance = sc.getServerConformance(createHttpServletRequest());
String conf = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance);
ourLog.info(conf);
Rest rest = conformance.getRestFirstRep();
RestResource res = rest.getResourceFirstRep();
assertEquals("DiagnosticReport", res.getType());
assertEquals(DiagnosticReport.SP_SUBJECT, res.getSearchParam().get(0).getName());
assertEquals("identifier", res.getSearchParam().get(0).getChain().get(0).getValue());
assertEquals(DiagnosticReport.SP_NAME, res.getSearchParam().get(2).getName());
assertEquals(DiagnosticReport.SP_DATE, res.getSearchParam().get(1).getName());
assertEquals(1, res.getSearchInclude().size());
assertEquals("DiagnosticReport.result", res.getSearchIncludeFirstRep().getValue());
}
@Test @Test
public void testReadAndVReadSupported() throws Exception { public void testReadAndVReadSupported() throws Exception {
@ -224,11 +242,12 @@ public class ServerConformanceProviderDstu2Test {
assertThat(conf, not(containsString("<interaction><code value=\"vread\"/></interaction>"))); assertThat(conf, not(containsString("<interaction><code value=\"vread\"/></interaction>")));
assertThat(conf, containsString("<interaction><code value=\"read\"/></interaction>")); assertThat(conf, containsString("<interaction><code value=\"read\"/></interaction>"));
} }
@Test
public void testProviderWithRequiredAndOptional() throws Exception { @Test
public void testConditionalOperations() throws Exception {
RestfulServer rs = new RestfulServer(ourCtx); RestfulServer rs = new RestfulServer(ourCtx);
rs.setProviders(new ProviderWithRequiredAndOptional()); rs.setProviders(new ConditionalProvider());
ServerConformanceProvider sc = new ServerConformanceProvider(rs); ServerConformanceProvider sc = new ServerConformanceProvider(rs);
rs.setServerConformanceProvider(sc); rs.setServerConformanceProvider(sc);
@ -239,72 +258,132 @@ public class ServerConformanceProviderDstu2Test {
String conf = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); String conf = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance);
ourLog.info(conf); ourLog.info(conf);
Rest rest = conformance.getRestFirstRep(); RestResource res = conformance.getRestFirstRep().getResourceFirstRep();
RestResource res = rest.getResourceFirstRep(); assertEquals("Patient", res.getType());
assertEquals("DiagnosticReport", res.getType());
assertEquals(DiagnosticReport.SP_SUBJECT , res.getSearchParam().get(0).getName()); assertTrue(res.getConditionalCreate());
assertEquals("identifier", res.getSearchParam().get(0).getChain().get(0).getValue()); assertTrue(res.getConditionalDelete());
assertTrue(res.getConditionalUpdate());
assertEquals(DiagnosticReport.SP_NAME, res.getSearchParam().get(2).getName());
assertEquals(DiagnosticReport.SP_DATE, res.getSearchParam().get(1).getName());
assertEquals(1,res.getSearchInclude().size());
assertEquals("DiagnosticReport.result", res.getSearchIncludeFirstRep().getValue());
} }
@Test
public void testNonConditionalOperations() throws Exception {
/** RestfulServer rs = new RestfulServer(ourCtx);
* Created by dsotnikov on 2/25/2014. rs.setProviders(new NonConditionalProvider());
*/
public static class SearchProvider {
ServerConformanceProvider sc = new ServerConformanceProvider(rs);
rs.setServerConformanceProvider(sc);
@Search(type = Patient.class) rs.init(createServletConfig());
public Patient findPatient(
@Description(shortDefinition = "The patient's identifier (MRN or other card number)") Conformance conformance = sc.getServerConformance(createHttpServletRequest());
@RequiredParam(name = Patient.SP_IDENTIFIER) IdentifierDt theIdentifier) { String conf = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance);
return null; ourLog.info(conf);
RestResource res = conformance.getRestFirstRep().getResourceFirstRep();
assertEquals("Patient", res.getType());
assertNull(res.getConditionalCreate());
assertNull(res.getConditionalDelete());
assertNull(res.getConditionalUpdate());
}
@Test
public void testSearchParameterDocumentation() throws Exception {
RestfulServer rs = new RestfulServer(ourCtx);
rs.setProviders(new SearchProvider());
ServerConformanceProvider sc = new ServerConformanceProvider(rs);
rs.setServerConformanceProvider(sc);
rs.init(createServletConfig());
boolean found = false;
Collection<ResourceBinding> resourceBindings = rs.getResourceBindings();
for (ResourceBinding resourceBinding : resourceBindings) {
if (resourceBinding.getResourceName().equals("Patient")) {
List<BaseMethodBinding<?>> methodBindings = resourceBinding.getMethodBindings();
SearchMethodBinding binding = (SearchMethodBinding) methodBindings.get(0);
SearchParameter param = (SearchParameter) binding.getParameters().iterator().next();
assertEquals("The patient's identifier (MRN or other card number)", param.getDescription());
found = true;
}
} }
assertTrue(found);
Conformance conformance = sc.getServerConformance(createHttpServletRequest());
String conf = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance);
ourLog.info(conf);
assertThat(conf, containsString("<documentation value=\"The patient's identifier (MRN or other card number)\"/>"));
assertThat(conf, containsString("<type value=\"token\"/>"));
} }
/** @Test
* Created by dsotnikov on 2/25/2014. public void testSystemHistorySupported() throws Exception {
*/
public static class VreadProvider {
@Read(version=true) RestfulServer rs = new RestfulServer(ourCtx);
public Patient readPatient( rs.setProviders(new SystemHistoryProvider());
@IdParam IdDt theId) {
return null;
}
@Search(type = Patient.class) ServerConformanceProvider sc = new ServerConformanceProvider(rs);
public Patient findPatient( rs.setServerConformanceProvider(sc);
@Description(shortDefinition = "The patient's identifier (MRN or other card number)")
@RequiredParam(name = Patient.SP_IDENTIFIER) IdentifierDt theIdentifier) {
return null;
}
rs.init(createServletConfig());
Conformance conformance = sc.getServerConformance(createHttpServletRequest());
String conf = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance);
ourLog.info(conf);
conf = ourCtx.newXmlParser().setPrettyPrint(false).encodeResourceToString(conformance);
assertThat(conf, containsString("<interaction><code value=\"" + SystemRestfulInteractionEnum.HISTORY_SYSTEM.getCode() + "\"/></interaction>"));
} }
/** @Test
* Created by dsotnikov on 2/25/2014. public void testTypeHistorySupported() throws Exception {
*/
public static class ReadProvider {
@Read(version=false) RestfulServer rs = new RestfulServer(ourCtx);
public Patient readPatient( rs.setProviders(new TypeHistoryProvider());
@IdParam IdDt theId) {
return null; ServerConformanceProvider sc = new ServerConformanceProvider(rs);
rs.setServerConformanceProvider(sc);
rs.init(createServletConfig());
Conformance conformance = sc.getServerConformance(createHttpServletRequest());
String conf = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance);
ourLog.info(conf);
conf = ourCtx.newXmlParser().setPrettyPrint(false).encodeResourceToString(conformance);
assertThat(conf, containsString("<interaction><code value=\"" + TypeRestfulInteractionEnum.HISTORY_TYPE.getCode() + "\"/></interaction>"));
}
@Test
public void testValidateGeneratedStatement() throws Exception {
RestfulServer rs = new RestfulServer(ourCtx);
rs.setProviders(new MultiOptionalProvider());
ServerConformanceProvider sc = new ServerConformanceProvider(rs);
rs.setServerConformanceProvider(sc);
rs.init(createServletConfig());
Conformance conformance = sc.getServerConformance(createHttpServletRequest());
assertTrue(ourCtx.newValidator().validateWithResult(conformance).isSuccessful());
}
public static class InstanceHistoryProvider implements IResourceProvider {
@Override
public Class<? extends IBaseResource> getResourceType() {
return Patient.class;
} }
@Search(type = Patient.class) @History
public Patient findPatient( public List<IBaseResource> history(@IdParam IdDt theId) {
@Description(shortDefinition = "The patient's identifier (MRN or other card number)")
@RequiredParam(name = Patient.SP_IDENTIFIER) IdentifierDt theIdentifier) {
return null; return null;
} }
@ -316,27 +395,7 @@ public class ServerConformanceProviderDstu2Test {
public static class MultiOptionalProvider { public static class MultiOptionalProvider {
@Search(type = Patient.class) @Search(type = Patient.class)
public Patient findPatient( public Patient findPatient(@Description(shortDefinition = "The patient's identifier") @OptionalParam(name = Patient.SP_IDENTIFIER) IdentifierDt theIdentifier, @Description(shortDefinition = "The patient's name") @OptionalParam(name = Patient.SP_NAME) StringDt theName) {
@Description(shortDefinition = "The patient's identifier")
@OptionalParam(name = Patient.SP_IDENTIFIER) IdentifierDt theIdentifier,
@Description(shortDefinition = "The patient's name")
@OptionalParam(name=Patient.SP_NAME) StringDt theName) {
return null;
}
}
public static class ProviderWithRequiredAndOptional {
@Description(shortDefinition="This is a search for stuff!")
@Search
public List<DiagnosticReport> findDiagnosticReportsByPatient (
@RequiredParam(name=DiagnosticReport.SP_SUBJECT + '.' + Patient.SP_IDENTIFIER) IdentifierDt thePatientId,
@OptionalParam(name=DiagnosticReport.SP_NAME) TokenOrListParam theNames,
@OptionalParam(name=DiagnosticReport.SP_DATE) DateRangeParam theDateRange,
@IncludeParam(allow= {"DiagnosticReport.result"}) Set<Include> theIncludes
) throws Exception {
return null; return null;
} }
@ -344,12 +403,8 @@ public class ServerConformanceProviderDstu2Test {
public static class ProviderWithExtendedOperationReturningBundle implements IResourceProvider { public static class ProviderWithExtendedOperationReturningBundle implements IResourceProvider {
@Operation(name="everything", idempotent=true) @Operation(name = "everything", idempotent = true)
public ca.uhn.fhir.rest.server.IBundleProvider everything( public ca.uhn.fhir.rest.server.IBundleProvider everything(javax.servlet.http.HttpServletRequest theServletRequest, @IdParam ca.uhn.fhir.model.primitive.IdDt theId, @OperationParam(name = "start") DateDt theStart, @OperationParam(name = "end") DateDt theEnd) {
javax.servlet.http.HttpServletRequest theServletRequest,
@IdParam ca.uhn.fhir.model.primitive.IdDt theId,
@OperationParam(name="start") DateDt theStart,
@OperationParam(name="end") DateDt theEnd) {
return null; return null;
} }
@ -360,19 +415,132 @@ public class ServerConformanceProviderDstu2Test {
} }
private HttpServletRequest createHttpServletRequest() { public static class ProviderWithRequiredAndOptional {
HttpServletRequest req = mock(HttpServletRequest.class);
when(req.getRequestURI()).thenReturn("/FhirStorm/fhir/Patient/_search"); @Description(shortDefinition = "This is a search for stuff!")
when(req.getServletPath()).thenReturn("/fhir"); @Search
when(req.getRequestURL()).thenReturn(new StringBuffer().append("http://fhirstorm.dyndns.org:8080/FhirStorm/fhir/Patient/_search")); public List<DiagnosticReport> findDiagnosticReportsByPatient(@RequiredParam(name = DiagnosticReport.SP_SUBJECT + '.' + Patient.SP_IDENTIFIER) IdentifierDt thePatientId, @OptionalParam(name = DiagnosticReport.SP_NAME) TokenOrListParam theNames,
when(req.getContextPath()).thenReturn("/FhirStorm"); @OptionalParam(name = DiagnosticReport.SP_DATE) DateRangeParam theDateRange, @IncludeParam(allow = { "DiagnosticReport.result" }) Set<Include> theIncludes) throws Exception {
return req; return null;
}
} }
private ServletConfig createServletConfig() { /**
ServletConfig sc = mock(ServletConfig.class); * Created by dsotnikov on 2/25/2014.
when (sc.getServletContext()).thenReturn(null); */
return sc; public static class ReadProvider {
@Search(type = Patient.class)
public Patient findPatient(@Description(shortDefinition = "The patient's identifier (MRN or other card number)") @RequiredParam(name = Patient.SP_IDENTIFIER) IdentifierDt theIdentifier) {
return null;
}
@Read(version = false)
public Patient readPatient(@IdParam IdDt theId) {
return null;
}
}
/**
* Created by dsotnikov on 2/25/2014.
*/
public static class SearchProvider {
@Search(type = Patient.class)
public Patient findPatient(@Description(shortDefinition = "The patient's identifier (MRN or other card number)") @RequiredParam(name = Patient.SP_IDENTIFIER) IdentifierDt theIdentifier) {
return null;
}
}
public static class SystemHistoryProvider {
@History
public List<IBaseResource> history() {
return null;
}
}
public static class TypeHistoryProvider implements IResourceProvider {
@Override
public Class<? extends IBaseResource> getResourceType() {
return Patient.class;
}
@History
public List<IBaseResource> history() {
return null;
}
}
public static class ConditionalProvider implements IResourceProvider {
@Override
public Class<? extends IBaseResource> getResourceType() {
return Patient.class;
}
@Create
public MethodOutcome create(@ResourceParam Patient thePatient, @ConditionalUrlParam String theConditionalUrl) {
return null;
}
@Update
public MethodOutcome update(@IdParam IdDt theId, @ResourceParam Patient thePatient, @ConditionalUrlParam String theConditionalUrl) {
return null;
}
@Delete
public MethodOutcome delete(@IdParam IdDt theId, @ConditionalUrlParam String theConditionalUrl) {
return null;
}
}
public static class NonConditionalProvider implements IResourceProvider {
@Override
public Class<? extends IBaseResource> getResourceType() {
return Patient.class;
}
@Create
public MethodOutcome create(@ResourceParam Patient thePatient) {
return null;
}
@Update
public MethodOutcome update(@IdParam IdDt theId, @ResourceParam Patient thePatient) {
return null;
}
@Delete
public MethodOutcome delete(@IdParam IdDt theId) {
return null;
}
}
/**
* Created by dsotnikov on 2/25/2014.
*/
public static class VreadProvider {
@Search(type = Patient.class)
public Patient findPatient(@Description(shortDefinition = "The patient's identifier (MRN or other card number)") @RequiredParam(name = Patient.SP_IDENTIFIER) IdentifierDt theIdentifier) {
return null;
}
@Read(version = true)
public Patient readPatient(@IdParam IdDt theId) {
return null;
}
} }
} }

View File

@ -92,7 +92,7 @@
and Bundle.entry.base is empty, it will now be and Bundle.entry.base is empty, it will now be
automatically set by the parser. automatically set by the parser.
</action> </action>
<action type="add"> <action type="add" issue="179">
Add fluent client method for validate operation, and support the Add fluent client method for validate operation, and support the
new DSTU2 style extended operation for $validate if the client is new DSTU2 style extended operation for $validate if the client is
in DSTU2 mode. Thanks to Eric from the FHIR Skype Implementers chat for in DSTU2 mode. Thanks to Eric from the FHIR Skype Implementers chat for
@ -105,7 +105,7 @@
<action type="add"> <action type="add">
Server in DSTU2 mode now indicates that whether it has support for Transaction operation or not. Thanks to Kevin Paschke for pointing out that this wasn't working! Server in DSTU2 mode now indicates that whether it has support for Transaction operation or not. Thanks to Kevin Paschke for pointing out that this wasn't working!
</action> </action>
<action type="add"> <action type="add" issue="166">
Questionnaire.title now gets correctly indexed in JPA server (it has no path, so it is a special case) Questionnaire.title now gets correctly indexed in JPA server (it has no path, so it is a special case)
</action> </action>
<action type="add"> <action type="add">
@ -133,6 +133,10 @@
<action type="fix"> <action type="fix">
JPA server supports _count parameter in transaction containing search URL (nested search) JPA server supports _count parameter in transaction containing search URL (nested search)
</action> </action>
<action type="fix">
DSTU2 servers now indicate support for conditional create/update/delete in their
conformance statement.
</action>
</release> </release>
<release version="1.0" date="2015-May-8"> <release version="1.0" date="2015-May-8">
<action type="add"> <action type="add">