Skip to content

Commit

Permalink
$validate Operation implementation
Browse files Browse the repository at this point in the history
* [APHL-752] getting NoSuchMethod

* Fixing HAPI dependencies; added test for validate operation

* [APHL-752] basic validate implementation

* [APHL-752] added common codesystems to validation chain

* [APHL-752] make validation failures more obvious

* [APHL-752] update external dep to use the correct version of r5

* [APHL-752] validate simple implementation

* [APHL-752] throw NotImplemented for mode and profile

* [APHL-752] add fullUrls to bundles

* [APHL-752] update active transaction bundle to pass validation

* [APHL-752] update tests

* $validate test case for unsatisfied PlanDef slice

* [APHL-752] updated test for plandefslice

* [APHL-752] validate each entry alone

* [APHL-752] add resource fetcher

* [APHL-778] update package output to distinguish collection and transaction

* [APHL-778] Updated tests

* [APHL-752] validate whole bundle

* [APHL-778] no total for transactions

* [APHL-752] expect extension validation error

---------

Co-authored-by: taha.attari@smilecdr.com <taha.attari@smilecdr.com>
Co-authored-by: c-schuler <hoofschu@gmail.com>
Co-authored-by: Adam Stevenson <stevenson_adam@yahoo.com>
  • Loading branch information
4 people authored Nov 14, 2023
1 parent 2ce23eb commit dd28edd
Show file tree
Hide file tree
Showing 16 changed files with 29,459 additions and 189,284 deletions.
4 changes: 2 additions & 2 deletions core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,13 @@
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>org.hl7.fhir.r5</artifactId>
<version>6.0.15</version>
<version>6.0.1</version>
</dependency>

<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>org.hl7.fhir.convertors</artifactId>
<version>6.0.15</version>
<version>6.0.1</version>
</dependency>
<dependency>
<groupId>org.opencds.cqf.cql</groupId>
Expand Down
6 changes: 6 additions & 0 deletions external/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-base</artifactId>
</dependency>

<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>org.hl7.fhir.r5</artifactId>
<version>6.0.1</version>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-storage</artifactId>
Expand Down
11 changes: 3 additions & 8 deletions plugin/cr/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,13 @@
<dependencies>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>org.hl7.fhir.r5</artifactId>
<version>6.0.15</version>
<artifactId>org.hl7.fhir.validation</artifactId>
<version>6.0.1</version>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>org.hl7.fhir.utilities</artifactId>
<version>6.0.15</version>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>org.hl7.fhir.convertors</artifactId>
<version>6.0.15</version>
<version>6.0.1</version>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -152,9 +152,21 @@ public class KnowledgeArtifactProcessor {
);

private BundleEntryComponent createEntry(IBaseResource theResource) {
return new Bundle.BundleEntryComponent()
.setResource((Resource) theResource)
.setRequest(createRequest(theResource));
BundleEntryComponent entry = new Bundle.BundleEntryComponent()
.setResource((Resource) theResource)
.setRequest(createRequest(theResource));
String fullUrl = entry.getRequest().getUrl();
if (theResource instanceof MetadataResource) {
MetadataResource resource = (MetadataResource) theResource;
if (resource.hasUrl()) {
fullUrl = resource.getUrl();
if (resource.hasVersion()) {
fullUrl += "|" + resource.getVersion();
}
}
}
entry.setFullUrl(fullUrl);
return entry;
}

private BundleEntryRequestComponent createRequest(IBaseResource theResource) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
package org.opencds.cqf.ruler.cr;

import java.io.IOException;
import java.util.Date;
import java.util.List;

import org.cqframework.fhir.api.FhirDal;
import org.hl7.fhir.common.hapi.validation.support.CommonCodeSystemsTerminologyService;
import org.hl7.fhir.common.hapi.validation.support.InMemoryTerminologyServerValidationSupport;
import org.hl7.fhir.common.hapi.validation.support.NpmPackageValidationSupport;
import org.hl7.fhir.common.hapi.validation.support.ValidationSupportChain;
import org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
Expand All @@ -15,6 +21,7 @@
import org.hl7.fhir.r4.model.Endpoint;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.MetadataResource;
import org.hl7.fhir.r4.model.OperationOutcome;
import org.hl7.fhir.r4.model.Reference;
import org.hl7.fhir.r4.model.Resource;
import org.opencds.cqf.cql.evaluator.fhir.util.Canonicals;
Expand All @@ -24,13 +31,19 @@
import org.opencds.cqf.ruler.provider.DaoRegistryOperationProvider;
import org.springframework.beans.factory.annotation.Autowired;

