diff --git a/examples/src/main/java/example/ServerOperations.java b/examples/src/main/java/example/ServerOperations.java index 9bfb9993d7d..069cc8a3ef4 100644 --- a/examples/src/main/java/example/ServerOperations.java +++ b/examples/src/main/java/example/ServerOperations.java @@ -2,6 +2,8 @@ package example; import java.util.List; +import org.hl7.fhir.dstu3.model.Parameters; + import ca.uhn.fhir.model.dstu2.composite.CodingDt; import ca.uhn.fhir.model.dstu2.resource.Bundle; import ca.uhn.fhir.model.dstu2.resource.ConceptMap; @@ -11,10 +13,40 @@ import ca.uhn.fhir.model.primitive.StringDt; import ca.uhn.fhir.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.Operation; import ca.uhn.fhir.rest.annotation.OperationParam; +import ca.uhn.fhir.rest.param.DateParam; +import ca.uhn.fhir.rest.param.DateRangeParam; +import ca.uhn.fhir.rest.param.StringParam; +import ca.uhn.fhir.rest.param.TokenAndListParam; +import ca.uhn.fhir.rest.param.TokenParam; public class ServerOperations { + //START SNIPPET: searchParamBasic + @Operation(name="$find-matches", idempotent=true) + public Parameters findMatchesBasic( + @OperationParam(name="date") DateParam theDate, + @OperationParam(name="code") TokenParam theCode) { + + Parameters retVal = new Parameters(); + // Populate bundle with matching resources + return retVal; + } + //END SNIPPET: searchParamBasic + + //START SNIPPET: searchParamAdvanced + @Operation(name="$find-matches", idempotent=true) + public Parameters findMatchesAdvanced( + @OperationParam(name="dateRange") DateRangeParam theDate, + @OperationParam(name="name") List theName, + @OperationParam(name="code") TokenAndListParam theEnd) { + + Parameters retVal = new Parameters(); + // Populate bundle with matching resources + return retVal; + } + //END SNIPPET: searchParamAdvanced + //START SNIPPET: patientTypeOperation @Operation(name="$everything", idempotent=true) public Bundle patientTypeOperation( diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationParam.java index ca143bd1ccd..56bcf1d47d9 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationParam.java @@ -41,6 +41,24 @@ public @interface OperationParam { */ final int MAX_UNLIMITED = -1; + + /** + * Value for {@link OperationParam#max()} indicating that the maximum will be inferred + * from the type. If the type is a single parameter type (e.g. StringDt, + * TokenParam, IBaseResource) the maximum will be + * 1. + *

+ * If the type is a collection, e.g. + * List<StringDt> or List<TokenOrListParam> + * the maximum will be set to *. If the param is a search parameter + * "and" type, such as TokenAndListParam the maximum will also be + * set to * + *

+ * + * @since 1.5 + */ + final int MAX_DEFAULT = -2; + /** * The name of the parameter */ @@ -66,9 +84,10 @@ public @interface OperationParam { /** * 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 (default is 1) + * repetitions. See {@link #MAX_DEFAULT} for a description of the default + * behaviour. */ - int max() default 1; + int max() default MAX_UNLIMITED; } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/GenericClient.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/GenericClient.java index c0454e24838..cb0732c53ab 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/GenericClient.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/GenericClient.java @@ -1553,6 +1553,33 @@ public class GenericClient extends BaseClient implements IGenericClient { return this; } + @Override + public IOperationUntypedWithInputAndPartialOutput andSearchParameter(String theName, IQueryParameterType theValue) { + addParam(theName, theValue); + + return this; + } + + @SuppressWarnings("unchecked") + @Override + public IOperationUntypedWithInputAndPartialOutput withSearchParameter(Class theParameterType, String theName, IQueryParameterType theValue) { + Validate.notNull(theParameterType, "theParameterType must not be null"); + Validate.notEmpty(theName, "theName must not be null"); + Validate.notNull(theValue, "theValue must not be null"); + + myParametersDef = myContext.getResourceDefinition(theParameterType); + myParameters = (IBaseParameters) myParametersDef.newInstance(); + + addParam(theName, theValue); + + return this; + } + + private void addParam(String theName, IQueryParameterType theValue) { + IPrimitiveType stringType = ParametersUtil.createString(myContext, theValue.getValueAsQueryToken(myContext)); + addParam(theName, stringType); + } + } private final class OperationOutcomeResponseHandler implements IClientResponseHandler { diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IOperationUntyped.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IOperationUntyped.java index 0499f08601b..708ff61e0c2 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IOperationUntyped.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IOperationUntyped.java @@ -24,6 +24,8 @@ import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBaseParameters; +import ca.uhn.fhir.model.api.IQueryParameterType; + public interface IOperationUntyped { /** @@ -54,4 +56,16 @@ public interface IOperationUntyped { */ IOperationUntypedWithInputAndPartialOutput withParameter(Class theParameterType, String theName, IBase theValue); + /** + * Use chained method calls to construct a Parameters input. This form is a convenience + * in order to allow simple method chaining to be used to build up a parameters + * resource for the input of an operation without needing to manually construct one. + * + * @param theParameterType The type to use for the output parameters (this should be set to + * Parameters.class drawn from the version of the FHIR structures you are using) + * @param theName The first parameter name + * @param theValue The first parameter value + */ + IOperationUntypedWithInputAndPartialOutput withSearchParameter(Class theParameterType, String theName, IQueryParameterType theValue); + } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IOperationUntypedWithInputAndPartialOutput.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IOperationUntypedWithInputAndPartialOutput.java index fcbfe7b4022..373fc5079de 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IOperationUntypedWithInputAndPartialOutput.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IOperationUntypedWithInputAndPartialOutput.java @@ -23,6 +23,8 @@ package ca.uhn.fhir.rest.gclient; import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBaseParameters; +import ca.uhn.fhir.model.api.IQueryParameterType; + public interface IOperationUntypedWithInputAndPartialOutput extends IOperationUntypedWithInput { /** @@ -35,4 +37,14 @@ public interface IOperationUntypedWithInputAndPartialOutput andParameter(String theName, IBase theValue); + /** + * Use chained method calls to construct a Parameters input. This form is a convenience + * in order to allow simple method chaining to be used to build up a parameters + * resource for the input of an operation without needing to manually construct one. + * + * @param theName The first parameter name + * @param theValue The first parameter value + */ + IOperationUntypedWithInputAndPartialOutput andSearchParameter(String theName, IQueryParameterType theValue); + } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/OperationMethodBinding.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/OperationMethodBinding.java index b7e16a497bf..81151e40c9d 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/OperationMethodBinding.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/OperationMethodBinding.java @@ -150,6 +150,9 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding { type.setName(next.name()); type.setMin(next.min()); type.setMax(next.max()); + if (type.getMax() == OperationParam.MAX_DEFAULT) { + type.setMax(1); + } if (!next.type().equals(IBase.class)) { if (next.type().isInterface() || Modifier.isAbstract(next.type().getModifiers())) { throw new ConfigurationException("Invalid value for @OperationParam.type(): " + next.type().getName()); diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/OperationParameter.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/OperationParameter.java index 9a8cecca753..501a4231717 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/OperationParameter.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/OperationParameter.java @@ -25,9 +25,11 @@ import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.Map; +import org.apache.commons.lang3.Validate; import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBaseDatatype; import org.hl7.fhir.instance.model.api.IBaseResource; @@ -45,9 +47,13 @@ import ca.uhn.fhir.context.RuntimePrimitiveDatatypeDefinition; import ca.uhn.fhir.context.RuntimeResourceDefinition; import ca.uhn.fhir.i18n.HapiLocalizer; import ca.uhn.fhir.model.api.IDatatype; +import ca.uhn.fhir.model.api.IQueryParameterAnd; +import ca.uhn.fhir.model.api.IQueryParameterOr; +import ca.uhn.fhir.model.api.IQueryParameterType; import ca.uhn.fhir.rest.annotation.OperationParam; import ca.uhn.fhir.rest.api.RequestTypeEnum; import ca.uhn.fhir.rest.api.ValidationModeEnum; +import ca.uhn.fhir.rest.param.BaseAndListParam; import ca.uhn.fhir.rest.param.CollectionBinder; import ca.uhn.fhir.rest.param.DateRangeParam; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; @@ -58,8 +64,14 @@ import ca.uhn.fhir.util.ParametersUtil; public class OperationParameter implements IParameter { + @SuppressWarnings("unchecked") + private static final Class[] COMPOSITE_TYPES = new Class[0]; + static final String REQUEST_CONTENTS_USERDATA_KEY = OperationParam.class.getName() + "_PARSED_RESOURCE"; - + + private boolean myAllowGet; + + private final FhirContext myContext; private IConverter myConverter; @SuppressWarnings("rawtypes") private Class myInnerCollectionType; @@ -69,8 +81,7 @@ public class OperationParameter implements IParameter { private final String myOperationName; private Class myParameterType; private String myParamType; - private final FhirContext myContext; - private boolean myAllowGet; + private SearchParameter mySearchParameterBinding; public OperationParameter(FhirContext theCtx, String theOperationName, OperationParam theOperationParam) { this(theCtx, theOperationName, theOperationParam.name(), theOperationParam.min(), theOperationParam.max()); @@ -84,6 +95,21 @@ public class OperationParameter implements IParameter { myContext = theCtx; } + @SuppressWarnings({ "rawtypes", "unchecked" }) + private void addValueToList(List matchingParamValues, Object values) { + if (values != null) { + if (BaseAndListParam.class.isAssignableFrom(myParameterType) && matchingParamValues.size() > 0) { + BaseAndListParam existing = (BaseAndListParam) matchingParamValues.get(0); + BaseAndListParam newAndList = (BaseAndListParam) values; + for (IQueryParameterOr nextAnd : newAndList.getValuesAsQueryTokens()) { + existing.addAnd(nextAnd); + } + } else { + matchingParamValues.add(values); + } + } + } + protected FhirContext getContext() { return myContext; } @@ -104,27 +130,60 @@ public class OperationParameter implements IParameter { return myParamType; } + public String getSearchParamType() { + if (mySearchParameterBinding != null) { + return mySearchParameterBinding.getParamType().getCode(); + } else { + return null; + } + } + @SuppressWarnings("unchecked") @Override public void initializeTypes(Method theMethod, Class> theOuterCollectionType, Class> theInnerCollectionType, Class theParameterType) { if (getContext().getVersion().getVersion().isRi()) { if (IDatatype.class.isAssignableFrom(theParameterType)) { - throw new ConfigurationException("Incorrect use of type " + theParameterType.getSimpleName() + " as parameter type for method when context is for version " - + getContext().getVersion().getVersion().name() + " in method: " + theMethod.toString()); + throw new ConfigurationException("Incorrect use of type " + theParameterType.getSimpleName() + " as parameter type for method when context is for version " + getContext().getVersion().getVersion().name() + " in method: " + theMethod.toString()); } } myParameterType = theParameterType; if (theInnerCollectionType != null) { myInnerCollectionType = CollectionBinder.getInstantiableCollectionType(theInnerCollectionType, myName); + if (myMax == OperationParam.MAX_DEFAULT) { + myMax = OperationParam.MAX_UNLIMITED; + } + } else if (IQueryParameterAnd.class.isAssignableFrom(myParameterType)) { + if (myMax == OperationParam.MAX_DEFAULT) { + myMax = OperationParam.MAX_UNLIMITED; + } } else { - myMax = 1; + if (myMax == OperationParam.MAX_DEFAULT) { + myMax = 1; + } } - myAllowGet = IPrimitiveType.class.isAssignableFrom(myParameterType) || String.class.equals(myParameterType); + boolean typeIsConcrete = !myParameterType.isInterface() && !Modifier.isAbstract(myParameterType.getModifiers()); + + //@formatter:off + boolean isSearchParam = + IQueryParameterType.class.isAssignableFrom(myParameterType) || + IQueryParameterOr.class.isAssignableFrom(myParameterType) || + IQueryParameterAnd.class.isAssignableFrom(myParameterType); + //@formatter:off /* - * The parameter can be of type string for validation methods - This is a bit weird. See ValidateDstu2Test. We should probably clean this up.. + * Note: We say here !IBase.class.isAssignableFrom because a bunch of DSTU1/2 datatypes also + * extend this interface. I'm not sure if they should in the end.. but they do, so we + * exclude them. + */ + isSearchParam &= typeIsConcrete && !IBase.class.isAssignableFrom(myParameterType); + + myAllowGet = IPrimitiveType.class.isAssignableFrom(myParameterType) || String.class.equals(myParameterType) || isSearchParam; + + /* + * The parameter can be of type string for validation methods - This is a bit weird. See ValidateDstu2Test. We + * should probably clean this up.. */ if (!myParameterType.equals(IBase.class) && !myParameterType.equals(String.class)) { if (IBaseResource.class.isAssignableFrom(myParameterType) && myParameterType.isInterface()) { @@ -135,13 +194,20 @@ public class OperationParameter implements IParameter { myAllowGet = true; } else if (myParameterType.equals(ValidationModeEnum.class)) { // this is ok - } else if (!IBase.class.isAssignableFrom(myParameterType) || myParameterType.isInterface() || Modifier.isAbstract(myParameterType.getModifiers())) { - throw new ConfigurationException("Invalid type for @OperationParam: " + myParameterType.getName()); } else if (myParameterType.equals(ValidationModeEnum.class)) { myParamType = "code"; - } else { + } else if (IBase.class.isAssignableFrom(myParameterType) && typeIsConcrete) { myParamType = myContext.getElementDefinition((Class) myParameterType).getName(); + } else if (isSearchParam) { + myParamType = "string"; + mySearchParameterBinding = new SearchParameter(myName, myMin > 0); + mySearchParameterBinding.setCompositeTypes(COMPOSITE_TYPES); + mySearchParameterBinding.setType(theParameterType, theInnerCollectionType, theOuterCollectionType); + myConverter = new QueryParameterConverter(); + } else { + throw new ConfigurationException("Invalid type for @OperationParam: " + myParameterType.getName()); } + } } @@ -151,9 +217,12 @@ public class OperationParameter implements IParameter { return this; } + private void throwWrongParamType(Object nextValue) { + throw new InvalidRequestException("Request has parameter " + myName + " of type " + nextValue.getClass().getSimpleName() + " but method expects type " + myParameterType.getSimpleName()); + } + @Override - public void translateClientArgumentIntoQueryArgument(FhirContext theContext, Object theSourceClientArgument, Map> theTargetQueryArguments, IBaseResource theTargetResource) - throws InternalErrorException { + public void translateClientArgumentIntoQueryArgument(FhirContext theContext, Object theSourceClientArgument, Map> theTargetQueryArguments, IBaseResource theTargetResource) throws InternalErrorException { assert theTargetResource != null; Object sourceClientArgument = theSourceClientArgument; if (sourceClientArgument == null) { @@ -173,46 +242,77 @@ public class OperationParameter implements IParameter { List matchingParamValues = new ArrayList(); if (theRequest.getRequestType() == RequestTypeEnum.GET) { - String[] paramValues = theRequest.getParameters().get(myName); - if (paramValues != null && paramValues.length > 0) { - if (myAllowGet) { + if (mySearchParameterBinding != null) { - if (DateRangeParam.class.isAssignableFrom(myParameterType)) { - List parameters = new ArrayList(); - parameters.add(QualifiedParamList.singleton(paramValues[0])); - if (paramValues.length > 1) { - parameters.add(QualifiedParamList.singleton(paramValues[1])); - } - DateRangeParam dateRangeParam = new DateRangeParam(); - dateRangeParam.setValuesAsQueryTokens(parameters); - matchingParamValues.add(dateRangeParam); - } else if (String.class.isAssignableFrom(myParameterType)) { - - for (String next : paramValues) { - matchingParamValues.add(next); - } + List params = new ArrayList(); + String nameWithQualifierColon = myName + ":"; + for (String nextParamName : theRequest.getParameters().keySet()) { + String qualifier; + if (nextParamName.equals(myName)) { + qualifier = null; + } else if (nextParamName.startsWith(nameWithQualifierColon)) { + qualifier = nextParamName.substring(nextParamName.indexOf(':')); } else { - for (String nextValue : paramValues) { - FhirContext ctx = theRequest.getServer().getFhirContext(); - RuntimePrimitiveDatatypeDefinition def = (RuntimePrimitiveDatatypeDefinition) ctx.getElementDefinition((Class) myParameterType); - IPrimitiveType instance = def.newInstance(); - instance.setValueAsString(nextValue); - matchingParamValues.add(instance); + // This is some other parameter, not the one bound by this instance + continue; + } + String[] values = theRequest.getParameters().get(nextParamName); + if (values != null) { + for (String nextValue : values) { + params.add(QualifiedParamList.splitQueryStringByCommasIgnoreEscape(qualifier, nextValue)); } } - } else { - HapiLocalizer localizer = theRequest.getServer().getFhirContext().getLocalizer(); - String msg = localizer.getMessage(OperationParameter.class, "urlParamNotPrimitive", myOperationName, myName); - throw new MethodNotAllowedException(msg, RequestTypeEnum.POST); + } + if (!params.isEmpty()) { + for (QualifiedParamList next : params) { + Object values = mySearchParameterBinding.parse(myContext, Collections.singletonList(next)); + addValueToList(matchingParamValues, values); + } + + } + + } else { + String[] paramValues = theRequest.getParameters().get(myName); + if (paramValues != null && paramValues.length > 0) { + if (myAllowGet) { + + if (DateRangeParam.class.isAssignableFrom(myParameterType)) { + List parameters = new ArrayList(); + parameters.add(QualifiedParamList.singleton(paramValues[0])); + if (paramValues.length > 1) { + parameters.add(QualifiedParamList.singleton(paramValues[1])); + } + DateRangeParam dateRangeParam = new DateRangeParam(); + dateRangeParam.setValuesAsQueryTokens(parameters); + matchingParamValues.add(dateRangeParam); + } else if (String.class.isAssignableFrom(myParameterType)) { + + for (String next : paramValues) { + matchingParamValues.add(next); + } + + } else { + for (String nextValue : paramValues) { + FhirContext ctx = theRequest.getServer().getFhirContext(); + RuntimePrimitiveDatatypeDefinition def = (RuntimePrimitiveDatatypeDefinition) ctx.getElementDefinition((Class) myParameterType); + IPrimitiveType instance = def.newInstance(); + instance.setValueAsString(nextValue); + matchingParamValues.add(instance); + } + } + } else { + HapiLocalizer localizer = theRequest.getServer().getFhirContext().getLocalizer(); + String msg = localizer.getMessage(OperationParameter.class, "urlParamNotPrimitive", myOperationName, myName); + throw new MethodNotAllowedException(msg, RequestTypeEnum.POST); + } } } + } else { - FhirContext ctx = theRequest.getServer().getFhirContext(); - IBaseResource requestContents = (IBaseResource) theRequest.getUserData().get(REQUEST_CONTENTS_USERDATA_KEY); - RuntimeResourceDefinition def = ctx.getResourceDefinition(requestContents); + RuntimeResourceDefinition def = myContext.getResourceDefinition(requestContents); if (def.getName().equals("Parameters")) { BaseRuntimeChildDefinition paramChild = def.getChildByName("parameter"); @@ -247,7 +347,7 @@ public class OperationParameter implements IParameter { } } else { - + if (myParameterType.isAssignableFrom(requestContents.getClass())) { tryToAddValues(Arrays.asList((IBase) requestContents), matchingParamValues); } @@ -264,8 +364,7 @@ public class OperationParameter implements IParameter { } try { - @SuppressWarnings("rawtypes") - Collection retVal = myInnerCollectionType.newInstance(); + Collection retVal = myInnerCollectionType.newInstance(); retVal.addAll(matchingParamValues); return retVal; } catch (InstantiationException e) { @@ -301,14 +400,11 @@ public class OperationParameter implements IParameter { } throwWrongParamType(nextValue); } - theMatchingParamValues.add(nextValue); + + addValueToList(theMatchingParamValues, nextValue); } } - private void throwWrongParamType(Object nextValue) { - throw new InvalidRequestException("Request has parameter " + myName + " of type " + nextValue.getClass().getSimpleName() + " but method expects type " + myParameterType.getSimpleName()); - } - public interface IConverter { Object incomingServer(Object theObject); @@ -317,4 +413,27 @@ public class OperationParameter implements IParameter { } + private class QueryParameterConverter implements IConverter { + + public QueryParameterConverter() { + Validate.isTrue(mySearchParameterBinding != null); + } + + @Override + public Object incomingServer(Object theObject) { + IPrimitiveType obj = (IPrimitiveType) theObject; + List paramList = Collections.singletonList(QualifiedParamList.splitQueryStringByCommasIgnoreEscape(null, obj.getValueAsString())); + return mySearchParameterBinding.parse(myContext, paramList); + } + + @Override + public Object outgoingClient(Object theObject) { + IQueryParameterType obj = (IQueryParameterType) theObject; + IPrimitiveType retVal = (IPrimitiveType) myContext.getElementDefinition("string").newInstance(); + retVal.setValueAsString(obj.getValueAsQueryToken(myContext)); + return retVal; + } + + } + } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/BaseOrListParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/BaseOrListParam.java index bafc9bfd550..8c016a25862 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/BaseOrListParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/BaseOrListParam.java @@ -27,9 +27,9 @@ import ca.uhn.fhir.model.api.IQueryParameterOr; import ca.uhn.fhir.model.api.IQueryParameterType; import ca.uhn.fhir.rest.method.QualifiedParamList; -abstract class BaseOrListParam implements IQueryParameterOr { +abstract class BaseOrListParam, PT extends IQueryParameterType> implements IQueryParameterOr { - private List myList=new ArrayList(); + private List myList=new ArrayList(); // public void addToken(T theParam) { // Validate.notNull(theParam,"Param can not be null"); @@ -40,25 +40,26 @@ abstract class BaseOrListParam implements IQueryP public void setValuesAsQueryTokens(QualifiedParamList theParameters) { myList.clear(); for (String next : theParameters) { - T nextParam = newInstance(); + PT nextParam = newInstance(); nextParam.setValueAsQueryToken(theParameters.getQualifier(), next); myList.add(nextParam); } } - abstract T newInstance(); + abstract PT newInstance(); - public abstract BaseOrListParam addOr(T theParameter); + public abstract MT addOr(PT theParameter); - public BaseOrListParam add(T theParameter) { + @SuppressWarnings("unchecked") + public MT add(PT theParameter) { if (theParameter != null) { myList.add(theParameter); } - return this; + return (MT) this; } @Override - public List getValuesAsQueryTokens() { + public List getValuesAsQueryTokens() { return myList; } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/CompositeOrListParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/CompositeOrListParam.java index d0650f804a0..76862d4d185 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/CompositeOrListParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/CompositeOrListParam.java @@ -24,7 +24,7 @@ import ca.uhn.fhir.util.CoverageIgnore; */ -public class CompositeOrListParam extends BaseOrListParam> { +public class CompositeOrListParam extends BaseOrListParam, CompositeParam> { private Class myLeftType; private Class myRightType; diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/DateOrListParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/DateOrListParam.java index d02369d0f7d..6b32ebee6c7 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/DateOrListParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/DateOrListParam.java @@ -23,7 +23,7 @@ import ca.uhn.fhir.util.CoverageIgnore; */ -public class DateOrListParam extends BaseOrListParam { +public class DateOrListParam extends BaseOrListParam { @Override DateParam newInstance() { diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/NumberOrListParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/NumberOrListParam.java index 08b6863afd2..497f8fd8079 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/NumberOrListParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/NumberOrListParam.java @@ -23,7 +23,7 @@ import ca.uhn.fhir.util.CoverageIgnore; */ -public class NumberOrListParam extends BaseOrListParam { +public class NumberOrListParam extends BaseOrListParam { @Override NumberParam newInstance() { diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/QuantityOrListParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/QuantityOrListParam.java index dffdb2d14a1..90e8830bb9e 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/QuantityOrListParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/QuantityOrListParam.java @@ -23,7 +23,7 @@ import ca.uhn.fhir.util.CoverageIgnore; */ -public class QuantityOrListParam extends BaseOrListParam { +public class QuantityOrListParam extends BaseOrListParam { @Override QuantityParam newInstance() { diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ReferenceOrListParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ReferenceOrListParam.java index ddd720b0533..cf24c9079f8 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ReferenceOrListParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/ReferenceOrListParam.java @@ -23,7 +23,7 @@ import ca.uhn.fhir.util.CoverageIgnore; */ -public class ReferenceOrListParam extends BaseOrListParam { +public class ReferenceOrListParam extends BaseOrListParam { @CoverageIgnore @Override diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/StringOrListParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/StringOrListParam.java index 9fb42eee821..e51c0e36a7e 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/StringOrListParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/StringOrListParam.java @@ -23,7 +23,7 @@ import ca.uhn.fhir.util.CoverageIgnore; */ -public class StringOrListParam extends BaseOrListParam { +public class StringOrListParam extends BaseOrListParam { @CoverageIgnore @Override diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/TokenOrListParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/TokenOrListParam.java index 1a6f373a2a6..544e305dd52 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/TokenOrListParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/TokenOrListParam.java @@ -31,7 +31,7 @@ import ca.uhn.fhir.util.CoverageIgnore; * This class represents a restful search operation parameter for an "OR list" of tokens (in other words, a * list which can contain one-or-more tokens, where the server should return results matching any of the tokens) */ -public class TokenOrListParam extends BaseOrListParam { +public class TokenOrListParam extends BaseOrListParam { /** * Create a new empty token "OR list" diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/TokenParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/TokenParam.java index b57e523abb6..fbf53ab199d 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/TokenParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/TokenParam.java @@ -112,20 +112,24 @@ public class TokenParam extends BaseParam implements IQueryParameterType { if (theQualifier != null) { TokenParamModifier modifier = TokenParamModifier.forValue(theQualifier); setModifier(modifier); - setSystem(null); - setValue(ParameterUtil.unescape(theParameter)); + + if (modifier == TokenParamModifier.TEXT) { + setSystem(null); + setValue(ParameterUtil.unescape(theParameter)); + return; + } + } + + setSystem(null); + if (theParameter == null) { + setValue(null); } else { - setSystem(null); - if (theParameter == null) { - setValue(null); + int barIndex = ParameterUtil.nonEscapedIndexOf(theParameter, '|'); + if (barIndex != -1) { + setSystem(theParameter.substring(0, barIndex)); + setValue(ParameterUtil.unescape(theParameter.substring(barIndex + 1))); } else { - int barIndex = ParameterUtil.nonEscapedIndexOf(theParameter, '|'); - if (barIndex != -1) { - setSystem(theParameter.substring(0, barIndex)); - setValue(ParameterUtil.unescape(theParameter.substring(barIndex + 1))); - } else { - setValue(ParameterUtil.unescape(theParameter)); - } + setValue(ParameterUtil.unescape(theParameter)); } } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/UriOrListParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/UriOrListParam.java index dd5941af8d5..85f989f50a4 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/UriOrListParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/UriOrListParam.java @@ -23,7 +23,7 @@ import ca.uhn.fhir.util.CoverageIgnore; */ -public class UriOrListParam extends BaseOrListParam { +public class UriOrListParam extends BaseOrListParam { @CoverageIgnore @Override diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ParametersUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ParametersUtil.java index bf41e4375cb..7fa7041ea7c 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ParametersUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ParametersUtil.java @@ -68,15 +68,21 @@ public class ParametersUtil { IBase parameter = paramChildElem.newInstance(); paramChild.getMutator().addValue(theTargetResource, parameter); IPrimitiveType value; - if (theContext.getVersion().getVersion().isRi()) { - value = (IPrimitiveType) theContext.getElementDefinition("string").newInstance(theName); - } else { - value = new StringDt(theName); - } + value = createString(theContext, theName); paramChildElem.getChildByName("name").getMutator().addValue(parameter, value); return parameter; } + public static IPrimitiveType createString(FhirContext theContext, String theValue) { + IPrimitiveType value; + if (theContext.getVersion().getVersion().isRi()) { + value = (IPrimitiveType) theContext.getElementDefinition("string").newInstance(theValue); + } else { + value = new StringDt(theValue); + } + return value; + } + public static IBaseParameters newInstance(FhirContext theContext) { return (IBaseParameters) theContext.getResourceDefinition("Parameters").newInstance(); } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ReflectionUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ReflectionUtil.java index f0241e0b743..33a4e902adf 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ReflectionUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ReflectionUtil.java @@ -29,8 +29,39 @@ import java.lang.reflect.WildcardType; import java.util.LinkedHashSet; import java.util.List; +import ca.uhn.fhir.context.ConfigurationException; +import ca.uhn.fhir.model.api.IQueryParameterType; + public class ReflectionUtil { + public static LinkedHashSet getDeclaredMethods(Class theClazz) { + LinkedHashSet retVal = new LinkedHashSet(); + for (Method next : theClazz.getDeclaredMethods()) { + try { + Method method = theClazz.getMethod(next.getName(), next.getParameterTypes()); + retVal.add(method); + } catch (NoSuchMethodException e) { + retVal.add(next); + } catch (SecurityException e) { + retVal.add(next); + } + } + return retVal; + } + + public static Class getGenericCollectionTypeOfField(Field next) { + Class type; + ParameterizedType collectionType = (ParameterizedType) next.getGenericType(); + Type firstArg = collectionType.getActualTypeArguments()[0]; + if (ParameterizedType.class.isAssignableFrom(firstArg.getClass())) { + ParameterizedType pt = ((ParameterizedType) firstArg); + type = (Class) pt.getRawType(); + } else { + type = (Class) firstArg; + } + return type; + } + /** * For a field of type List>, returns Foo */ @@ -52,19 +83,6 @@ public class ReflectionUtil { return type; } - public static Class getGenericCollectionTypeOfField(Field next) { - Class type; - ParameterizedType collectionType = (ParameterizedType) next.getGenericType(); - Type firstArg = collectionType.getActualTypeArguments()[0]; - if (ParameterizedType.class.isAssignableFrom(firstArg.getClass())) { - ParameterizedType pt = ((ParameterizedType) firstArg); - type = (Class) pt.getRawType(); - } else { - type = (Class) firstArg; - } - return type; - } - public static Class getGenericCollectionTypeOfMethodParameter(Method theMethod, int theParamIndex) { Class type; Type genericParameterType = theMethod.getGenericParameterTypes()[theParamIndex]; @@ -106,19 +124,16 @@ public class ReflectionUtil { return type; } - public static LinkedHashSet getDeclaredMethods(Class theClazz) { - LinkedHashSet retVal = new LinkedHashSet(); - for (Method next : theClazz.getDeclaredMethods()) { - try { - Method method = theClazz.getMethod(next.getName(), next.getParameterTypes()); - retVal.add(method); - } catch (NoSuchMethodException e) { - retVal.add(next); - } catch (SecurityException e) { - retVal.add(next); - } + /** + * Instantiate a class by no-arg constructor, throw {@link ConfigurationException} if we fail to do so + */ + @CoverageIgnore + public static T newInstance(Class theType) { + try { + return theType.newInstance(); + } catch (Exception e) { + throw new ConfigurationException("Failed to instantiate " + theType.getName(), e); } - return retVal; } } diff --git a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/narrative/DefaultThymeleafNarrativeGeneratorTestDstu2.java b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/narrative/DefaultThymeleafNarrativeGeneratorTestDstu2.java index d3b5ebf9835..654c2a41890 100644 --- a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/narrative/DefaultThymeleafNarrativeGeneratorTestDstu2.java +++ b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/narrative/DefaultThymeleafNarrativeGeneratorTestDstu2.java @@ -77,7 +77,6 @@ public class DefaultThymeleafNarrativeGeneratorTestDstu2 { enc.addIdentifier().setSystem("urn:visits").setValue("1234567"); enc.setClassElement(EncounterClassEnum.AMBULATORY); enc.setPeriod(new PeriodDt().setStart(new DateTimeDt("2001-01-02T11:11:00"))); - enc.setType(ca.uhn.fhir.model.dstu2.valueset.EncounterTypeEnum.ANNUAL_DIABETES_MELLITUS_SCREENING); NarrativeDt narrative = new NarrativeDt(); myGen.generateNarrative(ourCtx, enc, narrative); diff --git a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/OperationServerTest.java b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/OperationServerDstu2Test.java similarity index 99% rename from hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/OperationServerTest.java rename to hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/OperationServerDstu2Test.java index a755c01be51..a66ec0469f3 100644 --- a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/OperationServerTest.java +++ b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/OperationServerDstu2Test.java @@ -49,13 +49,13 @@ import ca.uhn.fhir.rest.annotation.Read; import ca.uhn.fhir.rest.client.IGenericClient; import ca.uhn.fhir.util.PortUtil; -public class OperationServerTest { +public class OperationServerDstu2Test { private static CloseableHttpClient ourClient; private static FhirContext ourCtx; private static StringDt ourLastParam1; private static Patient ourLastParam2; - private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(OperationServerTest.class); + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(OperationServerDstu2Test.class); private static int ourPort; private static IdDt ourLastId; private static Server ourServer; diff --git a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/OperationServerWithSearchParamTypesDstu2Test.java b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/OperationServerWithSearchParamTypesDstu2Test.java new file mode 100644 index 00000000000..6b9250d5762 --- /dev/null +++ b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/OperationServerWithSearchParamTypesDstu2Test.java @@ -0,0 +1,416 @@ +package ca.uhn.fhir.rest.server; + +import static org.hamcrest.Matchers.stringContainsInOrder; +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 java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import javax.servlet.ServletConfig; +import javax.servlet.http.HttpServletRequest; + +import org.apache.commons.io.IOUtils; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.servlet.ServletHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.model.dstu2.resource.Conformance; +import ca.uhn.fhir.model.dstu2.resource.OperationDefinition; +import ca.uhn.fhir.model.dstu2.resource.Parameters; +import ca.uhn.fhir.model.dstu2.resource.Patient; +import ca.uhn.fhir.model.primitive.IdDt; +import ca.uhn.fhir.model.primitive.StringDt; +import ca.uhn.fhir.rest.annotation.Operation; +import ca.uhn.fhir.rest.annotation.OperationParam; +import ca.uhn.fhir.rest.param.StringAndListParam; +import ca.uhn.fhir.rest.param.StringOrListParam; +import ca.uhn.fhir.rest.param.StringParam; +import ca.uhn.fhir.rest.param.TokenAndListParam; +import ca.uhn.fhir.rest.param.TokenOrListParam; +import ca.uhn.fhir.rest.param.TokenParam; +import ca.uhn.fhir.rest.param.TokenParamModifier; +import ca.uhn.fhir.rest.server.provider.dstu2.ServerConformanceProvider; +import ca.uhn.fhir.util.PortUtil; +import ca.uhn.fhir.util.UrlUtil; + +public class OperationServerWithSearchParamTypesDstu2Test { + private static CloseableHttpClient ourClient; + private static FhirContext ourCtx; + + private static String ourLastMethod; + private static List ourLastParamValStr; + private static List ourLastParamValTok; + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(OperationServerWithSearchParamTypesDstu2Test.class); + private static int ourPort; + private static Server ourServer; + + @Before + public void before() { + ourLastMethod = ""; + ourLastParamValStr = null; + ourLastParamValTok = null; + } + + 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; + } + + private ServletConfig createServletConfig() { + ServletConfig sc = mock(ServletConfig.class); + when(sc.getServletContext()).thenReturn(null); + return sc; + } + + @Test + public void testAndListWithParameters() throws Exception { + Parameters p = new Parameters(); + p.addParameter().setName("valstr").setValue(new StringDt("VALSTR1A,VALSTR1B")); + p.addParameter().setName("valstr").setValue(new StringDt("VALSTR2A,VALSTR2B")); + p.addParameter().setName("valtok").setValue(new StringDt("VALTOK1A|VALTOK1B")); + p.addParameter().setName("valtok").setValue(new StringDt("VALTOK2A|VALTOK2B")); + String inParamsStr = ourCtx.newXmlParser().encodeResourceToString(p); + + HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient/$andlist"); + httpPost.setEntity(new StringEntity(inParamsStr, ContentType.create(Constants.CT_FHIR_XML, "UTF-8"))); + HttpResponse status = ourClient.execute(httpPost); + + assertEquals(200, status.getStatusLine().getStatusCode()); + String response = IOUtils.toString(status.getEntity().getContent()); + ourLog.info(response); + IOUtils.closeQuietly(status.getEntity().getContent()); + + assertEquals(2, ourLastParamValStr.size()); + assertEquals(2, ourLastParamValStr.get(0).getValuesAsQueryTokens().size()); + assertEquals("VALSTR1A", ourLastParamValStr.get(0).getValuesAsQueryTokens().get(0).getValue()); + assertEquals("VALSTR1B", ourLastParamValStr.get(0).getValuesAsQueryTokens().get(1).getValue()); + assertEquals("VALSTR2A", ourLastParamValStr.get(1).getValuesAsQueryTokens().get(0).getValue()); + assertEquals("VALSTR2B", ourLastParamValStr.get(1).getValuesAsQueryTokens().get(1).getValue()); + assertEquals(2, ourLastParamValTok.size()); + assertEquals(1, ourLastParamValTok.get(0).getValuesAsQueryTokens().size()); + assertEquals("VALTOK1A", ourLastParamValTok.get(0).getValuesAsQueryTokens().get(0).getSystem()); + assertEquals("VALTOK1B", ourLastParamValTok.get(0).getValuesAsQueryTokens().get(0).getValue()); + assertEquals("VALTOK2A", ourLastParamValTok.get(1).getValuesAsQueryTokens().get(0).getSystem()); + assertEquals("VALTOK2B", ourLastParamValTok.get(1).getValuesAsQueryTokens().get(0).getValue()); + assertEquals("type $orlist", ourLastMethod); + } + + @Test + public void testAndListWithUrl() throws Exception { + HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/$andlist?valstr=VALSTR1A,VALSTR1B&valstr=VALSTR2A,VALSTR2B&valtok=" + UrlUtil.escape("VALTOK1A|VALTOK1B") + "&valtok=" + + UrlUtil.escape("VALTOK2A|VALTOK2B")); + HttpResponse status = ourClient.execute(httpGet); + + assertEquals(200, status.getStatusLine().getStatusCode()); + String response = IOUtils.toString(status.getEntity().getContent()); + ourLog.info(response); + IOUtils.closeQuietly(status.getEntity().getContent()); + + assertEquals(2, ourLastParamValStr.size()); + assertEquals(2, ourLastParamValStr.get(0).getValuesAsQueryTokens().size()); + assertEquals("VALSTR1A", ourLastParamValStr.get(0).getValuesAsQueryTokens().get(0).getValue()); + assertEquals("VALSTR1B", ourLastParamValStr.get(0).getValuesAsQueryTokens().get(1).getValue()); + assertEquals("VALSTR2A", ourLastParamValStr.get(1).getValuesAsQueryTokens().get(0).getValue()); + assertEquals("VALSTR2B", ourLastParamValStr.get(1).getValuesAsQueryTokens().get(1).getValue()); + assertEquals(2, ourLastParamValTok.size()); + assertEquals(1, ourLastParamValTok.get(0).getValuesAsQueryTokens().size()); + assertEquals("VALTOK1A", ourLastParamValTok.get(0).getValuesAsQueryTokens().get(0).getSystem()); + assertEquals("VALTOK1B", ourLastParamValTok.get(0).getValuesAsQueryTokens().get(0).getValue()); + assertEquals("VALTOK2A", ourLastParamValTok.get(1).getValuesAsQueryTokens().get(0).getSystem()); + assertEquals("VALTOK2B", ourLastParamValTok.get(1).getValuesAsQueryTokens().get(0).getValue()); + assertEquals("type $orlist", ourLastMethod); + } + + @Test + public void testGenerateConformance() throws Exception { + RestfulServer rs = new RestfulServer(ourCtx); + rs.setProviders(new PatientProvider()); + + 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); + //@formatter:off + assertThat(conf, stringContainsInOrder( + "", + "", + "" + )); + assertThat(conf, stringContainsInOrder( + "", + "", + "" + )); + assertThat(conf, stringContainsInOrder( + "", + "", + "" + )); + //@formatter:on + + /* + * Check the operation definitions themselves + */ + OperationDefinition andListDef = sc.readOperationDefinition(new IdDt("OperationDefinition/andlist")); + String def = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(andListDef); + ourLog.info(def); + //@formatter:off + assertThat(def, stringContainsInOrder( + "", + "", + "", + "", + "", + "", + "" + )); + //@formatter:on + + } + + @Test + public void testNonRepeatingWithParams() throws Exception { + Parameters p = new Parameters(); + p.addParameter().setName("valstr").setValue(new StringDt("VALSTR")); + p.addParameter().setName("valtok").setValue(new StringDt("VALTOKA|VALTOKB")); + String inParamsStr = ourCtx.newXmlParser().encodeResourceToString(p); + + HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient/$nonrepeating"); + httpPost.setEntity(new StringEntity(inParamsStr, ContentType.create(Constants.CT_FHIR_XML, "UTF-8"))); + HttpResponse status = ourClient.execute(httpPost); + + assertEquals(200, status.getStatusLine().getStatusCode()); + String response = IOUtils.toString(status.getEntity().getContent()); + ourLog.info(response); + IOUtils.closeQuietly(status.getEntity().getContent()); + + assertEquals(1, ourLastParamValStr.size()); + assertEquals(1, ourLastParamValStr.get(0).getValuesAsQueryTokens().size()); + assertEquals("VALSTR", ourLastParamValStr.get(0).getValuesAsQueryTokens().get(0).getValue()); + assertEquals(1, ourLastParamValTok.size()); + assertEquals(1, ourLastParamValTok.get(0).getValuesAsQueryTokens().size()); + assertEquals("VALTOKA", ourLastParamValTok.get(0).getValuesAsQueryTokens().get(0).getSystem()); + assertEquals("VALTOKB", ourLastParamValTok.get(0).getValuesAsQueryTokens().get(0).getValue()); + assertEquals("type $nonrepeating", ourLastMethod); + } + + @Test + public void testNonRepeatingWithUrl() throws Exception { + HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/$nonrepeating?valstr=VALSTR&valtok=" + UrlUtil.escape("VALTOKA|VALTOKB")); + HttpResponse status = ourClient.execute(httpGet); + + assertEquals(200, status.getStatusLine().getStatusCode()); + String response = IOUtils.toString(status.getEntity().getContent()); + ourLog.info(response); + IOUtils.closeQuietly(status.getEntity().getContent()); + + assertEquals(1, ourLastParamValStr.size()); + assertEquals(1, ourLastParamValStr.get(0).getValuesAsQueryTokens().size()); + assertEquals("VALSTR", ourLastParamValStr.get(0).getValuesAsQueryTokens().get(0).getValue()); + assertEquals(1, ourLastParamValTok.size()); + assertEquals(1, ourLastParamValTok.get(0).getValuesAsQueryTokens().size()); + assertEquals("VALTOKA", ourLastParamValTok.get(0).getValuesAsQueryTokens().get(0).getSystem()); + assertEquals("VALTOKB", ourLastParamValTok.get(0).getValuesAsQueryTokens().get(0).getValue()); + assertEquals("type $nonrepeating", ourLastMethod); + } + + @Test + public void testNonRepeatingWithUrlQualified() throws Exception { + HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/$nonrepeating?valstr:exact=VALSTR&valtok:not=" + UrlUtil.escape("VALTOKA|VALTOKB")); + HttpResponse status = ourClient.execute(httpGet); + + assertEquals(200, status.getStatusLine().getStatusCode()); + String response = IOUtils.toString(status.getEntity().getContent()); + ourLog.info(response); + IOUtils.closeQuietly(status.getEntity().getContent()); + + assertEquals(1, ourLastParamValStr.size()); + assertEquals(1, ourLastParamValStr.get(0).getValuesAsQueryTokens().size()); + assertEquals("VALSTR", ourLastParamValStr.get(0).getValuesAsQueryTokens().get(0).getValue()); + assertTrue(ourLastParamValStr.get(0).getValuesAsQueryTokens().get(0).isExact()); + assertEquals(1, ourLastParamValTok.size()); + assertEquals(1, ourLastParamValTok.get(0).getValuesAsQueryTokens().size()); + assertEquals("VALTOKA", ourLastParamValTok.get(0).getValuesAsQueryTokens().get(0).getSystem()); + assertEquals("VALTOKB", ourLastParamValTok.get(0).getValuesAsQueryTokens().get(0).getValue()); + assertEquals(TokenParamModifier.NOT, ourLastParamValTok.get(0).getValuesAsQueryTokens().get(0).getModifier()); + assertEquals("type $nonrepeating", ourLastMethod); + } + + @Test + public void testOrListWithParameters() throws Exception { + Parameters p = new Parameters(); + p.addParameter().setName("valstr").setValue(new StringDt("VALSTR1A,VALSTR1B")); + p.addParameter().setName("valstr").setValue(new StringDt("VALSTR2A,VALSTR2B")); + p.addParameter().setName("valtok").setValue(new StringDt("VALTOK1A|VALTOK1B")); + p.addParameter().setName("valtok").setValue(new StringDt("VALTOK2A|VALTOK2B")); + String inParamsStr = ourCtx.newXmlParser().encodeResourceToString(p); + + HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient/$orlist"); + httpPost.setEntity(new StringEntity(inParamsStr, ContentType.create(Constants.CT_FHIR_XML, "UTF-8"))); + HttpResponse status = ourClient.execute(httpPost); + + assertEquals(200, status.getStatusLine().getStatusCode()); + String response = IOUtils.toString(status.getEntity().getContent()); + ourLog.info(response); + IOUtils.closeQuietly(status.getEntity().getContent()); + + assertEquals(2, ourLastParamValStr.size()); + assertEquals(2, ourLastParamValStr.get(0).getValuesAsQueryTokens().size()); + assertEquals("VALSTR1A", ourLastParamValStr.get(0).getValuesAsQueryTokens().get(0).getValue()); + assertEquals("VALSTR1B", ourLastParamValStr.get(0).getValuesAsQueryTokens().get(1).getValue()); + assertEquals("VALSTR2A", ourLastParamValStr.get(1).getValuesAsQueryTokens().get(0).getValue()); + assertEquals("VALSTR2B", ourLastParamValStr.get(1).getValuesAsQueryTokens().get(1).getValue()); + assertEquals(2, ourLastParamValTok.size()); + assertEquals(1, ourLastParamValTok.get(0).getValuesAsQueryTokens().size()); + assertEquals("VALTOK1A", ourLastParamValTok.get(0).getValuesAsQueryTokens().get(0).getSystem()); + assertEquals("VALTOK1B", ourLastParamValTok.get(0).getValuesAsQueryTokens().get(0).getValue()); + assertEquals("VALTOK2A", ourLastParamValTok.get(1).getValuesAsQueryTokens().get(0).getSystem()); + assertEquals("VALTOK2B", ourLastParamValTok.get(1).getValuesAsQueryTokens().get(0).getValue()); + assertEquals("type $orlist", ourLastMethod); + } + + @Test + public void testOrListWithUrl() throws Exception { + HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/$orlist?valstr=VALSTR1A,VALSTR1B&valstr=VALSTR2A,VALSTR2B&valtok=" + UrlUtil.escape("VALTOK1A|VALTOK1B") + "&valtok=" + + UrlUtil.escape("VALTOK2A|VALTOK2B")); + HttpResponse status = ourClient.execute(httpGet); + + assertEquals(200, status.getStatusLine().getStatusCode()); + String response = IOUtils.toString(status.getEntity().getContent()); + ourLog.info(response); + IOUtils.closeQuietly(status.getEntity().getContent()); + + assertEquals(2, ourLastParamValStr.size()); + assertEquals(2, ourLastParamValStr.get(0).getValuesAsQueryTokens().size()); + assertEquals("VALSTR1A", ourLastParamValStr.get(0).getValuesAsQueryTokens().get(0).getValue()); + assertEquals("VALSTR1B", ourLastParamValStr.get(0).getValuesAsQueryTokens().get(1).getValue()); + assertEquals("VALSTR2A", ourLastParamValStr.get(1).getValuesAsQueryTokens().get(0).getValue()); + assertEquals("VALSTR2B", ourLastParamValStr.get(1).getValuesAsQueryTokens().get(1).getValue()); + assertEquals(2, ourLastParamValTok.size()); + assertEquals(1, ourLastParamValTok.get(0).getValuesAsQueryTokens().size()); + assertEquals("VALTOK1A", ourLastParamValTok.get(0).getValuesAsQueryTokens().get(0).getSystem()); + assertEquals("VALTOK1B", ourLastParamValTok.get(0).getValuesAsQueryTokens().get(0).getValue()); + assertEquals("VALTOK2A", ourLastParamValTok.get(1).getValuesAsQueryTokens().get(0).getSystem()); + assertEquals("VALTOK2B", ourLastParamValTok.get(1).getValuesAsQueryTokens().get(0).getValue()); + assertEquals("type $orlist", ourLastMethod); + } + + @AfterClass + public static void afterClass() throws Exception { + ourServer.stop(); + } + + @BeforeClass + public static void beforeClass() throws Exception { + ourCtx = FhirContext.forDstu2(); + ourPort = PortUtil.findFreePort(); + ourServer = new Server(ourPort); + + ServletHandler proxyHandler = new ServletHandler(); + RestfulServer servlet = new RestfulServer(ourCtx); + + servlet.setPagingProvider(new FifoMemoryPagingProvider(10).setDefaultPageSize(2)); + + servlet.setFhirContext(ourCtx); + servlet.setResourceProviders(new PatientProvider()); + ServletHolder servletHolder = new ServletHolder(servlet); + proxyHandler.addServletWithMapping(servletHolder, "/*"); + ourServer.setHandler(proxyHandler); + ourServer.start(); + + PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS); + HttpClientBuilder builder = HttpClientBuilder.create(); + builder.setConnectionManager(connectionManager); + ourClient = builder.build(); + + } + + public static class PatientProvider implements IResourceProvider { + + @Operation(name = "$andlist", idempotent = true) + public Parameters andlist( + //@formatter:off + @OperationParam(name="valstr", max=10) StringAndListParam theValStr, + @OperationParam(name="valtok", max=10) TokenAndListParam theValTok + //@formatter:on + ) { + ourLastMethod = "type $orlist"; + ourLastParamValStr = theValStr.getValuesAsQueryTokens(); + ourLastParamValTok = theValTok.getValuesAsQueryTokens(); + + return createEmptyParams(); + } + + /** + * Just so we have something to return + */ + private Parameters createEmptyParams() { + Parameters retVal = new Parameters(); + retVal.setId("100"); + return retVal; + } + + @Override + public Class getResourceType() { + return Patient.class; + } + + @Operation(name = "$nonrepeating", idempotent = true) + public Parameters nonrepeating( + //@formatter:off + @OperationParam(name="valstr") StringParam theValStr, + @OperationParam(name="valtok") TokenParam theValTok + //@formatter:on + ) { + ourLastMethod = "type $nonrepeating"; + ourLastParamValStr = Collections.singletonList(new StringOrListParam().add(theValStr)); + ourLastParamValTok = Collections.singletonList(new TokenOrListParam().add(theValTok)); + + return createEmptyParams(); + } + + @Operation(name = "$orlist", idempotent = true) + public Parameters orlist( + //@formatter:off + @OperationParam(name="valstr", max=10) List theValStr, + @OperationParam(name="valtok", max=10) List theValTok + //@formatter:on + ) { + ourLastMethod = "type $orlist"; + ourLastParamValStr = theValStr; + ourLastParamValTok = theValTok; + + return createEmptyParams(); + } + + } + +} diff --git a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/ReadDstu2Test.java b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/ReadDstu2Test.java index 7dfc1ca6faa..587a262c84e 100644 --- a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/ReadDstu2Test.java +++ b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/ReadDstu2Test.java @@ -45,7 +45,7 @@ public class ReadDstu2Test { @Before public void before() { - ourServlet.setAddProfileTag(AddProfileTagEnum.NEVER); + ourCtx.setAddProfileTagWhenEncoding(AddProfileTagEnum.NEVER); ourInitializeProfileList = false; ourLastId = null; } @@ -55,7 +55,7 @@ public class ReadDstu2Test { */ @Test public void testAddProfile() throws Exception { - ourServlet.setAddProfileTag(AddProfileTagEnum.ALWAYS); + ourCtx.setAddProfileTagWhenEncoding(AddProfileTagEnum.ALWAYS); HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/123?_format=xml"); HttpResponse status = ourClient.execute(httpGet); @@ -74,7 +74,7 @@ public class ReadDstu2Test { @Test public void testVread() throws Exception { - ourServlet.setAddProfileTag(AddProfileTagEnum.ALWAYS); + ourCtx.setAddProfileTagWhenEncoding(AddProfileTagEnum.ALWAYS); HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/123/_history/1"); HttpResponse status = ourClient.execute(httpGet); @@ -99,18 +99,19 @@ public class ReadDstu2Test { @Test public void testAddProfileToExistingList() throws Exception { ourInitializeProfileList = true; + ourCtx.setAddProfileTagWhenEncoding(AddProfileTagEnum.ONLY_FOR_CUSTOM); HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/123&_format=xml"); HttpResponse status = ourClient.execute(httpGet); String responseContent = IOUtils.toString(status.getEntity().getContent()); IOUtils.closeQuietly(status.getEntity().getContent()); + + ourLog.info(responseContent); assertEquals(200, status.getStatusLine().getStatusCode()); assertThat(responseContent, containsString("p1ReadValue")); assertThat(responseContent, containsString("p1ReadId")); assertEquals("", responseContent); - - ourLog.info(responseContent); } /** @@ -118,7 +119,7 @@ public class ReadDstu2Test { */ @Test public void testReadJson() throws Exception { - ourServlet.setAddProfileTag(AddProfileTagEnum.ALWAYS); + ourCtx.setAddProfileTagWhenEncoding(AddProfileTagEnum.ALWAYS); HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/123?_format=json"); HttpResponse status = ourClient.execute(httpGet); diff --git a/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/hapi/rest/server/ServerConformanceProvider.java b/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/hapi/rest/server/ServerConformanceProvider.java index 965250e9d5b..7ece557173c 100644 --- a/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/hapi/rest/server/ServerConformanceProvider.java +++ b/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/hapi/rest/server/ServerConformanceProvider.java @@ -538,6 +538,9 @@ public class ServerConformanceProvider implements IServerConformanceProvider searchType; + /** * A profile the specifies the rules that this parameter must conform to. */ - @Child(name = "profile", type = {StructureDefinition.class}, order=7, min=0, max=1, modifier=false, summary=false) + @Child(name = "profile", type = {StructureDefinition.class}, order=8, min=0, max=1, modifier=false, summary=false) @Description(shortDefinition="Profile on the type", formalDefinition="A profile the specifies the rules that this parameter must conform to." ) protected Reference profile; @@ -469,18 +473,18 @@ public class OperationDefinition extends DomainResource { /** * Binds to a value set if this parameter is coded (code, Coding, CodeableConcept). */ - @Child(name = "binding", type = {}, order=8, min=0, max=1, modifier=false, summary=false) + @Child(name = "binding", type = {}, order=9, min=0, max=1, modifier=false, summary=false) @Description(shortDefinition="ValueSet details if this is coded", formalDefinition="Binds to a value set if this parameter is coded (code, Coding, CodeableConcept)." ) protected OperationDefinitionParameterBindingComponent binding; /** * The parts of a Tuple Parameter. */ - @Child(name = "part", type = {OperationDefinitionParameterComponent.class}, order=9, min=0, max=Child.MAX_UNLIMITED, modifier=false, summary=false) + @Child(name = "part", type = {OperationDefinitionParameterComponent.class}, order=10, min=0, max=Child.MAX_UNLIMITED, modifier=false, summary=false) @Description(shortDefinition="Parts of a Tuple Parameter", formalDefinition="The parts of a Tuple Parameter." ) protected List part; - private static final long serialVersionUID = -1514145741L; + private static final long serialVersionUID = -885506257L; /** * Constructor @@ -778,6 +782,55 @@ public class OperationDefinition extends DomainResource { return this; } + /** + * @return {@link #searchType} (How the parameter is understood as a search parameter. This is only used if the parameter type is 'string'.). This is the underlying object with id, value and extensions. The accessor "getSearchType" gives direct access to the value + */ + public Enumeration getSearchTypeElement() { + if (this.searchType == null) + if (Configuration.errorOnAutoCreate()) + throw new Error("Attempt to auto-create OperationDefinitionParameterComponent.searchType"); + else if (Configuration.doAutoCreate()) + this.searchType = new Enumeration(new SearchParamTypeEnumFactory()); // bb + return this.searchType; + } + + public boolean hasSearchTypeElement() { + return this.searchType != null && !this.searchType.isEmpty(); + } + + public boolean hasSearchType() { + return this.searchType != null && !this.searchType.isEmpty(); + } + + /** + * @param value {@link #searchType} (How the parameter is understood as a search parameter. This is only used if the parameter type is 'string'.). This is the underlying object with id, value and extensions. The accessor "getSearchType" gives direct access to the value + */ + public OperationDefinitionParameterComponent setSearchTypeElement(Enumeration value) { + this.searchType = value; + return this; + } + + /** + * @return How the parameter is understood as a search parameter. This is only used if the parameter type is 'string'. + */ + public SearchParamType getSearchType() { + return this.searchType == null ? null : this.searchType.getValue(); + } + + /** + * @param value How the parameter is understood as a search parameter. This is only used if the parameter type is 'string'. + */ + public OperationDefinitionParameterComponent setSearchType(SearchParamType value) { + if (value == null) + this.searchType = null; + else { + if (this.searchType == null) + this.searchType = new Enumeration(new SearchParamTypeEnumFactory()); + this.searchType.setValue(value); + } + return this; + } + /** * @return {@link #profile} (A profile the specifies the rules that this parameter must conform to.) */ @@ -894,6 +947,7 @@ public class OperationDefinition extends DomainResource { childrenList.add(new Property("max", "string", "The maximum number of times this element is permitted to appear in the request or response.", 0, java.lang.Integer.MAX_VALUE, max)); childrenList.add(new Property("documentation", "string", "Describes the meaning or use of this parameter.", 0, java.lang.Integer.MAX_VALUE, documentation)); childrenList.add(new Property("type", "code", "The type for this parameter.", 0, java.lang.Integer.MAX_VALUE, type)); + childrenList.add(new Property("searchType", "code", "How the parameter is understood as a search parameter. This is only used if the parameter type is 'string'.", 0, java.lang.Integer.MAX_VALUE, searchType)); childrenList.add(new Property("profile", "Reference(StructureDefinition)", "A profile the specifies the rules that this parameter must conform to.", 0, java.lang.Integer.MAX_VALUE, profile)); childrenList.add(new Property("binding", "", "Binds to a value set if this parameter is coded (code, Coding, CodeableConcept).", 0, java.lang.Integer.MAX_VALUE, binding)); childrenList.add(new Property("part", "@OperationDefinition.parameter", "The parts of a Tuple Parameter.", 0, java.lang.Integer.MAX_VALUE, part)); @@ -913,6 +967,8 @@ public class OperationDefinition extends DomainResource { this.documentation = castToString(value); // StringType else if (name.equals("type")) this.type = castToCode(value); // CodeType + else if (name.equals("searchType")) + this.searchType = new SearchParamTypeEnumFactory().fromType(value); // Enumeration else if (name.equals("profile")) this.profile = castToReference(value); // Reference else if (name.equals("binding")) @@ -943,6 +999,9 @@ public class OperationDefinition extends DomainResource { else if (name.equals("type")) { throw new FHIRException("Cannot call addChild on a primitive type OperationDefinition.type"); } + else if (name.equals("searchType")) { + throw new FHIRException("Cannot call addChild on a primitive type OperationDefinition.searchType"); + } else if (name.equals("profile")) { this.profile = new Reference(); return this.profile; @@ -967,6 +1026,7 @@ public class OperationDefinition extends DomainResource { dst.max = max == null ? null : max.copy(); dst.documentation = documentation == null ? null : documentation.copy(); dst.type = type == null ? null : type.copy(); + dst.searchType = searchType == null ? null : searchType.copy(); dst.profile = profile == null ? null : profile.copy(); dst.binding = binding == null ? null : binding.copy(); if (part != null) { @@ -986,8 +1046,8 @@ public class OperationDefinition extends DomainResource { OperationDefinitionParameterComponent o = (OperationDefinitionParameterComponent) other; return compareDeep(name, o.name, true) && compareDeep(use, o.use, true) && compareDeep(min, o.min, true) && compareDeep(max, o.max, true) && compareDeep(documentation, o.documentation, true) && compareDeep(type, o.type, true) - && compareDeep(profile, o.profile, true) && compareDeep(binding, o.binding, true) && compareDeep(part, o.part, true) - ; + && compareDeep(searchType, o.searchType, true) && compareDeep(profile, o.profile, true) && compareDeep(binding, o.binding, true) + && compareDeep(part, o.part, true); } @Override @@ -999,14 +1059,12 @@ public class OperationDefinition extends DomainResource { OperationDefinitionParameterComponent o = (OperationDefinitionParameterComponent) other; return compareValues(name, o.name, true) && compareValues(use, o.use, true) && compareValues(min, o.min, true) && compareValues(max, o.max, true) && compareValues(documentation, o.documentation, true) && compareValues(type, o.type, true) - ; + && compareValues(searchType, o.searchType, true); } public boolean isEmpty() { - return super.isEmpty() && (name == null || name.isEmpty()) && (use == null || use.isEmpty()) - && (min == null || min.isEmpty()) && (max == null || max.isEmpty()) && (documentation == null || documentation.isEmpty()) - && (type == null || type.isEmpty()) && (profile == null || profile.isEmpty()) && (binding == null || binding.isEmpty()) - && (part == null || part.isEmpty()); + return super.isEmpty() && ca.uhn.fhir.util.ElementUtil.isEmpty( name, use, min, max, documentation + , type, searchType, profile, binding, part); } public String fhirType() { @@ -1202,8 +1260,7 @@ public class OperationDefinition extends DomainResource { } public boolean isEmpty() { - return super.isEmpty() && (strength == null || strength.isEmpty()) && (valueSet == null || valueSet.isEmpty()) - ; + return super.isEmpty() && ca.uhn.fhir.util.ElementUtil.isEmpty( strength, valueSet); } public String fhirType() { @@ -1255,27 +1312,27 @@ public class OperationDefinition extends DomainResource { @Description(shortDefinition="If for testing purposes, not real usage", formalDefinition="This profile was authored for testing purposes (or education/evaluation/marketing), and is not intended to be used for genuine usage." ) protected BooleanType experimental; + /** + * The date this version of the operation definition was published. The date must change when the business version changes, if it does, and it must change if the status code changes. In addition, it should change when the substantive content of the Operation Definition changes. + */ + @Child(name = "date", type = {DateTimeType.class}, order=6, min=0, max=1, modifier=false, summary=false) + @Description(shortDefinition="Date for this version of the operation definition", formalDefinition="The date this version of the operation definition was published. The date must change when the business version changes, if it does, and it must change if the status code changes. In addition, it should change when the substantive content of the Operation Definition changes." ) + protected DateTimeType date; + /** * The name of the individual or organization that published the operation definition. */ - @Child(name = "publisher", type = {StringType.class}, order=6, min=0, max=1, modifier=false, summary=true) + @Child(name = "publisher", type = {StringType.class}, order=7, min=0, max=1, modifier=false, summary=true) @Description(shortDefinition="Name of the publisher (Organization or individual)", formalDefinition="The name of the individual or organization that published the operation definition." ) protected StringType publisher; /** * Contacts to assist a user in finding and communicating with the publisher. */ - @Child(name = "contact", type = {}, order=7, min=0, max=Child.MAX_UNLIMITED, modifier=false, summary=true) + @Child(name = "contact", type = {}, order=8, min=0, max=Child.MAX_UNLIMITED, modifier=false, summary=true) @Description(shortDefinition="Contact details of the publisher", formalDefinition="Contacts to assist a user in finding and communicating with the publisher." ) protected List contact; - /** - * The date this version of the operation definition was published. The date must change when the business version changes, if it does, and it must change if the status code changes. In addition, it should change when the substantive content of the Operation Definition changes. - */ - @Child(name = "date", type = {DateTimeType.class}, order=8, min=0, max=1, modifier=false, summary=false) - @Description(shortDefinition="Date for this version of the operation definition", formalDefinition="The date this version of the operation definition was published. The date must change when the business version changes, if it does, and it must change if the status code changes. In addition, it should change when the substantive content of the Operation Definition changes." ) - protected DateTimeType date; - /** * A free text natural language description of the profile and its use. */ @@ -1283,38 +1340,45 @@ public class OperationDefinition extends DomainResource { @Description(shortDefinition="Natural language description of the operation", formalDefinition="A free text natural language description of the profile and its use." ) protected StringType description; + /** + * The content was developed with a focus and intent of supporting the contexts that are listed. These terms may be used to assist with indexing and searching of operation definitions. + */ + @Child(name = "useContext", type = {CodeableConcept.class}, order=10, min=0, max=Child.MAX_UNLIMITED, modifier=false, summary=true) + @Description(shortDefinition="Content intends to support these contexts", formalDefinition="The content was developed with a focus and intent of supporting the contexts that are listed. These terms may be used to assist with indexing and searching of operation definitions." ) + protected List useContext; + /** * Explains why this operation definition is needed and why it's been constrained as it has. */ - @Child(name = "requirements", type = {StringType.class}, order=10, min=0, max=1, modifier=false, summary=false) - @Description(shortDefinition="Why is this needed?", formalDefinition="Explains why this operation definition is needed and why it's been constrained as it has." ) + @Child(name = "requirements", type = {StringType.class}, order=11, min=0, max=1, modifier=false, summary=false) + @Description(shortDefinition="Why this resource has been created", formalDefinition="Explains why this operation definition is needed and why it's been constrained as it has." ) protected StringType requirements; /** * Operations that are idempotent (see [HTTP specification definition of idempotent](http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html)) may be invoked by performing an HTTP GET operation instead of a POST. */ - @Child(name = "idempotent", type = {BooleanType.class}, order=11, min=0, max=1, modifier=false, summary=false) - @Description(shortDefinition="Whether content is unchanged by operation", formalDefinition="Operations that are idempotent (see [HTTP specification definition of idempotent](http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html)) may be invoked by performing an HTTP GET operation instead of a POST." ) + @Child(name = "idempotent", type = {BooleanType.class}, order=12, min=0, max=1, modifier=false, summary=false) + @Description(shortDefinition="Whether content is unchanged by the operation", formalDefinition="Operations that are idempotent (see [HTTP specification definition of idempotent](http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html)) may be invoked by performing an HTTP GET operation instead of a POST." ) protected BooleanType idempotent; /** * The name used to invoke the operation. */ - @Child(name = "code", type = {CodeType.class}, order=12, min=1, max=1, modifier=false, summary=false) + @Child(name = "code", type = {CodeType.class}, order=13, min=1, max=1, modifier=false, summary=false) @Description(shortDefinition="Name used to invoke the operation", formalDefinition="The name used to invoke the operation." ) protected CodeType code; /** * Additional information about how to use this operation or named query. */ - @Child(name = "notes", type = {StringType.class}, order=13, min=0, max=1, modifier=false, summary=false) + @Child(name = "comment", type = {StringType.class}, order=14, min=0, max=1, modifier=false, summary=false) @Description(shortDefinition="Additional information about use", formalDefinition="Additional information about how to use this operation or named query." ) - protected StringType notes; + protected StringType comment; /** * Indicates that this operation definition is a constraining profile on the base. */ - @Child(name = "base", type = {OperationDefinition.class}, order=14, min=0, max=1, modifier=false, summary=false) + @Child(name = "base", type = {OperationDefinition.class}, order=15, min=0, max=1, modifier=false, summary=false) @Description(shortDefinition="Marks this as a profile of the base", formalDefinition="Indicates that this operation definition is a constraining profile on the base." ) protected Reference base; @@ -1326,32 +1390,32 @@ public class OperationDefinition extends DomainResource { /** * Indicates whether this operation or named query can be invoked at the system level (e.g. without needing to choose a resource type for the context). */ - @Child(name = "system", type = {BooleanType.class}, order=15, min=1, max=1, modifier=false, summary=false) + @Child(name = "system", type = {BooleanType.class}, order=16, min=1, max=1, modifier=false, summary=false) @Description(shortDefinition="Invoke at the system level?", formalDefinition="Indicates whether this operation or named query can be invoked at the system level (e.g. without needing to choose a resource type for the context)." ) protected BooleanType system; /** * Indicates whether this operation or named query can be invoked at the resource type level for any given resource type level (e.g. without needing to choose a resource type for the context). */ - @Child(name = "type", type = {CodeType.class}, order=16, min=0, max=Child.MAX_UNLIMITED, modifier=false, summary=false) + @Child(name = "type", type = {CodeType.class}, order=17, min=0, max=Child.MAX_UNLIMITED, modifier=false, summary=false) @Description(shortDefinition="Invoke at resource level for these type", formalDefinition="Indicates whether this operation or named query can be invoked at the resource type level for any given resource type level (e.g. without needing to choose a resource type for the context)." ) protected List type; /** * Indicates whether this operation can be invoked on a particular instance of one of the given types. */ - @Child(name = "instance", type = {BooleanType.class}, order=17, min=1, max=1, modifier=false, summary=false) + @Child(name = "instance", type = {BooleanType.class}, order=18, min=1, max=1, modifier=false, summary=false) @Description(shortDefinition="Invoke on an instance?", formalDefinition="Indicates whether this operation can be invoked on a particular instance of one of the given types." ) protected BooleanType instance; /** * The parameters for the operation/query. */ - @Child(name = "parameter", type = {}, order=18, min=0, max=Child.MAX_UNLIMITED, modifier=false, summary=false) + @Child(name = "parameter", type = {}, order=19, min=0, max=Child.MAX_UNLIMITED, modifier=false, summary=false) @Description(shortDefinition="Parameters for the operation/query", formalDefinition="The parameters for the operation/query." ) protected List parameter; - private static final long serialVersionUID = 148203484L; + private static final long serialVersionUID = 1780846105L; /** * Constructor @@ -1651,6 +1715,55 @@ public class OperationDefinition extends DomainResource { return this; } + /** + * @return {@link #date} (The date this version of the operation definition was published. The date must change when the business version changes, if it does, and it must change if the status code changes. In addition, it should change when the substantive content of the Operation Definition changes.). This is the underlying object with id, value and extensions. The accessor "getDate" gives direct access to the value + */ + public DateTimeType getDateElement() { + if (this.date == null) + if (Configuration.errorOnAutoCreate()) + throw new Error("Attempt to auto-create OperationDefinition.date"); + else if (Configuration.doAutoCreate()) + this.date = new DateTimeType(); // bb + return this.date; + } + + public boolean hasDateElement() { + return this.date != null && !this.date.isEmpty(); + } + + public boolean hasDate() { + return this.date != null && !this.date.isEmpty(); + } + + /** + * @param value {@link #date} (The date this version of the operation definition was published. The date must change when the business version changes, if it does, and it must change if the status code changes. In addition, it should change when the substantive content of the Operation Definition changes.). This is the underlying object with id, value and extensions. The accessor "getDate" gives direct access to the value + */ + public OperationDefinition setDateElement(DateTimeType value) { + this.date = value; + return this; + } + + /** + * @return The date this version of the operation definition was published. The date must change when the business version changes, if it does, and it must change if the status code changes. In addition, it should change when the substantive content of the Operation Definition changes. + */ + public Date getDate() { + return this.date == null ? null : this.date.getValue(); + } + + /** + * @param value The date this version of the operation definition was published. The date must change when the business version changes, if it does, and it must change if the status code changes. In addition, it should change when the substantive content of the Operation Definition changes. + */ + public OperationDefinition setDate(Date value) { + if (value == null) + this.date = null; + else { + if (this.date == null) + this.date = new DateTimeType(); + this.date.setValue(value); + } + return this; + } + /** * @return {@link #publisher} (The name of the individual or organization that published the operation definition.). This is the underlying object with id, value and extensions. The accessor "getPublisher" gives direct access to the value */ @@ -1740,55 +1853,6 @@ public class OperationDefinition extends DomainResource { return this; } - /** - * @return {@link #date} (The date this version of the operation definition was published. The date must change when the business version changes, if it does, and it must change if the status code changes. In addition, it should change when the substantive content of the Operation Definition changes.). This is the underlying object with id, value and extensions. The accessor "getDate" gives direct access to the value - */ - public DateTimeType getDateElement() { - if (this.date == null) - if (Configuration.errorOnAutoCreate()) - throw new Error("Attempt to auto-create OperationDefinition.date"); - else if (Configuration.doAutoCreate()) - this.date = new DateTimeType(); // bb - return this.date; - } - - public boolean hasDateElement() { - return this.date != null && !this.date.isEmpty(); - } - - public boolean hasDate() { - return this.date != null && !this.date.isEmpty(); - } - - /** - * @param value {@link #date} (The date this version of the operation definition was published. The date must change when the business version changes, if it does, and it must change if the status code changes. In addition, it should change when the substantive content of the Operation Definition changes.). This is the underlying object with id, value and extensions. The accessor "getDate" gives direct access to the value - */ - public OperationDefinition setDateElement(DateTimeType value) { - this.date = value; - return this; - } - - /** - * @return The date this version of the operation definition was published. The date must change when the business version changes, if it does, and it must change if the status code changes. In addition, it should change when the substantive content of the Operation Definition changes. - */ - public Date getDate() { - return this.date == null ? null : this.date.getValue(); - } - - /** - * @param value The date this version of the operation definition was published. The date must change when the business version changes, if it does, and it must change if the status code changes. In addition, it should change when the substantive content of the Operation Definition changes. - */ - public OperationDefinition setDate(Date value) { - if (value == null) - this.date = null; - else { - if (this.date == null) - this.date = new DateTimeType(); - this.date.setValue(value); - } - return this; - } - /** * @return {@link #description} (A free text natural language description of the profile and its use.). This is the underlying object with id, value and extensions. The accessor "getDescription" gives direct access to the value */ @@ -1838,6 +1902,46 @@ public class OperationDefinition extends DomainResource { return this; } + /** + * @return {@link #useContext} (The content was developed with a focus and intent of supporting the contexts that are listed. These terms may be used to assist with indexing and searching of operation definitions.) + */ + public List getUseContext() { + if (this.useContext == null) + this.useContext = new ArrayList(); + return this.useContext; + } + + public boolean hasUseContext() { + if (this.useContext == null) + return false; + for (CodeableConcept item : this.useContext) + if (!item.isEmpty()) + return true; + return false; + } + + /** + * @return {@link #useContext} (The content was developed with a focus and intent of supporting the contexts that are listed. These terms may be used to assist with indexing and searching of operation definitions.) + */ + // syntactic sugar + public CodeableConcept addUseContext() { //3 + CodeableConcept t = new CodeableConcept(); + if (this.useContext == null) + this.useContext = new ArrayList(); + this.useContext.add(t); + return t; + } + + // syntactic sugar + public OperationDefinition addUseContext(CodeableConcept t) { //3 + if (t == null) + return this; + if (this.useContext == null) + this.useContext = new ArrayList(); + this.useContext.add(t); + return this; + } + /** * @return {@link #requirements} (Explains why this operation definition is needed and why it's been constrained as it has.). This is the underlying object with id, value and extensions. The accessor "getRequirements" gives direct access to the value */ @@ -1978,50 +2082,50 @@ public class OperationDefinition extends DomainResource { } /** - * @return {@link #notes} (Additional information about how to use this operation or named query.). This is the underlying object with id, value and extensions. The accessor "getNotes" gives direct access to the value + * @return {@link #comment} (Additional information about how to use this operation or named query.). This is the underlying object with id, value and extensions. The accessor "getComment" gives direct access to the value */ - public StringType getNotesElement() { - if (this.notes == null) + public StringType getCommentElement() { + if (this.comment == null) if (Configuration.errorOnAutoCreate()) - throw new Error("Attempt to auto-create OperationDefinition.notes"); + throw new Error("Attempt to auto-create OperationDefinition.comment"); else if (Configuration.doAutoCreate()) - this.notes = new StringType(); // bb - return this.notes; + this.comment = new StringType(); // bb + return this.comment; } - public boolean hasNotesElement() { - return this.notes != null && !this.notes.isEmpty(); + public boolean hasCommentElement() { + return this.comment != null && !this.comment.isEmpty(); } - public boolean hasNotes() { - return this.notes != null && !this.notes.isEmpty(); + public boolean hasComment() { + return this.comment != null && !this.comment.isEmpty(); } /** - * @param value {@link #notes} (Additional information about how to use this operation or named query.). This is the underlying object with id, value and extensions. The accessor "getNotes" gives direct access to the value + * @param value {@link #comment} (Additional information about how to use this operation or named query.). This is the underlying object with id, value and extensions. The accessor "getComment" gives direct access to the value */ - public OperationDefinition setNotesElement(StringType value) { - this.notes = value; + public OperationDefinition setCommentElement(StringType value) { + this.comment = value; return this; } /** * @return Additional information about how to use this operation or named query. */ - public String getNotes() { - return this.notes == null ? null : this.notes.getValue(); + public String getComment() { + return this.comment == null ? null : this.comment.getValue(); } /** * @param value Additional information about how to use this operation or named query. */ - public OperationDefinition setNotes(String value) { + public OperationDefinition setComment(String value) { if (Utilities.noString(value)) - this.notes = null; + this.comment = null; else { - if (this.notes == null) - this.notes = new StringType(); - this.notes.setValue(value); + if (this.comment == null) + this.comment = new StringType(); + this.comment.setValue(value); } return this; } @@ -2262,14 +2366,15 @@ public class OperationDefinition extends DomainResource { childrenList.add(new Property("status", "code", "The status of the profile.", 0, java.lang.Integer.MAX_VALUE, status)); childrenList.add(new Property("kind", "code", "Whether this is an operation or a named query.", 0, java.lang.Integer.MAX_VALUE, kind)); childrenList.add(new Property("experimental", "boolean", "This profile was authored for testing purposes (or education/evaluation/marketing), and is not intended to be used for genuine usage.", 0, java.lang.Integer.MAX_VALUE, experimental)); + childrenList.add(new Property("date", "dateTime", "The date this version of the operation definition was published. The date must change when the business version changes, if it does, and it must change if the status code changes. In addition, it should change when the substantive content of the Operation Definition changes.", 0, java.lang.Integer.MAX_VALUE, date)); childrenList.add(new Property("publisher", "string", "The name of the individual or organization that published the operation definition.", 0, java.lang.Integer.MAX_VALUE, publisher)); childrenList.add(new Property("contact", "", "Contacts to assist a user in finding and communicating with the publisher.", 0, java.lang.Integer.MAX_VALUE, contact)); - childrenList.add(new Property("date", "dateTime", "The date this version of the operation definition was published. The date must change when the business version changes, if it does, and it must change if the status code changes. In addition, it should change when the substantive content of the Operation Definition changes.", 0, java.lang.Integer.MAX_VALUE, date)); childrenList.add(new Property("description", "string", "A free text natural language description of the profile and its use.", 0, java.lang.Integer.MAX_VALUE, description)); + childrenList.add(new Property("useContext", "CodeableConcept", "The content was developed with a focus and intent of supporting the contexts that are listed. These terms may be used to assist with indexing and searching of operation definitions.", 0, java.lang.Integer.MAX_VALUE, useContext)); childrenList.add(new Property("requirements", "string", "Explains why this operation definition is needed and why it's been constrained as it has.", 0, java.lang.Integer.MAX_VALUE, requirements)); childrenList.add(new Property("idempotent", "boolean", "Operations that are idempotent (see [HTTP specification definition of idempotent](http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html)) may be invoked by performing an HTTP GET operation instead of a POST.", 0, java.lang.Integer.MAX_VALUE, idempotent)); childrenList.add(new Property("code", "code", "The name used to invoke the operation.", 0, java.lang.Integer.MAX_VALUE, code)); - childrenList.add(new Property("notes", "string", "Additional information about how to use this operation or named query.", 0, java.lang.Integer.MAX_VALUE, notes)); + childrenList.add(new Property("comment", "string", "Additional information about how to use this operation or named query.", 0, java.lang.Integer.MAX_VALUE, comment)); childrenList.add(new Property("base", "Reference(OperationDefinition)", "Indicates that this operation definition is a constraining profile on the base.", 0, java.lang.Integer.MAX_VALUE, base)); childrenList.add(new Property("system", "boolean", "Indicates whether this operation or named query can be invoked at the system level (e.g. without needing to choose a resource type for the context).", 0, java.lang.Integer.MAX_VALUE, system)); childrenList.add(new Property("type", "code", "Indicates whether this operation or named query can be invoked at the resource type level for any given resource type level (e.g. without needing to choose a resource type for the context).", 0, java.lang.Integer.MAX_VALUE, type)); @@ -2291,22 +2396,24 @@ public class OperationDefinition extends DomainResource { this.kind = new OperationKindEnumFactory().fromType(value); // Enumeration else if (name.equals("experimental")) this.experimental = castToBoolean(value); // BooleanType + else if (name.equals("date")) + this.date = castToDateTime(value); // DateTimeType else if (name.equals("publisher")) this.publisher = castToString(value); // StringType else if (name.equals("contact")) this.getContact().add((OperationDefinitionContactComponent) value); - else if (name.equals("date")) - this.date = castToDateTime(value); // DateTimeType else if (name.equals("description")) this.description = castToString(value); // StringType + else if (name.equals("useContext")) + this.getUseContext().add(castToCodeableConcept(value)); else if (name.equals("requirements")) this.requirements = castToString(value); // StringType else if (name.equals("idempotent")) this.idempotent = castToBoolean(value); // BooleanType else if (name.equals("code")) this.code = castToCode(value); // CodeType - else if (name.equals("notes")) - this.notes = castToString(value); // StringType + else if (name.equals("comment")) + this.comment = castToString(value); // StringType else if (name.equals("base")) this.base = castToReference(value); // Reference else if (name.equals("system")) @@ -2341,18 +2448,21 @@ public class OperationDefinition extends DomainResource { else if (name.equals("experimental")) { throw new FHIRException("Cannot call addChild on a primitive type OperationDefinition.experimental"); } + else if (name.equals("date")) { + throw new FHIRException("Cannot call addChild on a primitive type OperationDefinition.date"); + } else if (name.equals("publisher")) { throw new FHIRException("Cannot call addChild on a primitive type OperationDefinition.publisher"); } else if (name.equals("contact")) { return addContact(); } - else if (name.equals("date")) { - throw new FHIRException("Cannot call addChild on a primitive type OperationDefinition.date"); - } else if (name.equals("description")) { throw new FHIRException("Cannot call addChild on a primitive type OperationDefinition.description"); } + else if (name.equals("useContext")) { + return addUseContext(); + } else if (name.equals("requirements")) { throw new FHIRException("Cannot call addChild on a primitive type OperationDefinition.requirements"); } @@ -2362,8 +2472,8 @@ public class OperationDefinition extends DomainResource { else if (name.equals("code")) { throw new FHIRException("Cannot call addChild on a primitive type OperationDefinition.code"); } - else if (name.equals("notes")) { - throw new FHIRException("Cannot call addChild on a primitive type OperationDefinition.notes"); + else if (name.equals("comment")) { + throw new FHIRException("Cannot call addChild on a primitive type OperationDefinition.comment"); } else if (name.equals("base")) { this.base = new Reference(); @@ -2399,18 +2509,23 @@ public class OperationDefinition extends DomainResource { dst.status = status == null ? null : status.copy(); dst.kind = kind == null ? null : kind.copy(); dst.experimental = experimental == null ? null : experimental.copy(); + dst.date = date == null ? null : date.copy(); dst.publisher = publisher == null ? null : publisher.copy(); if (contact != null) { dst.contact = new ArrayList(); for (OperationDefinitionContactComponent i : contact) dst.contact.add(i.copy()); }; - dst.date = date == null ? null : date.copy(); dst.description = description == null ? null : description.copy(); + if (useContext != null) { + dst.useContext = new ArrayList(); + for (CodeableConcept i : useContext) + dst.useContext.add(i.copy()); + }; dst.requirements = requirements == null ? null : requirements.copy(); dst.idempotent = idempotent == null ? null : idempotent.copy(); dst.code = code == null ? null : code.copy(); - dst.notes = notes == null ? null : notes.copy(); + dst.comment = comment == null ? null : comment.copy(); dst.base = base == null ? null : base.copy(); dst.system = system == null ? null : system.copy(); if (type != null) { @@ -2440,11 +2555,12 @@ public class OperationDefinition extends DomainResource { OperationDefinition o = (OperationDefinition) other; return compareDeep(url, o.url, true) && compareDeep(version, o.version, true) && compareDeep(name, o.name, true) && compareDeep(status, o.status, true) && compareDeep(kind, o.kind, true) && compareDeep(experimental, o.experimental, true) - && compareDeep(publisher, o.publisher, true) && compareDeep(contact, o.contact, true) && compareDeep(date, o.date, true) - && compareDeep(description, o.description, true) && compareDeep(requirements, o.requirements, true) - && compareDeep(idempotent, o.idempotent, true) && compareDeep(code, o.code, true) && compareDeep(notes, o.notes, true) - && compareDeep(base, o.base, true) && compareDeep(system, o.system, true) && compareDeep(type, o.type, true) - && compareDeep(instance, o.instance, true) && compareDeep(parameter, o.parameter, true); + && compareDeep(date, o.date, true) && compareDeep(publisher, o.publisher, true) && compareDeep(contact, o.contact, true) + && compareDeep(description, o.description, true) && compareDeep(useContext, o.useContext, true) + && compareDeep(requirements, o.requirements, true) && compareDeep(idempotent, o.idempotent, true) + && compareDeep(code, o.code, true) && compareDeep(comment, o.comment, true) && compareDeep(base, o.base, true) + && compareDeep(system, o.system, true) && compareDeep(type, o.type, true) && compareDeep(instance, o.instance, true) + && compareDeep(parameter, o.parameter, true); } @Override @@ -2456,21 +2572,16 @@ public class OperationDefinition extends DomainResource { OperationDefinition o = (OperationDefinition) other; return compareValues(url, o.url, true) && compareValues(version, o.version, true) && compareValues(name, o.name, true) && compareValues(status, o.status, true) && compareValues(kind, o.kind, true) && compareValues(experimental, o.experimental, true) - && compareValues(publisher, o.publisher, true) && compareValues(date, o.date, true) && compareValues(description, o.description, true) + && compareValues(date, o.date, true) && compareValues(publisher, o.publisher, true) && compareValues(description, o.description, true) && compareValues(requirements, o.requirements, true) && compareValues(idempotent, o.idempotent, true) - && compareValues(code, o.code, true) && compareValues(notes, o.notes, true) && compareValues(system, o.system, true) + && compareValues(code, o.code, true) && compareValues(comment, o.comment, true) && compareValues(system, o.system, true) && compareValues(type, o.type, true) && compareValues(instance, o.instance, true); } public boolean isEmpty() { - return super.isEmpty() && (url == null || url.isEmpty()) && (version == null || version.isEmpty()) - && (name == null || name.isEmpty()) && (status == null || status.isEmpty()) && (kind == null || kind.isEmpty()) - && (experimental == null || experimental.isEmpty()) && (publisher == null || publisher.isEmpty()) - && (contact == null || contact.isEmpty()) && (date == null || date.isEmpty()) && (description == null || description.isEmpty()) - && (requirements == null || requirements.isEmpty()) && (idempotent == null || idempotent.isEmpty()) - && (code == null || code.isEmpty()) && (notes == null || notes.isEmpty()) && (base == null || base.isEmpty()) - && (system == null || system.isEmpty()) && (type == null || type.isEmpty()) && (instance == null || instance.isEmpty()) - && (parameter == null || parameter.isEmpty()); + return super.isEmpty() && ca.uhn.fhir.util.ElementUtil.isEmpty( url, version, name, status, kind + , experimental, date, publisher, contact, description, useContext, requirements, idempotent + , code, comment, base, system, type, instance, parameter); } @Override @@ -2558,32 +2669,6 @@ public class OperationDefinition extends DomainResource { */ public static final ca.uhn.fhir.rest.gclient.TokenClientParam KIND = new ca.uhn.fhir.rest.gclient.TokenClientParam(SP_KIND); - /** - * Search parameter: profile - *

- * Description: Profile on the type
- * Type: reference
- * Path: OperationDefinition.parameter.profile
- *

- */ - @SearchParamDefinition(name="profile", path="OperationDefinition.parameter.profile", description="Profile on the type", type="reference" ) - public static final String SP_PROFILE = "profile"; - /** - * Fluent Client search parameter constant for profile - *

- * Description: Profile on the type
- * Type: reference
- * Path: OperationDefinition.parameter.profile
- *

- */ - public static final ca.uhn.fhir.rest.gclient.ReferenceClientParam PROFILE = new ca.uhn.fhir.rest.gclient.ReferenceClientParam(SP_PROFILE); - -/** - * Constant for fluent queries to be used to add include statements. Specifies - * the path value of "OperationDefinition:profile". - */ - public static final ca.uhn.fhir.model.api.Include INCLUDE_PROFILE = new ca.uhn.fhir.model.api.Include("OperationDefinition:profile").toLocked(); - /** * Search parameter: type *

@@ -2624,6 +2709,32 @@ public class OperationDefinition extends DomainResource { */ public static final ca.uhn.fhir.rest.gclient.TokenClientParam VERSION = new ca.uhn.fhir.rest.gclient.TokenClientParam(SP_VERSION); + /** + * Search parameter: paramprofile + *

+ * Description: Profile on the type
+ * Type: reference
+ * Path: OperationDefinition.parameter.profile
+ *

+ */ + @SearchParamDefinition(name="paramprofile", path="OperationDefinition.parameter.profile", description="Profile on the type", type="reference" ) + public static final String SP_PARAMPROFILE = "paramprofile"; + /** + * Fluent Client search parameter constant for paramprofile + *

+ * Description: Profile on the type
+ * Type: reference
+ * Path: OperationDefinition.parameter.profile
+ *

+ */ + public static final ca.uhn.fhir.rest.gclient.ReferenceClientParam PARAMPROFILE = new ca.uhn.fhir.rest.gclient.ReferenceClientParam(SP_PARAMPROFILE); + +/** + * Constant for fluent queries to be used to add include statements. Specifies + * the path value of "OperationDefinition:paramprofile". + */ + public static final ca.uhn.fhir.model.api.Include INCLUDE_PARAMPROFILE = new ca.uhn.fhir.model.api.Include("OperationDefinition:paramprofile").toLocked(); + /** * Search parameter: url *

@@ -2684,6 +2795,26 @@ public class OperationDefinition extends DomainResource { */ public static final ca.uhn.fhir.rest.gclient.StringClientParam NAME = new ca.uhn.fhir.rest.gclient.StringClientParam(SP_NAME); + /** + * Search parameter: context + *

+ * Description: A use context assigned to the operation definition
+ * Type: token
+ * Path: OperationDefinition.useContext
+ *

+ */ + @SearchParamDefinition(name="context", path="OperationDefinition.useContext", description="A use context assigned to the operation definition", type="token" ) + public static final String SP_CONTEXT = "context"; + /** + * Fluent Client search parameter constant for context + *

+ * Description: A use context assigned to the operation definition
+ * Type: token
+ * Path: OperationDefinition.useContext
+ *

+ */ + public static final ca.uhn.fhir.rest.gclient.TokenClientParam CONTEXT = new ca.uhn.fhir.rest.gclient.TokenClientParam(SP_CONTEXT); + /** * Search parameter: publisher *

diff --git a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/client/OperationClientDstu3Test.java b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/client/OperationClientDstu3Test.java new file mode 100644 index 00000000000..24199e58379 --- /dev/null +++ b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/client/OperationClientDstu3Test.java @@ -0,0 +1,186 @@ +package ca.uhn.fhir.rest.client; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.InputStream; +import java.io.StringReader; +import java.nio.charset.Charset; +import java.util.List; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.io.input.ReaderInputStream; +import org.apache.http.HttpResponse; +import org.apache.http.ProtocolVersion; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.message.BasicHeader; +import org.apache.http.message.BasicStatusLine; +import org.hl7.fhir.dstu3.model.Parameters; +import org.hl7.fhir.dstu3.model.StringType; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.internal.stubbing.defaultanswers.ReturnsDeepStubs; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.rest.annotation.Operation; +import ca.uhn.fhir.rest.annotation.OperationParam; +import ca.uhn.fhir.rest.client.api.IBasicClient; +import ca.uhn.fhir.rest.param.StringAndListParam; +import ca.uhn.fhir.rest.param.StringOrListParam; +import ca.uhn.fhir.rest.param.StringParam; +import ca.uhn.fhir.rest.param.TokenAndListParam; +import ca.uhn.fhir.rest.param.TokenOrListParam; +import ca.uhn.fhir.rest.param.TokenParam; +import ca.uhn.fhir.rest.server.Constants; + +public class OperationClientDstu3Test { + + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(OperationClientDstu3Test.class); + private FhirContext ourCtx; + private HttpClient ourHttpClient; + + private HttpResponse ourHttpResponse; + private IOpClient ourAnnClient; + private ArgumentCaptor capt; + private IGenericClient ourGenClient; + + @Before + public void before() throws Exception { + ourCtx = FhirContext.forDstu3(); + + ourHttpClient = mock(HttpClient.class, new ReturnsDeepStubs()); + ourCtx.getRestfulClientFactory().setHttpClient(ourHttpClient); + ourCtx.getRestfulClientFactory().setServerValidationMode(ServerValidationModeEnum.NEVER); + + ourHttpResponse = mock(HttpResponse.class, new ReturnsDeepStubs()); + + Parameters outParams = new Parameters(); + outParams.addParameter().setName("FOO"); + final String retVal = ourCtx.newXmlParser().encodeResourceToString(outParams); + + capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(ourHttpClient.execute(capt.capture())).thenReturn(ourHttpResponse); + when(ourHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); + when(ourHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); + when(ourHttpResponse.getEntity().getContent()).thenAnswer(new Answer() { + @Override + public InputStream answer(InvocationOnMock theInvocation) throws Throwable { + return new ReaderInputStream(new StringReader(retVal), Charset.forName("UTF-8")); + } + }); + + ourAnnClient = ourCtx.newRestfulClient(IOpClient.class, "http://foo"); + ourGenClient = ourCtx.newRestfulGenericClient("http://foo"); + } + + @Test + public void testNonRepeatingGenericUsingParameters() throws Exception { + ourGenClient + .operation() + .onServer() + .named("nonrepeating") + .withSearchParameter(Parameters.class, "valstr", new StringParam("str")) + .andSearchParameter("valtok", new TokenParam("sys2", "val2")) + .execute(); + Parameters response = ourAnnClient.nonrepeating(new StringParam("str"), new TokenParam("sys", "val")); + assertEquals("FOO", response.getParameter().get(0).getName()); + + HttpPost value = (HttpPost) capt.getAllValues().get(0); + String requestBody = IOUtils.toString(((HttpPost) value).getEntity().getContent()); + IOUtils.closeQuietly(((HttpPost) value).getEntity().getContent()); + ourLog.info(requestBody); + Parameters request = ourCtx.newXmlParser().parseResource(Parameters.class, requestBody); + assertEquals("http://foo/$nonrepeating", value.getURI().toASCIIString()); + assertEquals(2, request.getParameter().size()); + assertEquals("valstr", request.getParameter().get(0).getName()); + assertEquals("str", ((StringType) request.getParameter().get(0).getValue()).getValue()); + assertEquals("valtok", request.getParameter().get(1).getName()); + assertEquals("sys2|val2", ((StringType) request.getParameter().get(1).getValue()).getValue()); + } + + @Test + public void testNonRepeatingGenericUsingUrl() throws Exception { + ourGenClient + .operation() + .onServer() + .named("nonrepeating") + .withSearchParameter(Parameters.class, "valstr", new StringParam("str")) + .andSearchParameter("valtok", new TokenParam("sys2", "val2")) + .useHttpGet() + .execute(); + Parameters response = ourAnnClient.nonrepeating(new StringParam("str"), new TokenParam("sys", "val")); + assertEquals("FOO", response.getParameter().get(0).getName()); + + HttpGet value = (HttpGet) capt.getAllValues().get(0); + assertEquals("http://foo/$nonrepeating?valstr=str&valtok=sys2%7Cval2", value.getURI().toASCIIString()); + } + + @Test + public void testNonRepeatingUsingParameters() throws Exception { + Parameters response = ourAnnClient.nonrepeating(new StringParam("str"), new TokenParam("sys", "val")); + assertEquals("FOO", response.getParameter().get(0).getName()); + + HttpPost value = (HttpPost) capt.getAllValues().get(0); + String requestBody = IOUtils.toString(((HttpPost) value).getEntity().getContent()); + IOUtils.closeQuietly(((HttpPost) value).getEntity().getContent()); + ourLog.info(requestBody); + Parameters request = ourCtx.newXmlParser().parseResource(Parameters.class, requestBody); + assertEquals("http://foo/$nonrepeating", value.getURI().toASCIIString()); + assertEquals(2, request.getParameter().size()); + assertEquals("valstr", request.getParameter().get(0).getName()); + assertEquals("str", ((StringType) request.getParameter().get(0).getValue()).getValue()); + assertEquals("valtok", request.getParameter().get(1).getName()); + assertEquals("sys|val", ((StringType) request.getParameter().get(1).getValue()).getValue()); + } + + public interface IOpClient extends IBasicClient { + + @Operation(name = "$andlist", idempotent = true) + public Parameters andlist( + //@formatter:off + @OperationParam(name="valstr", max=10) StringAndListParam theValStr, + @OperationParam(name="valtok", max=10) TokenAndListParam theValTok + //@formatter:on + ); + + @Operation(name = "$andlist-withnomax", idempotent = true) + public Parameters andlistWithNoMax( + //@formatter:off + @OperationParam(name="valstr") StringAndListParam theValStr, + @OperationParam(name="valtok") TokenAndListParam theValTok + //@formatter:on + ); + + @Operation(name = "$nonrepeating", idempotent = true) + public Parameters nonrepeating( + //@formatter:off + @OperationParam(name="valstr") StringParam theValStr, + @OperationParam(name="valtok") TokenParam theValTok + //@formatter:on + ); + + @Operation(name = "$orlist", idempotent = true) + public Parameters orlist( + //@formatter:off + @OperationParam(name="valstr", max=10) List theValStr, + @OperationParam(name="valtok", max=10) List theValTok + //@formatter:on + ); + + @Operation(name = "$orlist-withnomax", idempotent = true) + public Parameters orlistWithNoMax( + //@formatter:off + @OperationParam(name="valstr") List theValStr, + @OperationParam(name="valtok") List theValTok + //@formatter:on + ); + + } +} diff --git a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/OperationServerWithSearchParamTypesDstu3Test.java b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/OperationServerWithSearchParamTypesDstu3Test.java new file mode 100644 index 00000000000..9476c45f1f7 --- /dev/null +++ b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/OperationServerWithSearchParamTypesDstu3Test.java @@ -0,0 +1,491 @@ +package ca.uhn.fhir.rest.server; + +import static org.hamcrest.Matchers.stringContainsInOrder; +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 java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import javax.servlet.ServletConfig; +import javax.servlet.http.HttpServletRequest; + +import org.apache.commons.io.IOUtils; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.servlet.ServletHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.hl7.fhir.dstu3.hapi.rest.server.ServerConformanceProvider; +import org.hl7.fhir.dstu3.model.Conformance; +import org.hl7.fhir.dstu3.model.IdType; +import org.hl7.fhir.dstu3.model.OperationDefinition; +import org.hl7.fhir.dstu3.model.Parameters; +import org.hl7.fhir.dstu3.model.Patient; +import org.hl7.fhir.dstu3.model.StringType; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.rest.annotation.Operation; +import ca.uhn.fhir.rest.annotation.OperationParam; +import ca.uhn.fhir.rest.param.StringAndListParam; +import ca.uhn.fhir.rest.param.StringOrListParam; +import ca.uhn.fhir.rest.param.StringParam; +import ca.uhn.fhir.rest.param.TokenAndListParam; +import ca.uhn.fhir.rest.param.TokenOrListParam; +import ca.uhn.fhir.rest.param.TokenParam; +import ca.uhn.fhir.rest.param.TokenParamModifier; +import ca.uhn.fhir.util.PortUtil; +import ca.uhn.fhir.util.UrlUtil; + +public class OperationServerWithSearchParamTypesDstu3Test { + private static CloseableHttpClient ourClient; + private static FhirContext ourCtx; + + private static String ourLastMethod; + private static List ourLastParamValStr; + private static List ourLastParamValTok; + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(OperationServerWithSearchParamTypesDstu3Test.class); + private static int ourPort; + private static Server ourServer; + + @Before + public void before() { + ourLastMethod = ""; + ourLastParamValStr = null; + ourLastParamValTok = null; + } + + 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; + } + + private ServletConfig createServletConfig() { + ServletConfig sc = mock(ServletConfig.class); + when(sc.getServletContext()).thenReturn(null); + return sc; + } + + @Test + public void testAndListWithParameters() throws Exception { + Parameters p = new Parameters(); + p.addParameter().setName("valstr").setValue(new StringType("VALSTR1A,VALSTR1B")); + p.addParameter().setName("valstr").setValue(new StringType("VALSTR2A,VALSTR2B")); + p.addParameter().setName("valtok").setValue(new StringType("VALTOK1A|VALTOK1B")); + p.addParameter().setName("valtok").setValue(new StringType("VALTOK2A|VALTOK2B")); + String inParamsStr = ourCtx.newXmlParser().encodeResourceToString(p); + + HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient/$andlist"); + httpPost.setEntity(new StringEntity(inParamsStr, ContentType.create(Constants.CT_FHIR_XML, "UTF-8"))); + HttpResponse status = ourClient.execute(httpPost); + + assertEquals(200, status.getStatusLine().getStatusCode()); + String response = IOUtils.toString(status.getEntity().getContent()); + ourLog.info(response); + IOUtils.closeQuietly(status.getEntity().getContent()); + + assertEquals(2, ourLastParamValStr.size()); + assertEquals(2, ourLastParamValStr.get(0).getValuesAsQueryTokens().size()); + assertEquals("VALSTR1A", ourLastParamValStr.get(0).getValuesAsQueryTokens().get(0).getValue()); + assertEquals("VALSTR1B", ourLastParamValStr.get(0).getValuesAsQueryTokens().get(1).getValue()); + assertEquals("VALSTR2A", ourLastParamValStr.get(1).getValuesAsQueryTokens().get(0).getValue()); + assertEquals("VALSTR2B", ourLastParamValStr.get(1).getValuesAsQueryTokens().get(1).getValue()); + assertEquals(2, ourLastParamValTok.size()); + assertEquals(1, ourLastParamValTok.get(0).getValuesAsQueryTokens().size()); + assertEquals("VALTOK1A", ourLastParamValTok.get(0).getValuesAsQueryTokens().get(0).getSystem()); + assertEquals("VALTOK1B", ourLastParamValTok.get(0).getValuesAsQueryTokens().get(0).getValue()); + assertEquals("VALTOK2A", ourLastParamValTok.get(1).getValuesAsQueryTokens().get(0).getSystem()); + assertEquals("VALTOK2B", ourLastParamValTok.get(1).getValuesAsQueryTokens().get(0).getValue()); + assertEquals("type $orlist", ourLastMethod); + } + + @Test + public void testAndListWithUrl() throws Exception { + HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/$andlist?valstr=VALSTR1A,VALSTR1B&valstr=VALSTR2A,VALSTR2B&valtok=" + UrlUtil.escape("VALTOK1A|VALTOK1B") + "&valtok=" + UrlUtil.escape("VALTOK2A|VALTOK2B")); + HttpResponse status = ourClient.execute(httpGet); + + assertEquals(200, status.getStatusLine().getStatusCode()); + String response = IOUtils.toString(status.getEntity().getContent()); + ourLog.info(response); + IOUtils.closeQuietly(status.getEntity().getContent()); + + assertEquals(2, ourLastParamValStr.size()); + assertEquals(2, ourLastParamValStr.get(0).getValuesAsQueryTokens().size()); + assertEquals("VALSTR1A", ourLastParamValStr.get(0).getValuesAsQueryTokens().get(0).getValue()); + assertEquals("VALSTR1B", ourLastParamValStr.get(0).getValuesAsQueryTokens().get(1).getValue()); + assertEquals("VALSTR2A", ourLastParamValStr.get(1).getValuesAsQueryTokens().get(0).getValue()); + assertEquals("VALSTR2B", ourLastParamValStr.get(1).getValuesAsQueryTokens().get(1).getValue()); + assertEquals(2, ourLastParamValTok.size()); + assertEquals(1, ourLastParamValTok.get(0).getValuesAsQueryTokens().size()); + assertEquals("VALTOK1A", ourLastParamValTok.get(0).getValuesAsQueryTokens().get(0).getSystem()); + assertEquals("VALTOK1B", ourLastParamValTok.get(0).getValuesAsQueryTokens().get(0).getValue()); + assertEquals("VALTOK2A", ourLastParamValTok.get(1).getValuesAsQueryTokens().get(0).getSystem()); + assertEquals("VALTOK2B", ourLastParamValTok.get(1).getValuesAsQueryTokens().get(0).getValue()); + assertEquals("type $orlist", ourLastMethod); + } + + @Test + public void testGenerateConformance() throws Exception { + RestfulServer rs = new RestfulServer(ourCtx); + rs.setProviders(new PatientProvider()); + + 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); + //@formatter:off + assertThat(conf, stringContainsInOrder( + "", + "", + "", + "" + )); + assertThat(conf, stringContainsInOrder( + "", + "", + "" + )); + assertThat(conf, stringContainsInOrder( + "", + "", + "" + )); + //@formatter:on + + /* + * Check the operation definitions themselves + */ + OperationDefinition andListDef = sc.readOperationDefinition(new IdType("OperationDefinition/andlist")); + String def = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(andListDef); + ourLog.info(def); + //@formatter:off + assertThat(def, stringContainsInOrder( + "", + "", + "", + "", + "", + "", + "", + "" + )); + //@formatter:on + + andListDef = sc.readOperationDefinition(new IdType("OperationDefinition/andlist-withnomax")); + def = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(andListDef); + ourLog.info(def); + //@formatter:off + assertThat(def, stringContainsInOrder( + "", + "", + "", + "", + "", + "", + "", + "" + )); + //@formatter:on + + OperationDefinition orListDef = sc.readOperationDefinition(new IdType("OperationDefinition/orlist")); + def = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(orListDef); + ourLog.info(def); + //@formatter:off + assertThat(def, stringContainsInOrder( + "", + "", + "", + "", + "", + "", + "", + "" + )); + //@formatter:on + + orListDef = sc.readOperationDefinition(new IdType("OperationDefinition/orlist-withnomax")); + def = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(orListDef); + ourLog.info(def); + //@formatter:off + assertThat(def, stringContainsInOrder( + "", + "", + "", + "", + "", + "", + "", + "" + )); + //@formatter:on + + } + @Test + public void testNonRepeatingWithParams() throws Exception { + Parameters p = new Parameters(); + p.addParameter().setName("valstr").setValue(new StringType("VALSTR")); + p.addParameter().setName("valtok").setValue(new StringType("VALTOKA|VALTOKB")); + String inParamsStr = ourCtx.newXmlParser().encodeResourceToString(p); + + HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient/$nonrepeating"); + httpPost.setEntity(new StringEntity(inParamsStr, ContentType.create(Constants.CT_FHIR_XML, "UTF-8"))); + HttpResponse status = ourClient.execute(httpPost); + + assertEquals(200, status.getStatusLine().getStatusCode()); + String response = IOUtils.toString(status.getEntity().getContent()); + ourLog.info(response); + IOUtils.closeQuietly(status.getEntity().getContent()); + + assertEquals(1, ourLastParamValStr.size()); + assertEquals(1, ourLastParamValStr.get(0).getValuesAsQueryTokens().size()); + assertEquals("VALSTR", ourLastParamValStr.get(0).getValuesAsQueryTokens().get(0).getValue()); + assertEquals(1, ourLastParamValTok.size()); + assertEquals(1, ourLastParamValTok.get(0).getValuesAsQueryTokens().size()); + assertEquals("VALTOKA", ourLastParamValTok.get(0).getValuesAsQueryTokens().get(0).getSystem()); + assertEquals("VALTOKB", ourLastParamValTok.get(0).getValuesAsQueryTokens().get(0).getValue()); + assertEquals("type $nonrepeating", ourLastMethod); + } + + @Test + public void testNonRepeatingWithUrl() throws Exception { + HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/$nonrepeating?valstr=VALSTR&valtok=" + UrlUtil.escape("VALTOKA|VALTOKB")); + HttpResponse status = ourClient.execute(httpGet); + + assertEquals(200, status.getStatusLine().getStatusCode()); + String response = IOUtils.toString(status.getEntity().getContent()); + ourLog.info(response); + IOUtils.closeQuietly(status.getEntity().getContent()); + + assertEquals(1, ourLastParamValStr.size()); + assertEquals(1, ourLastParamValStr.get(0).getValuesAsQueryTokens().size()); + assertEquals("VALSTR", ourLastParamValStr.get(0).getValuesAsQueryTokens().get(0).getValue()); + assertEquals(1, ourLastParamValTok.size()); + assertEquals(1, ourLastParamValTok.get(0).getValuesAsQueryTokens().size()); + assertEquals("VALTOKA", ourLastParamValTok.get(0).getValuesAsQueryTokens().get(0).getSystem()); + assertEquals("VALTOKB", ourLastParamValTok.get(0).getValuesAsQueryTokens().get(0).getValue()); + assertEquals("type $nonrepeating", ourLastMethod); + } + + @Test + public void testNonRepeatingWithUrlQualified() throws Exception { + HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/$nonrepeating?valstr:exact=VALSTR&valtok:not=" + UrlUtil.escape("VALTOKA|VALTOKB")); + HttpResponse status = ourClient.execute(httpGet); + + assertEquals(200, status.getStatusLine().getStatusCode()); + String response = IOUtils.toString(status.getEntity().getContent()); + ourLog.info(response); + IOUtils.closeQuietly(status.getEntity().getContent()); + + assertEquals(1, ourLastParamValStr.size()); + assertEquals(1, ourLastParamValStr.get(0).getValuesAsQueryTokens().size()); + assertEquals("VALSTR", ourLastParamValStr.get(0).getValuesAsQueryTokens().get(0).getValue()); + assertTrue(ourLastParamValStr.get(0).getValuesAsQueryTokens().get(0).isExact()); + assertEquals(1, ourLastParamValTok.size()); + assertEquals(1, ourLastParamValTok.get(0).getValuesAsQueryTokens().size()); + assertEquals("VALTOKA", ourLastParamValTok.get(0).getValuesAsQueryTokens().get(0).getSystem()); + assertEquals("VALTOKB", ourLastParamValTok.get(0).getValuesAsQueryTokens().get(0).getValue()); + assertEquals(TokenParamModifier.NOT, ourLastParamValTok.get(0).getValuesAsQueryTokens().get(0).getModifier()); + assertEquals("type $nonrepeating", ourLastMethod); + } + + @Test + public void testOrListWithParameters() throws Exception { + Parameters p = new Parameters(); + p.addParameter().setName("valstr").setValue(new StringType("VALSTR1A,VALSTR1B")); + p.addParameter().setName("valstr").setValue(new StringType("VALSTR2A,VALSTR2B")); + p.addParameter().setName("valtok").setValue(new StringType("VALTOK1A|VALTOK1B")); + p.addParameter().setName("valtok").setValue(new StringType("VALTOK2A|VALTOK2B")); + String inParamsStr = ourCtx.newXmlParser().encodeResourceToString(p); + + HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient/$orlist"); + httpPost.setEntity(new StringEntity(inParamsStr, ContentType.create(Constants.CT_FHIR_XML, "UTF-8"))); + HttpResponse status = ourClient.execute(httpPost); + + assertEquals(200, status.getStatusLine().getStatusCode()); + String response = IOUtils.toString(status.getEntity().getContent()); + ourLog.info(response); + IOUtils.closeQuietly(status.getEntity().getContent()); + + assertEquals(2, ourLastParamValStr.size()); + assertEquals(2, ourLastParamValStr.get(0).getValuesAsQueryTokens().size()); + assertEquals("VALSTR1A", ourLastParamValStr.get(0).getValuesAsQueryTokens().get(0).getValue()); + assertEquals("VALSTR1B", ourLastParamValStr.get(0).getValuesAsQueryTokens().get(1).getValue()); + assertEquals("VALSTR2A", ourLastParamValStr.get(1).getValuesAsQueryTokens().get(0).getValue()); + assertEquals("VALSTR2B", ourLastParamValStr.get(1).getValuesAsQueryTokens().get(1).getValue()); + assertEquals(2, ourLastParamValTok.size()); + assertEquals(1, ourLastParamValTok.get(0).getValuesAsQueryTokens().size()); + assertEquals("VALTOK1A", ourLastParamValTok.get(0).getValuesAsQueryTokens().get(0).getSystem()); + assertEquals("VALTOK1B", ourLastParamValTok.get(0).getValuesAsQueryTokens().get(0).getValue()); + assertEquals("VALTOK2A", ourLastParamValTok.get(1).getValuesAsQueryTokens().get(0).getSystem()); + assertEquals("VALTOK2B", ourLastParamValTok.get(1).getValuesAsQueryTokens().get(0).getValue()); + assertEquals("type $orlist", ourLastMethod); + } + + @Test + public void testOrListWithUrl() throws Exception { + HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/$orlist?valstr=VALSTR1A,VALSTR1B&valstr=VALSTR2A,VALSTR2B&valtok=" + UrlUtil.escape("VALTOK1A|VALTOK1B") + "&valtok=" + UrlUtil.escape("VALTOK2A|VALTOK2B")); + HttpResponse status = ourClient.execute(httpGet); + + assertEquals(200, status.getStatusLine().getStatusCode()); + String response = IOUtils.toString(status.getEntity().getContent()); + ourLog.info(response); + IOUtils.closeQuietly(status.getEntity().getContent()); + + assertEquals(2, ourLastParamValStr.size()); + assertEquals(2, ourLastParamValStr.get(0).getValuesAsQueryTokens().size()); + assertEquals("VALSTR1A", ourLastParamValStr.get(0).getValuesAsQueryTokens().get(0).getValue()); + assertEquals("VALSTR1B", ourLastParamValStr.get(0).getValuesAsQueryTokens().get(1).getValue()); + assertEquals("VALSTR2A", ourLastParamValStr.get(1).getValuesAsQueryTokens().get(0).getValue()); + assertEquals("VALSTR2B", ourLastParamValStr.get(1).getValuesAsQueryTokens().get(1).getValue()); + assertEquals(2, ourLastParamValTok.size()); + assertEquals(1, ourLastParamValTok.get(0).getValuesAsQueryTokens().size()); + assertEquals("VALTOK1A", ourLastParamValTok.get(0).getValuesAsQueryTokens().get(0).getSystem()); + assertEquals("VALTOK1B", ourLastParamValTok.get(0).getValuesAsQueryTokens().get(0).getValue()); + assertEquals("VALTOK2A", ourLastParamValTok.get(1).getValuesAsQueryTokens().get(0).getSystem()); + assertEquals("VALTOK2B", ourLastParamValTok.get(1).getValuesAsQueryTokens().get(0).getValue()); + assertEquals("type $orlist", ourLastMethod); + } + + @AfterClass + public static void afterClass() throws Exception { + ourServer.stop(); + } + @BeforeClass + public static void beforeClass() throws Exception { + ourCtx = FhirContext.forDstu3(); + ourPort = PortUtil.findFreePort(); + ourServer = new Server(ourPort); + + ServletHandler proxyHandler = new ServletHandler(); + RestfulServer servlet = new RestfulServer(ourCtx); + + servlet.setPagingProvider(new FifoMemoryPagingProvider(10).setDefaultPageSize(2)); + + servlet.setFhirContext(ourCtx); + servlet.setResourceProviders(new PatientProvider()); + ServletHolder servletHolder = new ServletHolder(servlet); + proxyHandler.addServletWithMapping(servletHolder, "/*"); + ourServer.setHandler(proxyHandler); + ourServer.start(); + + PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS); + HttpClientBuilder builder = HttpClientBuilder.create(); + builder.setConnectionManager(connectionManager); + ourClient = builder.build(); + + } + + public static class PatientProvider implements IResourceProvider { + + + @Operation(name = "$andlist", idempotent = true) + public Parameters andlist( + //@formatter:off + @OperationParam(name="valstr", max=10) StringAndListParam theValStr, + @OperationParam(name="valtok", max=10) TokenAndListParam theValTok + //@formatter:on + ) { + ourLastMethod = "type $orlist"; + ourLastParamValStr = theValStr.getValuesAsQueryTokens(); + ourLastParamValTok = theValTok.getValuesAsQueryTokens(); + + return createEmptyParams(); + } + + @Operation(name = "$andlist-withnomax", idempotent = true) + public Parameters andlistWithNoMax( + //@formatter:off + @OperationParam(name="valstr") StringAndListParam theValStr, + @OperationParam(name="valtok") TokenAndListParam theValTok + //@formatter:on + ) { + ourLastMethod = "type $orlist"; + ourLastParamValStr = theValStr.getValuesAsQueryTokens(); + ourLastParamValTok = theValTok.getValuesAsQueryTokens(); + + return createEmptyParams(); + } + + /** + * Just so we have something to return + */ + private Parameters createEmptyParams() { + Parameters retVal = new Parameters(); + retVal.setId("100"); + return retVal; + } + + @Override + public Class getResourceType() { + return Patient.class; + } + + @Operation(name = "$nonrepeating", idempotent = true) + public Parameters nonrepeating( + //@formatter:off + @OperationParam(name="valstr") StringParam theValStr, + @OperationParam(name="valtok") TokenParam theValTok + //@formatter:on + ) { + ourLastMethod = "type $nonrepeating"; + ourLastParamValStr = Collections.singletonList(new StringOrListParam().add(theValStr)); + ourLastParamValTok = Collections.singletonList(new TokenOrListParam().add(theValTok)); + + return createEmptyParams(); + } + + @Operation(name = "$orlist", idempotent = true) + public Parameters orlist( + //@formatter:off + @OperationParam(name="valstr", max=10) List theValStr, + @OperationParam(name="valtok", max=10) List theValTok + //@formatter:on + ) { + ourLastMethod = "type $orlist"; + ourLastParamValStr = theValStr; + ourLastParamValTok = theValTok; + + return createEmptyParams(); + } + + @Operation(name = "$orlist-withnomax", idempotent = true) + public Parameters orlistWithNoMax( + //@formatter:off + @OperationParam(name="valstr") List theValStr, + @OperationParam(name="valtok") List theValTok + //@formatter:on + ) { + ourLastMethod = "type $orlist"; + ourLastParamValStr = theValStr; + ourLastParamValTok = theValTok; + + return createEmptyParams(); + } + + } + +} diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 4b2adbcd8c6..e8f14775370 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -338,6 +338,29 @@ coded fields which use example bindings. Thanks to GitHub user Ricq for reporting! + + @Operation will now infer the maximum number of repetitions + of their parameters by the type of the parameter. Previously if + a default max() value was not specified in the + @OperationParam annotation on a parameter, the maximum + was assumed to be 1. Now, if a max value is not explicitly specified + and the type of the parameter is a basic type (e.g. StringDt) the + max will be 1. If the parameter is a collection type (e.g. List<StringDt>) + the max will be * + ]]> + + + @Operation + may now use search parameter types, such as + TokenParam and + TokenAndListParam as values. Thanks to + Christian Ohr for reporting! + ]]> + diff --git a/src/site/xdoc/doc_rest_operations.xml b/src/site/xdoc/doc_rest_operations.xml index 97f7b0c424d..1789692a6f9 100644 --- a/src/site/xdoc/doc_rest_operations.xml +++ b/src/site/xdoc/doc_rest_operations.xml @@ -1649,6 +1649,51 @@ + + +

+

+ To use a search parameter type, any of the search parameter + types listed in + Search + may be used. For example, the following is a simple operation method declaration + using search parameters: +

+ + + + + +

+ Example URL to invoke this operation (HTTP request body is Parameters resource): +
+ http://fhir.example.com/$find-matches?date=2011-01-02&code=http://system|value +

+ +

+ It is also fine to use collection types for search parameter types + if you want to be able to accept multiple values. For example, + a List<TokenParam> could be used if you want + to allow multiple repetitions of a given token parameter (this is + analogous to the "AND" semantics in a search). + A TokenOrListParam could be used if you want to allow + multiple values within a single repetition, separated by comma (this + is analogous to "OR" semantics in a search). +

+

For example:

+ + + + + + +

+ FHIR allows operation parameters to be of a + Search parameter type + (e.g. token) instead of a FHIR datatype (e.g. Coding). +