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> {
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 Method myMethod;
private List<IParameter> myParameters;
private Object myProvider;
private boolean mySupportsConditional;
public BaseMethodBinding(Method theMethod, FhirContext theContext, Object theProvider) {
assert theMethod != null;
@ -98,6 +103,14 @@ public abstract class BaseMethodBinding<T> implements IClientResponseHandler<T>
myContext = theContext;
myProvider = theProvider;
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) {
@ -223,7 +236,8 @@ public abstract class BaseMethodBinding<T> implements IClientResponseHandler<T>
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() {
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 {
/*
* 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
* 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
* don't think.
* 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 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 don't think.
*/
IRequestReader reader = ourRequestReader;
if (reader == null) {
@ -385,16 +408,14 @@ public abstract class BaseMethodBinding<T> implements IClientResponseHandler<T>
if (theProvider instanceof IResourceProvider) {
returnTypeFromRp = ((IResourceProvider) theProvider).getResourceType();
if (!verifyIsValidResourceReturnType(returnTypeFromRp)) {
throw new ConfigurationException("getResourceType() from " + IResourceProvider.class.getSimpleName() + " type " + theMethod.getDeclaringClass().getCanonicalName() + " returned "
+ toLogString(returnTypeFromRp) + " - Must return a resource type");
throw new ConfigurationException("getResourceType() from " + IResourceProvider.class.getSimpleName() + " type " + theMethod.getDeclaringClass().getCanonicalName() + " returned " + toLogString(returnTypeFromRp) + " - Must return a resource type");
}
}
Class<?> returnTypeFromMethod = theMethod.getReturnType();
if (getTags != null) {
if (!TagList.class.equals(returnTypeFromMethod)) {
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());
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());
}
} else if (MethodOutcome.class.equals(returnTypeFromMethod)) {
// returns a method outcome
@ -407,15 +428,13 @@ public abstract class BaseMethodBinding<T> implements IClientResponseHandler<T>
} else if (Collection.class.isAssignableFrom(returnTypeFromMethod)) {
returnTypeFromMethod = ReflectionUtil.getGenericCollectionTypeOfMethodReturnType(theMethod);
if (!verifyIsValidResourceReturnType(returnTypeFromMethod) && !IResource.class.equals(returnTypeFromMethod)) {
throw new ConfigurationException("Method '" + theMethod.getName() + "' from " + IResourceProvider.class.getSimpleName() + " type " + theMethod.getDeclaringClass().getCanonicalName()
+ " returns a collection with generic type " + toLogString(returnTypeFromMethod)
throw new ConfigurationException("Method '" + theMethod.getName() + "' from " + IResourceProvider.class.getSimpleName() + " type " + theMethod.getDeclaringClass().getCanonicalName() + " 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> )");
}
} else {
if (!IResource.class.equals(returnTypeFromMethod) && !verifyIsValidResourceReturnType(returnTypeFromMethod)) {
throw new ConfigurationException("Method '" + theMethod.getName() + "' from " + IResourceProvider.class.getSimpleName() + " type " + theMethod.getDeclaringClass().getCanonicalName()
+ " returns " + toLogString(returnTypeFromMethod) + " - Must return a resource type (eg Patient, " + Bundle.class.getSimpleName() + ", " + IBundleProvider.class.getSimpleName()
+ ", etc., see the documentation for more details)");
throw new ConfigurationException("Method '" + theMethod.getName() + "' from " + IResourceProvider.class.getSimpleName() + " type " + theMethod.getDeclaringClass().getCanonicalName() + " returns " + toLogString(returnTypeFromMethod) + " - Must return a resource type (eg Patient, "
+ Bundle.class.getSimpleName() + ", " + IBundleProvider.class.getSimpleName() + ", etc., see the documentation for more details)");
}
}
@ -445,13 +464,12 @@ public abstract class BaseMethodBinding<T> implements IClientResponseHandler<T>
if (returnTypeFromRp != null) {
if (returnTypeFromAnnotation != null && returnTypeFromAnnotation != IResource.class) {
if (!returnTypeFromRp.isAssignableFrom(returnTypeFromAnnotation)) {
throw new ConfigurationException("Method '" + theMethod.getName() + "' in type " + theMethod.getDeclaringClass().getCanonicalName() + " returns type "
+ returnTypeFromMethod.getCanonicalName() + " - Must return " + returnTypeFromRp.getCanonicalName() + " (or a subclass of it) per IResourceProvider contract");
throw new ConfigurationException("Method '" + theMethod.getName() + "' in type " + theMethod.getDeclaringClass().getCanonicalName() + " returns type " + returnTypeFromMethod.getCanonicalName() + " - Must return " + returnTypeFromRp.getCanonicalName()
+ " (or a subclass of it) per IResourceProvider contract");
}
if (!returnTypeFromRp.isAssignableFrom(returnTypeFromAnnotation)) {
throw new ConfigurationException("Method '" + theMethod.getName() + "' in type " + theMethod.getDeclaringClass().getCanonicalName() + " claims to return type "
+ returnTypeFromAnnotation.getCanonicalName() + " per method annotation - Must return " + returnTypeFromRp.getCanonicalName()
+ " (or a subclass of it) per IResourceProvider contract");
throw new ConfigurationException("Method '" + theMethod.getName() + "' in type " + theMethod.getDeclaringClass().getCanonicalName() + " claims to return type " + returnTypeFromAnnotation.getCanonicalName() + " per method annotation - Must return "
+ returnTypeFromRp.getCanonicalName() + " (or a subclass of it) per IResourceProvider contract");
}
returnType = returnTypeFromAnnotation;
} else {
@ -460,8 +478,8 @@ public abstract class BaseMethodBinding<T> implements IClientResponseHandler<T>
} else {
if (returnTypeFromAnnotation != IResource.class) {
if (!verifyIsValidResourceReturnType(returnTypeFromAnnotation)) {
throw new ConfigurationException("Method '" + theMethod.getName() + "' from " + IResourceProvider.class.getSimpleName() + " type " + theMethod.getDeclaringClass().getCanonicalName()
+ " returns " + toLogString(returnTypeFromAnnotation) + " according to annotation - Must return a resource type");
throw new ConfigurationException("Method '" + theMethod.getName() + "' from " + IResourceProvider.class.getSimpleName() + " type " + theMethod.getDeclaringClass().getCanonicalName() + " returns " + toLogString(returnTypeFromAnnotation)
+ " according to annotation - Must return a resource type");
}
returnType = returnTypeFromAnnotation;
} else {
@ -572,8 +590,7 @@ public abstract class BaseMethodBinding<T> implements IClientResponseHandler<T>
if (obj1 == null) {
obj1 = object;
} else {
throw new ConfigurationException("Method " + theNextMethod.getName() + " on type '" + theNextMethod.getDeclaringClass().getSimpleName() + " has annotations @"
+ obj1.getClass().getSimpleName() + " and @" + object.getClass().getSimpleName() + ". Can not have both.");
throw new ConfigurationException("Method " + theNextMethod.getName() + " on type '" + theNextMethod.getDeclaringClass().getSimpleName() + " has annotations @" + 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;
}
/**
* @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)
*/
@ -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;
abstract class BaseOutcomeReturningMethodBindingWithResourceParam extends BaseOutcomeReturningMethodBinding {
private int myResourceParameterIndex;
private String myResourceName;
private Class<? extends IBaseResource> myResourceType;
private Integer myIdParamIndex;
private String myResourceName;
private int myResourceParameterIndex;
private Class<? extends IBaseResource> myResourceType;
public BaseOutcomeReturningMethodBindingWithResourceParam(Method theMethod, FhirContext theContext, Class<?> theMethodAnnotation, Object theProvider) {
super(theMethod, theContext, theMethodAnnotation, theProvider);

View File

@ -99,7 +99,7 @@ abstract class BaseResourceReturningMethodBinding extends BaseMethodBinding<Obje
myMethodReturnType = MethodReturnTypeEnum.LIST_OF_RESOURCES;
Class<?> collectionType = ReflectionUtil.getGenericCollectionTypeOfMethodReturnType(theMethod);
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: "
+ 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
*
* <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
* <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.
* 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 <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>
*/
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
* value defaults to "Not provided" but may be set to null, which will cause this element to be omitted.
* 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 value defaults to
* "Not provided" but may be set to null, which will cause this element to be omitted.
*/
public String getPublisher() {
return myPublisher;
@ -107,7 +110,7 @@ public class ServerConformanceProvider implements IServerConformanceProvider<Con
retVal.setDate(DateTimeDt.withCurrentTime());
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
// needs to be modified to actually allow it
// needs to be modified to actually allow it
retVal.getImplementation().setDescription(myRestfulServer.getImplementationDescription());
retVal.getSoftware().setName(myRestfulServer.getServerName());
@ -172,6 +175,22 @@ public class ServerConformanceProvider implements IServerConformanceProvider<Con
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
* value defaults to "Not provided" but may be set to null, which will cause this element to be omitted.
* 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 value defaults to
* "Not provided" but may be set to null, which will cause this element to be omitted.
*/
public void setPublisher(String thePublisher) {
myPublisher = thePublisher;

View File

@ -1,11 +1,8 @@
package ca.uhn.fhir.rest.server;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import java.util.Collection;
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.DiagnosticReport;
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.IdDt;
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.IncludeParam;
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.Read;
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.Update;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.method.BaseMethodBinding;
import ca.uhn.fhir.rest.method.SearchMethodBinding;
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.TokenOrListParam;
import ca.uhn.fhir.rest.server.provider.dstu2.ServerConformanceProvider;
public class ServerConformanceProviderDstu2Test {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ServerConformanceProviderDstu2Test.class);
private static FhirContext ourCtx = FhirContext.forDstu2();
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ServerConformanceProviderDstu2Test.class);
@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\"/>"));
private HttpServletRequest createHttpServletRequest() {
HttpServletRequest req = mock(HttpServletRequest.class);
when(req.getRequestURI()).thenReturn("/FhirStorm/fhir/Patient/_search");
when(req.getServletPath()).thenReturn("/fhir");
when(req.getRequestURL()).thenReturn(new StringBuffer().append("http://fhirstorm.dyndns.org:8080/FhirStorm/fhir/Patient/_search"));
when(req.getContextPath()).thenReturn("/FhirStorm");
return req;
}
@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\"/>"));
private ServletConfig createServletConfig() {
ServletConfig sc = mock(ServletConfig.class);
when(sc.getServletContext()).thenReturn(null);
return sc;
}
@Test
@ -135,10 +89,10 @@ public class ServerConformanceProviderDstu2Test {
}
@Test
public void testValidateGeneratedStatement() throws Exception {
public void testInstanceHistorySupported() throws Exception {
RestfulServer rs = new RestfulServer(ourCtx);
rs.setProviders(new MultiOptionalProvider());
rs.setProviders(new InstanceHistoryProvider());
ServerConformanceProvider sc = new ServerConformanceProvider(rs);
rs.setServerConformanceProvider(sc);
@ -146,12 +100,13 @@ public class ServerConformanceProviderDstu2Test {
rs.init(createServletConfig());
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
public void testMultiOptionalDocumentation() throws Exception {
@ -163,7 +118,7 @@ public class ServerConformanceProviderDstu2Test {
rs.init(createServletConfig());
boolean found=false;
boolean found = false;
Collection<ResourceBinding> resourceBindings = rs.getResourceBindings();
for (ResourceBinding resourceBinding : resourceBindings) {
if (resourceBinding.getResourceName().equals("Patient")) {
@ -171,7 +126,7 @@ public class ServerConformanceProviderDstu2Test {
SearchMethodBinding binding = (SearchMethodBinding) methodBindings.get(0);
SearchParameter param = (SearchParameter) binding.getParameters().iterator().next();
assertEquals("The patient's identifier", param.getDescription());
found=true;
found = true;
}
}
@ -185,6 +140,69 @@ public class ServerConformanceProviderDstu2Test {
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
public void testReadAndVReadSupported() throws Exception {
@ -224,11 +242,12 @@ public class ServerConformanceProviderDstu2Test {
assertThat(conf, not(containsString("<interaction><code value=\"vread\"/></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);
rs.setProviders(new ProviderWithRequiredAndOptional());
rs.setProviders(new ConditionalProvider());
ServerConformanceProvider sc = new ServerConformanceProvider(rs);
rs.setServerConformanceProvider(sc);
@ -239,72 +258,132 @@ public class ServerConformanceProviderDstu2Test {
String conf = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance);
ourLog.info(conf);
Rest rest = conformance.getRestFirstRep();
RestResource res = rest.getResourceFirstRep();
assertEquals("DiagnosticReport", res.getType());
RestResource res = conformance.getRestFirstRep().getResourceFirstRep();
assertEquals("Patient", 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());
assertTrue(res.getConditionalCreate());
assertTrue(res.getConditionalDelete());
assertTrue(res.getConditionalUpdate());
}
@Test
public void testNonConditionalOperations() throws Exception {
/**
* Created by dsotnikov on 2/25/2014.
*/
public static class SearchProvider {
RestfulServer rs = new RestfulServer(ourCtx);
rs.setProviders(new NonConditionalProvider());
ServerConformanceProvider sc = new ServerConformanceProvider(rs);
rs.setServerConformanceProvider(sc);
@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;
rs.init(createServletConfig());
Conformance conformance = sc.getServerConformance(createHttpServletRequest());
String conf = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance);
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\"/>"));
}
/**
* Created by dsotnikov on 2/25/2014.
*/
public static class VreadProvider {
@Test
public void testSystemHistorySupported() throws Exception {
@Read(version=true)
public Patient readPatient(
@IdParam IdDt theId) {
return null;
}
RestfulServer rs = new RestfulServer(ourCtx);
rs.setProviders(new SystemHistoryProvider());
@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;
}
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=\"" + SystemRestfulInteractionEnum.HISTORY_SYSTEM.getCode() + "\"/></interaction>"));
}
/**
* Created by dsotnikov on 2/25/2014.
*/
public static class ReadProvider {
@Test
public void testTypeHistorySupported() throws Exception {
@Read(version=false)
public Patient readPatient(
@IdParam IdDt theId) {
return null;
RestfulServer rs = new RestfulServer(ourCtx);
rs.setProviders(new TypeHistoryProvider());
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)
public Patient findPatient(
@Description(shortDefinition = "The patient's identifier (MRN or other card number)")
@RequiredParam(name = Patient.SP_IDENTIFIER) IdentifierDt theIdentifier) {
@History
public List<IBaseResource> history(@IdParam IdDt theId) {
return null;
}
@ -316,27 +395,7 @@ public class ServerConformanceProviderDstu2Test {
public static class MultiOptionalProvider {
@Search(type = Patient.class)
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) {
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 {
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) {
return null;
}
@ -344,12 +403,8 @@ public class ServerConformanceProviderDstu2Test {
public static class ProviderWithExtendedOperationReturningBundle implements IResourceProvider {
@Operation(name="everything", idempotent=true)
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) {
@Operation(name = "everything", idempotent = true)
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) {
return null;
}
@ -360,19 +415,132 @@ public class ServerConformanceProviderDstu2Test {
}
private HttpServletRequest createHttpServletRequest() {
HttpServletRequest req = mock(HttpServletRequest.class);
when(req.getRequestURI()).thenReturn("/FhirStorm/fhir/Patient/_search");
when(req.getServletPath()).thenReturn("/fhir");
when(req.getRequestURL()).thenReturn(new StringBuffer().append("http://fhirstorm.dyndns.org:8080/FhirStorm/fhir/Patient/_search"));
when(req.getContextPath()).thenReturn("/FhirStorm");
return req;
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;
}
}
private ServletConfig createServletConfig() {
ServletConfig sc = mock(ServletConfig.class);
when (sc.getServletContext()).thenReturn(null);
return sc;
/**
* Created by dsotnikov on 2/25/2014.
*/
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
automatically set by the parser.
</action>
<action type="add">
<action type="add" issue="179">
Add fluent client method for validate operation, and support the
new DSTU2 style extended operation for $validate if the client is
in DSTU2 mode. Thanks to Eric from the FHIR Skype Implementers chat for
@ -105,7 +105,7 @@
<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!
</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)
</action>
<action type="add">
@ -133,6 +133,10 @@
<action type="fix">
JPA server supports _count parameter in transaction containing search URL (nested search)
</action>
<action type="fix">
DSTU2 servers now indicate support for conditional create/update/delete in their
conformance statement.
</action>
</release>
<release version="1.0" date="2015-May-8">
<action type="add">