diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ServerCapabilityStatementProviderJpaR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ServerCapabilityStatementProviderJpaR4Test.java index 6c73dff4c1d..9f4d6945e5c 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ServerCapabilityStatementProviderJpaR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ServerCapabilityStatementProviderJpaR4Test.java @@ -65,6 +65,19 @@ public class ServerCapabilityStatementProviderJpaR4Test extends BaseResourceProv } + @Test + public void testLastUpdatedIncluded() { + CapabilityStatement cs = myClient.capabilities().ofType(CapabilityStatement.class).execute(); + + List fooSearchParams = findSearchParams(cs, "Patient", "_lastUpdated"); + assertEquals(1, fooSearchParams.size()); + assertEquals("_lastUpdated", fooSearchParams.get(0).getName()); + assertEquals("http://localhost:" + ourPort + "/fhir/context/SearchParameter/Patient-_lastUpdated", fooSearchParams.get(0).getDefinition()); + assertEquals("Only return resources which were last updated as specified by the given range", fooSearchParams.get(0).getDocumentation()); + assertEquals(Enumerations.SearchParamType.DATE, fooSearchParams.get(0).getType()); + + } + @Override @AfterEach public void after() throws Exception { @@ -119,10 +132,10 @@ public class ServerCapabilityStatementProviderJpaR4Test extends BaseResourceProv .execute(); List includes = findIncludes(cs, "Patient"); - assertThat(includes.toString(), includes, contains("*", "Patient:general-practitioner", "Patient:link", "Patient:organization")); + assertThat(includes.toString(), includes, containsInAnyOrder("*", "Patient:general-practitioner", "Patient:link", "Patient:organization")); includes = findIncludes(cs, "Observation"); - assertThat(includes.toString(), includes, contains("*", "Observation:based-on", "Observation:derived-from", "Observation:device", "Observation:encounter", "Observation:focus", "Observation:foo", "Observation:has-member", "Observation:part-of", "Observation:patient", "Observation:performer", "Observation:specimen", "Observation:subject")); + assertThat(includes.toString(), includes, containsInAnyOrder("*", "Observation:based-on", "Observation:derived-from", "Observation:device", "Observation:encounter", "Observation:focus", "Observation:foo", "Observation:has-member", "Observation:part-of", "Observation:patient", "Observation:performer", "Observation:specimen", "Observation:subject")); List revIncludes = findRevIncludes(cs, "Patient"); assertThat(revIncludes.toString(), revIncludes, hasItems( diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ServerCapabilityStatementProvider.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ServerCapabilityStatementProvider.java index 59913098b86..230763bc8a6 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ServerCapabilityStatementProvider.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ServerCapabilityStatementProvider.java @@ -40,6 +40,7 @@ import org.slf4j.LoggerFactory; import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; import java.util.Date; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -82,624 +83,641 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank; */ public class ServerCapabilityStatementProvider implements IServerConformanceProvider { - public static final boolean DEFAULT_REST_RESOURCE_REV_INCLUDES_ENABLED = true; - private static final Logger ourLog = LoggerFactory.getLogger(ServerCapabilityStatementProvider.class); - private final FhirContext myContext; - private final RestfulServer myServer; - private final ISearchParamRegistry mySearchParamRegistry; - private final RestfulServerConfiguration myServerConfiguration; - private final IValidationSupport myValidationSupport; - private String myPublisher = "Not provided"; - private boolean myRestResourceRevIncludesEnabled = DEFAULT_REST_RESOURCE_REV_INCLUDES_ENABLED; - - /** - * Constructor - */ - public ServerCapabilityStatementProvider(RestfulServer theServer) { - myServer = theServer; - myContext = theServer.getFhirContext(); - mySearchParamRegistry = null; - myServerConfiguration = null; - myValidationSupport = null; - } - - /** - * Constructor - */ - public ServerCapabilityStatementProvider(FhirContext theContext, RestfulServerConfiguration theServerConfiguration) { - myContext = theContext; - myServerConfiguration = theServerConfiguration; - mySearchParamRegistry = null; - myServer = null; - myValidationSupport = null; - } - - /** - * Constructor - */ - public ServerCapabilityStatementProvider(RestfulServer theRestfulServer, ISearchParamRegistry theSearchParamRegistry, IValidationSupport theValidationSupport) { - myContext = theRestfulServer.getFhirContext(); - mySearchParamRegistry = theSearchParamRegistry; - myServer = theRestfulServer; - myServerConfiguration = null; - myValidationSupport = theValidationSupport; - } - - private void checkBindingForSystemOps(FhirTerser theTerser, IBase theRest, Set theSystemOps, BaseMethodBinding theMethodBinding) { - RestOperationTypeEnum restOperationType = theMethodBinding.getRestOperationType(); - if (restOperationType.isSystemLevel()) { - String sysOp = restOperationType.getCode(); - if (theSystemOps.contains(sysOp) == false) { - theSystemOps.add(sysOp); - IBase interaction = theTerser.addElement(theRest, "interaction"); - theTerser.addElement(interaction, "code", sysOp); - } - } - } - - - private String conformanceDate(RestfulServerConfiguration theServerConfiguration) { - IPrimitiveType buildDate = theServerConfiguration.getConformanceDate(); - if (buildDate != null && buildDate.getValue() != null) { - try { - return buildDate.getValueAsString(); - } catch (DataFormatException e) { - // fall through - } - } - return InstantDt.withCurrentTime().getValueAsString(); - } - - private RestfulServerConfiguration getServerConfiguration() { - if (myServer != null) { - return myServer.createConfiguration(); - } - return myServerConfiguration; - } - - - /** - * Gets the value of the "publisher" that will be placed in the generated conformance statement. As this is a mandatory element, the value should not be null (although this is not enforced). The - * value defaults to "Not provided" but may be set to null, which will cause this element to be omitted. - */ - public String getPublisher() { - return myPublisher; - } - - /** - * Sets the value of the "publisher" that will be placed in the generated conformance statement. As this is a mandatory element, the value should not be null (although this is not enforced). The - * value defaults to "Not provided" but may be set to null, which will cause this element to be omitted. - */ - public void setPublisher(String thePublisher) { - myPublisher = thePublisher; - } - - @Override - @Metadata - public IBaseConformance getServerConformance(HttpServletRequest theRequest, RequestDetails theRequestDetails) { - - HttpServletRequest servletRequest = null; - if (theRequestDetails instanceof ServletRequestDetails) { - servletRequest = ((ServletRequestDetails) theRequestDetails).getServletRequest(); - } - - RestfulServerConfiguration configuration = getServerConfiguration(); - Bindings bindings = configuration.provideBindings(); - - IBaseConformance retVal = (IBaseConformance) myContext.getResourceDefinition("CapabilityStatement").newInstance(); - - FhirTerser terser = myContext.newTerser(); - - TreeMultimap resourceTypeToSupportedProfiles = getSupportedProfileMultimap(terser); - - terser.addElement(retVal, "id", UUID.randomUUID().toString()); - terser.addElement(retVal, "name", "RestServer"); - terser.addElement(retVal, "publisher", myPublisher); - terser.addElement(retVal, "date", conformanceDate(configuration)); - terser.addElement(retVal, "fhirVersion", myContext.getVersion().getVersion().getFhirVersionString()); - - ServletContext servletContext = (ServletContext) (theRequest == null ? null : theRequest.getAttribute(RestfulServer.SERVLET_CONTEXT_ATTRIBUTE)); - String serverBase = configuration.getServerAddressStrategy().determineServerBase(servletContext, theRequest); - terser.addElement(retVal, "implementation.url", serverBase); - terser.addElement(retVal, "implementation.description", configuration.getImplementationDescription()); - terser.addElement(retVal, "kind", "instance"); - terser.addElement(retVal, "software.name", configuration.getServerName()); - terser.addElement(retVal, "software.version", configuration.getServerVersion()); - if (myContext.isFormatXmlSupported()) { - terser.addElement(retVal, "format", Constants.CT_FHIR_XML_NEW); - terser.addElement(retVal, "format", Constants.FORMAT_XML); - } - if (myContext.isFormatJsonSupported()) { - terser.addElement(retVal, "format", Constants.CT_FHIR_JSON_NEW); - terser.addElement(retVal, "format", Constants.FORMAT_JSON); - } - if (myContext.isFormatRdfSupported()) { - terser.addElement(retVal, "format", Constants.CT_RDF_TURTLE); - terser.addElement(retVal, "format", Constants.FORMAT_TURTLE); - } - terser.addElement(retVal, "status", "active"); - - IBase rest = terser.addElement(retVal, "rest"); - terser.addElement(rest, "mode", "server"); - - Set systemOps = new HashSet<>(); - Set operationNames = new HashSet<>(); - - Map>> resourceToMethods = configuration.collectMethodBindings(); - Map> resourceNameToSharedSupertype = configuration.getNameToSharedSupertype(); - - TreeMultimap resourceNameToIncludes = TreeMultimap.create(); - TreeMultimap resourceNameToRevIncludes = TreeMultimap.create(); - for (Entry>> nextEntry : resourceToMethods.entrySet()) { - String resourceName = nextEntry.getKey(); - for (BaseMethodBinding nextMethod : nextEntry.getValue()) { - if (nextMethod instanceof SearchMethodBinding) { - resourceNameToIncludes.putAll(resourceName, nextMethod.getIncludes()); - resourceNameToRevIncludes.putAll(resourceName, nextMethod.getRevIncludes()); - } - } - - } - - for (Entry>> nextEntry : resourceToMethods.entrySet()) { - - String resourceName = nextEntry.getKey(); - if (nextEntry.getKey().isEmpty() == false) { - Set resourceOps = new HashSet<>(); - IBase resource = terser.addElement(rest, "resource"); - - postProcessRestResource(terser, resource, resourceName); - - RuntimeResourceDefinition def; - FhirContext context = configuration.getFhirContext(); - if (resourceNameToSharedSupertype.containsKey(resourceName)) { - def = context.getResourceDefinition(resourceNameToSharedSupertype.get(resourceName)); - } else { - def = context.getResourceDefinition(resourceName); - } - terser.addElement(resource, "type", def.getName()); - terser.addElement(resource, "profile", def.getResourceProfile(serverBase)); - - for (BaseMethodBinding nextMethodBinding : nextEntry.getValue()) { - RestOperationTypeEnum resOpCode = nextMethodBinding.getRestOperationType(); - if (resOpCode.isTypeLevel() || resOpCode.isInstanceLevel()) { - String resOp; - resOp = resOpCode.getCode(); - if (resourceOps.contains(resOp) == false) { - resourceOps.add(resOp); - IBase interaction = terser.addElement(resource, "interaction"); - terser.addElement(interaction, "code", resOp); - } - if (RestOperationTypeEnum.VREAD.equals(resOpCode)) { - // vread implies read - resOp = "read"; - if (resourceOps.contains(resOp) == false) { - resourceOps.add(resOp); - IBase interaction = terser.addElement(resource, "interaction"); - terser.addElement(interaction, "code", resOp); - } - } - } - - if (nextMethodBinding.isSupportsConditional()) { - switch (resOpCode) { - case CREATE: - terser.setElement(resource, "conditionalCreate", "true"); - break; - case DELETE: - if (nextMethodBinding.isSupportsConditionalMultiple()) { - terser.setElement(resource, "conditionalDelete", "multiple"); - } else { - terser.setElement(resource, "conditionalDelete", "single"); - } - break; - case UPDATE: - terser.setElement(resource, "conditionalUpdate", "true"); - break; - case HISTORY_INSTANCE: - case HISTORY_SYSTEM: - case HISTORY_TYPE: - case READ: - case SEARCH_SYSTEM: - case SEARCH_TYPE: - case TRANSACTION: - case VALIDATE: - case VREAD: - case METADATA: - case META_ADD: - case META: - case META_DELETE: - case PATCH: - case BATCH: - case ADD_TAGS: - case DELETE_TAGS: - case GET_TAGS: - case GET_PAGE: - case GRAPHQL_REQUEST: - case EXTENDED_OPERATION_SERVER: - case EXTENDED_OPERATION_TYPE: - case EXTENDED_OPERATION_INSTANCE: - default: - break; - } - } - - checkBindingForSystemOps(terser, rest, systemOps, nextMethodBinding); - - if (nextMethodBinding instanceof SearchMethodBinding) { - SearchMethodBinding methodBinding = (SearchMethodBinding) nextMethodBinding; - if (methodBinding.getQueryName() != null) { - String queryName = bindings.getNamedSearchMethodBindingToName().get(methodBinding); - if (operationNames.add(queryName)) { - IBase operation = terser.addElement(rest, "operation"); - terser.addElement(operation, "name", methodBinding.getQueryName()); - terser.addElement(operation, "definition", (getOperationDefinitionPrefix(theRequestDetails) + "OperationDefinition/" + queryName)); - } - } - } else if (nextMethodBinding instanceof OperationMethodBinding) { - OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding; - String opName = bindings.getOperationBindingToName().get(methodBinding); - // Only add each operation (by name) once - if (operationNames.add(opName)) { - IBase operation = terser.addElement(rest, "operation"); - terser.addElement(operation, "name", methodBinding.getName().substring(1)); - terser.addElement(operation, "definition", getOperationDefinitionPrefix(theRequestDetails) + "OperationDefinition/" + opName); - } - } - - } - - ISearchParamRegistry searchParamRegistry; - if (mySearchParamRegistry != null) { - searchParamRegistry = mySearchParamRegistry; - } else if (myServerConfiguration != null) { - searchParamRegistry = myServerConfiguration; - } else { - searchParamRegistry = myServer.createConfiguration(); - } - - Map searchParams = searchParamRegistry.getActiveSearchParams(resourceName); - for (RuntimeSearchParam next : searchParams.values()) { - IBase searchParam = terser.addElement(resource, "searchParam"); - terser.addElement(searchParam, "name", next.getName()); - terser.addElement(searchParam, "type", next.getParamType().getCode()); - if (isNotBlank(next.getDescription())) { - terser.addElement(searchParam, "documentation", next.getDescription()); - } - - String spUri = next.getUri(); - if (isBlank(spUri) && servletRequest != null) { - String id; - if (next.getId() != null) { - id = next.getId().toUnqualifiedVersionless().getValue(); - } else { - id = resourceName + "-" + next.getName(); - } - spUri = configuration.getServerAddressStrategy().determineServerBase(servletRequest.getServletContext(), servletRequest) + "/" + id; - } - if (isNotBlank(spUri)) { - terser.addElement(searchParam, "definition", spUri); - } - } - - // Add Include to CapabilityStatement.rest.resource - NavigableSet resourceIncludes = resourceNameToIncludes.get(resourceName); - if (resourceIncludes.isEmpty()) { - List includes = searchParams - .values() - .stream() - .filter(t -> t.getParamType() == RestSearchParameterTypeEnum.REFERENCE) - .map(t -> resourceName + ":" + t.getName()) - .sorted() - .collect(Collectors.toList()); - terser.addElement(resource, "searchInclude", "*"); - for (String nextInclude : includes) { - terser.addElement(resource, "searchInclude", nextInclude); - } - } else { - for (String resourceInclude : resourceIncludes) { - terser.addElement(resource, "searchInclude", resourceInclude); - } - } - - // Add RevInclude to CapabilityStatement.rest.resource - if (myRestResourceRevIncludesEnabled) { - NavigableSet resourceRevIncludes = resourceNameToRevIncludes.get(resourceName); - if (resourceRevIncludes.isEmpty()) { - TreeSet revIncludes = new TreeSet<>(); - for (String nextResourceName : resourceToMethods.keySet()) { - if (isBlank(nextResourceName)) { - continue; - } - - for (RuntimeSearchParam t : searchParamRegistry - .getActiveSearchParams(nextResourceName) - .values()) { - if (t.getParamType() == RestSearchParameterTypeEnum.REFERENCE) { - if (isNotBlank(t.getName())) { - boolean appropriateTarget = false; - if (t.getTargets().contains(resourceName) || t.getTargets().isEmpty()) { - appropriateTarget = true; - } - - if (appropriateTarget) { - revIncludes.add(nextResourceName + ":" + t.getName()); - } - } - } - } - } - for (String nextInclude : revIncludes) { - terser.addElement(resource, "searchRevInclude", nextInclude); - } - } else { - for (String resourceInclude : resourceRevIncludes) { - terser.addElement(resource, "searchRevInclude", resourceInclude); - } - } - } - - // Add SupportedProfile to CapabilityStatement.rest.resource - for (String supportedProfile : resourceTypeToSupportedProfiles.get(resourceName)) { - terser.addElement(resource, "supportedProfile", supportedProfile); - } - - } else { - for (BaseMethodBinding nextMethodBinding : nextEntry.getValue()) { - checkBindingForSystemOps(terser, rest, systemOps, nextMethodBinding); - if (nextMethodBinding instanceof OperationMethodBinding) { - OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding; - String opName = bindings.getOperationBindingToName().get(methodBinding); - if (operationNames.add(opName)) { - ourLog.debug("Found bound operation: {}", opName); - IBase operation = terser.addElement(rest, "operation"); - terser.addElement(operation, "name", methodBinding.getName().substring(1)); - terser.addElement(operation, "definition", getOperationDefinitionPrefix(theRequestDetails) + "OperationDefinition/" + opName); - } - } - } - } - - postProcessRest(terser, rest); - - } - - postProcess(terser, retVal); - - return retVal; - } - - private TreeMultimap getSupportedProfileMultimap(FhirTerser terser) { - TreeMultimap resourceTypeToSupportedProfiles = TreeMultimap.create(); - if (myValidationSupport != null) { - List allStructureDefinitions = myValidationSupport.fetchAllNonBaseStructureDefinitions(); - if (allStructureDefinitions != null) { - for (IBaseResource next : allStructureDefinitions) { - String kind = terser.getSinglePrimitiveValueOrNull(next, "kind"); - String url = terser.getSinglePrimitiveValueOrNull(next, "url"); - String baseDefinition = defaultString(terser.getSinglePrimitiveValueOrNull(next, "baseDefinition")); - if ("resource".equals(kind) && isNotBlank(url)) { - - // Don't include the base resource definitions in the supported profile list - This isn't helpful - if (baseDefinition.equals("http://hl7.org/fhir/StructureDefinition/DomainResource") || baseDefinition.equals("http://hl7.org/fhir/StructureDefinition/Resource")) { - continue; - } - - String resourceType = terser.getSinglePrimitiveValueOrNull(next, "snapshot.element.path"); - if (isBlank(resourceType)) { - resourceType = terser.getSinglePrimitiveValueOrNull(next, "differential.element.path"); - } - - if (isNotBlank(resourceType)) { - resourceTypeToSupportedProfiles.put(resourceType, url); - } - } - } - } - } - return resourceTypeToSupportedProfiles; - } - - /** - * Subclasses may override - */ - protected void postProcess(FhirTerser theTerser, IBaseConformance theCapabilityStatement) { - // nothing - } - - /** - * Subclasses may override - */ - protected void postProcessRest(FhirTerser theTerser, IBase theRest) { - // nothing - } - - /** - * Subclasses may override - */ - protected void postProcessRestResource(FhirTerser theTerser, IBase theResource, String theResourceName) { - // nothing - } - - protected String getOperationDefinitionPrefix(RequestDetails theRequestDetails) { - if (theRequestDetails == null) { - return ""; - } - return theRequestDetails.getServerBaseForRequest() + "/"; - } - - - @Read(typeName = "OperationDefinition") - public IBaseResource readOperationDefinition(@IdParam IIdType theId, RequestDetails theRequestDetails) { - if (theId == null || theId.hasIdPart() == false) { - throw new ResourceNotFoundException(theId); - } - RestfulServerConfiguration configuration = getServerConfiguration(); - Bindings bindings = configuration.provideBindings(); - - List operationBindings = bindings.getOperationNameToBindings().get(theId.getIdPart()); - if (operationBindings != null && !operationBindings.isEmpty()) { - return readOperationDefinitionForOperation(operationBindings); - } - List searchBindings = bindings.getSearchNameToBindings().get(theId.getIdPart()); - if (searchBindings != null && !searchBindings.isEmpty()) { - return readOperationDefinitionForNamedSearch(searchBindings); - } - throw new ResourceNotFoundException(theId); - } - - private IBaseResource readOperationDefinitionForNamedSearch(List bindings) { - IBaseResource op = myContext.getResourceDefinition("OperationDefinition").newInstance(); - FhirTerser terser = myContext.newTerser(); - - terser.addElement(op, "status", "active"); - terser.addElement(op, "kind", "query"); - terser.addElement(op, "affectsState", "false"); - - terser.addElement(op, "instance", "false"); - - Set inParams = new HashSet<>(); - - String operationCode = null; - for (SearchMethodBinding binding : bindings) { - if (isNotBlank(binding.getDescription())) { - terser.addElement(op, "description", binding.getDescription()); - } - if (isBlank(binding.getResourceProviderResourceName())) { - terser.addElement(op, "system", "true"); - terser.addElement(op, "type", "false"); - } else { - terser.addElement(op, "system", "false"); - terser.addElement(op, "type", "true"); - terser.addElement(op, "resource", binding.getResourceProviderResourceName()); - } - - if (operationCode == null) { - operationCode = binding.getQueryName(); - } - - for (IParameter nextParamUntyped : binding.getParameters()) { - if (nextParamUntyped instanceof SearchParameter) { - SearchParameter nextParam = (SearchParameter) nextParamUntyped; - if (!inParams.add(nextParam.getName())) { - continue; - } - - IBase param = terser.addElement(op, "parameter"); - terser.addElement(param, "use", "in"); - terser.addElement(param, "type", "string"); - terser.addElement(param, "searchType", nextParam.getParamType().getCode()); - terser.addElement(param, "min", nextParam.isRequired() ? "1" : "0"); - terser.addElement(param, "max", "1"); - terser.addElement(param, "name", nextParam.getName()); - } - } - - } - - terser.addElement(op, "code", operationCode); - terser.addElement(op, "name", "Search_" + operationCode); - - return op; - } - - private IBaseResource readOperationDefinitionForOperation(List bindings) { - IBaseResource op = myContext.getResourceDefinition("OperationDefinition").newInstance(); - FhirTerser terser = myContext.newTerser(); - - terser.addElement(op, "status", "active"); - terser.addElement(op, "kind", "operation"); - - boolean systemLevel = false; - boolean typeLevel = false; - boolean instanceLevel = false; - boolean affectsState = false; - String description = null; - String code = null; - String name; - - Set resourceNames = new TreeSet<>(); - Set inParams = new HashSet<>(); - Set outParams = new HashSet<>(); - - for (OperationMethodBinding sharedDescription : bindings) { - if (isNotBlank(sharedDescription.getDescription()) && isBlank(description)) { - description = sharedDescription.getDescription(); - } - if (sharedDescription.isCanOperateAtInstanceLevel()) { - instanceLevel = true; - } - if (sharedDescription.isCanOperateAtServerLevel()) { - systemLevel = true; - } - if (sharedDescription.isCanOperateAtTypeLevel()) { - typeLevel = true; - } - if (!sharedDescription.isIdempotent()) { - affectsState |= true; - } - - code = sharedDescription.getName().substring(1); - - if (isNotBlank(sharedDescription.getResourceName())) { - resourceNames.add(sharedDescription.getResourceName()); - } - - for (IParameter nextParamUntyped : sharedDescription.getParameters()) { - if (nextParamUntyped instanceof OperationParameter) { - OperationParameter nextParam = (OperationParameter) nextParamUntyped; - if (!inParams.add(nextParam.getName())) { - continue; - } - IBase param = terser.addElement(op, "parameter"); - terser.addElement(param, "use", "in"); - if (nextParam.getParamType() != null) { - terser.addElement(param, "type", nextParam.getParamType()); - } - if (nextParam.getSearchParamType() != null) { - terser.addElement(param, "searchType", nextParam.getSearchParamType()); - } - terser.addElement(param, "min", Integer.toString(nextParam.getMin())); - terser.addElement(param, "max", (nextParam.getMax() == -1 ? "*" : Integer.toString(nextParam.getMax()))); - terser.addElement(param, "name", nextParam.getName()); - } - } - - for (ReturnType nextParam : sharedDescription.getReturnParams()) { - if (!outParams.add(nextParam.getName())) { - continue; - } - IBase param = terser.addElement(op, "parameter"); - terser.addElement(param, "use", "out"); - if (nextParam.getType() != null) { - terser.addElement(param, "type", nextParam.getType()); - } - terser.addElement(param, "min", Integer.toString(nextParam.getMin())); - terser.addElement(param, "max", (nextParam.getMax() == -1 ? "*" : Integer.toString(nextParam.getMax()))); - terser.addElement(param, "name", nextParam.getName()); - } - } - - name = "Operation_" + code; - - terser.addElements(op, "resource", resourceNames); - terser.addElement(op, "name", name); - terser.addElement(op, "code", code); - terser.addElement(op, "description", description); - terser.addElement(op, "affectsState", Boolean.toString(affectsState)); - terser.addElement(op, "system", Boolean.toString(systemLevel)); - terser.addElement(op, "type", Boolean.toString(typeLevel)); - terser.addElement(op, "instance", Boolean.toString(instanceLevel)); - - return op; - } - - @Override - public void setRestfulServer(RestfulServer theRestfulServer) { - // ignore - } - - public void setRestResourceRevIncludesEnabled(boolean theRestResourceRevIncludesEnabled) { - myRestResourceRevIncludesEnabled = theRestResourceRevIncludesEnabled; - } + public static final boolean DEFAULT_REST_RESOURCE_REV_INCLUDES_ENABLED = true; + private static final Logger ourLog = LoggerFactory.getLogger(ServerCapabilityStatementProvider.class); + private final FhirContext myContext; + private final RestfulServer myServer; + private final ISearchParamRegistry mySearchParamRegistry; + private final RestfulServerConfiguration myServerConfiguration; + private final IValidationSupport myValidationSupport; + private String myPublisher = "Not provided"; + private boolean myRestResourceRevIncludesEnabled = DEFAULT_REST_RESOURCE_REV_INCLUDES_ENABLED; + + /** + * Constructor + */ + public ServerCapabilityStatementProvider(RestfulServer theServer) { + myServer = theServer; + myContext = theServer.getFhirContext(); + mySearchParamRegistry = null; + myServerConfiguration = null; + myValidationSupport = null; + } + + /** + * Constructor + */ + public ServerCapabilityStatementProvider(FhirContext theContext, RestfulServerConfiguration theServerConfiguration) { + myContext = theContext; + myServerConfiguration = theServerConfiguration; + mySearchParamRegistry = null; + myServer = null; + myValidationSupport = null; + } + + /** + * Constructor + */ + public ServerCapabilityStatementProvider(RestfulServer theRestfulServer, ISearchParamRegistry theSearchParamRegistry, IValidationSupport theValidationSupport) { + myContext = theRestfulServer.getFhirContext(); + mySearchParamRegistry = theSearchParamRegistry; + myServer = theRestfulServer; + myServerConfiguration = null; + myValidationSupport = theValidationSupport; + } + + private void checkBindingForSystemOps(FhirTerser theTerser, IBase theRest, Set theSystemOps, BaseMethodBinding theMethodBinding) { + RestOperationTypeEnum restOperationType = theMethodBinding.getRestOperationType(); + if (restOperationType.isSystemLevel()) { + String sysOp = restOperationType.getCode(); + if (theSystemOps.contains(sysOp) == false) { + theSystemOps.add(sysOp); + IBase interaction = theTerser.addElement(theRest, "interaction"); + theTerser.addElement(interaction, "code", sysOp); + } + } + } + + + private String conformanceDate(RestfulServerConfiguration theServerConfiguration) { + IPrimitiveType buildDate = theServerConfiguration.getConformanceDate(); + if (buildDate != null && buildDate.getValue() != null) { + try { + return buildDate.getValueAsString(); + } catch (DataFormatException e) { + // fall through + } + } + return InstantDt.withCurrentTime().getValueAsString(); + } + + private RestfulServerConfiguration getServerConfiguration() { + if (myServer != null) { + return myServer.createConfiguration(); + } + return myServerConfiguration; + } + + + /** + * Gets the value of the "publisher" that will be placed in the generated conformance statement. As this is a mandatory element, the value should not be null (although this is not enforced). The + * value defaults to "Not provided" but may be set to null, which will cause this element to be omitted. + */ + public String getPublisher() { + return myPublisher; + } + + /** + * Sets the value of the "publisher" that will be placed in the generated conformance statement. As this is a mandatory element, the value should not be null (although this is not enforced). The + * value defaults to "Not provided" but may be set to null, which will cause this element to be omitted. + */ + public void setPublisher(String thePublisher) { + myPublisher = thePublisher; + } + + @Override + @Metadata + public IBaseConformance getServerConformance(HttpServletRequest theRequest, RequestDetails theRequestDetails) { + + HttpServletRequest servletRequest = null; + if (theRequestDetails instanceof ServletRequestDetails) { + servletRequest = ((ServletRequestDetails) theRequestDetails).getServletRequest(); + } + + RestfulServerConfiguration configuration = getServerConfiguration(); + Bindings bindings = configuration.provideBindings(); + + IBaseConformance retVal = (IBaseConformance) myContext.getResourceDefinition("CapabilityStatement").newInstance(); + + FhirTerser terser = myContext.newTerser(); + + TreeMultimap resourceTypeToSupportedProfiles = getSupportedProfileMultimap(terser); + + terser.addElement(retVal, "id", UUID.randomUUID().toString()); + terser.addElement(retVal, "name", "RestServer"); + terser.addElement(retVal, "publisher", myPublisher); + terser.addElement(retVal, "date", conformanceDate(configuration)); + terser.addElement(retVal, "fhirVersion", myContext.getVersion().getVersion().getFhirVersionString()); + + ServletContext servletContext = (ServletContext) (theRequest == null ? null : theRequest.getAttribute(RestfulServer.SERVLET_CONTEXT_ATTRIBUTE)); + String serverBase = configuration.getServerAddressStrategy().determineServerBase(servletContext, theRequest); + terser.addElement(retVal, "implementation.url", serverBase); + terser.addElement(retVal, "implementation.description", configuration.getImplementationDescription()); + terser.addElement(retVal, "kind", "instance"); + terser.addElement(retVal, "software.name", configuration.getServerName()); + terser.addElement(retVal, "software.version", configuration.getServerVersion()); + if (myContext.isFormatXmlSupported()) { + terser.addElement(retVal, "format", Constants.CT_FHIR_XML_NEW); + terser.addElement(retVal, "format", Constants.FORMAT_XML); + } + if (myContext.isFormatJsonSupported()) { + terser.addElement(retVal, "format", Constants.CT_FHIR_JSON_NEW); + terser.addElement(retVal, "format", Constants.FORMAT_JSON); + } + if (myContext.isFormatRdfSupported()) { + terser.addElement(retVal, "format", Constants.CT_RDF_TURTLE); + terser.addElement(retVal, "format", Constants.FORMAT_TURTLE); + } + terser.addElement(retVal, "status", "active"); + + IBase rest = terser.addElement(retVal, "rest"); + terser.addElement(rest, "mode", "server"); + + Set systemOps = new HashSet<>(); + Set operationNames = new HashSet<>(); + + Map>> resourceToMethods = configuration.collectMethodBindings(); + Map> resourceNameToSharedSupertype = configuration.getNameToSharedSupertype(); + + TreeMultimap resourceNameToIncludes = TreeMultimap.create(); + TreeMultimap resourceNameToRevIncludes = TreeMultimap.create(); + for (Entry>> nextEntry : resourceToMethods.entrySet()) { + String resourceName = nextEntry.getKey(); + for (BaseMethodBinding nextMethod : nextEntry.getValue()) { + if (nextMethod instanceof SearchMethodBinding) { + resourceNameToIncludes.putAll(resourceName, nextMethod.getIncludes()); + resourceNameToRevIncludes.putAll(resourceName, nextMethod.getRevIncludes()); + } + } + + } + + for (Entry>> nextEntry : resourceToMethods.entrySet()) { + + String resourceName = nextEntry.getKey(); + if (nextEntry.getKey().isEmpty() == false) { + Set resourceOps = new HashSet<>(); + IBase resource = terser.addElement(rest, "resource"); + + postProcessRestResource(terser, resource, resourceName); + + RuntimeResourceDefinition def; + FhirContext context = configuration.getFhirContext(); + if (resourceNameToSharedSupertype.containsKey(resourceName)) { + def = context.getResourceDefinition(resourceNameToSharedSupertype.get(resourceName)); + } else { + def = context.getResourceDefinition(resourceName); + } + terser.addElement(resource, "type", def.getName()); + terser.addElement(resource, "profile", def.getResourceProfile(serverBase)); + + for (BaseMethodBinding nextMethodBinding : nextEntry.getValue()) { + RestOperationTypeEnum resOpCode = nextMethodBinding.getRestOperationType(); + if (resOpCode.isTypeLevel() || resOpCode.isInstanceLevel()) { + String resOp; + resOp = resOpCode.getCode(); + if (resourceOps.contains(resOp) == false) { + resourceOps.add(resOp); + IBase interaction = terser.addElement(resource, "interaction"); + terser.addElement(interaction, "code", resOp); + } + if (RestOperationTypeEnum.VREAD.equals(resOpCode)) { + // vread implies read + resOp = "read"; + if (resourceOps.contains(resOp) == false) { + resourceOps.add(resOp); + IBase interaction = terser.addElement(resource, "interaction"); + terser.addElement(interaction, "code", resOp); + } + } + } + + if (nextMethodBinding.isSupportsConditional()) { + switch (resOpCode) { + case CREATE: + terser.setElement(resource, "conditionalCreate", "true"); + break; + case DELETE: + if (nextMethodBinding.isSupportsConditionalMultiple()) { + terser.setElement(resource, "conditionalDelete", "multiple"); + } else { + terser.setElement(resource, "conditionalDelete", "single"); + } + break; + case UPDATE: + terser.setElement(resource, "conditionalUpdate", "true"); + break; + case HISTORY_INSTANCE: + case HISTORY_SYSTEM: + case HISTORY_TYPE: + case READ: + case SEARCH_SYSTEM: + case SEARCH_TYPE: + case TRANSACTION: + case VALIDATE: + case VREAD: + case METADATA: + case META_ADD: + case META: + case META_DELETE: + case PATCH: + case BATCH: + case ADD_TAGS: + case DELETE_TAGS: + case GET_TAGS: + case GET_PAGE: + case GRAPHQL_REQUEST: + case EXTENDED_OPERATION_SERVER: + case EXTENDED_OPERATION_TYPE: + case EXTENDED_OPERATION_INSTANCE: + default: + break; + } + } + + checkBindingForSystemOps(terser, rest, systemOps, nextMethodBinding); + + if (nextMethodBinding instanceof SearchMethodBinding) { + SearchMethodBinding methodBinding = (SearchMethodBinding) nextMethodBinding; + if (methodBinding.getQueryName() != null) { + String queryName = bindings.getNamedSearchMethodBindingToName().get(methodBinding); + if (operationNames.add(queryName)) { + IBase operation = terser.addElement(rest, "operation"); + terser.addElement(operation, "name", methodBinding.getQueryName()); + terser.addElement(operation, "definition", (getOperationDefinitionPrefix(theRequestDetails) + "OperationDefinition/" + queryName)); + } + } + } else if (nextMethodBinding instanceof OperationMethodBinding) { + OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding; + String opName = bindings.getOperationBindingToName().get(methodBinding); + // Only add each operation (by name) once + if (operationNames.add(opName)) { + IBase operation = terser.addElement(rest, "operation"); + terser.addElement(operation, "name", methodBinding.getName().substring(1)); + terser.addElement(operation, "definition", getOperationDefinitionPrefix(theRequestDetails) + "OperationDefinition/" + opName); + } + } + + } + + + ISearchParamRegistry serverConfiguration; + if (myServerConfiguration != null) { + serverConfiguration = myServerConfiguration; + } else { + serverConfiguration = myServer.createConfiguration(); + } + + /* + * If we have an explicit registry (which will be the case in the JPA server) we use it as priority, + * but also fill in any gaps using params from the server itself. This makes sure we include + * global params like _lastUpdated + */ + Map searchParams; + ISearchParamRegistry searchParamRegistry; + if (mySearchParamRegistry != null) { + searchParamRegistry = mySearchParamRegistry; + searchParams = new HashMap<>(mySearchParamRegistry.getActiveSearchParams(resourceName)); + for (Entry nextBuiltInSp : serverConfiguration.getActiveSearchParams(resourceName).entrySet()) { + if (nextBuiltInSp.getKey().startsWith("_") && !searchParams.containsKey(nextBuiltInSp.getKey())) { + searchParams.put(nextBuiltInSp.getKey(), nextBuiltInSp.getValue()); + } + } + } else { + searchParamRegistry = serverConfiguration; + searchParams = serverConfiguration.getActiveSearchParams(resourceName); + } + + + for (RuntimeSearchParam next : searchParams.values()) { + IBase searchParam = terser.addElement(resource, "searchParam"); + terser.addElement(searchParam, "name", next.getName()); + terser.addElement(searchParam, "type", next.getParamType().getCode()); + if (isNotBlank(next.getDescription())) { + terser.addElement(searchParam, "documentation", next.getDescription()); + } + + String spUri = next.getUri(); + if (isBlank(spUri) && servletRequest != null) { + String id; + if (next.getId() != null) { + id = next.getId().toUnqualifiedVersionless().getValue(); + } else { + id = resourceName + "-" + next.getName(); + } + spUri = configuration.getServerAddressStrategy().determineServerBase(servletRequest.getServletContext(), servletRequest) + "/" + id; + } + if (isNotBlank(spUri)) { + terser.addElement(searchParam, "definition", spUri); + } + } + + // Add Include to CapabilityStatement.rest.resource + NavigableSet resourceIncludes = resourceNameToIncludes.get(resourceName); + if (resourceIncludes.isEmpty()) { + List includes = searchParams + .values() + .stream() + .filter(t -> t.getParamType() == RestSearchParameterTypeEnum.REFERENCE) + .map(t -> resourceName + ":" + t.getName()) + .sorted() + .collect(Collectors.toList()); + terser.addElement(resource, "searchInclude", "*"); + for (String nextInclude : includes) { + terser.addElement(resource, "searchInclude", nextInclude); + } + } else { + for (String resourceInclude : resourceIncludes) { + terser.addElement(resource, "searchInclude", resourceInclude); + } + } + + // Add RevInclude to CapabilityStatement.rest.resource + if (myRestResourceRevIncludesEnabled) { + NavigableSet resourceRevIncludes = resourceNameToRevIncludes.get(resourceName); + if (resourceRevIncludes.isEmpty()) { + TreeSet revIncludes = new TreeSet<>(); + for (String nextResourceName : resourceToMethods.keySet()) { + if (isBlank(nextResourceName)) { + continue; + } + + for (RuntimeSearchParam t : searchParamRegistry.getActiveSearchParams(nextResourceName).values()) { + if (t.getParamType() == RestSearchParameterTypeEnum.REFERENCE) { + if (isNotBlank(t.getName())) { + boolean appropriateTarget = false; + if (t.getTargets().contains(resourceName) || t.getTargets().isEmpty()) { + appropriateTarget = true; + } + + if (appropriateTarget) { + revIncludes.add(nextResourceName + ":" + t.getName()); + } + } + } + } + } + for (String nextInclude : revIncludes) { + terser.addElement(resource, "searchRevInclude", nextInclude); + } + } else { + for (String resourceInclude : resourceRevIncludes) { + terser.addElement(resource, "searchRevInclude", resourceInclude); + } + } + } + + // Add SupportedProfile to CapabilityStatement.rest.resource + for (String supportedProfile : resourceTypeToSupportedProfiles.get(resourceName)) { + terser.addElement(resource, "supportedProfile", supportedProfile); + } + + } else { + for (BaseMethodBinding nextMethodBinding : nextEntry.getValue()) { + checkBindingForSystemOps(terser, rest, systemOps, nextMethodBinding); + if (nextMethodBinding instanceof OperationMethodBinding) { + OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding; + String opName = bindings.getOperationBindingToName().get(methodBinding); + if (operationNames.add(opName)) { + ourLog.debug("Found bound operation: {}", opName); + IBase operation = terser.addElement(rest, "operation"); + terser.addElement(operation, "name", methodBinding.getName().substring(1)); + terser.addElement(operation, "definition", getOperationDefinitionPrefix(theRequestDetails) + "OperationDefinition/" + opName); + } + } + } + } + + postProcessRest(terser, rest); + + } + + postProcess(terser, retVal); + + return retVal; + } + + private TreeMultimap getSupportedProfileMultimap(FhirTerser terser) { + TreeMultimap resourceTypeToSupportedProfiles = TreeMultimap.create(); + if (myValidationSupport != null) { + List allStructureDefinitions = myValidationSupport.fetchAllNonBaseStructureDefinitions(); + if (allStructureDefinitions != null) { + for (IBaseResource next : allStructureDefinitions) { + String kind = terser.getSinglePrimitiveValueOrNull(next, "kind"); + String url = terser.getSinglePrimitiveValueOrNull(next, "url"); + String baseDefinition = defaultString(terser.getSinglePrimitiveValueOrNull(next, "baseDefinition")); + if ("resource".equals(kind) && isNotBlank(url)) { + + // Don't include the base resource definitions in the supported profile list - This isn't helpful + if (baseDefinition.equals("http://hl7.org/fhir/StructureDefinition/DomainResource") || baseDefinition.equals("http://hl7.org/fhir/StructureDefinition/Resource")) { + continue; + } + + String resourceType = terser.getSinglePrimitiveValueOrNull(next, "snapshot.element.path"); + if (isBlank(resourceType)) { + resourceType = terser.getSinglePrimitiveValueOrNull(next, "differential.element.path"); + } + + if (isNotBlank(resourceType)) { + resourceTypeToSupportedProfiles.put(resourceType, url); + } + } + } + } + } + return resourceTypeToSupportedProfiles; + } + + /** + * Subclasses may override + */ + protected void postProcess(FhirTerser theTerser, IBaseConformance theCapabilityStatement) { + // nothing + } + + /** + * Subclasses may override + */ + protected void postProcessRest(FhirTerser theTerser, IBase theRest) { + // nothing + } + + /** + * Subclasses may override + */ + protected void postProcessRestResource(FhirTerser theTerser, IBase theResource, String theResourceName) { + // nothing + } + + protected String getOperationDefinitionPrefix(RequestDetails theRequestDetails) { + if (theRequestDetails == null) { + return ""; + } + return theRequestDetails.getServerBaseForRequest() + "/"; + } + + + @Read(typeName = "OperationDefinition") + public IBaseResource readOperationDefinition(@IdParam IIdType theId, RequestDetails theRequestDetails) { + if (theId == null || theId.hasIdPart() == false) { + throw new ResourceNotFoundException(theId); + } + RestfulServerConfiguration configuration = getServerConfiguration(); + Bindings bindings = configuration.provideBindings(); + + List operationBindings = bindings.getOperationNameToBindings().get(theId.getIdPart()); + if (operationBindings != null && !operationBindings.isEmpty()) { + return readOperationDefinitionForOperation(operationBindings); + } + List searchBindings = bindings.getSearchNameToBindings().get(theId.getIdPart()); + if (searchBindings != null && !searchBindings.isEmpty()) { + return readOperationDefinitionForNamedSearch(searchBindings); + } + throw new ResourceNotFoundException(theId); + } + + private IBaseResource readOperationDefinitionForNamedSearch(List bindings) { + IBaseResource op = myContext.getResourceDefinition("OperationDefinition").newInstance(); + FhirTerser terser = myContext.newTerser(); + + terser.addElement(op, "status", "active"); + terser.addElement(op, "kind", "query"); + terser.addElement(op, "affectsState", "false"); + + terser.addElement(op, "instance", "false"); + + Set inParams = new HashSet<>(); + + String operationCode = null; + for (SearchMethodBinding binding : bindings) { + if (isNotBlank(binding.getDescription())) { + terser.addElement(op, "description", binding.getDescription()); + } + if (isBlank(binding.getResourceProviderResourceName())) { + terser.addElement(op, "system", "true"); + terser.addElement(op, "type", "false"); + } else { + terser.addElement(op, "system", "false"); + terser.addElement(op, "type", "true"); + terser.addElement(op, "resource", binding.getResourceProviderResourceName()); + } + + if (operationCode == null) { + operationCode = binding.getQueryName(); + } + + for (IParameter nextParamUntyped : binding.getParameters()) { + if (nextParamUntyped instanceof SearchParameter) { + SearchParameter nextParam = (SearchParameter) nextParamUntyped; + if (!inParams.add(nextParam.getName())) { + continue; + } + + IBase param = terser.addElement(op, "parameter"); + terser.addElement(param, "use", "in"); + terser.addElement(param, "type", "string"); + terser.addElement(param, "searchType", nextParam.getParamType().getCode()); + terser.addElement(param, "min", nextParam.isRequired() ? "1" : "0"); + terser.addElement(param, "max", "1"); + terser.addElement(param, "name", nextParam.getName()); + } + } + + } + + terser.addElement(op, "code", operationCode); + terser.addElement(op, "name", "Search_" + operationCode); + + return op; + } + + private IBaseResource readOperationDefinitionForOperation(List bindings) { + IBaseResource op = myContext.getResourceDefinition("OperationDefinition").newInstance(); + FhirTerser terser = myContext.newTerser(); + + terser.addElement(op, "status", "active"); + terser.addElement(op, "kind", "operation"); + + boolean systemLevel = false; + boolean typeLevel = false; + boolean instanceLevel = false; + boolean affectsState = false; + String description = null; + String code = null; + String name; + + Set resourceNames = new TreeSet<>(); + Set inParams = new HashSet<>(); + Set outParams = new HashSet<>(); + + for (OperationMethodBinding sharedDescription : bindings) { + if (isNotBlank(sharedDescription.getDescription()) && isBlank(description)) { + description = sharedDescription.getDescription(); + } + if (sharedDescription.isCanOperateAtInstanceLevel()) { + instanceLevel = true; + } + if (sharedDescription.isCanOperateAtServerLevel()) { + systemLevel = true; + } + if (sharedDescription.isCanOperateAtTypeLevel()) { + typeLevel = true; + } + if (!sharedDescription.isIdempotent()) { + affectsState |= true; + } + + code = sharedDescription.getName().substring(1); + + if (isNotBlank(sharedDescription.getResourceName())) { + resourceNames.add(sharedDescription.getResourceName()); + } + + for (IParameter nextParamUntyped : sharedDescription.getParameters()) { + if (nextParamUntyped instanceof OperationParameter) { + OperationParameter nextParam = (OperationParameter) nextParamUntyped; + if (!inParams.add(nextParam.getName())) { + continue; + } + IBase param = terser.addElement(op, "parameter"); + terser.addElement(param, "use", "in"); + if (nextParam.getParamType() != null) { + terser.addElement(param, "type", nextParam.getParamType()); + } + if (nextParam.getSearchParamType() != null) { + terser.addElement(param, "searchType", nextParam.getSearchParamType()); + } + terser.addElement(param, "min", Integer.toString(nextParam.getMin())); + terser.addElement(param, "max", (nextParam.getMax() == -1 ? "*" : Integer.toString(nextParam.getMax()))); + terser.addElement(param, "name", nextParam.getName()); + } + } + + for (ReturnType nextParam : sharedDescription.getReturnParams()) { + if (!outParams.add(nextParam.getName())) { + continue; + } + IBase param = terser.addElement(op, "parameter"); + terser.addElement(param, "use", "out"); + if (nextParam.getType() != null) { + terser.addElement(param, "type", nextParam.getType()); + } + terser.addElement(param, "min", Integer.toString(nextParam.getMin())); + terser.addElement(param, "max", (nextParam.getMax() == -1 ? "*" : Integer.toString(nextParam.getMax()))); + terser.addElement(param, "name", nextParam.getName()); + } + } + + name = "Operation_" + code; + + terser.addElements(op, "resource", resourceNames); + terser.addElement(op, "name", name); + terser.addElement(op, "code", code); + terser.addElement(op, "description", description); + terser.addElement(op, "affectsState", Boolean.toString(affectsState)); + terser.addElement(op, "system", Boolean.toString(systemLevel)); + terser.addElement(op, "type", Boolean.toString(typeLevel)); + terser.addElement(op, "instance", Boolean.toString(instanceLevel)); + + return op; + } + + @Override + public void setRestfulServer(RestfulServer theRestfulServer) { + // ignore + } + + public void setRestResourceRevIncludesEnabled(boolean theRestResourceRevIncludesEnabled) { + myRestResourceRevIncludesEnabled = theRestResourceRevIncludesEnabled; + } } diff --git a/hapi-fhir-validation/src/test/java/ca/uhn/fhir/rest/server/ServerCapabilityStatementProviderR4Test.java b/hapi-fhir-validation/src/test/java/ca/uhn/fhir/rest/server/ServerCapabilityStatementProviderR4Test.java index 1cbef706f14..f151466e3aa 100644 --- a/hapi-fhir-validation/src/test/java/ca/uhn/fhir/rest/server/ServerCapabilityStatementProviderR4Test.java +++ b/hapi-fhir-validation/src/test/java/ca/uhn/fhir/rest/server/ServerCapabilityStatementProviderR4Test.java @@ -100,1303 +100,1345 @@ import static org.mockito.Mockito.when; public class ServerCapabilityStatementProviderR4Test { - public static final String PATIENT_SUB = "PatientSub"; - public static final String PATIENT_SUB_SUB = "PatientSubSub"; - public static final String PATIENT_SUB_SUB_2 = "PatientSubSub2"; - public static final String PATIENT_TRIPLE_SUB = "PatientTripleSub"; - private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ServerCapabilityStatementProviderR4Test.class); - private final FhirContext myCtx = FhirContext.forCached(FhirVersionEnum.R4); - private FhirValidator myValidator; - - @BeforeEach - public void before() { - myValidator = myCtx.newValidator(); - myValidator.registerValidatorModule(new FhirInstanceValidator(myCtx)); - } - - 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; - } - - private CapabilityStatementRestResourceComponent findRestResource(CapabilityStatement conformance, String wantResource) throws Exception { - CapabilityStatementRestResourceComponent resource = null; - for (CapabilityStatementRestResourceComponent next : conformance.getRest().get(0).getResource()) { - if (next.getType().equals(wantResource)) { - resource = next; - } - } - if (resource == null) { - throw new Exception("Could not find resource: " + wantResource); - } - return resource; - } - - @Test - public void testFormats() throws ServletException { - RestfulServer rs = new RestfulServer(myCtx); - rs.setProviders(new ConditionalProvider()); - - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); - rs.setServerConformanceProvider(sc); - - rs.init(createServletConfig()); - - CapabilityStatement cs = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - List formats = cs - .getFormat() - .stream() - .map(t -> t.getCode()) - .collect(Collectors.toList()); - assertThat(formats.toString(), formats, containsInAnyOrder( - "application/fhir+xml", - "xml", - "application/fhir+json", - "json", - "application/x-turtle", - "ttl" - )); - } - - - @Test - public void testConditionalOperations() throws Exception { - - RestfulServer rs = new RestfulServer(myCtx); - rs.setProviders(new ConditionalProvider()); - - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); - rs.setServerConformanceProvider(sc); - - rs.init(createServletConfig()); - - CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - String conf = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); - ourLog.info(conf); - - assertEquals(2, conformance.getRest().get(0).getResource().size()); - CapabilityStatementRestResourceComponent res = conformance.getRest().get(0).getResource().get(1); - assertEquals("Patient", res.getType()); - - assertTrue(res.getConditionalCreate()); - assertEquals(ConditionalDeleteStatus.MULTIPLE, res.getConditionalDelete()); - assertTrue(res.getConditionalUpdate()); - } - - private RequestDetails createRequestDetails(RestfulServer theServer) { - ServletRequestDetails retVal = new ServletRequestDetails(null); - retVal.setServer(theServer); - retVal.setFhirServerBase("http://localhost/baseR4"); - return retVal; - } - - @Test - public void testExtendedOperationReturningBundle() throws Exception { - - RestfulServer rs = new RestfulServer(myCtx); - rs.setProviders(new ProviderWithExtendedOperationReturningBundle()); - rs.setServerAddressStrategy(new HardcodedServerAddressStrategy("http://localhost/baseR4")); - - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); - rs.setServerConformanceProvider(sc); - - rs.init(createServletConfig()); - - CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - validate(conformance); - - assertEquals(1, conformance.getRest().get(0).getOperation().size()); - assertEquals("everything", conformance.getRest().get(0).getOperation().get(0).getName()); - - OperationDefinition opDef = (OperationDefinition) sc.readOperationDefinition(new IdType("OperationDefinition/Patient-i-everything"), createRequestDetails(rs)); - validate(opDef); - assertEquals("everything", opDef.getCode()); - assertThat(opDef.getSystem(), is(false)); - assertThat(opDef.getType(), is(false)); - assertThat(opDef.getInstance(), is(true)); - } - - @Test - public void testExtendedOperationReturningBundleOperation() throws Exception { + public static final String PATIENT_SUB = "PatientSub"; + public static final String PATIENT_SUB_SUB = "PatientSubSub"; + public static final String PATIENT_SUB_SUB_2 = "PatientSubSub2"; + public static final String PATIENT_TRIPLE_SUB = "PatientTripleSub"; + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ServerCapabilityStatementProviderR4Test.class); + private final FhirContext myCtx = FhirContext.forCached(FhirVersionEnum.R4); + private FhirValidator myValidator; + + @BeforeEach + public void before() { + myValidator = myCtx.newValidator(); + myValidator.registerValidatorModule(new FhirInstanceValidator(myCtx)); + } + + 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; + } + + private CapabilityStatementRestResourceComponent findRestResource(CapabilityStatement conformance, String wantResource) throws Exception { + CapabilityStatementRestResourceComponent resource = null; + for (CapabilityStatementRestResourceComponent next : conformance.getRest().get(0).getResource()) { + if (next.getType().equals(wantResource)) { + resource = next; + } + } + if (resource == null) { + throw new Exception("Could not find resource: " + wantResource); + } + return resource; + } + + @Test + public void testFormats() throws ServletException { + RestfulServer rs = new RestfulServer(myCtx); + rs.setProviders(new ConditionalProvider()); + + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + rs.setServerConformanceProvider(sc); + + rs.init(createServletConfig()); + + CapabilityStatement cs = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + List formats = cs + .getFormat() + .stream() + .map(t -> t.getCode()) + .collect(Collectors.toList()); + assertThat(formats.toString(), formats, containsInAnyOrder( + "application/fhir+xml", + "xml", + "application/fhir+json", + "json", + "application/x-turtle", + "ttl" + )); + } + + + @Test + public void testConditionalOperations() throws Exception { + + RestfulServer rs = new RestfulServer(myCtx); + rs.setProviders(new ConditionalProvider()); + + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + rs.setServerConformanceProvider(sc); + + rs.init(createServletConfig()); + + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + String conf = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); + ourLog.info(conf); + + assertEquals(2, conformance.getRest().get(0).getResource().size()); + CapabilityStatementRestResourceComponent res = conformance.getRest().get(0).getResource().get(1); + assertEquals("Patient", res.getType()); + + assertTrue(res.getConditionalCreate()); + assertEquals(ConditionalDeleteStatus.MULTIPLE, res.getConditionalDelete()); + assertTrue(res.getConditionalUpdate()); + } + + private RequestDetails createRequestDetails(RestfulServer theServer) { + ServletRequestDetails retVal = new ServletRequestDetails(null); + retVal.setServer(theServer); + retVal.setFhirServerBase("http://localhost/baseR4"); + return retVal; + } + + @Test + public void testExtendedOperationReturningBundle() throws Exception { + + RestfulServer rs = new RestfulServer(myCtx); + rs.setProviders(new ProviderWithExtendedOperationReturningBundle()); + rs.setServerAddressStrategy(new HardcodedServerAddressStrategy("http://localhost/baseR4")); + + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + rs.setServerConformanceProvider(sc); + + rs.init(createServletConfig()); + + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + validate(conformance); + + assertEquals(1, conformance.getRest().get(0).getOperation().size()); + assertEquals("everything", conformance.getRest().get(0).getOperation().get(0).getName()); + + OperationDefinition opDef = (OperationDefinition) sc.readOperationDefinition(new IdType("OperationDefinition/Patient-i-everything"), createRequestDetails(rs)); + validate(opDef); + assertEquals("everything", opDef.getCode()); + assertThat(opDef.getSystem(), is(false)); + assertThat(opDef.getType(), is(false)); + assertThat(opDef.getInstance(), is(true)); + } + + @Test + public void testExtendedOperationReturningBundleOperation() throws Exception { - RestfulServer rs = new RestfulServer(myCtx); - rs.setProviders(new ProviderWithExtendedOperationReturningBundle()); + RestfulServer rs = new RestfulServer(myCtx); + rs.setProviders(new ProviderWithExtendedOperationReturningBundle()); - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs) { - }; - rs.setServerConformanceProvider(sc); + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs) { + }; + rs.setServerConformanceProvider(sc); - rs.init(createServletConfig()); + rs.init(createServletConfig()); - OperationDefinition opDef = (OperationDefinition) sc.readOperationDefinition(new IdType("OperationDefinition/Patient-i-everything"), createRequestDetails(rs)); - validate(opDef); + OperationDefinition opDef = (OperationDefinition) sc.readOperationDefinition(new IdType("OperationDefinition/Patient-i-everything"), createRequestDetails(rs)); + validate(opDef); - String conf = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(opDef); - ourLog.info(conf); + String conf = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(opDef); + ourLog.info(conf); - assertEquals("everything", opDef.getCode()); - assertEquals(false, opDef.getAffectsState()); - } + assertEquals("everything", opDef.getCode()); + assertEquals(false, opDef.getAffectsState()); + } - @Test - public void testInstanceHistorySupported() throws Exception { + @Test + public void testInstanceHistorySupported() throws Exception { - RestfulServer rs = new RestfulServer(myCtx); - rs.setProviders(new InstanceHistoryProvider()); + RestfulServer rs = new RestfulServer(myCtx); + rs.setProviders(new InstanceHistoryProvider()); - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); - rs.setServerConformanceProvider(sc); + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + rs.setServerConformanceProvider(sc); - rs.init(createServletConfig()); + rs.init(createServletConfig()); - CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - String conf = validate(conformance); + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + String conf = validate(conformance); - conf = myCtx.newXmlParser().setPrettyPrint(false).encodeResourceToString(conformance); - assertThat(conf, containsString("")); - } + conf = myCtx.newXmlParser().setPrettyPrint(false).encodeResourceToString(conformance); + assertThat(conf, containsString("")); + } - @Test - public void testMultiOptionalDocumentation() throws Exception { + @Test + public void testMultiOptionalDocumentation() throws Exception { - RestfulServer rs = new RestfulServer(myCtx); - rs.setProviders(new MultiOptionalProvider()); - - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); - rs.setServerConformanceProvider(sc); - - rs.init(createServletConfig()); - - boolean found = false; - Collection resourceBindings = rs.getResourceBindings(); - for (ResourceBinding resourceBinding : resourceBindings) { - if (resourceBinding.getResourceName().equals("Patient")) { - List> methodBindings = resourceBinding.getMethodBindings(); - SearchMethodBinding binding = (SearchMethodBinding) methodBindings.get(0); - SearchParameter param = (SearchParameter) binding.getParameters().iterator().next(); - assertEquals("The patient's identifier", param.getDescription()); - found = true; - } - } - - assertTrue(found); - CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - String conf = validate(conformance); - - assertThat(conf, containsString("")); - assertThat(conf, containsString("")); - assertThat(conf, containsString("")); - } - - @Test - public void testNonConditionalOperations() throws Exception { - - RestfulServer rs = new RestfulServer(myCtx); - rs.setProviders(new NonConditionalProvider()); - - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); - rs.setServerConformanceProvider(sc); - - rs.init(createServletConfig()); - - CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - validate(conformance); - - CapabilityStatementRestResourceComponent res = conformance.getRest().get(0).getResource().get(1); - assertEquals("Patient", res.getType()); - - assertNull(res.getConditionalCreateElement().getValue()); - assertNull(res.getConditionalDeleteElement().getValue()); - assertNull(res.getConditionalUpdateElement().getValue()); - } - - /** - * See #379 - */ - @Test - public void testOperationAcrossMultipleTypes() throws Exception { - RestfulServer rs = new RestfulServer(myCtx); - rs.setProviders(new MultiTypePatientProvider(), new MultiTypeEncounterProvider()); - rs.setServerAddressStrategy(new HardcodedServerAddressStrategy("http://localhost/baseR4")); - - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); - rs.setServerConformanceProvider(sc); - - rs.init(createServletConfig()); - - CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - - validate(conformance); - - assertEquals(4, conformance.getRest().get(0).getOperation().size()); - List operationNames = toOperationNames(conformance.getRest().get(0).getOperation()); - assertThat(operationNames, containsInAnyOrder("someOp", "validate", "someOp", "validate")); - - List operationIdParts = toOperationIdParts(conformance.getRest().get(0).getOperation()); - assertThat(operationIdParts, containsInAnyOrder("Patient-i-someOp", "Encounter-i-someOp", "Patient-i-validate", "Encounter-i-validate")); - - { - OperationDefinition opDef = (OperationDefinition) sc.readOperationDefinition(new IdType("OperationDefinition/Patient-i-someOp"), createRequestDetails(rs)); - validate(opDef); - ourLog.info(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(opDef)); - Set types = toStrings(opDef.getResource()); - assertEquals("someOp", opDef.getCode()); - assertEquals(true, opDef.getInstance()); - assertEquals(false, opDef.getSystem()); - assertThat(types, containsInAnyOrder("Patient")); - assertEquals(2, opDef.getParameter().size()); - assertEquals("someOpParam1", opDef.getParameter().get(0).getName()); - assertEquals("date", opDef.getParameter().get(0).getType()); - assertEquals("someOpParam2", opDef.getParameter().get(1).getName()); - assertEquals("Patient", opDef.getParameter().get(1).getType()); - } - { - OperationDefinition opDef = (OperationDefinition) sc.readOperationDefinition(new IdType("OperationDefinition/Encounter-i-someOp"), createRequestDetails(rs)); - validate(opDef); - ourLog.info(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(opDef)); - Set types = toStrings(opDef.getResource()); - assertEquals("someOp", opDef.getCode()); - assertEquals(true, opDef.getInstance()); - assertEquals(false, opDef.getSystem()); - assertThat(types, containsInAnyOrder("Encounter")); - assertEquals(2, opDef.getParameter().size()); - assertEquals("someOpParam1", opDef.getParameter().get(0).getName()); - assertEquals("date", opDef.getParameter().get(0).getType()); - assertEquals("someOpParam2", opDef.getParameter().get(1).getName()); - assertEquals("Encounter", opDef.getParameter().get(1).getType()); - } - { - OperationDefinition opDef = (OperationDefinition) sc.readOperationDefinition(new IdType("OperationDefinition/Patient-i-validate"), createRequestDetails(rs)); - validate(opDef); - ourLog.info(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(opDef)); - Set types = toStrings(opDef.getResource()); - assertEquals("validate", opDef.getCode()); - assertEquals(true, opDef.getInstance()); - assertEquals(false, opDef.getSystem()); - assertThat(types, containsInAnyOrder("Patient")); - assertEquals(1, opDef.getParameter().size()); - assertEquals("resource", opDef.getParameter().get(0).getName()); - assertEquals("Patient", opDef.getParameter().get(0).getType()); - } - } - - @Test - public void testOperationDocumentation() throws Exception { - - RestfulServer rs = new RestfulServer(myCtx); - rs.setProviders(new SearchProvider()); - - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); - rs.setServerConformanceProvider(sc); - - rs.init(createServletConfig()); - - CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - - String conf = validate(conformance); - - assertThat(conf, containsString("")); - assertThat(conf, containsString("")); - - } - - @Test - public void testOperationOnNoTypes() throws Exception { - RestfulServer rs = new RestfulServer(myCtx); - rs.setProviders(new PlainProviderWithExtendedOperationOnNoType()); - - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs) { - @Override - public CapabilityStatement getServerConformance(HttpServletRequest theRequest, RequestDetails theRequestDetails) { - return (CapabilityStatement) super.getServerConformance(theRequest, createRequestDetails(rs)); - } - }; - rs.setServerConformanceProvider(sc); - - rs.init(createServletConfig()); - - OperationDefinition opDef = (OperationDefinition) sc.readOperationDefinition(new IdType("OperationDefinition/-is-plain"), createRequestDetails(rs)); - validate(opDef); - - assertEquals("plain", opDef.getCode()); - assertEquals(false, opDef.getAffectsState()); - assertEquals(3, opDef.getParameter().size()); - - assertTrue(opDef.getParameter().get(0).hasName()); - assertEquals("start", opDef.getParameter().get(0).getName()); - assertEquals("in", opDef.getParameter().get(0).getUse().toCode()); - assertEquals("0", opDef.getParameter().get(0).getMinElement().getValueAsString()); - assertEquals("date", opDef.getParameter().get(0).getTypeElement().getValueAsString()); - - assertEquals("out1", opDef.getParameter().get(2).getName()); - assertEquals("out", opDef.getParameter().get(2).getUse().toCode()); - 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 - public void testProviderWithRequiredAndOptional() throws Exception { - - RestfulServer rs = new RestfulServer(myCtx); - rs.setProviders(new ProviderWithRequiredAndOptional()); - - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); - rs.setServerConformanceProvider(sc); - - rs.init(createServletConfig()); - - CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - validate(conformance); - - CapabilityStatementRestComponent rest = conformance.getRest().get(0); - CapabilityStatementRestResourceComponent res = rest.getResource().get(0); - assertEquals("DiagnosticReport", res.getType()); - - assertEquals("subject.identifier", res.getSearchParam().get(0).getName()); + RestfulServer rs = new RestfulServer(myCtx); + rs.setProviders(new MultiOptionalProvider()); + + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + rs.setServerConformanceProvider(sc); + + rs.init(createServletConfig()); + + boolean found = false; + Collection resourceBindings = rs.getResourceBindings(); + for (ResourceBinding resourceBinding : resourceBindings) { + if (resourceBinding.getResourceName().equals("Patient")) { + List> methodBindings = resourceBinding.getMethodBindings(); + SearchMethodBinding binding = (SearchMethodBinding) methodBindings.get(0); + SearchParameter param = (SearchParameter) binding.getParameters().iterator().next(); + assertEquals("The patient's identifier", param.getDescription()); + found = true; + } + } + + assertTrue(found); + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + String conf = validate(conformance); + + assertThat(conf, containsString("")); + assertThat(conf, containsString("")); + assertThat(conf, containsString("")); + } + + @Test + public void testNonConditionalOperations() throws Exception { + + RestfulServer rs = new RestfulServer(myCtx); + rs.setProviders(new NonConditionalProvider()); + + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + rs.setServerConformanceProvider(sc); + + rs.init(createServletConfig()); + + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + validate(conformance); + + CapabilityStatementRestResourceComponent res = conformance.getRest().get(0).getResource().get(1); + assertEquals("Patient", res.getType()); + + assertNull(res.getConditionalCreateElement().getValue()); + assertNull(res.getConditionalDeleteElement().getValue()); + assertNull(res.getConditionalUpdateElement().getValue()); + } + + /** + * See #379 + */ + @Test + public void testOperationAcrossMultipleTypes() throws Exception { + RestfulServer rs = new RestfulServer(myCtx); + rs.setProviders(new MultiTypePatientProvider(), new MultiTypeEncounterProvider()); + rs.setServerAddressStrategy(new HardcodedServerAddressStrategy("http://localhost/baseR4")); + + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + rs.setServerConformanceProvider(sc); + + rs.init(createServletConfig()); + + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + + validate(conformance); + + assertEquals(4, conformance.getRest().get(0).getOperation().size()); + List operationNames = toOperationNames(conformance.getRest().get(0).getOperation()); + assertThat(operationNames, containsInAnyOrder("someOp", "validate", "someOp", "validate")); + + List operationIdParts = toOperationIdParts(conformance.getRest().get(0).getOperation()); + assertThat(operationIdParts, containsInAnyOrder("Patient-i-someOp", "Encounter-i-someOp", "Patient-i-validate", "Encounter-i-validate")); + + { + OperationDefinition opDef = (OperationDefinition) sc.readOperationDefinition(new IdType("OperationDefinition/Patient-i-someOp"), createRequestDetails(rs)); + validate(opDef); + ourLog.info(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(opDef)); + Set types = toStrings(opDef.getResource()); + assertEquals("someOp", opDef.getCode()); + assertEquals(true, opDef.getInstance()); + assertEquals(false, opDef.getSystem()); + assertThat(types, containsInAnyOrder("Patient")); + assertEquals(2, opDef.getParameter().size()); + assertEquals("someOpParam1", opDef.getParameter().get(0).getName()); + assertEquals("date", opDef.getParameter().get(0).getType()); + assertEquals("someOpParam2", opDef.getParameter().get(1).getName()); + assertEquals("Patient", opDef.getParameter().get(1).getType()); + } + { + OperationDefinition opDef = (OperationDefinition) sc.readOperationDefinition(new IdType("OperationDefinition/Encounter-i-someOp"), createRequestDetails(rs)); + validate(opDef); + ourLog.info(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(opDef)); + Set types = toStrings(opDef.getResource()); + assertEquals("someOp", opDef.getCode()); + assertEquals(true, opDef.getInstance()); + assertEquals(false, opDef.getSystem()); + assertThat(types, containsInAnyOrder("Encounter")); + assertEquals(2, opDef.getParameter().size()); + assertEquals("someOpParam1", opDef.getParameter().get(0).getName()); + assertEquals("date", opDef.getParameter().get(0).getType()); + assertEquals("someOpParam2", opDef.getParameter().get(1).getName()); + assertEquals("Encounter", opDef.getParameter().get(1).getType()); + } + { + OperationDefinition opDef = (OperationDefinition) sc.readOperationDefinition(new IdType("OperationDefinition/Patient-i-validate"), createRequestDetails(rs)); + validate(opDef); + ourLog.info(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(opDef)); + Set types = toStrings(opDef.getResource()); + assertEquals("validate", opDef.getCode()); + assertEquals(true, opDef.getInstance()); + assertEquals(false, opDef.getSystem()); + assertThat(types, containsInAnyOrder("Patient")); + assertEquals(1, opDef.getParameter().size()); + assertEquals("resource", opDef.getParameter().get(0).getName()); + assertEquals("Patient", opDef.getParameter().get(0).getType()); + } + } + + @Test + public void testOperationDocumentation() throws Exception { + + RestfulServer rs = new RestfulServer(myCtx); + rs.setProviders(new SearchProvider()); + + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + rs.setServerConformanceProvider(sc); + + rs.init(createServletConfig()); + + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + + String conf = validate(conformance); + + assertThat(conf, containsString("")); + assertThat(conf, containsString("")); + + } + + @Test + public void testOperationOnNoTypes() throws Exception { + RestfulServer rs = new RestfulServer(myCtx); + rs.setProviders(new PlainProviderWithExtendedOperationOnNoType()); + + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs) { + @Override + public CapabilityStatement getServerConformance(HttpServletRequest theRequest, RequestDetails theRequestDetails) { + return (CapabilityStatement) super.getServerConformance(theRequest, createRequestDetails(rs)); + } + }; + rs.setServerConformanceProvider(sc); + + rs.init(createServletConfig()); + + OperationDefinition opDef = (OperationDefinition) sc.readOperationDefinition(new IdType("OperationDefinition/-is-plain"), createRequestDetails(rs)); + validate(opDef); + + assertEquals("plain", opDef.getCode()); + assertEquals(false, opDef.getAffectsState()); + assertEquals(3, opDef.getParameter().size()); + + assertTrue(opDef.getParameter().get(0).hasName()); + assertEquals("start", opDef.getParameter().get(0).getName()); + assertEquals("in", opDef.getParameter().get(0).getUse().toCode()); + assertEquals("0", opDef.getParameter().get(0).getMinElement().getValueAsString()); + assertEquals("date", opDef.getParameter().get(0).getTypeElement().getValueAsString()); + + assertEquals("out1", opDef.getParameter().get(2).getName()); + assertEquals("out", opDef.getParameter().get(2).getUse().toCode()); + 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 + public void testProviderWithRequiredAndOptional() throws Exception { + + RestfulServer rs = new RestfulServer(myCtx); + rs.setProviders(new ProviderWithRequiredAndOptional()); + + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + rs.setServerConformanceProvider(sc); + + rs.init(createServletConfig()); + + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + validate(conformance); + + CapabilityStatementRestComponent rest = conformance.getRest().get(0); + CapabilityStatementRestResourceComponent res = rest.getResource().get(0); + assertEquals("DiagnosticReport", res.getType()); + + assertEquals("subject.identifier", res.getSearchParam().get(0).getName()); // assertEquals("identifier", res.getSearchParam().get(0).getChain().get(0).getValue()); - assertEquals(DiagnosticReport.SP_CODE, res.getSearchParam().get(1).getName()); + assertEquals(DiagnosticReport.SP_CODE, res.getSearchParam().get(1).getName()); - assertEquals(DiagnosticReport.SP_DATE, res.getSearchParam().get(2).getName()); + assertEquals(DiagnosticReport.SP_DATE, res.getSearchParam().get(2).getName()); - assertEquals(1, res.getSearchInclude().size()); - assertEquals("DiagnosticReport.result", res.getSearchInclude().get(0).getValue()); - } + assertEquals(1, res.getSearchInclude().size()); + assertEquals("DiagnosticReport.result", res.getSearchInclude().get(0).getValue()); + } - @Test - public void testReadAndVReadSupported() throws Exception { + @Test + public void testReadAndVReadSupported() throws Exception { - RestfulServer rs = new RestfulServer(myCtx); - rs.setProviders(new VreadProvider()); + RestfulServer rs = new RestfulServer(myCtx); + rs.setProviders(new VreadProvider()); - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); - rs.setServerConformanceProvider(sc); + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + rs.setServerConformanceProvider(sc); - rs.init(createServletConfig()); + rs.init(createServletConfig()); - CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - String conf = validate(conformance); + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + String conf = validate(conformance); - assertThat(conf, containsString("")); - assertThat(conf, containsString("")); - } + assertThat(conf, containsString("")); + assertThat(conf, containsString("")); + } - @Test - public void testReadSupported() throws Exception { + @Test + public void testReadSupported() throws Exception { - RestfulServer rs = new RestfulServer(myCtx); - rs.setProviders(new ReadProvider()); + RestfulServer rs = new RestfulServer(myCtx); + rs.setProviders(new ReadProvider()); - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); - rs.setServerConformanceProvider(sc); + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + rs.setServerConformanceProvider(sc); - rs.init(createServletConfig()); + rs.init(createServletConfig()); - CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - String conf = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); - ourLog.info(conf); + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + String conf = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); + ourLog.info(conf); - conf = myCtx.newXmlParser().setPrettyPrint(false).encodeResourceToString(conformance); - assertThat(conf, not(containsString(""))); - assertThat(conf, containsString("")); - } + conf = myCtx.newXmlParser().setPrettyPrint(false).encodeResourceToString(conformance); + assertThat(conf, not(containsString(""))); + assertThat(conf, containsString("")); + } - @Test - public void testSearchParameterDocumentation() throws Exception { + @Test + public void testSearchParameterDocumentation() throws Exception { - RestfulServer rs = new RestfulServer(myCtx); - rs.setProviders(new SearchProvider()); + RestfulServer rs = new RestfulServer(myCtx); + rs.setProviders(new SearchProvider()); - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); - rs.setServerConformanceProvider(sc); + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + rs.setServerConformanceProvider(sc); - rs.init(createServletConfig()); + rs.init(createServletConfig()); - boolean found = false; - Collection resourceBindings = rs.getResourceBindings(); - for (ResourceBinding resourceBinding : resourceBindings) { - if (resourceBinding.getResourceName().equals("Patient")) { - List> methodBindings = resourceBinding.getMethodBindings(); - SearchMethodBinding binding = (SearchMethodBinding) methodBindings.get(0); - for (IParameter next : binding.getParameters()) { - SearchParameter param = (SearchParameter) next; - if (param.getDescription().contains("The patient's identifier (MRN or other card number")) { - found = true; - } - } - found = true; - } - } - assertTrue(found); - CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + boolean found = false; + Collection resourceBindings = rs.getResourceBindings(); + for (ResourceBinding resourceBinding : resourceBindings) { + if (resourceBinding.getResourceName().equals("Patient")) { + List> methodBindings = resourceBinding.getMethodBindings(); + SearchMethodBinding binding = (SearchMethodBinding) methodBindings.get(0); + for (IParameter next : binding.getParameters()) { + SearchParameter param = (SearchParameter) next; + if (param.getDescription().contains("The patient's identifier (MRN or other card number")) { + found = true; + } + } + found = true; + } + } + assertTrue(found); + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - String conf = validate(conformance); + String conf = validate(conformance); - assertThat(conf, containsString("")); - assertThat(conf, containsString("")); + assertThat(conf, containsString("")); + assertThat(conf, containsString("")); - } + } - @Test - public void testFormatIncludesSpecialNonMediaTypeFormats() throws ServletException { - RestfulServer rs = new RestfulServer(myCtx); - rs.setProviders(new SearchProvider()); + @Test + public void testFormatIncludesSpecialNonMediaTypeFormats() throws ServletException { + RestfulServer rs = new RestfulServer(myCtx); + rs.setProviders(new SearchProvider()); - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); - rs.setServerConformanceProvider(sc); + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + rs.setServerConformanceProvider(sc); - rs.init(createServletConfig()); - CapabilityStatement serverConformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + rs.init(createServletConfig()); + CapabilityStatement serverConformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - List formatCodes = serverConformance.getFormat().stream().map(c -> c.getCode()).collect(Collectors.toList()); + List formatCodes = serverConformance.getFormat().stream().map(c -> c.getCode()).collect(Collectors.toList()); - assertThat(formatCodes, hasItem(Constants.FORMAT_XML)); - assertThat(formatCodes, hasItem(Constants.FORMAT_JSON)); - assertThat(formatCodes, hasItem(Constants.CT_FHIR_JSON_NEW)); - assertThat(formatCodes, hasItem(Constants.CT_FHIR_XML_NEW)); - } + assertThat(formatCodes, hasItem(Constants.FORMAT_XML)); + assertThat(formatCodes, hasItem(Constants.FORMAT_JSON)); + assertThat(formatCodes, hasItem(Constants.CT_FHIR_JSON_NEW)); + assertThat(formatCodes, hasItem(Constants.CT_FHIR_XML_NEW)); + } - /** - * See #286 - */ - @Test - public void testSearchReferenceParameterDocumentation() throws Exception { + /** + * See #286 + */ + @Test + public void testSearchReferenceParameterDocumentation() throws Exception { - RestfulServer rs = new RestfulServer(myCtx); - rs.setProviders(new PatientResourceProvider()); + RestfulServer rs = new RestfulServer(myCtx); + rs.setProviders(new PatientResourceProvider()); - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); - rs.setServerConformanceProvider(sc); + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + rs.setServerConformanceProvider(sc); - rs.init(createServletConfig()); + rs.init(createServletConfig()); - boolean found = false; - Collection resourceBindings = rs.getResourceBindings(); - for (ResourceBinding resourceBinding : resourceBindings) { - if (resourceBinding.getResourceName().equals("Patient")) { - List> methodBindings = resourceBinding.getMethodBindings(); - SearchMethodBinding binding = (SearchMethodBinding) methodBindings.get(0); - SearchParameter param = (SearchParameter) binding.getParameters().get(25); - assertEquals("The organization at which this person is a patient", param.getDescription()); - found = true; - } - } - assertTrue(found); - CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + boolean found = false; + Collection resourceBindings = rs.getResourceBindings(); + for (ResourceBinding resourceBinding : resourceBindings) { + if (resourceBinding.getResourceName().equals("Patient")) { + List> methodBindings = resourceBinding.getMethodBindings(); + SearchMethodBinding binding = (SearchMethodBinding) methodBindings.get(0); + SearchParameter param = (SearchParameter) binding.getParameters().get(25); + assertEquals("The organization at which this person is a patient", param.getDescription()); + found = true; + } + } + assertTrue(found); + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - String conf = validate(conformance); + String conf = validate(conformance); - } + } - /** - * See #286 - */ - @Test - public void testSearchReferenceParameterWithWhitelistDocumentation() throws Exception { + /** + * See #286 + */ + @Test + public void testSearchReferenceParameterWithWhitelistDocumentation() throws Exception { - RestfulServer rs = new RestfulServer(myCtx); - rs.setProviders(new SearchProviderWithWhitelist()); + RestfulServer rs = new RestfulServer(myCtx); + rs.setProviders(new SearchProviderWithWhitelist()); - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); - rs.setServerConformanceProvider(sc); + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + rs.setServerConformanceProvider(sc); - rs.init(createServletConfig()); + rs.init(createServletConfig()); - boolean found = false; - Collection resourceBindings = rs.getResourceBindings(); - for (ResourceBinding resourceBinding : resourceBindings) { - if (resourceBinding.getResourceName().equals("Patient")) { - List> methodBindings = resourceBinding.getMethodBindings(); - SearchMethodBinding binding = (SearchMethodBinding) methodBindings.get(0); - SearchParameter param = (SearchParameter) binding.getParameters().get(0); - assertEquals("The organization at which this person is a patient", param.getDescription()); - found = true; - } - } - assertTrue(found); - CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + boolean found = false; + Collection resourceBindings = rs.getResourceBindings(); + for (ResourceBinding resourceBinding : resourceBindings) { + if (resourceBinding.getResourceName().equals("Patient")) { + List> methodBindings = resourceBinding.getMethodBindings(); + SearchMethodBinding binding = (SearchMethodBinding) methodBindings.get(0); + SearchParameter param = (SearchParameter) binding.getParameters().get(0); + assertEquals("The organization at which this person is a patient", param.getDescription()); + found = true; + } + } + assertTrue(found); + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - String conf = validate(conformance); + String conf = validate(conformance); - CapabilityStatementRestResourceComponent resource = findRestResource(conformance, "Patient"); + CapabilityStatementRestResourceComponent resource = findRestResource(conformance, "Patient"); - CapabilityStatementRestResourceSearchParamComponent param = resource.getSearchParam().get(0); + CapabilityStatementRestResourceSearchParamComponent param = resource.getSearchParam().get(0); // assertEquals("bar", param.getChain().get(0).getValue()); // assertEquals("foo", param.getChain().get(1).getValue()); // assertEquals(2, param.getChain().size()); - } + } - @Test - public void testSearchReferenceParameterWithList() throws Exception { + @Test + public void testSearchReferenceParameterWithList() throws Exception { - RestfulServer rsNoType = new RestfulServer(myCtx) { - @Override - public RestfulServerConfiguration createConfiguration() { - RestfulServerConfiguration retVal = super.createConfiguration(); - retVal.setConformanceDate(new InstantDt("2011-02-22T11:22:33Z")); - return retVal; - } - }; - rsNoType.registerProvider(new SearchProviderWithListNoType()); - ServerCapabilityStatementProvider scNoType = new ServerCapabilityStatementProvider(rsNoType); - rsNoType.setServerConformanceProvider(scNoType); - rsNoType.init(createServletConfig()); + RestfulServer rsNoType = new RestfulServer(myCtx) { + @Override + public RestfulServerConfiguration createConfiguration() { + RestfulServerConfiguration retVal = super.createConfiguration(); + retVal.setConformanceDate(new InstantDt("2011-02-22T11:22:33Z")); + return retVal; + } + }; + rsNoType.registerProvider(new SearchProviderWithListNoType()); + ServerCapabilityStatementProvider scNoType = new ServerCapabilityStatementProvider(rsNoType); + rsNoType.setServerConformanceProvider(scNoType); + rsNoType.init(createServletConfig()); - CapabilityStatement conformance = (CapabilityStatement) scNoType.getServerConformance(createHttpServletRequest(), createRequestDetails(rsNoType)); - conformance.setId(""); - String confNoType = validate(conformance); + CapabilityStatement conformance = (CapabilityStatement) scNoType.getServerConformance(createHttpServletRequest(), createRequestDetails(rsNoType)); + conformance.setId(""); + String confNoType = validate(conformance); - RestfulServer rsWithType = new RestfulServer(myCtx) { - @Override - public RestfulServerConfiguration createConfiguration() { - RestfulServerConfiguration retVal = super.createConfiguration(); - retVal.setConformanceDate(new InstantDt("2011-02-22T11:22:33Z")); - return retVal; - } - }; - rsWithType.registerProvider(new SearchProviderWithListWithType()); - ServerCapabilityStatementProvider scWithType = new ServerCapabilityStatementProvider(rsWithType); - rsWithType.setServerConformanceProvider(scWithType); - rsWithType.init(createServletConfig()); + RestfulServer rsWithType = new RestfulServer(myCtx) { + @Override + public RestfulServerConfiguration createConfiguration() { + RestfulServerConfiguration retVal = super.createConfiguration(); + retVal.setConformanceDate(new InstantDt("2011-02-22T11:22:33Z")); + return retVal; + } + }; + rsWithType.registerProvider(new SearchProviderWithListWithType()); + ServerCapabilityStatementProvider scWithType = new ServerCapabilityStatementProvider(rsWithType); + rsWithType.setServerConformanceProvider(scWithType); + rsWithType.init(createServletConfig()); - CapabilityStatement conformanceWithType = (CapabilityStatement) scWithType.getServerConformance(createHttpServletRequest(), createRequestDetails(rsWithType)); - conformanceWithType.setId(""); - String confWithType = validate(conformanceWithType); + CapabilityStatement conformanceWithType = (CapabilityStatement) scWithType.getServerConformance(createHttpServletRequest(), createRequestDetails(rsWithType)); + conformanceWithType.setId(""); + String confWithType = validate(conformanceWithType); - assertEquals(confNoType, confWithType); - assertThat(confNoType, containsString("")); - } + assertEquals(confNoType, confWithType); + assertThat(confNoType, containsString("")); + } - @Test - public void testSystemHistorySupported() throws Exception { + @Test + public void testSystemHistorySupported() throws Exception { - RestfulServer rs = new RestfulServer(myCtx); - rs.setProviders(new SystemHistoryProvider()); - - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); - rs.setServerConformanceProvider(sc); - - rs.init(createServletConfig()); - - CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - String conf = validate(conformance); - - assertThat(conf, containsString("")); - } - - @Test - public void testTypeHistorySupported() throws Exception { - - RestfulServer rs = new RestfulServer(myCtx); - rs.setProviders(new TypeHistoryProvider()); - - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); - rs.setServerConformanceProvider(sc); - - rs.init(createServletConfig()); - - CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - String conf = validate(conformance); - - assertThat(conf, containsString("")); - } - - @Test - public void testStaticIncludeChains() throws Exception { - - class MyProvider implements IResourceProvider { - - @Override - public Class getResourceType() { - return DiagnosticReport.class; - } - - @Search - public List search(@RequiredParam(name = DiagnosticReport.SP_PATIENT + "." + Patient.SP_FAMILY) StringParam lastName, - @RequiredParam(name = DiagnosticReport.SP_PATIENT + "." + Patient.SP_GIVEN) StringParam firstName, - @RequiredParam(name = DiagnosticReport.SP_PATIENT + "." + Patient.SP_BIRTHDATE) DateParam dob, - @OptionalParam(name = DiagnosticReport.SP_DATE) DateRangeParam range) { - return null; - } - - } - - RestfulServer rs = new RestfulServer(myCtx); - rs.setProviders(new MyProvider()); - - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs) { - }; - rs.setServerConformanceProvider(sc); - - rs.init(createServletConfig()); - - CapabilityStatement opDef = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - - validate(opDef); - - CapabilityStatementRestResourceComponent resource = opDef.getRest().get(0).getResource().get(0); - assertEquals("DiagnosticReport", resource.getType()); - List searchParamNames = resource.getSearchParam().stream().map(t -> t.getName()).collect(Collectors.toList()); - assertThat(searchParamNames, containsInAnyOrder("patient.birthdate", "patient.family", "patient.given", "date")); - } - - @Test - public void testSystemLevelNamedQueryWithParameters() throws Exception { - RestfulServer rs = new RestfulServer(myCtx); - rs.setProviders(new NamedQueryPlainProvider()); - rs.setServerAddressStrategy(new HardcodedServerAddressStrategy("http://localhost/baseR4")); - - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); - rs.setServerConformanceProvider(sc); - - rs.init(createServletConfig()); - - CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - validate(conformance); - - CapabilityStatementRestComponent restComponent = conformance.getRest().get(0); - CapabilityStatementRestResourceOperationComponent operationComponent = restComponent.getOperation().get(0); - assertThat(operationComponent.getName(), is(NamedQueryPlainProvider.QUERY_NAME)); - - String operationReference = operationComponent.getDefinition(); - assertThat(operationReference, not(nullValue())); - - OperationDefinition operationDefinition = (OperationDefinition) sc.readOperationDefinition(new IdType(operationReference), createRequestDetails(rs)); - ourLog.info(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(operationDefinition)); - validate(operationDefinition); - assertThat(operationDefinition.getCode(), is(NamedQueryPlainProvider.QUERY_NAME)); - assertThat(operationDefinition.getName(), is("Search_" + NamedQueryPlainProvider.QUERY_NAME)); - assertThat(operationDefinition.getStatus(), is(PublicationStatus.ACTIVE)); - assertThat(operationDefinition.getKind(), is(OperationKind.QUERY)); - assertThat(operationDefinition.getDescription(), is(NamedQueryPlainProvider.DESCRIPTION)); - assertThat(operationDefinition.getAffectsState(), is(false)); - 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(myCtx); - rs.setProviders(new NamedQueryResourceProvider()); - rs.setServerAddressStrategy(new HardcodedServerAddressStrategy("http://localhost/baseR4")); - - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); - rs.setServerConformanceProvider(sc); - - rs.init(createServletConfig()); - - CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - validate(conformance); - - CapabilityStatementRestComponent restComponent = conformance.getRest().get(0); - CapabilityStatementRestResourceOperationComponent operationComponent = restComponent.getOperation().get(0); - String operationReference = operationComponent.getDefinition(); - assertThat(operationReference, not(nullValue())); - - OperationDefinition operationDefinition = (OperationDefinition) sc.readOperationDefinition(new IdType(operationReference), createRequestDetails(rs)); - ourLog.info(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(operationDefinition)); - validate(operationDefinition); - assertThat("The operation name should be the code if no description is set", operationDefinition.getName(), is("Search_" + 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(myCtx); - rs.setProviders(new TypeLevelOperationProvider()); - rs.setServerAddressStrategy(new HardcodedServerAddressStrategy("http://localhost/baseR4")); - - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); - rs.setServerConformanceProvider(sc); - - rs.init(createServletConfig()); - - CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - - validate(conformance); - - List operations = conformance.getRest().get(0).getOperation(); - assertThat(operations.size(), is(1)); - assertThat(operations.get(0).getName(), is(TypeLevelOperationProvider.OPERATION_NAME)); - - OperationDefinition opDef = (OperationDefinition) sc.readOperationDefinition(new IdType(operations.get(0).getDefinition()), createRequestDetails(rs)); - validate(opDef); - assertEquals(TypeLevelOperationProvider.OPERATION_NAME, opDef.getCode()); - assertThat(opDef.getSystem(), is(false)); - assertThat(opDef.getType(), is(true)); - assertThat(opDef.getInstance(), is(false)); - } - - @Test - public void testProfiledResourceStructureDefinitionLinks() throws Exception { - RestfulServer rs = new RestfulServer(myCtx); - rs.setResourceProviders(new ProfiledPatientProvider(), new MultipleProfilesPatientProvider()); - - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); - rs.setServerConformanceProvider(sc); - - rs.init(createServletConfig()); - - CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - ourLog.info(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance)); - - List resources = conformance.getRestFirstRep().getResource(); - CapabilityStatementRestResourceComponent patientResource = resources.stream() - .filter(resource -> "Patient".equals(resource.getType())) - .findFirst().get(); - assertThat(patientResource.getProfile(), containsString(PATIENT_SUB)); - } - - @Test - public void testRevIncludes_Explicit() throws Exception { - - class PatientResourceProvider implements IResourceProvider { - - @Override - public Class getResourceType() { - return Patient.class; - } - - @Search - public List search(@IncludeParam(reverse = true, allow = {"Observation:foo", "Provenance:bar"}) Set theRevIncludes) { - return Collections.emptyList(); - } - - } - - class ObservationResourceProvider implements IResourceProvider { - - @Override - public Class getResourceType() { - return Observation.class; - } - - @Search - public List search(@OptionalParam(name = "subject") ReferenceParam theSubject) { - return Collections.emptyList(); - } - - } - - RestfulServer rs = new RestfulServer(myCtx); - rs.setResourceProviders(new PatientResourceProvider(), new ObservationResourceProvider()); - - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); - sc.setRestResourceRevIncludesEnabled(true); - rs.setServerConformanceProvider(sc); - - rs.init(createServletConfig()); - - CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - ourLog.info(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance)); - - List resources = conformance.getRestFirstRep().getResource(); - CapabilityStatementRestResourceComponent patientResource = resources.stream() - .filter(resource -> "Patient".equals(resource.getType())) - .findFirst().get(); - assertThat(toStrings(patientResource.getSearchRevInclude()), containsInAnyOrder("Observation:foo", "Provenance:bar")); - } + RestfulServer rs = new RestfulServer(myCtx); + rs.setProviders(new SystemHistoryProvider()); - @Test - public void testRevIncludes_Inferred() throws Exception { + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + rs.setServerConformanceProvider(sc); - class PatientResourceProvider implements IResourceProvider { + rs.init(createServletConfig()); - @Override - public Class getResourceType() { - return Patient.class; - } + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + String conf = validate(conformance); - @Search - public List search(@IncludeParam(reverse = true) Set theRevIncludes) { - return Collections.emptyList(); - } - - } - - class ObservationResourceProvider implements IResourceProvider { - - @Override - public Class getResourceType() { - return Observation.class; - } - - @Search - public List search(@OptionalParam(name = "subject") ReferenceParam theSubject) { - return Collections.emptyList(); - } + assertThat(conf, containsString("")); + } - } - - RestfulServer rs = new RestfulServer(myCtx); - rs.setResourceProviders(new PatientResourceProvider(), new ObservationResourceProvider()); + @Test + public void testTypeHistorySupported() throws Exception { - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); - sc.setRestResourceRevIncludesEnabled(true); - rs.setServerConformanceProvider(sc); + RestfulServer rs = new RestfulServer(myCtx); + rs.setProviders(new TypeHistoryProvider()); - rs.init(createServletConfig()); + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + rs.setServerConformanceProvider(sc); - CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - ourLog.info(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance)); - - List resources = conformance.getRestFirstRep().getResource(); - CapabilityStatementRestResourceComponent patientResource = resources.stream() - .filter(resource -> "Patient".equals(resource.getType())) - .findFirst().get(); - assertThat(toStrings(patientResource.getSearchRevInclude()), containsInAnyOrder("Observation:subject")); - } + rs.init(createServletConfig()); - private List toOperationIdParts(List theOperation) { - ArrayList retVal = Lists.newArrayList(); - for (CapabilityStatementRestResourceOperationComponent next : theOperation) { - retVal.add(new IdType(next.getDefinition()).getIdPart()); - } - return retVal; - } - - private List toOperationNames(List theOperation) { - ArrayList retVal = Lists.newArrayList(); - for (CapabilityStatementRestResourceOperationComponent next : theOperation) { - retVal.add(next.getName()); - } - return retVal; - } - - private String validate(IBaseResource theResource) { - String conf = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(theResource); - ourLog.info("Def:\n{}", conf); - - ValidationResult result = myValidator.validateWithResult(conf); - OperationOutcome operationOutcome = (OperationOutcome) result.toOperationOutcome(); - String outcome = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(operationOutcome); - ourLog.info("Outcome: {}", outcome); - - assertTrue(result.isSuccessful(), outcome); - List warningsAndErrors = operationOutcome - .getIssue() - .stream() - .filter(t -> t.getSeverity().ordinal() <= OperationOutcome.IssueSeverity.WARNING.ordinal()) // <= because this enum has a strange order - .collect(Collectors.toList()); - assertThat(outcome, warningsAndErrors, is(empty())); - - return myCtx.newXmlParser().setPrettyPrint(false).encodeResourceToString(theResource); - } - - @SuppressWarnings("unused") - public static class ConditionalProvider implements IResourceProvider { - - @Create - public MethodOutcome create(@ResourceParam Patient thePatient, @ConditionalUrlParam String theConditionalUrl) { - return null; - } - - @Delete - public MethodOutcome delete(@IdParam IdType theId, @ConditionalUrlParam(supportsMultiple = true) String theConditionalUrl) { - return null; - } - - @Override - public Class getResourceType() { - return Patient.class; - } - - @Update - public MethodOutcome update(@IdParam IdType theId, @ResourceParam Patient thePatient, @ConditionalUrlParam String theConditionalUrl) { - return null; - } - - } - - @SuppressWarnings("unused") - public static class InstanceHistoryProvider implements IResourceProvider { - @Override - public Class getResourceType() { - return Patient.class; - } - - @History - public List history(@IdParam IdType theId) { - return null; - } - - } - - @SuppressWarnings("unused") - public static class MultiOptionalProvider { - - @Search(type = Patient.class) - public Patient findPatient(@Description(shortDefinition = "The patient's identifier") @OptionalParam(name = Patient.SP_IDENTIFIER) TokenParam theIdentifier, - @Description(shortDefinition = "The patient's name") @OptionalParam(name = Patient.SP_NAME) StringParam theName) { - return null; - } - - } - - @SuppressWarnings("unused") - public static class MultiTypeEncounterProvider implements IResourceProvider { - - @Operation(name = "someOp") - public IBundleProvider everything(HttpServletRequest theServletRequest, @IdParam IdType theId, - @OperationParam(name = "someOpParam1") DateType theStart, @OperationParam(name = "someOpParam2") Encounter theEnd) { - return null; - } - - @Override - public Class getResourceType() { - return Encounter.class; - } - - @Validate - public IBundleProvider validate(HttpServletRequest theServletRequest, @IdParam IdType theId, @ResourceParam Encounter thePatient) { - return null; - } - - } - - @SuppressWarnings("unused") - public static class MultiTypePatientProvider implements IResourceProvider { - - @Operation(name = "someOp") - public IBundleProvider everything(HttpServletRequest theServletRequest, @IdParam IdType theId, - @OperationParam(name = "someOpParam1") DateType theStart, @OperationParam(name = "someOpParam2") Patient theEnd) { - return null; - } - - @Override - public Class getResourceType() { - return Patient.class; - } - - @Validate - public IBundleProvider validate(HttpServletRequest theServletRequest, @IdParam IdType theId, @ResourceParam Patient thePatient) { - return null; - } - - } - - @SuppressWarnings("unused") - public static class NonConditionalProvider implements IResourceProvider { - - @Create - public MethodOutcome create(@ResourceParam Patient thePatient) { - return null; - } + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + String conf = validate(conformance); - @Delete - public MethodOutcome delete(@IdParam IdType theId) { - return null; - } - - @Override - public Class getResourceType() { - return Patient.class; - } + assertThat(conf, containsString("")); + } - @Update - public MethodOutcome update(@IdParam IdType theId, @ResourceParam Patient thePatient) { - return null; - } + @Test + public void testStaticIncludeChains() throws Exception { - } + class MyProvider implements IResourceProvider { - @SuppressWarnings("unused") - public static class PlainProviderWithExtendedOperationOnNoType { + @Override + public Class getResourceType() { + return DiagnosticReport.class; + } - @Operation(name = "plain", idempotent = true, returnParameters = {@OperationParam(min = 1, max = 2, name = "out1", type = StringType.class)}) - public IBundleProvider everything(HttpServletRequest theServletRequest, @IdParam IdType theId, @OperationParam(name = "start") DateType theStart, - @OperationParam(name = "end") DateType theEnd) { - return null; - } + @Search + public List search(@RequiredParam(name = DiagnosticReport.SP_PATIENT + "." + Patient.SP_FAMILY) StringParam lastName, + @RequiredParam(name = DiagnosticReport.SP_PATIENT + "." + Patient.SP_GIVEN) StringParam firstName, + @RequiredParam(name = DiagnosticReport.SP_PATIENT + "." + Patient.SP_BIRTHDATE) DateParam dob, + @OptionalParam(name = DiagnosticReport.SP_DATE) DateRangeParam range) { + return null; + } - } + } + + RestfulServer rs = new RestfulServer(myCtx); + rs.setProviders(new MyProvider()); + + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs) { + }; + rs.setServerConformanceProvider(sc); + + rs.init(createServletConfig()); + + CapabilityStatement opDef = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + + validate(opDef); + + CapabilityStatementRestResourceComponent resource = opDef.getRest().get(0).getResource().get(0); + assertEquals("DiagnosticReport", resource.getType()); + List searchParamNames = resource.getSearchParam().stream().map(t -> t.getName()).collect(Collectors.toList()); + assertThat(searchParamNames, containsInAnyOrder("patient.birthdate", "patient.family", "patient.given", "date")); + } - @SuppressWarnings("unused") - public static class ProviderWithExtendedOperationReturningBundle implements IResourceProvider { + @Test + public void testIncludeLastUpdatedSearchParam() throws Exception { - @Operation(name = "everything", idempotent = true) - public IBundleProvider everything(HttpServletRequest theServletRequest, @IdParam IdType theId, @OperationParam(name = "start") DateType theStart, - @OperationParam(name = "end") DateType theEnd) { - return null; - } + class MyProvider implements IResourceProvider { - @Override - public Class getResourceType() { - return Patient.class; - } + @Override + public Class getResourceType() { + return DiagnosticReport.class; + } - } + @Search + public List search(@OptionalParam(name = DiagnosticReport.SP_DATE) + DateRangeParam range, - @SuppressWarnings("unused") - public static class ProviderWithRequiredAndOptional { + @Description(shortDefinition = "Only return resources which were last updated as specified by the given range") + @OptionalParam(name = "_lastUpdated") + DateRangeParam theLastUpdated + ) { + return null; + } + + } + + RestfulServer rs = new RestfulServer(myCtx); + rs.setProviders(new MyProvider()); + + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs) { + }; + rs.setServerConformanceProvider(sc); + + rs.init(createServletConfig()); + + CapabilityStatement opDef = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + + validate(opDef); + + CapabilityStatementRestResourceComponent resource = opDef.getRest().get(0).getResource().get(0); + assertEquals("DiagnosticReport", resource.getType()); + List searchParamNames = resource.getSearchParam().stream().map(t -> t.getName()).collect(Collectors.toList()); + assertThat(searchParamNames, containsInAnyOrder("date", "_lastUpdated")); + } + + @Test + public void testSystemLevelNamedQueryWithParameters() throws Exception { + RestfulServer rs = new RestfulServer(myCtx); + rs.setProviders(new NamedQueryPlainProvider()); + rs.setServerAddressStrategy(new HardcodedServerAddressStrategy("http://localhost/baseR4")); + + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + rs.setServerConformanceProvider(sc); + + rs.init(createServletConfig()); + + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + validate(conformance); + + CapabilityStatementRestComponent restComponent = conformance.getRest().get(0); + CapabilityStatementRestResourceOperationComponent operationComponent = restComponent.getOperation().get(0); + assertThat(operationComponent.getName(), is(NamedQueryPlainProvider.QUERY_NAME)); + + String operationReference = operationComponent.getDefinition(); + assertThat(operationReference, not(nullValue())); + + OperationDefinition operationDefinition = (OperationDefinition) sc.readOperationDefinition(new IdType(operationReference), createRequestDetails(rs)); + ourLog.info(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(operationDefinition)); + validate(operationDefinition); + assertThat(operationDefinition.getCode(), is(NamedQueryPlainProvider.QUERY_NAME)); + assertThat(operationDefinition.getName(), is("Search_" + NamedQueryPlainProvider.QUERY_NAME)); + assertThat(operationDefinition.getStatus(), is(PublicationStatus.ACTIVE)); + assertThat(operationDefinition.getKind(), is(OperationKind.QUERY)); + assertThat(operationDefinition.getDescription(), is(NamedQueryPlainProvider.DESCRIPTION)); + assertThat(operationDefinition.getAffectsState(), is(false)); + 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(myCtx); + rs.setProviders(new NamedQueryResourceProvider()); + rs.setServerAddressStrategy(new HardcodedServerAddressStrategy("http://localhost/baseR4")); + + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + rs.setServerConformanceProvider(sc); + + rs.init(createServletConfig()); + + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + validate(conformance); + + CapabilityStatementRestComponent restComponent = conformance.getRest().get(0); + CapabilityStatementRestResourceOperationComponent operationComponent = restComponent.getOperation().get(0); + String operationReference = operationComponent.getDefinition(); + assertThat(operationReference, not(nullValue())); + + OperationDefinition operationDefinition = (OperationDefinition) sc.readOperationDefinition(new IdType(operationReference), createRequestDetails(rs)); + ourLog.info(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(operationDefinition)); + validate(operationDefinition); + assertThat("The operation name should be the code if no description is set", operationDefinition.getName(), is("Search_" + 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(myCtx); + rs.setProviders(new TypeLevelOperationProvider()); + rs.setServerAddressStrategy(new HardcodedServerAddressStrategy("http://localhost/baseR4")); + + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + rs.setServerConformanceProvider(sc); + + rs.init(createServletConfig()); + + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + + validate(conformance); + + List operations = conformance.getRest().get(0).getOperation(); + assertThat(operations.size(), is(1)); + assertThat(operations.get(0).getName(), is(TypeLevelOperationProvider.OPERATION_NAME)); + + OperationDefinition opDef = (OperationDefinition) sc.readOperationDefinition(new IdType(operations.get(0).getDefinition()), createRequestDetails(rs)); + validate(opDef); + assertEquals(TypeLevelOperationProvider.OPERATION_NAME, opDef.getCode()); + assertThat(opDef.getSystem(), is(false)); + assertThat(opDef.getType(), is(true)); + assertThat(opDef.getInstance(), is(false)); + } + + @Test + public void testProfiledResourceStructureDefinitionLinks() throws Exception { + RestfulServer rs = new RestfulServer(myCtx); + rs.setResourceProviders(new ProfiledPatientProvider(), new MultipleProfilesPatientProvider()); + + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + rs.setServerConformanceProvider(sc); + + rs.init(createServletConfig()); + + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + ourLog.info(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance)); + + List resources = conformance.getRestFirstRep().getResource(); + CapabilityStatementRestResourceComponent patientResource = resources.stream() + .filter(resource -> "Patient".equals(resource.getType())) + .findFirst().get(); + assertThat(patientResource.getProfile(), containsString(PATIENT_SUB)); + } + + @Test + public void testRevIncludes_Explicit() throws Exception { + + class PatientResourceProvider implements IResourceProvider { + + @Override + public Class getResourceType() { + return Patient.class; + } + + @Search + public List search(@IncludeParam(reverse = true, allow = {"Observation:foo", "Provenance:bar"}) Set theRevIncludes) { + return Collections.emptyList(); + } + + } + + class ObservationResourceProvider implements IResourceProvider { + + @Override + public Class getResourceType() { + return Observation.class; + } + + @Search + public List search(@OptionalParam(name = "subject") ReferenceParam theSubject) { + return Collections.emptyList(); + } + + } + + RestfulServer rs = new RestfulServer(myCtx); + rs.setResourceProviders(new PatientResourceProvider(), new ObservationResourceProvider()); + + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + sc.setRestResourceRevIncludesEnabled(true); + rs.setServerConformanceProvider(sc); + + rs.init(createServletConfig()); + + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + ourLog.info(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance)); + + List resources = conformance.getRestFirstRep().getResource(); + CapabilityStatementRestResourceComponent patientResource = resources.stream() + .filter(resource -> "Patient".equals(resource.getType())) + .findFirst().get(); + assertThat(toStrings(patientResource.getSearchRevInclude()), containsInAnyOrder("Observation:foo", "Provenance:bar")); + } - @Description(shortDefinition = "This is a search for stuff!") - @Search - public List findDiagnosticReportsByPatient(@RequiredParam(name = DiagnosticReport.SP_SUBJECT + '.' + Patient.SP_IDENTIFIER) TokenParam thePatientId, - @OptionalParam(name = DiagnosticReport.SP_CODE) TokenOrListParam theNames, @OptionalParam(name = DiagnosticReport.SP_DATE) DateRangeParam theDateRange, - @IncludeParam(allow = {"DiagnosticReport.result"}) Set theIncludes) throws Exception { - return null; - } + @Test + public void testRevIncludes_Inferred() throws Exception { - } + class PatientResourceProvider implements IResourceProvider { - @SuppressWarnings("unused") - public static class ReadProvider { + @Override + public Class getResourceType() { + return Patient.class; + } - @Search(type = Patient.class) - public Patient findPatient(@Description(shortDefinition = "The patient's identifier (MRN or other card number)") @RequiredParam(name = Patient.SP_IDENTIFIER) TokenParam theIdentifier) { - return null; - } + @Search + public List search(@IncludeParam(reverse = true) Set theRevIncludes) { + return Collections.emptyList(); + } + + } + + class ObservationResourceProvider implements IResourceProvider { + + @Override + public Class getResourceType() { + return Observation.class; + } + + @Search + public List search(@OptionalParam(name = "subject") ReferenceParam theSubject) { + return Collections.emptyList(); + } - @Read(version = false) - public Patient readPatient(@IdParam IdType theId) { - return null; - } + } + + RestfulServer rs = new RestfulServer(myCtx); + rs.setResourceProviders(new PatientResourceProvider(), new ObservationResourceProvider()); - } + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + sc.setRestResourceRevIncludesEnabled(true); + rs.setServerConformanceProvider(sc); - @SuppressWarnings("unused") - public static class SearchProvider { + rs.init(createServletConfig()); - @Search(type = Patient.class) - public Patient findPatient1(@Description(shortDefinition = "The patient's identifier (MRN or other card number)") @RequiredParam(name = Patient.SP_IDENTIFIER) TokenParam theIdentifier) { - return null; - } + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + ourLog.info(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance)); + + List resources = conformance.getRestFirstRep().getResource(); + CapabilityStatementRestResourceComponent patientResource = resources.stream() + .filter(resource -> "Patient".equals(resource.getType())) + .findFirst().get(); + assertThat(toStrings(patientResource.getSearchRevInclude()), containsInAnyOrder("Observation:subject")); + } - @Search(type = Patient.class) - public Patient findPatient2( - @Description(shortDefinition = "All patients linked to the given patient") @OptionalParam(name = "link", targetTypes = {Patient.class}) ReferenceAndListParam theLink) { - return null; - } + private List toOperationIdParts(List theOperation) { + ArrayList retVal = Lists.newArrayList(); + for (CapabilityStatementRestResourceOperationComponent next : theOperation) { + retVal.add(new IdType(next.getDefinition()).getIdPart()); + } + return retVal; + } + + private List toOperationNames(List theOperation) { + ArrayList retVal = Lists.newArrayList(); + for (CapabilityStatementRestResourceOperationComponent next : theOperation) { + retVal.add(next.getName()); + } + return retVal; + } + + private String validate(IBaseResource theResource) { + String conf = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(theResource); + ourLog.info("Def:\n{}", conf); + + ValidationResult result = myValidator.validateWithResult(conf); + OperationOutcome operationOutcome = (OperationOutcome) result.toOperationOutcome(); + String outcome = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(operationOutcome); + ourLog.info("Outcome: {}", outcome); + + assertTrue(result.isSuccessful(), outcome); + List warningsAndErrors = operationOutcome + .getIssue() + .stream() + .filter(t -> t.getSeverity().ordinal() <= OperationOutcome.IssueSeverity.WARNING.ordinal()) // <= because this enum has a strange order + .collect(Collectors.toList()); + assertThat(outcome, warningsAndErrors, is(empty())); + + return myCtx.newXmlParser().setPrettyPrint(false).encodeResourceToString(theResource); + } + + @SuppressWarnings("unused") + public static class ConditionalProvider implements IResourceProvider { + + @Create + public MethodOutcome create(@ResourceParam Patient thePatient, @ConditionalUrlParam String theConditionalUrl) { + return null; + } + + @Delete + public MethodOutcome delete(@IdParam IdType theId, @ConditionalUrlParam(supportsMultiple = true) String theConditionalUrl) { + return null; + } + + @Override + public Class getResourceType() { + return Patient.class; + } + + @Update + public MethodOutcome update(@IdParam IdType theId, @ResourceParam Patient thePatient, @ConditionalUrlParam String theConditionalUrl) { + return null; + } + + } + + @SuppressWarnings("unused") + public static class InstanceHistoryProvider implements IResourceProvider { + @Override + public Class getResourceType() { + return Patient.class; + } + + @History + public List history(@IdParam IdType theId) { + return null; + } + + } + + @SuppressWarnings("unused") + public static class MultiOptionalProvider { + + @Search(type = Patient.class) + public Patient findPatient(@Description(shortDefinition = "The patient's identifier") @OptionalParam(name = Patient.SP_IDENTIFIER) TokenParam theIdentifier, + @Description(shortDefinition = "The patient's name") @OptionalParam(name = Patient.SP_NAME) StringParam theName) { + return null; + } + + } + + @SuppressWarnings("unused") + public static class MultiTypeEncounterProvider implements IResourceProvider { + + @Operation(name = "someOp") + public IBundleProvider everything(HttpServletRequest theServletRequest, @IdParam IdType theId, + @OperationParam(name = "someOpParam1") DateType theStart, @OperationParam(name = "someOpParam2") Encounter theEnd) { + return null; + } + + @Override + public Class getResourceType() { + return Encounter.class; + } + + @Validate + public IBundleProvider validate(HttpServletRequest theServletRequest, @IdParam IdType theId, @ResourceParam Encounter thePatient) { + return null; + } + + } + + @SuppressWarnings("unused") + public static class MultiTypePatientProvider implements IResourceProvider { + + @Operation(name = "someOp") + public IBundleProvider everything(HttpServletRequest theServletRequest, @IdParam IdType theId, + @OperationParam(name = "someOpParam1") DateType theStart, @OperationParam(name = "someOpParam2") Patient theEnd) { + return null; + } + + @Override + public Class getResourceType() { + return Patient.class; + } + + @Validate + public IBundleProvider validate(HttpServletRequest theServletRequest, @IdParam IdType theId, @ResourceParam Patient thePatient) { + return null; + } + + } + + @SuppressWarnings("unused") + public static class NonConditionalProvider implements IResourceProvider { + + @Create + public MethodOutcome create(@ResourceParam Patient thePatient) { + return null; + } - } + @Delete + public MethodOutcome delete(@IdParam IdType theId) { + return null; + } + + @Override + public Class getResourceType() { + return Patient.class; + } - @SuppressWarnings("unused") - public static class SearchProviderWithWhitelist { + @Update + public MethodOutcome update(@IdParam IdType theId, @ResourceParam Patient thePatient) { + return null; + } - @Search(type = Patient.class) - public Patient findPatient1(@Description(shortDefinition = "The organization at which this person is a patient") @RequiredParam(name = Patient.SP_ORGANIZATION, chainWhitelist = {"foo", - "bar"}) ReferenceAndListParam theIdentifier) { - return null; - } + } - } + @SuppressWarnings("unused") + public static class PlainProviderWithExtendedOperationOnNoType { - @SuppressWarnings("unused") - public static class SearchProviderWithListNoType implements IResourceProvider { + @Operation(name = "plain", idempotent = true, returnParameters = {@OperationParam(min = 1, max = 2, name = "out1", type = StringType.class)}) + public IBundleProvider everything(HttpServletRequest theServletRequest, @IdParam IdType theId, @OperationParam(name = "start") DateType theStart, + @OperationParam(name = "end") DateType theEnd) { + return null; + } - @Override - public Class getResourceType() { - return Patient.class; - } + } + @SuppressWarnings("unused") + public static class ProviderWithExtendedOperationReturningBundle implements IResourceProvider { - @Search() - public List findPatient1(@Description(shortDefinition = "The organization at which this person is a patient") @RequiredParam(name = Patient.SP_ORGANIZATION) ReferenceAndListParam theIdentifier) { - return null; - } + @Operation(name = "everything", idempotent = true) + public IBundleProvider everything(HttpServletRequest theServletRequest, @IdParam IdType theId, @OperationParam(name = "start") DateType theStart, + @OperationParam(name = "end") DateType theEnd) { + return null; + } - } + @Override + public Class getResourceType() { + return Patient.class; + } - @SuppressWarnings("unused") - public static class SearchProviderWithListWithType implements IResourceProvider { + } - @Override - public Class getResourceType() { - return Patient.class; - } + @SuppressWarnings("unused") + public static class ProviderWithRequiredAndOptional { + @Description(shortDefinition = "This is a search for stuff!") + @Search + public List findDiagnosticReportsByPatient(@RequiredParam(name = DiagnosticReport.SP_SUBJECT + '.' + Patient.SP_IDENTIFIER) TokenParam thePatientId, + @OptionalParam(name = DiagnosticReport.SP_CODE) TokenOrListParam theNames, @OptionalParam(name = DiagnosticReport.SP_DATE) DateRangeParam theDateRange, + @IncludeParam(allow = {"DiagnosticReport.result"}) Set theIncludes) throws Exception { + return null; + } - @Search(type = Patient.class) - public List findPatient1(@Description(shortDefinition = "The organization at which this person is a patient") @RequiredParam(name = Patient.SP_ORGANIZATION) ReferenceAndListParam theIdentifier) { - return null; - } + } - } + @SuppressWarnings("unused") + public static class ReadProvider { - public static class SystemHistoryProvider { + @Search(type = Patient.class) + public Patient findPatient(@Description(shortDefinition = "The patient's identifier (MRN or other card number)") @RequiredParam(name = Patient.SP_IDENTIFIER) TokenParam theIdentifier) { + return null; + } - @History - public List history() { - return null; - } + @Read(version = false) + public Patient readPatient(@IdParam IdType theId) { + return null; + } - } + } - public static class TypeHistoryProvider implements IResourceProvider { + @SuppressWarnings("unused") + public static class SearchProvider { - @Override - public Class getResourceType() { - return Patient.class; - } + @Search(type = Patient.class) + public Patient findPatient1(@Description(shortDefinition = "The patient's identifier (MRN or other card number)") @RequiredParam(name = Patient.SP_IDENTIFIER) TokenParam theIdentifier) { + return null; + } - @History - public List history() { - return null; - } + @Search(type = Patient.class) + public Patient findPatient2( + @Description(shortDefinition = "All patients linked to the given patient") @OptionalParam(name = "link", targetTypes = {Patient.class}) ReferenceAndListParam theLink) { + return null; + } - } + } - @SuppressWarnings("unused") - public static class VreadProvider { + @SuppressWarnings("unused") + public static class SearchProviderWithWhitelist { - @Search(type = Patient.class) - public Patient findPatient(@Description(shortDefinition = "The patient's identifier (MRN or other card number)") @RequiredParam(name = Patient.SP_IDENTIFIER) TokenParam theIdentifier) { - return null; - } + @Search(type = Patient.class) + public Patient findPatient1(@Description(shortDefinition = "The organization at which this person is a patient") @RequiredParam(name = Patient.SP_ORGANIZATION, chainWhitelist = {"foo", + "bar"}) ReferenceAndListParam theIdentifier) { + return null; + } - @Read(version = true) - public Patient readPatient(@IdParam IdType theId) { - return null; - } + } - } + @SuppressWarnings("unused") + public static class SearchProviderWithListNoType implements IResourceProvider { - public static class TypeLevelOperationProvider implements IResourceProvider { + @Override + public Class getResourceType() { + return Patient.class; + } - public static final String OPERATION_NAME = "op"; - @Operation(name = OPERATION_NAME, idempotent = true) - public IBundleProvider op() { - return null; - } + @Search() + public List findPatient1(@Description(shortDefinition = "The organization at which this person is a patient") @RequiredParam(name = Patient.SP_ORGANIZATION) ReferenceAndListParam theIdentifier) { + return null; + } - @Override - public Class getResourceType() { - return Patient.class; - } + } - } + @SuppressWarnings("unused") + public static class SearchProviderWithListWithType implements IResourceProvider { - public static class NamedQueryPlainProvider { + @Override + public Class getResourceType() { + return Patient.class; + } - 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; - } - } + @Search(type = Patient.class) + public List findPatient1(@Description(shortDefinition = "The organization at which this person is a patient") @RequiredParam(name = Patient.SP_ORGANIZATION) ReferenceAndListParam theIdentifier) { + return null; + } - public static class NamedQueryResourceProvider implements IResourceProvider { + } - public static final String QUERY_NAME = "testQuery"; - public static final String SP_PARAM = "param"; + public static class SystemHistoryProvider { - @Override - public Class getResourceType() { - return Patient.class; - } + @History + public List history() { + return null; + } - @Search(queryName = QUERY_NAME) - public Bundle findAllGivenParameter(@OptionalParam(name = SP_PARAM) StringParam param) { - return null; - } + } - } + public static class TypeHistoryProvider implements IResourceProvider { - public static class ProfiledPatientProvider implements IResourceProvider { + @Override + public Class getResourceType() { + return Patient.class; + } - @Override - public Class getResourceType() { - return PatientSubSub2.class; - } + @History + public List history() { + return null; + } - @Search - public List find() { - return null; - } - } + } - public static class MultipleProfilesPatientProvider implements IResourceProvider { + @SuppressWarnings("unused") + public static class VreadProvider { - @Override - public Class getResourceType() { - return PatientSubSub.class; - } + @Search(type = Patient.class) + public Patient findPatient(@Description(shortDefinition = "The patient's identifier (MRN or other card number)") @RequiredParam(name = Patient.SP_IDENTIFIER) TokenParam theIdentifier) { + return null; + } - @Read(type = PatientTripleSub.class) - public PatientTripleSub read(@IdParam IdType theId) { - return null; - } + @Read(version = true) + public Patient readPatient(@IdParam IdType theId) { + return null; + } - } + } - @ResourceDef(id = PATIENT_SUB) - public static class PatientSub extends Patient { - } + public static class TypeLevelOperationProvider implements IResourceProvider { - @ResourceDef(id = PATIENT_SUB_SUB) - public static class PatientSubSub extends PatientSub { - } + public static final String OPERATION_NAME = "op"; - @ResourceDef(id = PATIENT_SUB_SUB_2) - public static class PatientSubSub2 extends PatientSub { - } + @Operation(name = OPERATION_NAME, idempotent = true) + public IBundleProvider op() { + return null; + } - @ResourceDef(id = PATIENT_TRIPLE_SUB) - public static class PatientTripleSub extends PatientSubSub { - } + @Override + public Class getResourceType() { + return Patient.class; + } - private static Set toStrings(Collection theType) { - HashSet retVal = new HashSet(); - for (IPrimitiveType next : theType) { - retVal.add(next.getValueAsString()); - } - return retVal; - } + } - @AfterAll - public static void afterClassClearContext() { - TestUtil.clearAllStaticFieldsForUnitTest(); - } + 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; + } + + } + + public static class ProfiledPatientProvider implements IResourceProvider { + + @Override + public Class getResourceType() { + return PatientSubSub2.class; + } + + @Search + public List find() { + return null; + } + } + + public static class MultipleProfilesPatientProvider implements IResourceProvider { + + @Override + public Class getResourceType() { + return PatientSubSub.class; + } + + @Read(type = PatientTripleSub.class) + public PatientTripleSub read(@IdParam IdType theId) { + return null; + } + + } + + @ResourceDef(id = PATIENT_SUB) + public static class PatientSub extends Patient { + } + + @ResourceDef(id = PATIENT_SUB_SUB) + public static class PatientSubSub extends PatientSub { + } + + @ResourceDef(id = PATIENT_SUB_SUB_2) + public static class PatientSubSub2 extends PatientSub { + } + + @ResourceDef(id = PATIENT_TRIPLE_SUB) + public static class PatientTripleSub extends PatientSubSub { + } + + private static Set toStrings(Collection theType) { + HashSet retVal = new HashSet(); + for (IPrimitiveType next : theType) { + retVal.add(next.getValueAsString()); + } + return retVal; + } + + @AfterAll + public static void afterClassClearContext() { + TestUtil.clearAllStaticFieldsForUnitTest(); + } }