Work on conformance generation for operations

This commit is contained in:
jamesagnew 2015-06-04 22:23:57 -04:00
parent 4d9b2c6e8c
commit fc4d2ae7e2
7 changed files with 158 additions and 62 deletions

View File

@ -33,6 +33,11 @@ import org.hl7.fhir.instance.model.api.IBase;
@Target(value=ElementType.PARAMETER)
public @interface OperationParam {
/**
* Value for {@link OperationParam#max()} indicating no maximum
*/
int MAX_UNLIMITED = -1;
/**
* The name of the parameter
*/
@ -44,4 +49,17 @@ public @interface OperationParam {
*/
Class<? extends IBase> type() default IBase.class;
/**
* The minimum number of repetitions allowed for this child
*/
int min() default 0;
/**
* The maximum number of repetitions allowed for this child. Should be
* set to {@link #MAX_UNLIMITED} if there is no limit to the number of
* repetitions.
*/
int max() default MAX_UNLIMITED;
}

View File

@ -440,12 +440,12 @@ public class MethodUtil {
param = new ConditionalParamBinder(theRestfulOperationTypeEnum);
} else if (nextAnnotation instanceof OperationParam) {
Operation op = theMethod.getAnnotation(Operation.class);
param = new OperationParameter(op.name(), ((OperationParam) nextAnnotation).name());
param = new OperationParameter(op.name(), ((OperationParam) nextAnnotation));
} else if (nextAnnotation instanceof Validate.Mode) {
if (parameterType.equals(ValidationModeEnum.class) == false) {
throw new ConfigurationException("Parameter annotated with @" + Validate.class.getSimpleName() + "." + Validate.Mode.class.getSimpleName() + " must be of type " + ValidationModeEnum.class.getName());
}
param = new OperationParameter(Constants.EXTOP_VALIDATE, Constants.EXTOP_VALIDATE_MODE).setConverter(new IConverter() {
param = new OperationParameter(Constants.EXTOP_VALIDATE, Constants.EXTOP_VALIDATE_MODE, 0, 1).setConverter(new IConverter() {
@Override
public Object outgoingClient(Object theObject) {
return new StringDt(((ValidationModeEnum)theObject).name().toLowerCase());
@ -460,7 +460,7 @@ public class MethodUtil {
if (parameterType.equals(String.class) == false) {
throw new ConfigurationException("Parameter annotated with @" + Validate.class.getSimpleName() + "." + Validate.Profile.class.getSimpleName() + " must be of type " + String.class.getName());
}
param = new OperationParameter(Constants.EXTOP_VALIDATE, Constants.EXTOP_VALIDATE_PROFILE).setConverter(new IConverter() {
param = new OperationParameter(Constants.EXTOP_VALIDATE, Constants.EXTOP_VALIDATE_PROFILE, 0, 1).setConverter(new IConverter() {
@Override
public Object outgoingClient(Object theObject) {
return new StringDt(theObject.toString());

View File

@ -39,6 +39,7 @@ import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.model.api.Bundle;
import ca.uhn.fhir.model.api.annotation.Description;
import ca.uhn.fhir.model.dstu.valueset.RestfulOperationSystemEnum;
import ca.uhn.fhir.model.dstu.valueset.RestfulOperationTypeEnum;
import ca.uhn.fhir.model.primitive.IdDt;
@ -55,33 +56,37 @@ import ca.uhn.fhir.util.FhirTerser;
public class OperationMethodBinding extends BaseResourceReturningMethodBinding {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(OperationMethodBinding.class);
private final boolean myHttpGetPermitted;
private String myDescription;
private final boolean myIdempotent;
private final Integer myIdParamIndex;
private final String myName;
private final ReturnTypeEnum myReturnType;
private final OtherOperationTypeEnum myOtherOperatiopnType;
private final ReturnTypeEnum myReturnType;
public OperationMethodBinding(Class<?> theReturnResourceType, Class<? extends IBaseResource> theReturnTypeFromRp, Method theMethod, FhirContext theContext, Object theProvider,
Operation theAnnotation) {
this(theReturnResourceType, theReturnTypeFromRp, theMethod, theContext, theProvider, theAnnotation.idempotent(), theAnnotation.name(), theAnnotation.type());
}
public OperationMethodBinding(Class<?> theReturnResourceType, Class<? extends IBaseResource> theReturnTypeFromRp, Method theMethod, FhirContext theContext, Object theProvider, boolean theIdempotent, String theOperationName,
Class<? extends IBaseResource> theOperationType) {
public OperationMethodBinding(Class<?> theReturnResourceType, Class<? extends IBaseResource> theReturnTypeFromRp, Method theMethod, FhirContext theContext, Object theProvider, boolean theIdempotent, String theOperationName, Class<? extends IBaseResource> theOperationType) {
super(theReturnResourceType, theMethod, theContext, theProvider);
myHttpGetPermitted = theIdempotent;
myIdempotent = theIdempotent;
myIdParamIndex = MethodUtil.findIdParameterIndex(theMethod);
Description description = theMethod.getAnnotation(Description.class);
if (description != null) {
myDescription = description.formalDefinition();
if (isBlank(myDescription)) {
myDescription = description.shortDefinition();
}
}
if (isBlank(myDescription)) {
myDescription = null;
}
if (isBlank(theOperationName)) {
throw new ConfigurationException("Method '" + theMethod.getName() + "' on type " + theMethod.getDeclaringClass().getName() + " is annotated with @" + Operation.class.getSimpleName()
+ " but this annotation has no name defined");
throw new ConfigurationException("Method '" + theMethod.getName() + "' on type " + theMethod.getDeclaringClass().getName() + " is annotated with @" + Operation.class.getSimpleName() + " but this annotation has no name defined");
}
if (theOperationName.startsWith("$") == false) {
theOperationName = "$" + theOperationName;
}
myName = theOperationName;
if (theContext.getVersion().getVersion().isEquivalentTo(FhirVersionEnum.DSTU1)) {
throw new ConfigurationException("@" + Operation.class.getSimpleName() + " methods are not supported on servers for FHIR version " + theContext.getVersion().getVersion().name());
}
@ -97,16 +102,15 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding {
}
if (theMethod.getReturnType().isAssignableFrom(Bundle.class)) {
throw new ConfigurationException("Can not return a DSTU1 bundle from an @" + Operation.class.getSimpleName() + " method. Found in method " + theMethod.getName() + " defined in type "
+ theMethod.getDeclaringClass().getName());
throw new ConfigurationException("Can not return a DSTU1 bundle from an @" + Operation.class.getSimpleName() + " method. Found in method " + theMethod.getName() + " defined in type " + theMethod.getDeclaringClass().getName());
}
if (theMethod.getReturnType().equals(IBundleProvider.class)) {
myReturnType = ReturnTypeEnum.BUNDLE;
} else {
myReturnType = ReturnTypeEnum.RESOURCE;
}
if (getResourceName() == null) {
myOtherOperatiopnType = OtherOperationTypeEnum.EXTENDED_OPERATION_SERVER;
} else if (myIdParamIndex == null) {
@ -116,6 +120,18 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding {
}
}
public OperationMethodBinding(Class<?> theReturnResourceType, Class<? extends IBaseResource> theReturnTypeFromRp, Method theMethod, FhirContext theContext, Object theProvider, Operation theAnnotation) {
this(theReturnResourceType, theReturnTypeFromRp, theMethod, theContext, theProvider, theAnnotation.idempotent(), theAnnotation.name(), theAnnotation.type());
}
public String getDescription() {
return myDescription;
}
public String getName() {
return myName;
}
@Override
public OtherOperationTypeEnum getOtherOperationType() {
return myOtherOperatiopnType;
@ -183,17 +199,16 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding {
if (theRequest.getRequestType() == RequestTypeEnum.POST) {
// always ok
} else if (theRequest.getRequestType() == RequestTypeEnum.GET) {
if (!myHttpGetPermitted) {
if (!myIdempotent) {
String message = getContext().getLocalizer().getMessage(OperationMethodBinding.class, "methodNotSupported", theRequest.getRequestType(), RequestTypeEnum.POST.name());
throw new MethodNotAllowedException(message, RequestTypeEnum.POST);
}
} else {
if (!myHttpGetPermitted) {
if (!myIdempotent) {
String message = getContext().getLocalizer().getMessage(OperationMethodBinding.class, "methodNotSupported", theRequest.getRequestType(), RequestTypeEnum.POST.name());
throw new MethodNotAllowedException(message, RequestTypeEnum.POST);
} else {
String message = getContext().getLocalizer().getMessage(OperationMethodBinding.class, "methodNotSupported", theRequest.getRequestType(), RequestTypeEnum.GET.name(),
RequestTypeEnum.POST.name());
String message = getContext().getLocalizer().getMessage(OperationMethodBinding.class, "methodNotSupported", theRequest.getRequestType(), RequestTypeEnum.GET.name(), RequestTypeEnum.POST.name());
throw new MethodNotAllowedException(message, RequestTypeEnum.GET, RequestTypeEnum.POST);
}
}
@ -207,8 +222,15 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding {
return retVal;
}
public static BaseHttpClientInvocation createOperationInvocation(FhirContext theContext, String theResourceName, String theId, String theOperationName, IBaseParameters theInput,
boolean theUseHttpGet) {
public boolean isIdempotent() {
return myIdempotent;
}
public boolean isInstanceLevel() {
return myIdParamIndex != null;
}
public static BaseHttpClientInvocation createOperationInvocation(FhirContext theContext, String theResourceName, String theId, String theOperationName, IBaseParameters theInput, boolean theUseHttpGet) {
StringBuilder b = new StringBuilder();
if (theResourceName != null) {
b.append(theResourceName);
@ -242,7 +264,7 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding {
if (!params.containsKey(nextName)) {
params.put(nextName, new ArrayList<String>());
}
IBaseDatatype value = (IBaseDatatype) t.getSingleValueOrNull((IBase) nextParameter, "value[x]");
if (value == null) {
continue;

View File

@ -40,7 +40,9 @@ import ca.uhn.fhir.context.RuntimeChildPrimitiveDatatypeDefinition;
import ca.uhn.fhir.context.RuntimePrimitiveDatatypeDefinition;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.i18n.HapiLocalizer;
import ca.uhn.fhir.model.primitive.CodeDt;
import ca.uhn.fhir.model.primitive.StringDt;
import ca.uhn.fhir.rest.annotation.OperationParam;
import ca.uhn.fhir.rest.api.RequestTypeEnum;
import ca.uhn.fhir.rest.param.CollectionBinder;
import ca.uhn.fhir.rest.param.ResourceParameter;
@ -48,18 +50,27 @@ import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException;
class OperationParameter implements IParameter {
public class OperationParameter implements IParameter {
private IConverter myConverter;
@SuppressWarnings("rawtypes")
private Class<? extends Collection> myInnerCollectionType;
private int myMax;
private int myMin;
private final String myName;
private final String myOperationName;
private Class<?> myParameterType;
private RestSearchParameterTypeEnum myParamType;
OperationParameter(String theOperationName, String theParameterName) {
public OperationParameter(String theOperationName, OperationParam theOperationParam) {
this(theOperationName, theOperationParam.name(), theOperationParam.min(), theOperationParam.max());
}
OperationParameter(String theOperationName, String theParameterName, int theMin, int theMax) {
myOperationName = theOperationName;
myName = theParameterName;
myMin = theMin;
myMax = theMax;
}
private void addClientParameter(FhirContext theContext, Object theSourceClientArgument, IBaseResource theTargetResource, BaseRuntimeChildDefinition paramChild, BaseRuntimeElementCompositeDefinition<?> paramChildElem) {
@ -78,7 +89,7 @@ class OperationParameter implements IParameter {
throw new IllegalArgumentException("Don't know how to handle value of type " + theSourceClientArgument.getClass() + " for paramater " + myName);
}
}
private IBase createParameterRepetition(FhirContext theContext, IBaseResource theTargetResource, BaseRuntimeChildDefinition paramChild, BaseRuntimeElementCompositeDefinition<?> paramChildElem) {
IBase parameter = paramChildElem.newInstance();
paramChild.getMutator().addValue(theTargetResource, parameter);
@ -92,11 +103,29 @@ class OperationParameter implements IParameter {
return parameter;
}
public int getMax() {
return myMax;
}
public int getMin() {
return myMin;
}
public String getName() {
return myName;
}
public RestSearchParameterTypeEnum getParamType() {
return myParamType;
}
@Override
public void initializeTypes(Method theMethod, Class<? extends Collection<?>> theOuterCollectionType, Class<? extends Collection<?>> theInnerCollectionType, Class<?> theParameterType) {
myParameterType = theParameterType;
if (theInnerCollectionType != null) {
myInnerCollectionType = CollectionBinder.getInstantiableCollectionType(theInnerCollectionType, myName);
} else {
myMax = 1;
}
}
@ -106,8 +135,7 @@ class OperationParameter implements IParameter {
}
@Override
public void translateClientArgumentIntoQueryArgument(FhirContext theContext, Object theSourceClientArgument, Map<String, List<String>> theTargetQueryArguments, IBaseResource theTargetResource)
throws InternalErrorException {
public void translateClientArgumentIntoQueryArgument(FhirContext theContext, Object theSourceClientArgument, Map<String, List<String>> theTargetQueryArguments, IBaseResource theTargetResource) throws InternalErrorException {
assert theTargetResource != null;
Object sourceClientArgument = theSourceClientArgument;
if (sourceClientArgument == null) {
@ -117,7 +145,7 @@ class OperationParameter implements IParameter {
if (myConverter != null) {
sourceClientArgument = myConverter.outgoingClient(sourceClientArgument);
}
RuntimeResourceDefinition def = theContext.getResourceDefinition(theTargetResource);
BaseRuntimeChildDefinition paramChild = def.getChildByName("parameter");
@ -151,7 +179,7 @@ class OperationParameter implements IParameter {
} else {
FhirContext ctx = theRequest.getServer().getFhirContext();
if (theRequest.getRequestType() == RequestTypeEnum.GET) {
return null;
}
@ -223,19 +251,18 @@ class OperationParameter implements IParameter {
nextValue = myConverter.incomingServer(nextValue);
}
if (!myParameterType.isAssignableFrom(nextValue.getClass())) {
throw new InvalidRequestException("Request has parameter " + myName + " of type " + nextValue.getClass().getSimpleName() + " but method expects type "
+ myParameterType.getSimpleName());
throw new InvalidRequestException("Request has parameter " + myName + " of type " + nextValue.getClass().getSimpleName() + " but method expects type " + myParameterType.getSimpleName());
}
theMatchingParamValues.add(nextValue);
}
}
public interface IConverter {
Object incomingServer(Object theObject);
Object outgoingClient(Object theObject);
}
}

View File

@ -27,7 +27,7 @@ public class ValidateMethodBindingDstu2 extends OperationMethodBinding {
if (String.class.equals(parameterType) || EncodingEnum.class.equals(parameterType)) {
newParams.add(next);
} else {
OperationParameter parameter = new OperationParameter(Constants.EXTOP_VALIDATE, Constants.EXTOP_VALIDATE_RESOURCE);
OperationParameter parameter = new OperationParameter(Constants.EXTOP_VALIDATE, Constants.EXTOP_VALIDATE_RESOURCE, 0, 1);
parameter.initializeTypes(theMethod, null, null, parameterType);
newParams.add(parameter);
}

View File

@ -41,6 +41,9 @@ 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.RestResourceInteraction;
import ca.uhn.fhir.model.dstu2.resource.Conformance.RestResourceSearchParam;
import ca.uhn.fhir.model.dstu2.resource.OperationDefinition.Parameter;
import ca.uhn.fhir.model.dstu2.valueset.ConformanceResourceStatusEnum;
import ca.uhn.fhir.model.dstu2.valueset.OperationParameterUseEnum;
import ca.uhn.fhir.model.dstu2.valueset.ResourceTypeEnum;
import ca.uhn.fhir.model.dstu2.valueset.RestfulConformanceModeEnum;
import ca.uhn.fhir.model.dstu2.valueset.SystemRestfulInteractionEnum;
@ -48,10 +51,12 @@ import ca.uhn.fhir.model.dstu2.valueset.TypeRestfulInteractionEnum;
import ca.uhn.fhir.model.primitive.DateTimeDt;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.annotation.Metadata;
import ca.uhn.fhir.rest.annotation.OperationParam;
import ca.uhn.fhir.rest.method.BaseMethodBinding;
import ca.uhn.fhir.rest.method.DynamicSearchMethodBinding;
import ca.uhn.fhir.rest.method.IParameter;
import ca.uhn.fhir.rest.method.OperationMethodBinding;
import ca.uhn.fhir.rest.method.OperationParameter;
import ca.uhn.fhir.rest.method.SearchMethodBinding;
import ca.uhn.fhir.rest.method.SearchParameter;
import ca.uhn.fhir.rest.server.Constants;
@ -171,7 +176,28 @@ public class ServerConformanceProvider implements IServerConformanceProvider<Con
} else if (nextMethodBinding instanceof OperationMethodBinding) {
OperationMethodBinding methodBinding = (OperationMethodBinding)nextMethodBinding;
OperationDefinition op = new OperationDefinition();
// op.
rest.addOperation().setName(methodBinding.getName()).getDefinition().setResource(op);;
op.setStatus(ConformanceResourceStatusEnum.ACTIVE);
op.setDescription(methodBinding.getDescription());
op.setIdempotent(methodBinding.isIdempotent());
op.setCode(methodBinding.getName());
op.setInstance(methodBinding.isInstanceLevel());
op.addType().setValue(methodBinding.getResourceName());
for (IParameter nextParamUntyped : methodBinding.getParameters()) {
if (nextParamUntyped instanceof OperationParameter) {
OperationParameter nextParam = (OperationParameter)nextParamUntyped;
Parameter param = op.addParameter();
param.setUse(OperationParameterUseEnum.IN);
if (nextParam.getParamType() != null) {
param.setType(nextParam.getParamType().getCode());
}
param.setMin(nextParam.getMin());
param.setMax(nextParam.getMax() == -1 ? "*" : Integer.toString(nextParam.getMax()));
param.setName(nextParam.getName());
}
}
}
Collections.sort(resource.getInteraction(), new Comparator<RestResourceInteraction>() {

View File

@ -26,16 +26,19 @@ 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.primitive.DateDt;
import ca.uhn.fhir.model.primitive.StringDt;
import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.IncludeParam;
import ca.uhn.fhir.rest.annotation.Operation;
import ca.uhn.fhir.rest.annotation.OperationParam;
import ca.uhn.fhir.rest.annotation.OptionalParam;
import ca.uhn.fhir.rest.annotation.RequiredParam;
import ca.uhn.fhir.rest.annotation.Search;
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;
@ -78,25 +81,6 @@ public class ServerConformanceProviderDstu2Test {
}
@Test
public void testEverythingOperationDocumentation() throws Exception {
RestfulServer rs = new RestfulServer();
rs.setProviders(new ProviderWithOperations());
ServerConformanceProvider sc = new ServerConformanceProvider(rs);
rs.setServerConformanceProvider(sc);
rs.init(createServletConfig());
Conformance conformance = sc.getServerConformance(createHttpServletRequest());
String conf = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance);
ourLog.info(conf);
}
@Test
public void testOperationDocumentation() throws Exception {
@ -130,6 +114,23 @@ public class ServerConformanceProviderDstu2Test {
}
@Test
public void testExtendedOperationReturningBundle() throws Exception {
RestfulServer rs = new RestfulServer();
rs.setProviders(new ProviderWithExtendedOperationReturningBundle());
ServerConformanceProvider sc = new ServerConformanceProvider(rs);
rs.setServerConformanceProvider(sc);
rs.init(createServletConfig());
Conformance conformance = sc.getServerConformance(createHttpServletRequest());
String conf = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance);
ourLog.info(conf);
}
@Test
public void testValidateGeneratedStatement() throws Exception {
@ -260,12 +261,14 @@ public class ServerConformanceProviderDstu2Test {
}
public static class ProviderWithOperations implements IResourceProvider {
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) {
@IdParam ca.uhn.fhir.model.primitive.IdDt theId,
@OperationParam(name="start") DateDt theStart,
@OperationParam(name="end") DateDt theEnd) {
return null;
}