diff --git a/docs/src/pages/guides/FHIRServerUsersGuide.md b/docs/src/pages/guides/FHIRServerUsersGuide.md index 3729de4bbcd..3d7cc301f04 100644 --- a/docs/src/pages/guides/FHIRServerUsersGuide.md +++ b/docs/src/pages/guides/FHIRServerUsersGuide.md @@ -955,9 +955,16 @@ For example, you can configure a set of FHIRPath Constraints to run for resource } ``` +It is also possible to configure a set of profiles, one or more of which a resource must claim conformance to and be successfully validated against in order to be persisted to the FHIR server. The FHIR server supports this optional behavior via the `fhirServer/resources//profiles/atLeastOne` configuration parameter. If this configuration parameter is set to a non-empty list of profiles, then the FHIR server will perform the following validation, returning FAILURE if not successful: + * Validate that at least one profile in the list is specified in the resource's `meta.profile` element + * Validate that all profiles specified in the resource's `meta.profile` element are supported by the FHIR server + * Validate that the resource's data conforms to all profiles specified in the resource's `meta.profile` element + +If this configuration parameter is not set or is set to an empty list, then the FHIR server will perform its standard validation. + The IBM FHIR Server pre-packages all conformance resources from the core specification. -See [Validation Guide - Optional profile support](https://ibm.github.io/FHIR/guides/FHIRValidationGuide#optional-profile-support) for a list of pre-built Implmentation Guide resources and how to load them into the IBM FHIR server. +See [Validation Guide - Optional profile support](https://ibm.github.io/FHIR/guides/FHIRValidationGuide#optional-profile-support) for a list of pre-built Implementation Guide resources and how to load them into the IBM FHIR server. See [Validation Guide - Making profiles available to the fhir registry](https://ibm.github.io/FHIR/guides/FHIRValidationGuide#making-profiles-available-to-the-fhir-registry-component-fhirregistry) for information about how to extend the server with additional Implementation Guide artifacts. @@ -1794,12 +1801,14 @@ This section contains reference information about each of the configuration prop |`fhirServer/resources/Resource/searchIncludes`|string list|A comma-separated list of \_include values supported for all resource types. Individual resource types may override this value via `fhirServer/resources//searchIncludes`. Omitting this property is equivalent to supporting all \_include values for the supported resources. An empty list, `[]`, can be used to indicate that no \_include values are supported.| |`fhirServer/resources/Resource/searchRevIncludes`|string list|A comma-separated list of \_revinclude values supported for all resource types. Individual resource types may override this value via `fhirServer/resources//searchRevIncludes`. Omitting this property is equivalent to supporting all \_revinclude values for the supported resources. An empty list, `[]`, can be used to indicate that no \_revinclude values are supported.| |`fhirServer/resources/Resource/searchParameterCombinations`|string list|A comma-separated list of search parameter combinations supported for all resource types. Each search parameter combination is a string, where a plus sign, `+`, separates the search parameters that can be used in combination. To indicate that searching without any search parameters is allowed, an empty string must be included in the list. Including an asterisk, `*`, in the list indicates support of any search parameter combination. Individual resource types may override this value via `fhirServer/resources//searchParameterCombinations`. Omitting this property is equivalent to supporting any search parameter combination.| +|`fhirServer/resources/Resource/profiles/atLeastOne`|string list|A comma-separated list of profiles, at least one of which must be specified in a resource's `meta.profile` element and successfully validated against in order for a resource to be persisted to the FHIR server. Individual resource types may override this value via `fhirServer/resources//profiles/atLeastOne`. Omitting this property or specifying an empty list is equivalent to not requiring any profile assertions for a resource.| |`fhirServer/resources//interactions`|string list|A list of strings that represent the RESTful interactions (create, read, vread, update, patch, delete, history, and/or search) to support for this resource type. For resources without the property, the value of `fhirServer/resources/Resource/interactions` is used.| |`fhirServer/resources//searchParameters`|object|The set of search parameters to support for this resource type. Global search parameters defined on the `Resource` resource can be overridden on a per-resourceType basis.| |`fhirServer/resources//searchParameters/`|string|The URL of the search parameter definition to use for the search parameter `` on resources of type ``.| |`fhirServer/resources//searchIncludes`|string list|A comma-separated list of \_include values supported for this resource type. An empty list, `[]`, can be used to indicate that no \_include values are supported. For resources without the property, the value of `fhirServer/resources/Resource/searchIncludes` is used.| |`fhirServer/resources//searchRevIncludes`|string list|A comma-separated list of \_revinclude values supported for this resource type. An empty list, `[]`, can be used to indicate that no \_revinclude values are supported. For resources without the property, the value of `fhirServer/resources/Resource/searchRevIncludes` is used.| |`fhirServer/resources//searchParameterCombinations`|string list|A comma-separated list of search parameter combinations supported for this resource type. Each search parameter combination is a string, where a plus sign, `+`, separates the search parameters that can be used in combination. To indicate that searching without any search parameters is allowed, an empty string must be included in the list. Including an asterisk, `*`, in the list indicates support of any search parameter combination. For resources without the property, the value of `fhirServer/resources/Resource/searchParameterCombinations` is used.| +|`fhirServer/resources//profiles/atLeastOne`|string list|A comma-separated list of profiles, at least one of which must be specified in a resource's `meta.profile` element and be successfully validated against in order for a resource of this type to be persisted to the FHIR server. If this property is not specified, or if an empty list is specified, the value of `fhirServer/resources/Resource/profiles/atLeastOne` will be used.| |`fhirServer/notifications/common/includeResourceTypes`|string list|A comma-separated list of resource types for which notification event messages should be published.| |`fhirServer/notifications/websocket/enabled`|boolean|A boolean flag which indicates whether or not websocket notifications are enabled.| |`fhirServer/notifications/kafka/enabled`|boolean|A boolean flag which indicates whether or not kafka notifications are enabled.| @@ -1884,12 +1893,14 @@ This section contains reference information about each of the configuration prop |`fhirServer/resources/Resource/searchIncludes`|null (all \_include values supported)| |`fhirServer/resources/Resource/searchRevIncludes`|null (all \_revinclude values supported)| |`fhirServer/resources/Resource/searchParameterCombinations`|null (all search parameter combinations supported)| -|`fhirServer/resources//interactions`|null (inherets from `fhirServer/resources/Resource/interactions`)| +|`fhirServer/resources/Resource/profiles/atLeastOne`|null (no resource profile assertions required)| +|`fhirServer/resources//interactions`|null (inherits from `fhirServer/resources/Resource/interactions`)| |`fhirServer/resources//searchParameters`|null (all type-specific search parameters supported)| |`fhirServer/resources//searchParameters/`|null| -|`fhirServer/resources//searchIncludes`|null (inherets from `fhirServer/resources/Resource/searchIncludes`)| -|`fhirServer/resources//searchRevIncludes`|null (inherets from `fhirServer/resources/Resource/searchRevIncludes`)| -|`fhirServer/resources//searchParameterCombinations`|null (inherets from `fhirServer/resources/Resource/searchParameterCombinations`)| +|`fhirServer/resources//searchIncludes`|null (inherits from `fhirServer/resources/Resource/searchIncludes`)| +|`fhirServer/resources//searchRevIncludes`|null (inherits from `fhirServer/resources/Resource/searchRevIncludes`)| +|`fhirServer/resources//searchParameterCombinations`|null (inherits from `fhirServer/resources/Resource/searchParameterCombinations`)| +|`fhirServer/resources//profiles/atLeastOne`|null (inherits from `fhirServer/resources/Resource/profiles/atLeastOne`)| |`fhirServer/notifications/common/includeResourceTypes`|`["*"]`| |`fhirServer/notifications/websocket/enabled`|false| |`fhirServer/notifications/kafka/enabled`|false| @@ -1966,12 +1977,14 @@ must restart the server for that change to take effect. |`fhirServer/resources/Resource/searchIncludes`|Y|Y| |`fhirServer/resources/Resource/searchRevIncludes`|Y|Y| |`fhirServer/resources/Resource/searchParameterCombinations`|Y|Y| +|`fhirServer/resources/Resource/profiles/atLeastOne`|Y|Y| |`fhirServer/resources//interactions`|Y|Y| |`fhirServer/resources//searchParameters`|Y|Y| |`fhirServer/resources//searchParameters/`|Y|Y| |`fhirServer/resources//searchIncludes`|Y|Y| |`fhirServer/resources//searchRevIncludes`|Y|Y| |`fhirServer/resources//searchParameterCombinations`|Y|Y| +|`fhirServer/resources//profiles/atLeastOne`|Y|Y| |`fhirServer/notifications/common/includeResourceTypes`|N|N| |`fhirServer/notifications/websocket/enabled`|N|N| |`fhirServer/notifications/kafka/enabled`|N|N| diff --git a/fhir-config/src/main/java/com/ibm/fhir/config/FHIRConfiguration.java b/fhir-config/src/main/java/com/ibm/fhir/config/FHIRConfiguration.java index 00c2779bea0..92a4ae6e090 100644 --- a/fhir-config/src/main/java/com/ibm/fhir/config/FHIRConfiguration.java +++ b/fhir-config/src/main/java/com/ibm/fhir/config/FHIRConfiguration.java @@ -44,6 +44,8 @@ public class FHIRConfiguration { public static final String PROPERTY_FIELD_RESOURCES_SEARCH_INCLUDES = "searchIncludes"; public static final String PROPERTY_FIELD_RESOURCES_SEARCH_REV_INCLUDES = "searchRevIncludes"; public static final String PROPERTY_FIELD_RESOURCES_SEARCH_PARAMETER_COMBINATIONS = "searchParameterCombinations"; + public static final String PROPERTY_FIELD_RESOURCES_PROFILES = "profiles"; + public static final String PROPERTY_FIELD_RESOURCES_PROFILES_AT_LEAST_ONE = "atLeastOne"; // Auth and security properties public static final String PROPERTY_SECURITY_CORS = "fhirServer/security/cors"; diff --git a/fhir-server/src/main/java/com/ibm/fhir/server/util/FHIRRestHelper.java b/fhir-server/src/main/java/com/ibm/fhir/server/util/FHIRRestHelper.java index 3afb163364f..97090f997bf 100644 --- a/fhir-server/src/main/java/com/ibm/fhir/server/util/FHIRRestHelper.java +++ b/fhir-server/src/main/java/com/ibm/fhir/server/util/FHIRRestHelper.java @@ -52,6 +52,7 @@ import com.ibm.fhir.model.resource.OperationOutcome.Issue; import com.ibm.fhir.model.resource.Parameters; import com.ibm.fhir.model.resource.Resource; +import com.ibm.fhir.model.resource.StructureDefinition; import com.ibm.fhir.model.type.Code; import com.ibm.fhir.model.type.CodeableConcept; import com.ibm.fhir.model.type.Extension; @@ -79,6 +80,7 @@ import com.ibm.fhir.persistence.interceptor.impl.FHIRPersistenceInterceptorMgr; import com.ibm.fhir.persistence.jdbc.exception.FHIRPersistenceDataAccessException; import com.ibm.fhir.persistence.util.FHIRPersistenceUtil; +import com.ibm.fhir.profile.ProfileSupport; import com.ibm.fhir.provider.util.FHIRUrlParser; import com.ibm.fhir.search.SearchConstants; import com.ibm.fhir.search.SummaryValueSet; @@ -219,7 +221,6 @@ public FHIRRestOperationResponse doCreate(String type, Resource resource, String // If there were no validation errors, then create the resource and return the location header. - // First, invoke the 'beforeCreate' interceptor methods. FHIRPersistenceEvent event = new FHIRPersistenceEvent(resource, buildPersistenceEventProperties(type, null, null, requestProperties)); @@ -430,7 +431,6 @@ private FHIRRestOperationResponse doPatchOrUpdate(String type, String id, FHIRPa } } - FHIRPersistenceContext persistenceContext = FHIRPersistenceContextFactory.createPersistenceContext(event); SingleResourceResult result = persistence.update(persistenceContext, id, newResource); @@ -709,7 +709,6 @@ public Resource doRead(String type, String id, boolean throwExcOnNull, boolean i HTTPHandlingPreference.LENIENT.equals(requestContext.getHandlingPreference())); } - // First, invoke the 'beforeRead' interceptor methods. FHIRPersistenceEvent event = new FHIRPersistenceEvent(contextResource, buildPersistenceEventProperties(type, id, null, requestProperties)); @@ -782,7 +781,6 @@ public Resource doVRead(String type, String id, String versionId, Class resourceType = getResourceType(resourceTypeName); - // First, invoke the 'beforeVread' interceptor methods. FHIRPersistenceEvent event = new FHIRPersistenceEvent(null, buildPersistenceEventProperties(type, id, versionId, requestProperties)); @@ -859,7 +857,6 @@ public Bundle doHistory(String type, String id, MultivaluedMap q FHIRHistoryContext historyContext = FHIRPersistenceUtil.parseHistoryParameters(queryParameters, HTTPHandlingPreference.LENIENT.equals(requestContext.getHandlingPreference())); - // First, invoke the 'beforeHistory' interceptor methods. FHIRPersistenceEvent event = new FHIRPersistenceEvent(null, buildPersistenceEventProperties(type, id, null, requestProperties)); @@ -934,7 +931,6 @@ public Bundle doSearch(String type, String compartment, String compartmentId, Class resourceType = getResourceType(resourceTypeName); - // First, invoke the 'beforeSearch' interceptor methods. FHIRPersistenceEvent event = new FHIRPersistenceEvent(contextResource, buildPersistenceEventProperties(type, null, null, requestProperties)); @@ -1105,7 +1101,7 @@ public FHIRPersistenceTransaction getTransaction() throws Exception { */ private List validateInput(Resource resource) throws FHIRValidationException, FHIROperationException { - List issues = FHIRValidator.validator().validate(resource); + List issues = validateResource(resource); if (!issues.isEmpty()) { for (OperationOutcome.Issue issue : issues) { if (FHIRUtil.isFailure(issue.getSeverity())) { @@ -1242,8 +1238,7 @@ private Bundle validateBundle(Bundle bundle) throws Exception { // If the request entry contains a resource, then validate it now. if (resource != null) { - List issues = - FHIRValidator.validator().validate(resource); + List issues = validateResource(resource); if (!issues.isEmpty()) { if (anyFailureInIssues(issues)) { if (requestType == BundleType.ValueSet.TRANSACTION) { @@ -2695,7 +2690,96 @@ public int doReindex(FHIROperationContext operationContext, OperationOutcome.Bui txn.end(); } } while (attempt++ < TX_ATTEMPTS); - + return result; } + + /** + * Validate a resource. First validate profile assertions for the resource if configured to do so, + * then validate the resource itself. + * + * @param resource + * the resource to be validated + * @return A list of validation errors and warnings + * @throws FHIRValidationException + */ + private List validateResource(Resource resource) throws FHIRValidationException { + List profiles = null; + + // Retrieve the profile configuration + try { + StringBuilder defaultProfileConfigPath = new StringBuilder(FHIRConfiguration.PROPERTY_RESOURCES).append("/Resource/") + .append(FHIRConfiguration.PROPERTY_FIELD_RESOURCES_PROFILES).append("/") + .append(FHIRConfiguration.PROPERTY_FIELD_RESOURCES_PROFILES_AT_LEAST_ONE); + StringBuilder resourceSpecificProfileConfigPath = new StringBuilder(FHIRConfiguration.PROPERTY_RESOURCES).append("/") + .append(resource.getClass().getSimpleName()).append("/").append(FHIRConfiguration.PROPERTY_FIELD_RESOURCES_PROFILES) + .append("/").append(FHIRConfiguration.PROPERTY_FIELD_RESOURCES_PROFILES_AT_LEAST_ONE); + + // Get the 'atLeastOne' property + List resourceSpecificProfiles = FHIRConfigHelper.getStringListProperty(resourceSpecificProfileConfigPath.toString()); + if (resourceSpecificProfiles != null) { + profiles = resourceSpecificProfiles; + } else { + List defaultProfiles = FHIRConfigHelper.getStringListProperty(defaultProfileConfigPath.toString()); + if (defaultProfiles != null) { + profiles = defaultProfiles; + } + } + + if (log.isLoggable(Level.FINE)) { + log.fine("Required profile list: " + profiles); + } + } catch (Exception e) { + return Collections.singletonList(buildOperationOutcomeIssue(IssueSeverity.ERROR, IssueType.UNKNOWN, + "Error retrieving profile configuration.")); + } + + // If required profiles were configured, perform validation of asserted profiles against required profiles + if (profiles != null && !profiles.isEmpty()) { + + // Get the profiles asserted for this resource + List resourceAssertedProfiles = ProfileSupport.getResourceAssertedProfiles(resource); + if (log.isLoggable(Level.FINE)) { + log.fine("Asserted profiles: " + resourceAssertedProfiles); + } + + // Check if a profile is required but none specified + if (resourceAssertedProfiles.isEmpty()) { + return Collections.singletonList(buildOperationOutcomeIssue(IssueSeverity.ERROR, IssueType.REQUIRED, + "A profile is required but no profile was specified.")); + } + + // Check if at least one asserted profile is in list of required profiles + boolean validProfileFound = false; + for (String resourceAssertedProfile : resourceAssertedProfiles) { + if (profiles.contains(resourceAssertedProfile)) { + if (log.isLoggable(Level.FINE)) { + log.fine("Valid asserted profile found: '" + resourceAssertedProfile + "'"); + } + validProfileFound = true; + break; + } + } + if (!validProfileFound) { + return Collections.singletonList(buildOperationOutcomeIssue(IssueSeverity.ERROR, IssueType.INVALID, + "A required profile was not specified.")); + } + + // Check if asserted profiles are supported + List issues = new ArrayList<>(); + for (String resourceAssertedProfile : resourceAssertedProfiles) { + StructureDefinition profile = ProfileSupport.getProfile(resourceAssertedProfile); + if (profile == null) { + issues.add(buildOperationOutcomeIssue(IssueSeverity.ERROR, IssueType.NOT_SUPPORTED, + "Profile '" + resourceAssertedProfile + "' is not supported")); + } + } + if (!issues.isEmpty()) { + return issues; + } + } + + return FHIRValidator.validator().validate(resource); + } + } diff --git a/fhir-server/src/test/java/com/ibm/fhir/server/test/MockRegistryResourceProvider.java b/fhir-server/src/test/java/com/ibm/fhir/server/test/MockRegistryResourceProvider.java new file mode 100644 index 00000000000..1742fbd4b7a --- /dev/null +++ b/fhir-server/src/test/java/com/ibm/fhir/server/test/MockRegistryResourceProvider.java @@ -0,0 +1,196 @@ +/* + * (C) Copyright IBM Corp. 2020 + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.ibm.fhir.server.test; + +import java.util.Collection; + +import com.ibm.fhir.model.resource.Resource; +import com.ibm.fhir.model.resource.StructureDefinition; +import com.ibm.fhir.model.type.Uri; +import com.ibm.fhir.model.type.code.PublicationStatus; +import com.ibm.fhir.model.type.code.StructureDefinitionKind; +import com.ibm.fhir.model.type.code.TypeDerivationRule; +import com.ibm.fhir.registry.resource.FHIRRegistryResource; +import com.ibm.fhir.registry.resource.FHIRRegistryResource.Version; +import com.ibm.fhir.registry.spi.FHIRRegistryResourceProvider; + +/** + * A mock FHIRRegistryResourceProvider + */ +public class MockRegistryResourceProvider implements FHIRRegistryResourceProvider { + StructureDefinition profile1sd = StructureDefinition.builder() + .url(Uri.of("profile1")) + .name(com.ibm.fhir.model.type.String.of("profile1")) + .status(PublicationStatus.ACTIVE) + .kind(StructureDefinitionKind.RESOURCE) + ._abstract(com.ibm.fhir.model.type.Boolean.FALSE) + .type(Uri.of("Patient")) + .derivation(TypeDerivationRule.CONSTRAINT) + .build(); + + FHIRRegistryResource profile1 = new FHIRRegistryResource( + StructureDefinition.class, + profile1sd.getId(), + profile1sd.getUrl().getValue(), + Version.from("1"), + profile1sd.getKind().getValue(), + profile1sd.getType().getValue()) { + @Override + public Resource getResource() { + return profile1sd; + } + }; + + StructureDefinition profile2sd = StructureDefinition.builder() + .url(Uri.of("profile2")) + .name(com.ibm.fhir.model.type.String.of("profile2")) + .status(PublicationStatus.ACTIVE) + .kind(StructureDefinitionKind.RESOURCE) + ._abstract(com.ibm.fhir.model.type.Boolean.FALSE) + .type(Uri.of("Patient")) + .derivation(TypeDerivationRule.CONSTRAINT) + .build(); + + FHIRRegistryResource profile2 = new FHIRRegistryResource( + StructureDefinition.class, + profile2sd.getId(), + profile2sd.getUrl().getValue(), + Version.from("1"), + profile2sd.getKind().getValue(), + profile2sd.getType().getValue()) { + @Override + public Resource getResource() { + return profile2sd; + } + }; + + StructureDefinition profile3sd = StructureDefinition.builder() + .url(Uri.of("profile3")) + .name(com.ibm.fhir.model.type.String.of("profile3")) + .status(PublicationStatus.ACTIVE) + .kind(StructureDefinitionKind.RESOURCE) + ._abstract(com.ibm.fhir.model.type.Boolean.FALSE) + .type(Uri.of("Patient")) + .derivation(TypeDerivationRule.CONSTRAINT) + .build(); + + FHIRRegistryResource profile3 = new FHIRRegistryResource( + StructureDefinition.class, + profile3sd.getId(), + profile3sd.getUrl().getValue(), + Version.from("1"), + profile3sd.getKind().getValue(), + profile3sd.getType().getValue()) { + @Override + public Resource getResource() { + return profile3sd; + } + }; + + StructureDefinition profile4sd = StructureDefinition.builder() + .url(Uri.of("profile4")) + .name(com.ibm.fhir.model.type.String.of("profile4")) + .status(PublicationStatus.ACTIVE) + .kind(StructureDefinitionKind.RESOURCE) + ._abstract(com.ibm.fhir.model.type.Boolean.FALSE) + .type(Uri.of("Encounter")) + .derivation(TypeDerivationRule.CONSTRAINT) + .build(); + + FHIRRegistryResource profile4 = new FHIRRegistryResource( + StructureDefinition.class, + profile4sd.getId(), + profile4sd.getUrl().getValue(), + Version.from("1"), + profile4sd.getKind().getValue(), + profile4sd.getType().getValue()) { + @Override + public Resource getResource() { + return profile4sd; + } + }; + + /** + * Get the registry resource from this provider for the given resource type, url and version + * + *

