diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java index 2d233a73c6a..2df94c2cb41 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java @@ -450,8 +450,7 @@ public abstract class BaseMethodBinding { if (returnTypeFromRp != null) { if (returnTypeFromAnnotation != null && !isResourceInterface(returnTypeFromAnnotation)) { - if (!returnTypeFromRp.isAssignableFrom(returnTypeFromAnnotation)) { - //FIXME potential null access on retunrTypeFromMethod + if (returnTypeFromMethod != null && !returnTypeFromRp.isAssignableFrom(returnTypeFromMethod)) { throw new ConfigurationException("Method '" + theMethod.getName() + "' in type " + theMethod.getDeclaringClass().getCanonicalName() + " returns type " + returnTypeFromMethod.getCanonicalName() + " - Must return " + returnTypeFromRp.getCanonicalName() + " (or a subclass of it) per IResourceProvider contract"); } @@ -479,7 +478,7 @@ public abstract class BaseMethodBinding { if (read != null) { return new ReadMethodBinding(returnType, theMethod, theContext, theProvider); } else if (search != null) { - return new SearchMethodBinding(returnType, theMethod, theContext, theProvider); + return new SearchMethodBinding(returnType, returnTypeFromRp, theMethod, theContext, theProvider); } else if (conformance != null) { return new ConformanceMethodBinding(theMethod, theContext, theProvider); } else if (create != null) { diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java index bfb26bace25..700a66c0686 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java @@ -59,7 +59,7 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding { private final boolean myIdempotent; private final Integer myIdParamIndex; private final String myName; - private final RestOperationTypeEnum myOtherOperatiopnType; + private final RestOperationTypeEnum myOtherOperationType; private final ReturnTypeEnum myReturnType; private BundleTypeEnum myBundleType; private boolean myCanOperateAtInstanceLevel; @@ -69,22 +69,12 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding { private List myReturnParams; protected OperationMethodBinding(Class theReturnResourceType, Class theReturnTypeFromRp, Method theMethod, FhirContext theContext, Object theProvider, - boolean theIdempotent, String theOperationName, Class theOperationType, - OperationParam[] theReturnParams, BundleTypeEnum theBundleType) { + boolean theIdempotent, String theOperationName, Class theOperationType, + OperationParam[] theReturnParams, BundleTypeEnum theBundleType) { super(theReturnResourceType, theMethod, theContext, theProvider); myBundleType = theBundleType; myIdempotent = theIdempotent; - myIdParamIndex = ParameterUtil.findIdParameterIndex(theMethod, getContext()); - if (myIdParamIndex != null) { - for (Annotation next : theMethod.getParameterAnnotations()[myIdParamIndex]) { - if (next instanceof IdParam) { - myCanOperateAtTypeLevel = ((IdParam) next).optional() == true; - } - } - } else { - myCanOperateAtTypeLevel = true; - } Description description = theMethod.getAnnotation(Description.class); if (description != null) { @@ -99,7 +89,7 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding { if (isBlank(theOperationName)) { throw new ConfigurationException("Method '" + theMethod.getName() + "' on type " + theMethod.getDeclaringClass().getName() + " is annotated with @" + Operation.class.getSimpleName() - + " but this annotation has no name defined"); + + " but this annotation has no name defined"); } if (theOperationName.startsWith("$") == false) { theOperationName = "$" + theOperationName; @@ -108,13 +98,11 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding { if (theReturnTypeFromRp != null) { setResourceName(theContext.getResourceDefinition(theReturnTypeFromRp).getName()); - } else { - if (Modifier.isAbstract(theOperationType.getModifiers()) == false) { + } else if (Modifier.isAbstract(theOperationType.getModifiers()) == false) { setResourceName(theContext.getResourceDefinition(theOperationType).getName()); } else { setResourceName(null); } - } if (theMethod.getReturnType().equals(IBundleProvider.class)) { myReturnType = ReturnTypeEnum.BUNDLE; @@ -122,12 +110,24 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding { myReturnType = ReturnTypeEnum.RESOURCE; } + myIdParamIndex = ParameterUtil.findIdParameterIndex(theMethod, getContext()); if (getResourceName() == null) { - myOtherOperatiopnType = RestOperationTypeEnum.EXTENDED_OPERATION_SERVER; + myOtherOperationType = RestOperationTypeEnum.EXTENDED_OPERATION_SERVER; + myCanOperateAtServerLevel = true; + if (myIdParamIndex != null) { + myCanOperateAtInstanceLevel = true; + } } else if (myIdParamIndex == null) { - myOtherOperatiopnType = RestOperationTypeEnum.EXTENDED_OPERATION_TYPE; + myOtherOperationType = RestOperationTypeEnum.EXTENDED_OPERATION_TYPE; + myCanOperateAtTypeLevel = true; } else { - myOtherOperatiopnType = RestOperationTypeEnum.EXTENDED_OPERATION_INSTANCE; + myOtherOperationType = RestOperationTypeEnum.EXTENDED_OPERATION_INSTANCE; + myCanOperateAtInstanceLevel = true; + for (Annotation next : theMethod.getParameterAnnotations()[myIdParamIndex]) { + if (next instanceof IdParam) { + myCanOperateAtTypeLevel = ((IdParam) next).optional() == true; + } + } } myReturnParams = new ArrayList<>(); @@ -149,20 +149,12 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding { myReturnParams.add(type); } } - - if (myIdParamIndex != null) { - myCanOperateAtInstanceLevel = true; - } - if (getResourceName() == null) { - myCanOperateAtServerLevel = true; - } - } public OperationMethodBinding(Class theReturnResourceType, Class theReturnTypeFromRp, Method theMethod, FhirContext theContext, Object theProvider, - Operation theAnnotation) { + Operation theAnnotation) { this(theReturnResourceType, theReturnTypeFromRp, theMethod, theContext, theProvider, theAnnotation.idempotent(), theAnnotation.name(), theAnnotation.type(), theAnnotation.returnParameters(), - theAnnotation.bundleType()); + theAnnotation.bundleType()); } public String getDescription() { @@ -188,7 +180,7 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding { @Nonnull @Override public RestOperationTypeEnum getRestOperationType() { - return myOtherOperatiopnType; + return myOtherOperationType; } public List getReturnParams() { @@ -230,16 +222,12 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding { boolean requestHasId = theRequest.getId() != null; if (requestHasId) { - if (isCanOperateAtInstanceLevel() == false) { - return false; + return myCanOperateAtInstanceLevel; } - } else { - if (myCanOperateAtTypeLevel == false) { - return false; + if (isNotBlank(theRequest.getResourceName())) { + return myCanOperateAtTypeLevel; } - } - - return true; + return myCanOperateAtServerLevel; } @Override diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/SearchMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/SearchMethodBinding.java index 05a1dc9be6a..735c2cc56c2 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/SearchMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/SearchMethodBinding.java @@ -56,6 +56,7 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding { private Integer myIdParamIndex; private String myQueryName; private boolean myAllowUnknownParams; + private final String myResourceProviderResourceName; static { HashSet specialSearchParams = new HashSet<>(); @@ -64,7 +65,7 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding { SPECIAL_SEARCH_PARAMS = Collections.unmodifiableSet(specialSearchParams); } - public SearchMethodBinding(Class theReturnResourceType, Method theMethod, FhirContext theContext, Object theProvider) { + public SearchMethodBinding(Class theReturnResourceType, Class theResourceProviderResourceType, Method theMethod, FhirContext theContext, Object theProvider) { super(theReturnResourceType, theMethod, theContext, theProvider); Search search = theMethod.getAnnotation(Search.class); this.myQueryName = StringUtils.defaultIfBlank(search.queryName(), null); @@ -89,12 +90,26 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding { throw new ConfigurationException(msg); } + if (theResourceProviderResourceType != null) { + this.myResourceProviderResourceName = theContext.getResourceDefinition(theResourceProviderResourceType).getName(); + } else { + this.myResourceProviderResourceName = null; + } + } public String getDescription() { return myDescription; } + public String getQueryName() { + return myQueryName; + } + + public String getResourceProviderResourceName() { + return myResourceProviderResourceName; + } + @Nonnull @Override public RestOperationTypeEnum getRestOperationType() { diff --git a/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/hapi/rest/server/ServerCapabilityStatementProvider.java b/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/hapi/rest/server/ServerCapabilityStatementProvider.java index 9841662fb21..39b20e66c36 100644 --- a/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/hapi/rest/server/ServerCapabilityStatementProvider.java +++ b/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/hapi/rest/server/ServerCapabilityStatementProvider.java @@ -71,6 +71,8 @@ public class ServerCapabilityStatementProvider implements IServerConformanceProv private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ServerCapabilityStatementProvider.class); private boolean myCache = true; private volatile CapabilityStatement myCapabilityStatement; + private IdentityHashMap myNamedSearchMethodBindingToName; + private HashMap> mySearchNameToBindings; private IdentityHashMap myOperationBindingToName; private HashMap> myOperationNameToBindings; private String myPublisher = "Not provided"; @@ -151,6 +153,17 @@ public class ServerCapabilityStatementProvider implements IServerConformanceProv return DateTimeType.now(); } + private String createNamedQueryName(SearchMethodBinding searchMethodBinding) { + StringBuilder retVal = new StringBuilder(); + if (searchMethodBinding.getResourceName() != null) { + retVal.append(searchMethodBinding.getResourceName()); + } + retVal.append("-query-"); + retVal.append(searchMethodBinding.getQueryName()); + + return retVal.toString(); + } + private String createOperationName(OperationMethodBinding theMethodBinding) { StringBuilder retVal = new StringBuilder(); if (theMethodBinding.getResourceName() != null) { @@ -297,7 +310,15 @@ public class ServerCapabilityStatementProvider implements IServerConformanceProv checkBindingForSystemOps(rest, systemOps, nextMethodBinding); if (nextMethodBinding instanceof SearchMethodBinding) { - handleSearchMethodBinding(rest, resource, resourceName, def, includes, (SearchMethodBinding) nextMethodBinding); + SearchMethodBinding methodBinding = (SearchMethodBinding) nextMethodBinding; + if (methodBinding.getQueryName() != null) { + String queryName = myNamedSearchMethodBindingToName.get(methodBinding); + if (operationNames.add(queryName)) { + rest.addOperation().setName(methodBinding.getQueryName()).setDefinition(new Reference("OperationDefinition/" + queryName)); + } + } else { + handleNamelessSearchMethodBinding(rest, resource, resourceName, def, includes, (SearchMethodBinding) nextMethodBinding); + } } else if (nextMethodBinding instanceof OperationMethodBinding) { OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding; String opName = myOperationBindingToName.get(methodBinding); @@ -349,7 +370,7 @@ public class ServerCapabilityStatementProvider implements IServerConformanceProv return retVal; } - private void handleSearchMethodBinding(CapabilityStatementRestComponent rest, CapabilityStatementRestResourceComponent resource, String resourceName, RuntimeResourceDefinition def, TreeSet includes, + private void handleNamelessSearchMethodBinding(CapabilityStatementRestComponent rest, CapabilityStatementRestResourceComponent resource, String resourceName, RuntimeResourceDefinition def, TreeSet includes, SearchMethodBinding searchMethodBinding) { includes.addAll(searchMethodBinding.getIncludes()); @@ -438,8 +459,10 @@ public class ServerCapabilityStatementProvider implements IServerConformanceProv @Initialize public void initializeOperations() { - myOperationBindingToName = new IdentityHashMap(); - myOperationNameToBindings = new HashMap>(); + myNamedSearchMethodBindingToName = new IdentityHashMap<>(); + mySearchNameToBindings = new HashMap<>(); + myOperationBindingToName = new IdentityHashMap<>(); + myOperationNameToBindings = new HashMap<>(); Map>> resourceToMethods = collectMethodBindings(); for (Entry>> nextEntry : resourceToMethods.entrySet()) { @@ -456,24 +479,96 @@ public class ServerCapabilityStatementProvider implements IServerConformanceProv myOperationBindingToName.put(methodBinding, name); if (myOperationNameToBindings.containsKey(name) == false) { - myOperationNameToBindings.put(name, new ArrayList()); + myOperationNameToBindings.put(name, new ArrayList<>()); } myOperationNameToBindings.get(name).add(methodBinding); + } else if (nextMethodBinding instanceof SearchMethodBinding) { + SearchMethodBinding methodBinding = (SearchMethodBinding) nextMethodBinding; + if (myNamedSearchMethodBindingToName.containsKey(methodBinding)) { + continue; + } + + String name = createNamedQueryName(methodBinding); + ourLog.debug("Detected named query: {}", name); + + myNamedSearchMethodBindingToName.put(methodBinding, name); + if (!mySearchNameToBindings.containsKey(name)) { + mySearchNameToBindings.put(name, new ArrayList<>()); + } + mySearchNameToBindings.get(name).add(methodBinding); } } } } - + @Read(type = OperationDefinition.class) public OperationDefinition readOperationDefinition(@IdParam IdType theId) { if (theId == null || theId.hasIdPart() == false) { throw new ResourceNotFoundException(theId); } - List sharedDescriptions = myOperationNameToBindings.get(theId.getIdPart()); - if (sharedDescriptions == null || sharedDescriptions.isEmpty()) { - throw new ResourceNotFoundException(theId); + List operationBindings = myOperationNameToBindings.get(theId.getIdPart()); + if (operationBindings != null && !operationBindings.isEmpty()) { + return readOperationDefinitionForOperation(operationBindings); + } + List searchBindings = mySearchNameToBindings.get(theId.getIdPart()); + if (searchBindings != null && !searchBindings.isEmpty()) { + return readOperationDefinitionForNamedSearch(searchBindings); + } + throw new ResourceNotFoundException(theId); + } + + private OperationDefinition readOperationDefinitionForNamedSearch(List bindings) { + OperationDefinition op = new OperationDefinition(); + op.setStatus(PublicationStatus.ACTIVE); + op.setKind(OperationKind.QUERY); + op.setIdempotent(true); + + op.setSystem(false); + op.setType(false); + op.setInstance(false); + + Set inParams = new HashSet<>(); + + for (SearchMethodBinding binding : bindings) { + if (isNotBlank(binding.getDescription())) { + op.setDescription(binding.getDescription()); + } + if (isBlank(binding.getResourceProviderResourceName())) { + op.setSystem(true); + } else { + op.setType(true); + op.addResourceElement().setValue(binding.getResourceProviderResourceName()); + } + op.setCode(binding.getQueryName()); + for (IParameter nextParamUntyped : binding.getParameters()) { + if (nextParamUntyped instanceof SearchParameter) { + SearchParameter nextParam = (SearchParameter) nextParamUntyped; + if (!inParams.add(nextParam.getName())) { + continue; + } + OperationDefinitionParameterComponent param = op.addParameter(); + param.setUse(OperationParameterUse.IN); + param.setType("string"); + param.getSearchTypeElement().setValueAsString(nextParam.getParamType().getCode()); + param.setMin(nextParam.isRequired() ? 1 : 0); + param.setMax("1"); + param.setName(nextParam.getName()); + } + } + + if (isBlank(op.getName())) { + if (isNotBlank(op.getDescription())) { + op.setName(op.getDescription()); + } else { + op.setName(op.getCode()); + } + } } + return op; + } + + private OperationDefinition readOperationDefinitionForOperation(List bindings) { OperationDefinition op = new OperationDefinition(); op.setStatus(PublicationStatus.ACTIVE); op.setKind(OperationKind.OPERATION); @@ -484,10 +579,10 @@ public class ServerCapabilityStatementProvider implements IServerConformanceProv op.setType(false); op.setInstance(false); - Set inParams = new HashSet(); - Set outParams = new HashSet(); + Set inParams = new HashSet<>(); + Set outParams = new HashSet<>(); - for (OperationMethodBinding sharedDescription : sharedDescriptions) { + for (OperationMethodBinding sharedDescription : bindings) { if (isNotBlank(sharedDescription.getDescription())) { op.setDescription(sharedDescription.getDescription()); } diff --git a/hapi-fhir-structures-dstu3/src/test/java/org/hl7/fhir/dstu3/hapi/rest/server/ServerCapabilityStatementProviderDstu3Test.java b/hapi-fhir-structures-dstu3/src/test/java/org/hl7/fhir/dstu3/hapi/rest/server/ServerCapabilityStatementProviderDstu3Test.java index 766bdabb910..345b41dab0f 100644 --- a/hapi-fhir-structures-dstu3/src/test/java/org/hl7/fhir/dstu3/hapi/rest/server/ServerCapabilityStatementProviderDstu3Test.java +++ b/hapi-fhir-structures-dstu3/src/test/java/org/hl7/fhir/dstu3/hapi/rest/server/ServerCapabilityStatementProviderDstu3Test.java @@ -29,6 +29,7 @@ import ca.uhn.fhir.model.api.Include; import ca.uhn.fhir.model.api.annotation.Description; import ca.uhn.fhir.rest.annotation.*; import ca.uhn.fhir.rest.api.MethodOutcome; +import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.param.*; import ca.uhn.fhir.rest.server.*; @@ -37,6 +38,14 @@ import ca.uhn.fhir.rest.server.method.SearchParameter; import ca.uhn.fhir.util.TestUtil; import ca.uhn.fhir.validation.FhirValidator; import ca.uhn.fhir.validation.ValidationResult; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import org.hl7.fhir.dstu3.model.Enumerations.PublicationStatus; +import org.hl7.fhir.dstu3.model.OperationDefinition.OperationDefinitionParameterComponent; +import org.hl7.fhir.dstu3.model.OperationDefinition.OperationKind; +import org.hl7.fhir.dstu3.model.OperationDefinition.OperationParameterUse; public class ServerCapabilityStatementProviderDstu3Test { @@ -124,6 +133,9 @@ public class ServerCapabilityStatementProviderDstu3Test { OperationDefinition opDef = sc.readOperationDefinition(new IdType("OperationDefinition/Patient-i-everything")); validate(opDef); assertEquals("everything", opDef.getCode()); + assertThat(opDef.getSystem(), is(false)); + assertThat(opDef.getType(), is(false)); + assertThat(opDef.getInstance(), is(true)); } @Test @@ -344,6 +356,10 @@ public class ServerCapabilityStatementProviderDstu3Test { assertEquals("1", opDef.getParameter().get(2).getMinElement().getValueAsString()); assertEquals("2", opDef.getParameter().get(2).getMaxElement().getValueAsString()); assertEquals("string", opDef.getParameter().get(2).getTypeElement().getValueAsString()); + + assertThat(opDef.getSystem(), is(true)); + assertThat(opDef.getType(), is(false)); + assertThat(opDef.getInstance(), is(true)); } @Test @@ -622,6 +638,120 @@ public class ServerCapabilityStatementProviderDstu3Test { assertTrue(result.getMessages().toString(), result.isSuccessful()); } + @Test + public void testSystemLevelNamedQueryWithParameters() throws Exception { + RestfulServer rs = new RestfulServer(ourCtx); + rs.setProviders(new NamedQueryPlainProvider()); + + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + rs.setServerConformanceProvider(sc); + + rs.init(createServletConfig()); + + CapabilityStatement conformance = sc.getServerConformance(createHttpServletRequest()); + ourLog.info(ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance)); + + CapabilityStatementRestComponent restComponent = conformance.getRest().get(0); + CapabilityStatementRestOperationComponent operationComponent = restComponent.getOperation().get(0); + assertThat(operationComponent.getName(), is(NamedQueryPlainProvider.QUERY_NAME)); + + String operationReference = operationComponent.getDefinition().getReference(); + assertThat(operationReference, not(nullValue())); + + OperationDefinition operationDefinition = sc.readOperationDefinition(new IdType(operationReference)); + ourLog.info(ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(operationDefinition)); + validate(operationDefinition); + assertThat(operationDefinition.getCode(), is(NamedQueryPlainProvider.QUERY_NAME)); + assertThat("The operation name should be the description, if a description is set", operationDefinition.getName(), is(NamedQueryPlainProvider.DESCRIPTION)); + assertThat(operationDefinition.getStatus(), is(PublicationStatus.ACTIVE)); + assertThat(operationDefinition.getKind(), is(OperationKind.QUERY)); + assertThat(operationDefinition.getDescription(), is(NamedQueryPlainProvider.DESCRIPTION)); + assertThat(operationDefinition.getIdempotent(), is(true)); + assertThat("A system level search has no target resources", operationDefinition.getResource(), is(empty())); + assertThat(operationDefinition.getSystem(), is(true)); + assertThat(operationDefinition.getType(), is(false)); + assertThat(operationDefinition.getInstance(), is(false)); + List parameters = operationDefinition.getParameter(); + assertThat(parameters.size(), is(1)); + OperationDefinitionParameterComponent param = parameters.get(0); + assertThat(param.getName(), is(NamedQueryPlainProvider.SP_QUANTITY)); + assertThat(param.getType(), is("string")); + assertThat(param.getSearchTypeElement().asStringValue(), is(RestSearchParameterTypeEnum.QUANTITY.getCode())); + assertThat(param.getMin(), is(1)); + assertThat(param.getMax(), is("1")); + assertThat(param.getUse(), is(OperationParameterUse.IN)); + } + + @Test + public void testResourceLevelNamedQueryWithParameters() throws Exception { + RestfulServer rs = new RestfulServer(ourCtx); + rs.setProviders(new NamedQueryResourceProvider()); + + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + rs.setServerConformanceProvider(sc); + + rs.init(createServletConfig()); + + CapabilityStatement conformance = sc.getServerConformance(createHttpServletRequest()); + ourLog.info(ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance)); + + CapabilityStatementRestComponent restComponent = conformance.getRest().get(0); + CapabilityStatementRestOperationComponent operationComponent = restComponent.getOperation().get(0); + String operationReference = operationComponent.getDefinition().getReference(); + assertThat(operationReference, not(nullValue())); + + OperationDefinition operationDefinition = sc.readOperationDefinition(new IdType(operationReference)); + ourLog.info(ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(operationDefinition)); + validate(operationDefinition); + assertThat("The operation name should be the code if no description is set", operationDefinition.getName(), is(NamedQueryResourceProvider.QUERY_NAME)); + String patientResourceName = "Patient"; + assertThat("A resource level search targets the resource of the provider it's defined in", operationDefinition.getResource().get(0).getValue(), is(patientResourceName)); + assertThat(operationDefinition.getSystem(), is(false)); + assertThat(operationDefinition.getType(), is(true)); + assertThat(operationDefinition.getInstance(), is(false)); + List parameters = operationDefinition.getParameter(); + assertThat(parameters.size(), is(1)); + OperationDefinitionParameterComponent param = parameters.get(0); + assertThat(param.getName(), is(NamedQueryResourceProvider.SP_PARAM)); + assertThat(param.getType(), is("string")); + assertThat(param.getSearchTypeElement().asStringValue(), is(RestSearchParameterTypeEnum.STRING.getCode())); + assertThat(param.getMin(), is(0)); + assertThat(param.getMax(), is("1")); + assertThat(param.getUse(), is(OperationParameterUse.IN)); + + CapabilityStatementRestResourceComponent patientResource = restComponent.getResource().stream() + .filter(r -> patientResourceName.equals(r.getType())) + .findAny().get(); + assertThat("Named query parameters should not appear in the resource search params", patientResource.getSearchParam(), is(empty())); + } + + @Test + public void testExtendedOperationAtTypeLevel() throws Exception { + RestfulServer rs = new RestfulServer(ourCtx); + rs.setProviders(new TypeLevelOperationProvider()); + + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + rs.setServerConformanceProvider(sc); + + rs.init(createServletConfig()); + + CapabilityStatement conformance = sc.getServerConformance(createHttpServletRequest()); + + String conf = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); + ourLog.info(conf); + + List operations = conformance.getRest().get(0).getOperation(); + assertThat(operations.size(), is(1)); + assertThat(operations.get(0).getName(), is(TypeLevelOperationProvider.OPERATION_NAME)); + + OperationDefinition opDef = sc.readOperationDefinition(new IdType(operations.get(0).getDefinition().getReference())); + validate(opDef); + assertEquals(TypeLevelOperationProvider.OPERATION_NAME, opDef.getCode()); + assertThat(opDef.getSystem(), is(false)); + assertThat(opDef.getType(), is(true)); + assertThat(opDef.getInstance(), is(false)); + } + private List toOperationIdParts(List theOperation) { ArrayList retVal = Lists.newArrayList(); for (CapabilityStatementRestOperationComponent next : theOperation) { @@ -933,5 +1063,51 @@ public class ServerCapabilityStatementProviderDstu3Test { } } + + public static class TypeLevelOperationProvider implements IResourceProvider { + + public static final String OPERATION_NAME = "op"; + + @Operation(name = OPERATION_NAME, idempotent = true) + public IBundleProvider op() { + return null; + } + + @Override + public Class getResourceType() { + return Patient.class; + } + + } + + public static class NamedQueryPlainProvider { + + public static final String QUERY_NAME = "testQuery"; + public static final String DESCRIPTION = "A query description"; + public static final String SP_QUANTITY = "quantity"; + + @Search(queryName = QUERY_NAME) + @Description(formalDefinition = DESCRIPTION) + public Bundle findAllGivenParameter(@RequiredParam(name = SP_QUANTITY) QuantityParam quantity) { + return null; + } + } + + public static class NamedQueryResourceProvider implements IResourceProvider { + + public static final String QUERY_NAME = "testQuery"; + public static final String SP_PARAM = "param"; + + @Override + public Class getResourceType() { + return Patient.class; + } + + @Search(queryName = QUERY_NAME) + public Bundle findAllGivenParameter(@OptionalParam(name = SP_PARAM) StringParam param) { + return null; + } + + } }