Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Aphl 752 validate operation #747

Merged
merged 25 commits into from
Nov 14, 2023
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
e4eb830
[APHL-752] getting NoSuchMethod
Oct 16, 2023
d6c9b92
Fixing HAPI dependencies; added test for validate operation
c-schuler Oct 17, 2023
19be687
[APHL-752] basic validate implementation
Oct 18, 2023
3e46683
[APHL-752] added common codesystems to validation chain
Oct 18, 2023
2bf896e
[APHL-752] make validation failures more obvious
Oct 18, 2023
bac6552
[APHL-752] update external dep to use the correct version of r5
Oct 23, 2023
1cc4320
[APHL-752] validate simple implementation
Oct 24, 2023
de21dc9
[APHL-752] semicolon
Oct 24, 2023
02e347e
[APHL-752] throw NotImplemented for mode and profile
Oct 24, 2023
824c07b
unused logger
Oct 24, 2023
00b59e8
[APHL-752] add fullUrls to bundles
Oct 24, 2023
4181bb1
[APHL-752] update active transaction bundle to pass validation
Oct 24, 2023
baf75c3
[APHL-752] update tests
Oct 24, 2023
db2f319
$validate test case for unsatisfied PlanDef slice
sliver007 Oct 26, 2023
059e373
[APHL-752] updated test for plandefslice
Oct 26, 2023
5077954
[APHL-752] validate each entry alone
Oct 27, 2023
ea041c3
[APHL-752] add resource fetcher
Nov 9, 2023
4142481
[APHL-778] update package output to distinguish collection and transa…
Nov 9, 2023
f303a05
[APHL-778] Updated tests
Nov 9, 2023
4757147
Merge branch 'APHL-778-package-update' into aphl-752-validate-operation
Nov 9, 2023
a04d036
[APHL-752] validate whole bundle
Nov 9, 2023
2241168
[APHL-778] no total for transactions
Nov 9, 2023
e3a52a1
Merge branch 'APHL-778-package-update' into aphl-752-validate-operation
Nov 9, 2023
f1ebbd5
[APHL-752] expect extension validation error
Nov 9, 2023
fd1194c
Merge branch 'vsm_operations' into aphl-752-validate-operation
sliver007 Nov 14, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -151,9 +151,21 @@ public class KnowledgeArtifactProcessor {
);

private BundleEntryComponent createEntry(IBaseResource theResource) {
return new Bundle.BundleEntryComponent()
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();
sliver007 marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
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,18 @@
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.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 +74,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 +140,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 +157,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 +187,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 @@ -213,13 +225,55 @@ 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 validator = ctx.newValidator();
validator.setValidateAgainstStandardSchema(false);
validator.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 myInstanceVal = new FhirInstanceValidator(chain);
validator.registerValidatorModule(myInstanceVal);
return (OperationOutcome) validator.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 @@ -36,6 +36,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 @@ -68,7 +71,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 @@ -128,7 +131,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 @@ -1153,5 +1156,105 @@ 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() == 0);

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();
Parameters packagedBundleParams = parameters(
part("resource", packagedBundle)
);
OperationOutcome packagedBundleOutcome = getClient().operation()
.onServer()
.named("$validate")
.withParameters(packagedBundleParams)
.returnResourceType(OperationOutcome.class)
.execute();
List<OperationOutcomeIssueComponent> packagedBundleValidationErrors = packagedBundleOutcome.getIssue().stream().filter((issue) -> issue.getSeverity() == IssueSeverity.ERROR || issue.getSeverity() == IssueSeverity.FATAL).collect(Collectors.toList());
// currently the packaged bundle is not valid due to:
// - bundle.total
// - bundle.entry.request
// being present on a bundle with type = collection
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

may need new profiles to avoid these validation failures

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't profile at the bundle level because of the possibility that the package will span multiple bundles.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍🏾 I'll split the test into individual validations on example resources to match our future workflow

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

back to validating whole bundle based on discussion

assertTrue(packagedBundleValidationErrors.size() == 2);
}
}

Loading