If the version is null, then the latest version of the registry resource is returned (if available) + * + * @param resourceType + * the resource type of the registry resource + * @param url + * the url of the registry resource + * @param version + * the version of the registry resource (optional) + * @return + * the registry resource from this provider for the given resource type, url and version if exists, null otherwise + */ + @Override + public FHIRRegistryResource getRegistryResource(Class resourceType, String url, String version) { + if (url.equals("profile1")) { + return profile1; + } else if (url.equals("profile2")) { + return profile2; + } else if (url.equals("profile3")) { + return profile3; + } else if (url.equals("profile4")) { + return profile4; + } else { + return null; + } + } + + /** + * Get the registry resources from this provider for the given resource type + * + * @param resourceType + * the resource type of the registry resource + * @return + * the registry resources from this provider for the given resource type + */ + @Override + public Collection getRegistryResources(Class resourceType) { + return null; + } + + /** + * Get all the registry resources from this provider + * + * @return + * all of the registry resources from this provider + */ + @Override + public Collection getRegistryResources() { + return null; + } + + /** + * Get the profile resources from this provider that constrain the given resource type + * + * @param type + * the constrained resource type + * @return + * the profile resources from this provider that constrain the given resource type + */ + @Override + public Collection getProfileResources(String type) { + return null; + } + + /** + * Get the search parameter resources from this provider with the given search parameter type + * (e.g. string, token, etc.) + * + * @param type + * the search parameter type + * @return + * the search parameter resources from this provider with the given search parameter type + */ + @Override + public Collection getSearchParameterResources(String type) { + return null; + } +} \ No newline at end of file diff --git a/fhir-server/src/test/java/com/ibm/fhir/server/test/ProfileValidationConfigTest.java b/fhir-server/src/test/java/com/ibm/fhir/server/test/ProfileValidationConfigTest.java new file mode 100644 index 00000000000..95accfbd290 --- /dev/null +++ b/fhir-server/src/test/java/com/ibm/fhir/server/test/ProfileValidationConfigTest.java @@ -0,0 +1,713 @@ +/* + * (C) Copyright IBM Corp. 2020 + * + * SPDX-License-Identifier: Apache-2.0 + */ +package com.ibm.fhir.server.test; + +import static com.ibm.fhir.model.type.String.string; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.fail; + +import java.util.List; + +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import com.ibm.fhir.config.FHIRConfiguration; +import com.ibm.fhir.config.FHIRRequestContext; +import com.ibm.fhir.core.HTTPReturnPreference; +import com.ibm.fhir.exception.FHIRException; +import com.ibm.fhir.exception.FHIROperationException; +import com.ibm.fhir.model.resource.Bundle; +import com.ibm.fhir.model.resource.Encounter; +import com.ibm.fhir.model.resource.OperationOutcome; +import com.ibm.fhir.model.resource.OperationOutcome.Issue; +import com.ibm.fhir.model.resource.Patient; +import com.ibm.fhir.model.resource.Procedure; +import com.ibm.fhir.model.type.Canonical; +import com.ibm.fhir.model.type.Code; +import com.ibm.fhir.model.type.CodeableConcept; +import com.ibm.fhir.model.type.Coding; +import com.ibm.fhir.model.type.Meta; +import com.ibm.fhir.model.type.Narrative; +import com.ibm.fhir.model.type.Reference; +import com.ibm.fhir.model.type.Uri; +import com.ibm.fhir.model.type.Xhtml; +import com.ibm.fhir.model.type.code.BundleType; +import com.ibm.fhir.model.type.code.EncounterStatus; +import com.ibm.fhir.model.type.code.HTTPVerb; +import com.ibm.fhir.model.type.code.IssueSeverity; +import com.ibm.fhir.model.type.code.IssueType; +import com.ibm.fhir.model.type.code.NarrativeStatus; +import com.ibm.fhir.model.type.code.ProcedureStatus; +import com.ibm.fhir.persistence.FHIRPersistence; +import com.ibm.fhir.registry.FHIRRegistry; +import com.ibm.fhir.server.operation.spi.FHIRRestOperationResponse; +import com.ibm.fhir.server.util.FHIRRestHelper; +import com.ibm.fhir.validation.exception.FHIRValidationException; + +public class ProfileValidationConfigTest { + + FHIRPersistence persistence; + FHIRRestHelper helper; + + public static final OperationOutcome ALL_OK = OperationOutcome.builder() + .issue(Issue.builder() + .severity(IssueSeverity.INFORMATION) + .code(IssueType.INFORMATIONAL) + .details(CodeableConcept.builder() + .text(string("All OK")) + .build()) + .build()) + .build(); + + @BeforeClass + void setup() throws FHIRException { + FHIRConfiguration.setConfigHome("src/test/resources"); + FHIRRequestContext.get().setTenantId("profileValidationConfigTest"); + FHIRRegistry.getInstance().register(new MockRegistryResourceProvider()); + persistence = new MockPersistenceImpl(); + helper = new FHIRRestHelper(persistence); + } + + @AfterClass + void tearDown() throws FHIRException { + FHIRConfiguration.setConfigHome(""); + FHIRRequestContext.get().setTenantId("default"); + } + + /** + * Test a create with no profile specified but profile required. + */ + @Test + public void testCreateWithNoProfileSpecified() throws Exception { + Patient patient = Patient.builder() + .generalPractitioner(Reference.builder() + .reference(string("Practitioner/1")) + .build()) + .text(Narrative.builder() + .div(Xhtml.of("

Some narrative
")) + .status(NarrativeStatus.GENERATED) + .build()) + .build(); + + // Process request + FHIRRequestContext.get().setOriginalRequestUri("test"); + FHIRRequestContext.get().setReturnPreference(HTTPReturnPreference.OPERATION_OUTCOME); + try { + helper.doCreate("Patient", patient, null, null, true); + fail(); + } catch (FHIROperationException e) { + // Validate results + List issues = e.getIssues(); + assertEquals(1, issues.size()); + assertEquals("A profile is required but no profile was specified.", issues.get(0).getDetails().getText().getValue()); + assertEquals(IssueSeverity.ERROR, issues.get(0).getSeverity()); + assertEquals(IssueType.REQUIRED, issues.get(0).getCode()); + } + } + + /** + * Test a create with a profile specified, but not a required profile. + */ + @Test + public void testCreateWithNonRequiredProfileSpecified() throws Exception { + Patient patient = Patient.builder() + .meta(Meta.builder() + .profile(Canonical.of("profile4")) + .build()) + .generalPractitioner(Reference.builder() + .reference(string("Practitioner/1")) + .build()) + .text(Narrative.builder() + .div(Xhtml.of("
Some narrative
")) + .status(NarrativeStatus.GENERATED) + .build()) + .build(); + + // Process request + FHIRRequestContext.get().setOriginalRequestUri("test"); + FHIRRequestContext.get().setReturnPreference(HTTPReturnPreference.OPERATION_OUTCOME); + try { + helper.doCreate("Patient", patient, null, null, true); + fail(); + } catch (FHIROperationException e) { + // Validate results + List issues = e.getIssues(); + assertEquals(1, issues.size()); + assertEquals("A required profile was not specified.", issues.get(0).getDetails().getText().getValue()); + assertEquals(IssueSeverity.ERROR, issues.get(0).getSeverity()); + assertEquals(IssueType.INVALID, issues.get(0).getCode()); + } + } + + /** + * Test a create with an unsupported profile specified. + */ + @Test + public void testCreateWithUnsupportedProfileSpecified() throws Exception { + Patient patient = Patient.builder() + .meta(Meta.builder() + .profile(Canonical.of("profile1"), Canonical.of("profile5")) + .build()) + .generalPractitioner(Reference.builder() + .reference(string("Practitioner/1")) + .build()) + .text(Narrative.builder() + .div(Xhtml.of("
Some narrative
")) + .status(NarrativeStatus.GENERATED) + .build()) + .build(); + + // Process request + FHIRRequestContext.get().setOriginalRequestUri("test"); + FHIRRequestContext.get().setReturnPreference(HTTPReturnPreference.OPERATION_OUTCOME); + try { + helper.doCreate("Patient", patient, null, null, true); + fail(); + } catch (FHIROperationException e) { + // Validate results + List issues = e.getIssues(); + assertEquals(1, issues.size()); + assertEquals("Profile 'profile5' is not supported", issues.get(0).getDetails().getText().getValue()); + assertEquals(IssueSeverity.ERROR, issues.get(0).getSeverity()); + assertEquals(IssueType.NOT_SUPPORTED, issues.get(0).getCode()); + } + } + + /** + * Test a create with a valid required profile specified. + */ + @Test + public void testCreateWithRequiredProfileSpecified() throws Exception { + Patient patient = Patient.builder() + .meta(Meta.builder() + .profile(Canonical.of("profile1")) + .build()) + .generalPractitioner(Reference.builder() + .reference(string("Practitioner/1")) + .build()) + .text(Narrative.builder() + .div(Xhtml.of("
Some narrative
")) + .status(NarrativeStatus.GENERATED) + .build()) + .build(); + + // Process request + FHIRRequestContext.get().setOriginalRequestUri("test"); + FHIRRequestContext.get().setReturnPreference(HTTPReturnPreference.OPERATION_OUTCOME); + try { + helper.doCreate("Patient", patient, null, null, true); + fail(); + } catch (FHIRValidationException e) { + // Validate results. + // Profile assertion validation successful. + // Expected FHIRValidator error due to profile not actually being loaded. + assertEquals("An error occurred during validation", e.getMessage()); + } + } + + /** + * Test a create with no profile specified but default config profile required. + */ + @Test + public void testCreateWithNoProfileSpecifiedDefault() throws Exception { + Encounter encounter = Encounter.builder() + .status(EncounterStatus.FINISHED) + .clazz(Coding.builder() + .code(Code.of("AMB")) + .build()) + .reasonReference(Reference.builder() + .reference(string("urn:2")) + .build()) + .build(); + + // Process request + FHIRRequestContext.get().setOriginalRequestUri("test"); + FHIRRequestContext.get().setReturnPreference(HTTPReturnPreference.OPERATION_OUTCOME); + try { + helper.doCreate("Encounter", encounter, null, null, true); + fail(); + } catch (FHIROperationException e) { + // Validate results + List issues = e.getIssues(); + assertEquals(1, issues.size()); + assertEquals("A profile is required but no profile was specified.", issues.get(0).getDetails().getText().getValue()); + assertEquals(IssueSeverity.ERROR, issues.get(0).getSeverity()); + assertEquals(IssueType.REQUIRED, issues.get(0).getCode()); + } + } + + /** + * Test a create with a profile specified, but not a default config required profile. + */ + @Test + public void testCreateWithNonRequiredProfileSpecifiedDefault() throws Exception { + Encounter encounter = Encounter.builder() + .meta(Meta.builder() + .profile(Canonical.of("profile1"), Canonical.of("badprofile")) + .build()) + .status(EncounterStatus.FINISHED) + .clazz(Coding.builder() + .code(Code.of("AMB")) + .build()) + .reasonReference(Reference.builder() + .reference(string("urn:2")) + .build()) + .build(); + + // Process request + FHIRRequestContext.get().setOriginalRequestUri("test"); + FHIRRequestContext.get().setReturnPreference(HTTPReturnPreference.OPERATION_OUTCOME); + try { + helper.doCreate("Encounter", encounter, null, null, true); + fail(); + } catch (FHIROperationException e) { + // Validate results + List issues = e.getIssues(); + assertEquals(1, issues.size()); + assertEquals("A required profile was not specified.", issues.get(0).getDetails().getText().getValue()); + assertEquals(IssueSeverity.ERROR, issues.get(0).getSeverity()); + assertEquals(IssueType.INVALID, issues.get(0).getCode()); + } + } + + /** + * Test a create with an unsupported default config profile specified. + */ + @Test + public void testCreateWithUnsupportedProfileSpecifiedDefault() throws Exception { + Encounter encounter = Encounter.builder() + .meta(Meta.builder() + .profile(Canonical.of("profile4"), Canonical.of("badprofile")) + .build()) + .status(EncounterStatus.FINISHED) + .clazz(Coding.builder() + .code(Code.of("AMB")) + .build()) + .reasonReference(Reference.builder() + .reference(string("urn:2")) + .build()) + .build(); + + // Process request + FHIRRequestContext.get().setOriginalRequestUri("test"); + FHIRRequestContext.get().setReturnPreference(HTTPReturnPreference.OPERATION_OUTCOME); + try { + helper.doCreate("Encounter", encounter, null, null, true); + fail(); + } catch (FHIROperationException e) { + // Validate results + List issues = e.getIssues(); + assertEquals(1, issues.size()); + assertEquals("Profile 'badprofile' is not supported", issues.get(0).getDetails().getText().getValue()); + assertEquals(IssueSeverity.ERROR, issues.get(0).getSeverity()); + assertEquals(IssueType.NOT_SUPPORTED, issues.get(0).getCode()); + } + } + + /** + * Test a create with a valid default config required profile specified. + */ + @Test + public void testCreateWithRequiredProfileSpecifiedDefault() throws Exception { + Encounter encounter = Encounter.builder() + .meta(Meta.builder() + .profile(Canonical.of("profile4")) + .build()) + .status(EncounterStatus.FINISHED) + .clazz(Coding.builder() + .code(Code.of("AMB")) + .build()) + .reasonReference(Reference.builder() + .reference(string("urn:2")) + .build()) + .build(); + + // Process request + FHIRRequestContext.get().setOriginalRequestUri("test"); + FHIRRequestContext.get().setReturnPreference(HTTPReturnPreference.OPERATION_OUTCOME); + try { + helper.doCreate("Encounter", encounter, null, null, true); + fail(); + } catch (FHIRValidationException e) { + // Validate results. + // Profile assertion validation successful. + // Expected FHIRValidator error due to profile not actually being loaded. + assertEquals("An error occurred during validation", e.getMessage()); + } + } + + /** + * Test a create for resource with empty required list. + */ + @Test + public void testCreateForResourceWithEmptyRequiredList() throws Exception { + Procedure procedure = Procedure.builder() + .text(Narrative.builder() + .div(Xhtml.of("
Some narrative
")) + .status(NarrativeStatus.GENERATED) + .build()) + .status(ProcedureStatus.COMPLETED) + .subject(Reference.builder() + .reference(string("urn:3")) + .build()) + .encounter(Reference.builder() + .reference(string("urn:1")) + .build()) + .reasonReference(Reference.builder() + .reference(string("urn:5")) + .build()) + .build(); + + // Process request + FHIRRequestContext.get().setOriginalRequestUri("test"); + FHIRRequestContext.get().setReturnPreference(HTTPReturnPreference.OPERATION_OUTCOME); + try { + FHIRRestOperationResponse response = helper.doCreate("Procedure", procedure, null, null, true); + assertEquals(ALL_OK, response.getOperationOutcome()); + } catch (Exception e) { + fail(); + } + } + + /** + * Test an update with no profile specified but profile required. + */ + @Test + public void testUpdateWithNoProfileSpecified() throws Exception { + Patient patient = Patient.builder() + .id("1") + .generalPractitioner(Reference.builder() + .reference(string("Practitioner/1")) + .build()) + .text(Narrative.builder() + .div(Xhtml.of("
Some narrative
")) + .status(NarrativeStatus.GENERATED) + .build()) + .build(); + + // Process request + FHIRRequestContext.get().setOriginalRequestUri("test"); + FHIRRequestContext.get().setReturnPreference(HTTPReturnPreference.OPERATION_OUTCOME); + try { + helper.doUpdate("Patient", "1", patient, null, null, null, true); + fail(); + } catch (FHIROperationException e) { + // Validate results + List issues = e.getIssues(); + assertEquals(1, issues.size()); + assertEquals("A profile is required but no profile was specified.", issues.get(0).getDetails().getText().getValue()); + assertEquals(IssueSeverity.ERROR, issues.get(0).getSeverity()); + assertEquals(IssueType.REQUIRED, issues.get(0).getCode()); + } + } + + /** + * Test an update with a profile specified, but not a required profile. + */ + @Test + public void testUpdateWithNonRequiredProfileSpecified() throws Exception { + Patient patient = Patient.builder() + .id("1") + .meta(Meta.builder() + .profile(Canonical.of("profile4")) + .build()) + .generalPractitioner(Reference.builder() + .reference(string("Practitioner/1")) + .build()) + .text(Narrative.builder() + .div(Xhtml.of("
Some narrative
")) + .status(NarrativeStatus.GENERATED) + .build()) + .build(); + + // Process request + FHIRRequestContext.get().setOriginalRequestUri("test"); + FHIRRequestContext.get().setReturnPreference(HTTPReturnPreference.OPERATION_OUTCOME); + try { + helper.doUpdate("Patient", "1", patient, null, null, null, true); + fail(); + } catch (FHIROperationException e) { + // Validate results + List issues = e.getIssues(); + assertEquals(1, issues.size()); + assertEquals("A required profile was not specified.", issues.get(0).getDetails().getText().getValue()); + assertEquals(IssueSeverity.ERROR, issues.get(0).getSeverity()); + assertEquals(IssueType.INVALID, issues.get(0).getCode()); + } + } + + /** + * Test an update with an unsupported profile specified. + */ + @Test + public void testUpdateWithUnsupportedProfileSpecified() throws Exception { + Patient patient = Patient.builder() + .id("1") + .meta(Meta.builder() + .profile(Canonical.of("profile1"), Canonical.of("profile5")) + .build()) + .generalPractitioner(Reference.builder() + .reference(string("Practitioner/1")) + .build()) + .text(Narrative.builder() + .div(Xhtml.of("
Some narrative
")) + .status(NarrativeStatus.GENERATED) + .build()) + .build(); + + // Process request + FHIRRequestContext.get().setOriginalRequestUri("test"); + FHIRRequestContext.get().setReturnPreference(HTTPReturnPreference.OPERATION_OUTCOME); + try { + helper.doUpdate("Patient", "1", patient, null, null, null, true); + fail(); + } catch (FHIROperationException e) { + // Validate results + List issues = e.getIssues(); + assertEquals(1, issues.size()); + assertEquals("Profile 'profile5' is not supported", issues.get(0).getDetails().getText().getValue()); + assertEquals(IssueSeverity.ERROR, issues.get(0).getSeverity()); + assertEquals(IssueType.NOT_SUPPORTED, issues.get(0).getCode()); + } + } + + /** + * Test an update with a valid required profile specified. + */ + @Test + public void testUpdateWithRequiredProfileSpecified() throws Exception { + Patient patient = Patient.builder() + .id("1") + .meta(Meta.builder() + .profile(Canonical.of("profile1")) + .build()) + .generalPractitioner(Reference.builder() + .reference(string("Practitioner/1")) + .build()) + .text(Narrative.builder() + .div(Xhtml.of("
Some narrative
")) + .status(NarrativeStatus.GENERATED) + .build()) + .build(); + + // Process request + FHIRRequestContext.get().setOriginalRequestUri("test"); + FHIRRequestContext.get().setReturnPreference(HTTPReturnPreference.OPERATION_OUTCOME); + try { + helper.doUpdate("Patient", "1", patient, null, null, null, true); + fail(); + } catch (FHIRValidationException e) { + // Validate results. + // Profile assertion validation successful. + // Expected FHIRValidator error due to profile not actually being loaded. + assertEquals("An error occurred during validation", e.getMessage()); + } + } + + /** + * Test bundle with no profile specified but profile required. + */ + @Test + public void testBundleWithNoProfileSpecified() throws Exception { + Patient patient = Patient.builder() + .generalPractitioner(Reference.builder() + .reference(string("Practitioner/1")) + .build()) + .text(Narrative.builder() + .div(Xhtml.of("
Some narrative
")) + .status(NarrativeStatus.GENERATED) + .build()) + .build(); + + Bundle.Entry.Request bundleEntryRequest = Bundle.Entry.Request.builder() + .method(HTTPVerb.POST) + .url(Uri.of("Patient")) + .build(); + Bundle.Entry bundleEntry = Bundle.Entry.builder() + .resource(patient) + .request(bundleEntryRequest) + .build(); + + Bundle requestBundle = Bundle.builder() + .id("bundle1") + .type(BundleType.TRANSACTION) + .entry(bundleEntry) + .build(); + + // Process bundle + FHIRRequestContext.get().setOriginalRequestUri("test"); + FHIRRequestContext.get().setReturnPreference(HTTPReturnPreference.OPERATION_OUTCOME); + try { + helper.doBundle(requestBundle, null); + fail(); + } catch (FHIROperationException e) { + // Validate results + List issues = e.getIssues(); + assertEquals(2, issues.size()); + assertEquals("One or more errors were encountered while validating a 'transaction' request bundle.", issues.get(0).getDetails().getText().getValue()); + assertEquals(IssueSeverity.FATAL, issues.get(0).getSeverity()); + assertEquals(IssueType.INVALID, issues.get(0).getCode()); + assertEquals("A profile is required but no profile was specified.", issues.get(1).getDetails().getText().getValue()); + assertEquals(IssueSeverity.ERROR, issues.get(1).getSeverity()); + assertEquals(IssueType.REQUIRED, issues.get(1).getCode()); + } + } + + /** + * Test bundle with a profile specified, but not a required profile. + */ + @Test + public void testBundleWithNonRequiredProfileSpecified() throws Exception { + Patient patient = Patient.builder() + .meta(Meta.builder() + .profile(Canonical.of("profile4")) + .build()) + .generalPractitioner(Reference.builder() + .reference(string("Practitioner/1")) + .build()) + .text(Narrative.builder() + .div(Xhtml.of("
Some narrative
")) + .status(NarrativeStatus.GENERATED) + .build()) + .build(); + + Bundle.Entry.Request bundleEntryRequest = Bundle.Entry.Request.builder() + .method(HTTPVerb.POST) + .url(Uri.of("Patient")) + .build(); + Bundle.Entry bundleEntry = Bundle.Entry.builder() + .resource(patient) + .request(bundleEntryRequest) + .build(); + + Bundle requestBundle = Bundle.builder() + .id("bundle1") + .type(BundleType.TRANSACTION) + .entry(bundleEntry) + .build(); + + // Process request + FHIRRequestContext.get().setOriginalRequestUri("test"); + FHIRRequestContext.get().setReturnPreference(HTTPReturnPreference.OPERATION_OUTCOME); + try { + helper.doBundle(requestBundle, null); + fail(); + } catch (FHIROperationException e) { + // Validate results + List issues = e.getIssues(); + assertEquals(2, issues.size()); + assertEquals("One or more errors were encountered while validating a 'transaction' request bundle.", issues.get(0).getDetails().getText().getValue()); + assertEquals(IssueSeverity.FATAL, issues.get(0).getSeverity()); + assertEquals(IssueType.INVALID, issues.get(0).getCode()); + assertEquals("A required profile was not specified.", issues.get(1).getDetails().getText().getValue()); + assertEquals(IssueSeverity.ERROR, issues.get(1).getSeverity()); + assertEquals(IssueType.INVALID, issues.get(1).getCode()); + } + } + + /** + * Test bundle with an unsupported profile specified. + */ + @Test + public void testBundleWithUnsupportedProfileSpecified() throws Exception { + Patient patient = Patient.builder() + .id("1") + .meta(Meta.builder() + .profile(Canonical.of("profile1"), Canonical.of("profile5")) + .build()) + .generalPractitioner(Reference.builder() + .reference(string("Practitioner/1")) + .build()) + .text(Narrative.builder() + .div(Xhtml.of("
Some narrative
")) + .status(NarrativeStatus.GENERATED) + .build()) + .build(); + + Bundle.Entry.Request bundleEntryRequest = Bundle.Entry.Request.builder() + .method(HTTPVerb.PUT) + .url(Uri.of("Patient/1")) + .build(); + Bundle.Entry bundleEntry = Bundle.Entry.builder() + .resource(patient) + .request(bundleEntryRequest) + .build(); + + Bundle requestBundle = Bundle.builder() + .id("bundle1") + .type(BundleType.TRANSACTION) + .entry(bundleEntry) + .build(); + + // Process request + FHIRRequestContext.get().setOriginalRequestUri("test"); + FHIRRequestContext.get().setReturnPreference(HTTPReturnPreference.OPERATION_OUTCOME); + try { + helper.doBundle(requestBundle, null); + fail(); + } catch (FHIROperationException e) { + // Validate results + List issues = e.getIssues(); + assertEquals(2, issues.size()); + assertEquals("One or more errors were encountered while validating a 'transaction' request bundle.", issues.get(0).getDetails().getText().getValue()); + assertEquals(IssueSeverity.FATAL, issues.get(0).getSeverity()); + assertEquals(IssueType.INVALID, issues.get(0).getCode()); + assertEquals("Profile 'profile5' is not supported", issues.get(1).getDetails().getText().getValue()); + assertEquals(IssueSeverity.ERROR, issues.get(1).getSeverity()); + assertEquals(IssueType.NOT_SUPPORTED, issues.get(1).getCode()); + } + } + + /** + * Test bundle with a valid required profile specified. + */ + @Test + public void testBundleWithRequiredProfileSpecified() throws Exception { + Patient patient = Patient.builder() + .id("1") + .meta(Meta.builder() + .profile(Canonical.of("profile1")) + .build()) + .generalPractitioner(Reference.builder() + .reference(string("Practitioner/1")) + .build()) + .text(Narrative.builder() + .div(Xhtml.of("
Some narrative
")) + .status(NarrativeStatus.GENERATED) + .build()) + .build(); + + Bundle.Entry.Request bundleEntryRequest = Bundle.Entry.Request.builder() + .method(HTTPVerb.PUT) + .url(Uri.of("Patient/1")) + .build(); + Bundle.Entry bundleEntry = Bundle.Entry.builder() + .resource(patient) + .request(bundleEntryRequest) + .build(); + + Bundle requestBundle = Bundle.builder() + .id("bundle1") + .type(BundleType.TRANSACTION) + .entry(bundleEntry) + .build(); + + // Process request + FHIRRequestContext.get().setOriginalRequestUri("test"); + FHIRRequestContext.get().setReturnPreference(HTTPReturnPreference.OPERATION_OUTCOME); + try { + helper.doBundle(requestBundle, null); + fail(); + } catch (FHIRValidationException e) { + // Validate results. + // Profile assertion validation successful. + // Expected FHIRValidator error due to profile not actually being loaded. + assertEquals("An error occurred during validation", e.getMessage()); + } + } + +} diff --git a/fhir-server/src/test/resources/config/profileValidationConfigTest/fhir-server-config.json b/fhir-server/src/test/resources/config/profileValidationConfigTest/fhir-server-config.json new file mode 100644 index 00000000000..d9f3dbbda66 --- /dev/null +++ b/fhir-server/src/test/resources/config/profileValidationConfigTest/fhir-server-config.json @@ -0,0 +1,42 @@ +{ + "__comment": "FHIR Server configuration", + "fhirServer": { + "core": { + "tenantIdHeaderName": "X-FHIR-TENANT-ID", + "datastoreIdHeaderName": "X-FHIR-DSID", + "checkReferenceTypes": true, + "conditionalDeleteMaxNumber": 10, + "serverRegistryResourceProviderEnabled": true + }, + "resources": { + "open": true, + "Patient": { + "profiles": { + "atLeastOne": [ + "profile1", + "profile2", + "profile3" + ] + } + }, + "Procedure": { + "profiles": { + "atLeastOne": [ + ] + } + }, + "Resource": { + "profiles": { + "atLeastOne": [ + "profile4" + ] + } + } + }, + "audit": { + "serviceClassName" : "com.ibm.fhir.audit.logging.impl.DisabledAuditLogService", + "serviceProperties" : { + } + } + } +}