import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.support.DefaultProfileValidationSupport;
import ca.uhn.fhir.jpa.validation.ValidatorResourceFetcher;
import ca.uhn.fhir.model.api.annotation.Description;
import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.Operation;
import ca.uhn.fhir.rest.annotation.OperationParam;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.NotImplementedOperationException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.fhir.validation.FhirValidator;

public class RepositoryService extends DaoRegistryOperationProvider {

Expand Down Expand Up @@ -62,7 +75,7 @@ public class RepositoryService extends DaoRegistryOperationProvider {
* @return An IBaseResource that is the targeted resource, updated with the approval
*/
@Operation(name = "$crmi.approve", idempotent = true, global = true, type = MetadataResource.class)
@Description(shortDefinition = "$approve", value = "Apply an approval to an existing artifact, regardless of status.")
@Description(shortDefinition = "$crmi.approve", value = "Apply an approval to an existing artifact, regardless of status.")
public Bundle approveOperation(
RequestDetails requestDetails,
@IdParam IdType theId,
Expand Down Expand Up @@ -128,7 +141,7 @@ public Bundle approveOperation(
* @return A transaction bundle result of the newly created resources
*/
@Operation(name = "$crmi.draft", idempotent = true, global = true, type = MetadataResource.class)
@Description(shortDefinition = "$draft", value = "Create a new draft version of the reference artifact")
@Description(shortDefinition = "$crmi.draft", value = "Create a new draft version of the reference artifact")
public Bundle draftOperation(RequestDetails requestDetails, @IdParam IdType theId, @OperationParam(name = "version") String version)
throws FHIRException {
FhirDal fhirDal = this.fhirDalFactory.create(requestDetails);
Expand All @@ -145,7 +158,7 @@ public Bundle draftOperation(RequestDetails requestDetails, @IdParam IdType theI
* @return A transaction bundle result of the updated resources
*/
@Operation(name = "$crmi.release", idempotent = true, global = true, type = MetadataResource.class)
@Description(shortDefinition = "$release", value = "Release an existing draft artifact")
@Description(shortDefinition = "$crmi.release", value = "Release an existing draft artifact")
public Bundle releaseOperation(
RequestDetails requestDetails,
@IdParam IdType theId,
Expand Down Expand Up @@ -175,7 +188,7 @@ public Bundle releaseOperation(
}

@Operation(name = "$crmi.package", idempotent = true, global = true, type = MetadataResource.class)
@Description(shortDefinition = "$package", value = "Package an artifact and components / dependencies")
@Description(shortDefinition = "$crmi.package", value = "Package an artifact and components / dependencies")
public Bundle packageOperation(
RequestDetails requestDetails,
@IdParam IdType theId,
Expand Down Expand Up @@ -211,14 +224,56 @@ public Bundle packageOperation(
}

@Operation(name = "$crmi.revise", idempotent = true, global = true, type = MetadataResource.class)
@Description(shortDefinition = "$revise", value = "Update an existing artifact in 'draft' status")
@Description(shortDefinition = "$crmi.revise", value = "Update an existing artifact in 'draft' status")
public IBaseResource reviseOperation(RequestDetails requestDetails, @OperationParam(name = "resource") IBaseResource resource)
throws FHIRException {

FhirDal fhirDal = fhirDalFactory.create(requestDetails);
return (IBaseResource)this.artifactProcessor.revise(fhirDal, (MetadataResource) resource);
}

@Operation(name = "$validate", idempotent = true, global = true, type = MetadataResource.class)
@Description(shortDefinition = "$validate", value = "Validate a bundle")
public OperationOutcome validateOperation(RequestDetails requestDetails,
@OperationParam(name = "resource") IBaseResource resource,
@OperationParam(name = "mode") CodeType mode,
@OperationParam(name = "profile") String profile
)
throws FHIRException {
if (mode != null) {
throw new NotImplementedOperationException("'mode' Parameter not implemented yet.");
}
if (profile != null) {
throw new NotImplementedOperationException("'profile' Parameter not implemented yet.");
}
if (resource == null) {
throw new UnprocessableEntityException("A FHIR resource must be provided for validation");
}
FhirContext ctx = this.getFhirContext();
if (ctx != null) {
FhirValidator fhirValidator = ctx.newValidator();
fhirValidator.setValidateAgainstStandardSchema(false);
fhirValidator.setValidateAgainstStandardSchematron(false);
NpmPackageValidationSupport npm = new NpmPackageValidationSupport(ctx);
try {
npm.loadPackageFromClasspath("classpath:hl7.fhir.us.ecr-2.1.0.tgz");
} catch (IOException e) {
throw new InternalErrorException("Could not load package");
}
ValidationSupportChain chain = new ValidationSupportChain(
npm,
new DefaultProfileValidationSupport(ctx),
new InMemoryTerminologyServerValidationSupport(ctx),
new CommonCodeSystemsTerminologyService(ctx)
);
FhirInstanceValidator instanceValidatorModule = new FhirInstanceValidator(chain);
instanceValidatorModule.setValidatorResourceFetcher(new ValidatorResourceFetcher(ctx, chain, getDaoRegistry()));
fhirValidator.registerValidatorModule(instanceValidatorModule);
return (OperationOutcome) fhirValidator.validateWithResult(resource, null).toOperationOutcome();
} else {
throw new InternalErrorException("Could not load FHIR Context");
}
}
private BundleEntryComponent createEntry(IBaseResource theResource) {
return new Bundle.BundleEntryComponent()
.setResource((Resource) theResource)
Expand Down
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@
import org.hl7.fhir.r4.model.Library;
import org.hl7.fhir.r4.model.Measure;
import org.hl7.fhir.r4.model.MetadataResource;
import org.hl7.fhir.r4.model.OperationOutcome;
import org.hl7.fhir.r4.model.OperationOutcome.IssueSeverity;
import org.hl7.fhir.r4.model.OperationOutcome.OperationOutcomeIssueComponent;
import org.hl7.fhir.r4.model.Parameters;
import org.hl7.fhir.r4.model.PlanDefinition;
import org.hl7.fhir.r4.model.Reference;
Expand Down Expand Up @@ -69,7 +72,7 @@
properties = {"hapi.fhir.fhir_version=r4", "hapi.fhir.security.basic_auth.enabled=false"})
class RepositoryServiceTest extends RestIntegrationTest {
private final String specificationLibReference = "Library/SpecificationLibrary";
private final String minimalLibReference = "Library/SpecificationLibraryDraftVersion-1-1-1-23";
private final String minimalLibReference = "Library/SpecificationLibraryDraftVersion-1-0-0-23";
private final List<String> badVersionList = Arrays.asList(
"11asd1",
"1.1.3.1.1",
Expand Down Expand Up @@ -129,7 +132,7 @@ void draftOperation_test() {
void draftOperation_version_conflict_test() {
loadTransaction("ersd-active-transaction-bundle-example.json");
loadResource("minimal-draft-to-test-version-conflict.json");
Parameters params = parameters(part("version", "1.1.1.23") );
Parameters params = parameters(part("version", "1.0.0.23") );
String maybeException = null;
try {
getClient().operation()
Expand Down Expand Up @@ -1222,5 +1225,108 @@ void packageOperation_big_bundle() {
.execute();
assertTrue(packagedBundle.getEntry().size() == loadedBundle.getEntry().size());
}
}

@Test
void validateOperation() {
Bundle ersdExampleSpecBundle = (Bundle) loadResource("ersd-bundle-example.json");
Parameters specBundleParams = parameters(
part("resource", ersdExampleSpecBundle)
);
OperationOutcome specBundleOutcome = getClient().operation()
.onServer()
.named("$validate")
.withParameters(specBundleParams)
.returnResourceType(OperationOutcome.class)
.execute();
List<OperationOutcomeIssueComponent> specBundleValidationErrors = specBundleOutcome.getIssue().stream().filter((issue) -> issue.getSeverity() == IssueSeverity.ERROR || issue.getSeverity() == IssueSeverity.FATAL).collect(Collectors.toList());
assertTrue(specBundleValidationErrors.size() == 3);
// expect errors for Variable extension which bubble up and invalidate the PlanDefinition slice
assertTrue(specBundleValidationErrors.get(0).getDiagnostics().contains("slicePlanDefinition"));
assertTrue(specBundleValidationErrors.get(1).getDiagnostics().contains("variable"));
assertTrue(specBundleValidationErrors.get(2).getDiagnostics().contains("variable"));
Bundle ersdExampleSupplementalBundle = (Bundle) loadResource("ersd-supplemental-bundle-example.json");
Parameters supplementalBundleParams = parameters(
part("resource", ersdExampleSupplementalBundle)
);
OperationOutcome supplementalBundleOutcome = getClient().operation()
.onServer()
.named("$validate")
.withParameters(supplementalBundleParams)
.returnResourceType(OperationOutcome.class)
.execute();
List<OperationOutcomeIssueComponent> supplementalBundleErrors = supplementalBundleOutcome.getIssue().stream().filter((issue) -> issue.getSeverity() == IssueSeverity.ERROR || issue.getSeverity() == IssueSeverity.FATAL).collect(Collectors.toList());
assertTrue(supplementalBundleErrors.size() == 0);

Library validationErrorLibrary = (Library) loadResource("ersd-active-library-us-ph-validation-failure-example.json");
Parameters validationFailedParams = parameters(
part("resource", validationErrorLibrary)
);
OperationOutcome failedValidationOutcome = getClient().operation()
.onServer()
.named("$validate")
.withParameters(validationFailedParams)
.returnResourceType(OperationOutcome.class)
.execute();
List<OperationOutcomeIssueComponent> invalidLibraryErrors = failedValidationOutcome.getIssue().stream().filter((issue) -> issue.getSeverity() == IssueSeverity.ERROR || issue.getSeverity() == IssueSeverity.FATAL).collect(Collectors.toList());
assertTrue(invalidLibraryErrors.size() == 5);

Parameters noResourceParams = parameters();
UnprocessableEntityException noResourceException = null;
try {
getClient().operation()
.onServer()
.named("$validate")
.withParameters(noResourceParams)
.returnResourceType(OperationOutcome.class)
.execute();
} catch (UnprocessableEntityException e) {
noResourceException = e;
}
assertNotNull(noResourceException);
assertTrue(noResourceException.getMessage().contains("resource must be provided"));
}

@Test
void validateOperationUnqualifiedRelatedArtifact() {
Bundle ersdExampleSpecBundleUnqualifiedPlanDefinition = (Bundle) loadResource("ersd-library-validation-failure-unqualified-plandefinition-bundle.json");
Parameters validationFailedUnqualifiedPlanDefinitionParams = parameters(
part("resource", ersdExampleSpecBundleUnqualifiedPlanDefinition)
);
OperationOutcome failedValidationUnqualifiedPlanDefinitionOutcome = getClient().operation()
.onServer()
.named("$validate")
.withParameters(validationFailedUnqualifiedPlanDefinitionParams)
.returnResourceType(OperationOutcome.class)
.execute();
boolean missingPlanDefinitionSliceErrorExists = failedValidationUnqualifiedPlanDefinitionOutcome.getIssue().stream()
.anyMatch((issue) -> issue.getDiagnostics().contains("Library.relatedArtifact:slicePlanDefinition: minimum required = 1, but only found 0 (from http://hl7.org/fhir/us/ecr/StructureDefinition/us-ph-specification-library|2.1.0)"));
assertTrue(missingPlanDefinitionSliceErrorExists);
}

@Test
void validatePackageOutput() {
loadTransaction("ersd-active-transaction-bundle-example.json");
Bundle packagedBundle = getClient().operation()
.onInstance(specificationLibReference)
.named("$crmi.package")
.withParameters(parameters())
.returnResourceType(Bundle.class)
.execute();
assertTrue(packagedBundle.getEntry().size() == 37);
Parameters packagedBundleParams = parameters(
part("resource", packagedBundle)
);
OperationOutcome packagedBundleOutcome = getClient().operation()
.onServer()
.named("$validate")
.withParameters(packagedBundleParams)
.returnResourceType(OperationOutcome.class)
.execute();
List<OperationOutcomeIssueComponent> errors = packagedBundleOutcome.getIssue().stream().filter((issue) -> issue.getSeverity() == IssueSeverity.ERROR || issue.getSeverity() == IssueSeverity.FATAL).collect(Collectors.toList());
assertTrue(errors.size() == 3);
// expect errors for Variable extension which bubble up and invalidate the PlanDefinition slice
assertTrue(errors.get(0).getDiagnostics().contains("slicePlanDefinition"));
assertTrue(errors.get(1).getDiagnostics().contains("variable"));
assertTrue(errors.get(2).getDiagnostics().contains("variable"));
}
}
Loading

0 comments on commit dd28edd

Please sign in to comment.