Skip to content

Commit

Permalink
[APHL-780] initial implementation (#751)
Browse files Browse the repository at this point in the history
* [APHL-750] initial implementation

* [APHL-750] draft implementation with Parameters tree

* [APHL-750] cache and remove duplicates

* [APHL-750] fix draft bug where dependencies pointing at owned resources were not updated

* [APHL-750] fix draft issues

* [APHL-780] fix tests

* [APHL-780] updated test and test data, now handling gaps in reference lists

* [APHL-780] updated addition deletion logic

* [APHL-780] temporarily expand ValueSets as part of diff

* [APHL-780] refactoring

* [APHL-780] fix array indices on output

* [APHL-780] update test data and tests

* [APHL-780] fix tests and test cases

* [APHL-780] remove extra file

* [APHL-780] cleanup

* [APHL-780] cleanup

* [APHL-780] implement cloneable

* [APHL-780] remove namespace

* [APHL-780] remove crmi namespace

* Updated test case to use new forEachMetadataResource method introduced by previous merge.

---------

Co-authored-by: taha.attari@smilecdr.com <taha.attari@smilecdr.com>
Co-authored-by: Adam Stevenson <stevenson_adam@yahoo.com>
  • Loading branch information
3 people authored Jan 5, 2024
1 parent def35cc commit cef0efe
Show file tree
Hide file tree
Showing 7 changed files with 1,568 additions and 462 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,15 @@
import org.hl7.fhir.r4.model.ValueSet;
import org.opencds.cqf.ruler.utility.SemanticVersion;

public class KnowledgeArtifactAdapter<T extends MetadataResource> {
public class KnowledgeArtifactAdapter<T extends MetadataResource> implements Cloneable {
protected T resource;

public KnowledgeArtifactAdapter(T resource) {
this.resource = resource;
}

public KnowledgeArtifactAdapter<MetadataResource> clone() {
return new KnowledgeArtifactAdapter<MetadataResource>(this.copy());
}
public Date getApprovalDate() {
switch (resource.getClass().getSimpleName()) {
case "ActivityDefinition":
Expand Down Expand Up @@ -142,10 +144,10 @@ public List<RelatedArtifact> getOwnedRelatedArtifacts(){
}
private List<RelatedArtifact> getOwnedRelatedArtifactsOfKnowledgeArtifact() {
return getRelatedArtifact().stream()
.filter(ra -> checkIfRelatedArtifactIsOwned(ra))
.filter(KnowledgeArtifactAdapter::checkIfRelatedArtifactIsOwned)
.collect(Collectors.toList());
}
static Boolean checkIfRelatedArtifactIsOwned(RelatedArtifact ra){
public static Boolean checkIfRelatedArtifactIsOwned(RelatedArtifact ra){
return ra.getExtension()
.stream()
.filter(ext -> ext.getUrl().equals("http://hl7.org/fhir/StructureDefinition/crmi-isOwned"))
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@
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.Parameters;
import org.hl7.fhir.r4.model.Reference;
import org.hl7.fhir.r4.model.Resource;
import org.hl7.fhir.r4.model.ValueSet;
import org.opencds.cqf.cql.evaluator.fhir.util.Canonicals;
import org.opencds.cqf.ruler.cr.r4.ArtifactAssessment;
import org.opencds.cqf.ruler.cr.r4.CRMIReleaseExperimentalBehavior.CRMIReleaseExperimentalBehaviorCodes;
Expand All @@ -33,6 +35,7 @@

import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.support.DefaultProfileValidationSupport;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoValueSet;
import ca.uhn.fhir.jpa.validation.ValidatorResourceFetcher;
import ca.uhn.fhir.model.api.annotation.Description;
import ca.uhn.fhir.rest.annotation.IdParam;
Expand Down Expand Up @@ -274,6 +277,30 @@ public OperationOutcome validateOperation(RequestDetails requestDetails,
throw new InternalErrorException("Could not load FHIR Context");
}
}

@Operation(name = "$artifact-diff", idempotent = true, global = true, type = MetadataResource.class)
@Description(shortDefinition = "$artifact-diff", value = "Diff two knowledge artifacts")
public Parameters crmiArtifactDiff(RequestDetails requestDetails,
@OperationParam(name = "source") String source,
@OperationParam(name = "target") String target,
@OperationParam(name = "compareExecutable", typeName = "Boolean") IPrimitiveType<Boolean> compareExecutable,
@OperationParam(name = "compareComputable", typeName = "Boolean") IPrimitiveType<Boolean> compareComputable
) throws UnprocessableEntityException, ResourceNotFoundException{
FhirDal fhirDal = fhirDalFactory.create(requestDetails);
IBaseResource theSourceResource = fhirDal.read(new IdType(source));
if (theSourceResource == null || !(theSourceResource instanceof MetadataResource)) {
throw new UnprocessableEntityException("Source resource must exist and be a Knowledge Artifact type.");
}
IBaseResource theTargetResource = fhirDal.read(new IdType(target));
if (theTargetResource == null || !(theTargetResource instanceof MetadataResource)) {
throw new UnprocessableEntityException("Target resource must exist and be a Knowledge Artifact type.");
}
if (theSourceResource.getClass() != theTargetResource.getClass()) {
throw new UnprocessableEntityException("Source and target resources must be of the same type.");
}
IFhirResourceDaoValueSet<ValueSet> dao = (IFhirResourceDaoValueSet<ValueSet>)this.getDaoRegistry().getResourceDao(ValueSet.class);
return this.artifactProcessor.artifactDiff((MetadataResource)theSourceResource,(MetadataResource)theTargetResource,this.getFhirContext(),fhirDal,compareComputable == null ? false : compareComputable.getValue(), compareExecutable == null ? false : compareExecutable.getValue(),dao);
}
private BundleEntryComponent createEntry(IBaseResource theResource) {
return new Bundle.BundleEntryComponent()
.setResource((Resource) theResource)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import java.util.Map.Entry;
import java.util.Optional;
import java.util.concurrent.ThreadLocalRandom;
import java.util.function.Consumer;
import java.util.stream.Collectors;

import org.hl7.fhir.r4.model.ActivityDefinition;
Expand All @@ -41,6 +42,7 @@
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.Parameters.ParametersParameterComponent;
import org.hl7.fhir.r4.model.PlanDefinition;
import org.hl7.fhir.r4.model.Reference;
import org.hl7.fhir.r4.model.RelatedArtifact;
Expand Down Expand Up @@ -94,10 +96,10 @@ class RepositoryServiceTest extends RestIntegrationTest {
void draftOperation_test() {
loadTransaction("ersd-active-transaction-bundle-example.json");
Library baseLib = getClient()
.read()
.resource(Library.class)
.withId(specificationLibReference.split("/")[1])
.execute();
.read()
.resource(Library.class)
.withId(specificationLibReference.split("/")[1])
.execute();
// Root Artifact must have approval date, releaseLabel and releaseDescription for this test
assertTrue(baseLib.hasApprovalDate());
assertTrue(baseLib.hasExtension(KnowledgeArtifactProcessor.releaseDescriptionUrl));
Expand Down Expand Up @@ -125,8 +127,16 @@ void draftOperation_test() {
assertFalse(lib.hasExtension(KnowledgeArtifactProcessor.releaseLabelUrl));
List<RelatedArtifact> relatedArtifacts = lib.getRelatedArtifact();
assertTrue(!relatedArtifacts.isEmpty());
assertTrue(Canonicals.getVersion(relatedArtifacts.get(0).getResource()).equals(draftedVersion));
assertTrue(Canonicals.getVersion(relatedArtifacts.get(1).getResource()).equals(draftedVersion));
forEachMetadataResource(returnedBundle.getEntry(), resource -> {
List<RelatedArtifact> relatedArtifacts2 = new KnowledgeArtifactAdapter<MetadataResource>(resource).getRelatedArtifact();
if (relatedArtifacts2 != null && relatedArtifacts2.size() > 0) {
for (RelatedArtifact relatedArtifact : relatedArtifacts2) {
if (KnowledgeArtifactAdapter.checkIfRelatedArtifactIsOwned(relatedArtifact)) {
assertTrue(Canonicals.getVersion(relatedArtifact.getResource()).equals(draftedVersion));
}
}
}
});
}
@Test
void draftOperation_no_effectivePeriod_test() {
Expand All @@ -151,12 +161,11 @@ void draftOperation_no_effectivePeriod_test() {
.withParameters(params)
.returnResourceType(Bundle.class)
.execute();
getMetadataResourcesFromBundle(returnedBundle)
.stream()
.forEach(resource -> {
KnowledgeArtifactAdapter<MetadataResource> adapter = new KnowledgeArtifactAdapter<MetadataResource>(resource);
assertFalse(adapter.getEffectivePeriod().hasStart() || adapter.getEffectivePeriod().hasEnd());
});

forEachMetadataResource(returnedBundle.getEntry(), resource -> {
KnowledgeArtifactAdapter<MetadataResource> adapter = new KnowledgeArtifactAdapter<MetadataResource>(resource);
assertFalse(adapter.getEffectivePeriod().hasStart() || adapter.getEffectivePeriod().hasEnd());
});
}
@Test
void draftOperation_version_conflict_test() {
Expand Down Expand Up @@ -471,27 +480,24 @@ void releaseResource_propagate_effective_period() {
.execute();

assertNotNull(returnResource);
getMetadataResourcesFromBundle(returnResource)
.stream()
.forEach(resource -> {
assertNotNull(resource);
if(!resource.getClass().getSimpleName().equals("ValueSet")){
KnowledgeArtifactAdapter<MetadataResource> adapter = new KnowledgeArtifactAdapter<>(resource);
assertTrue(adapter.getEffectivePeriod().hasStart());
Date start = adapter.getEffectivePeriod().getStart();
Calendar calendar = new GregorianCalendar();
calendar.setTime(start);
int year = calendar.get(Calendar.YEAR);
int month = calendar.get(Calendar.MONTH) + 1;
int day = calendar.get(Calendar.DAY_OF_MONTH);
String startString = year + "-" + month + "-" + day;
assertTrue(startString.equals(effectivePeriodToPropagate));
}
});
forEachMetadataResource(returnResource.getEntry(), resource -> {
assertNotNull(resource);
if(!resource.getClass().getSimpleName().equals("ValueSet")){
KnowledgeArtifactAdapter<MetadataResource> adapter = new KnowledgeArtifactAdapter<>(resource);
assertTrue(adapter.getEffectivePeriod().hasStart());
Date start = adapter.getEffectivePeriod().getStart();
Calendar calendar = new GregorianCalendar();
calendar.setTime(start);
int year = calendar.get(Calendar.YEAR);
int month = calendar.get(Calendar.MONTH) + 1;
int day = calendar.get(Calendar.DAY_OF_MONTH);
String startString = year + "-" + month + "-" + day;
assertTrue(startString.equals(effectivePeriodToPropagate));
}
});
}
List<MetadataResource> getMetadataResourcesFromBundle(Bundle bundle) {
return bundle.getEntry()
.stream()
private void forEachMetadataResource(List<BundleEntryComponent> entries, Consumer<MetadataResource> callback) {
entries.stream()
.map(entry -> entry.getResponse().getLocation())
.map(location -> {
switch (location.split("/")[0]) {
Expand All @@ -508,7 +514,8 @@ List<MetadataResource> getMetadataResourcesFromBundle(Bundle bundle) {
default:
return null;
}
}).collect(Collectors.toList());
})
.forEach(callback);
}
@Test
void releaseResource_latestFromTx_NotSupported_test() {
Expand Down Expand Up @@ -1392,4 +1399,152 @@ void validatePackageOutput() {
assertTrue(errors.get(2).getDiagnostics().contains("variable"));
assertTrue(errors.get(3).getDiagnostics().contains("variable"));
}

@Test
void basic_artifact_diff() {
loadTransaction("ersd-small-active-bundle.json");
Bundle bundle = (Bundle) loadTransaction("small-drafted-ersd-bundle.json");
Optional<BundleEntryComponent> maybeLib = bundle.getEntry().stream().filter(entry -> entry.getResponse().getLocation().contains("Library")).findFirst();
loadResource("artifactAssessment-search-parameter.json");
Parameters diffParams = parameters(
part("source", specificationLibReference),
part("target", maybeLib.get().getResponse().getLocation())
);
Parameters returnedParams = getClient().operation()
.onServer()
.named("artifact-diff")
.withParameters(diffParams)
.returnResourceType(Parameters.class)
.execute();
List<ParametersParameterComponent> parameters = returnedParams.getParameter();
List<ParametersParameterComponent> libraryReplaceOperations = getOperationsByType(parameters, "replace");
List<ParametersParameterComponent> libraryInsertOperations = getOperationsByType(parameters, "insert");
List<ParametersParameterComponent> libraryDeleteOperations = getOperationsByType(parameters, "delete");
List<String> libraryReplacedPaths = List.of(
"Library.id",
"Library.version",
"Library.status",
"Library.relatedArtifact[0].resource", // planDefinition version update
"Library.relatedArtifact[1].resource", // RCTC lib version update
"Library.relatedArtifact[4].resource" // DXTC Grouper version update
);
List<String> libraryDeletedPaths = List.of(
"Library.relatedArtifact[5]" // deleted DXTC leaf VS
);
List<String> libraryInsertedPaths = List.of(
"Library.relatedArtifact" // new DXTC leaf VS
);
for (ParametersParameterComponent param: libraryReplaceOperations) {
String path = param.getPart().stream()
.filter(part -> part.getName().equals("path"))
.map(part -> ((StringType)part.getValue()).getValue())
.findFirst().get();
assertTrue(libraryReplacedPaths.contains(path));
}
for (ParametersParameterComponent param: libraryInsertOperations) {
String path = param.getPart().stream()
.filter(part -> part.getName().equals("path"))
.map(part -> ((StringType)part.getValue()).getValue())
.findFirst().get();
assertTrue(libraryInsertedPaths.contains(path));
}
for (ParametersParameterComponent param: libraryDeleteOperations) {
String path = param.getPart().stream()
.filter(part -> part.getName().equals("path"))
.map(part -> ((StringType)part.getValue()).getValue())
.findFirst().get();
assertTrue(libraryDeletedPaths.contains(path));
}
List<String> libraryNestedChanges = List.of(
"http://ersd.aimsplatform.org/fhir/PlanDefinition/us-ecr-specification",
"http://ersd.aimsplatform.org/fhir/Library/rctc",
"http://notOwnedTest.com/Library/notOwnedRoot", // will be empty / unable to retrieve
"http://cts.nlm.nih.gov/fhir/ValueSet/123-this-will-be-routine",
"http://ersd.aimsplatform.org/fhir/ValueSet/dxtc",
"http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1146.163", // the new VS added to the DXTC
"http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1146.6" // the VS deleted from the DXTC
);
libraryNestedChanges.stream()
.forEach(nestedChangeUrl -> {
ParametersParameterComponent nestedChange = returnedParams.getParameter(nestedChangeUrl);
assertNotNull(nestedChange);
if (nestedChange.hasResource()) {
assertTrue(nestedChange.getResource() instanceof Parameters);
}
});
}
@Test
void artifact_diff_compare_computable() {
loadTransaction("ersd-small-active-bundle.json");
Bundle bundle = (Bundle) loadTransaction("small-drafted-ersd-bundle.json");
Optional<BundleEntryComponent> maybeLib = bundle.getEntry().stream().filter(entry -> entry.getResponse().getLocation().contains("Library")).findFirst();
loadResource("artifactAssessment-search-parameter.json");
Parameters diffParams = parameters(
part("source", specificationLibReference),
part("target", maybeLib.get().getResponse().getLocation()),
part("compareComputable", new BooleanType(true))
);
Parameters returnedParams = getClient().operation()
.onServer()
.named("$artifact-diff")
.withParameters(diffParams)
.returnResourceType(Parameters.class)
.execute();
List<Parameters> nestedChanges = returnedParams.getParameter().stream()
.filter(p -> !p.getName().equals("operation"))
.map(p -> (Parameters)p.getResource())
.filter(p -> p != null)
.collect(Collectors.toList());
assertTrue(nestedChanges.size() == 3);
Parameters grouperChanges = returnedParams.getParameter().stream().filter(p -> p.getName().contains("/dxtc")).map(p-> (Parameters)p.getResource()).findFirst().get();
List<ParametersParameterComponent> deleteOperations = getOperationsByType(grouperChanges.getParameter(), "delete");
List<ParametersParameterComponent> insertOperations = getOperationsByType(grouperChanges.getParameter(), "insert");
// delete the old leaf
assertTrue(deleteOperations.size() == 1);
// there aren't actually 2 operations here
assertTrue(insertOperations.size() == 2);
String path1 = insertOperations.get(0).getPart().stream().filter(p -> p.getName().equals("path")).map(p -> ((StringType)p.getValue()).getValue()).findFirst().get();
String path2 = insertOperations.get(1).getPart().stream().filter(p -> p.getName().equals("path")).map(p -> ((StringType)p.getValue()).getValue()).findFirst().get();
// insert the new leaf; adding a node takes multiple operations if
// the thing being added isn't a defined complex FHIR type
assertTrue(path1.equals("ValueSet.compose.include"));
assertTrue(path2.equals("ValueSet.compose.include[1].valueSet"));
}
@Test
void artifact_diff_compare_executable() {
loadTransaction("ersd-small-active-bundle.json");
Bundle bundle = (Bundle) loadTransaction("small-drafted-ersd-bundle.json");
Optional<BundleEntryComponent> maybeLib = bundle.getEntry().stream().filter(entry -> entry.getResponse().getLocation().contains("Library")).findFirst();
loadResource("artifactAssessment-search-parameter.json");
Parameters diffParams = parameters(
part("source", specificationLibReference),
part("target", maybeLib.get().getResponse().getLocation()),
part("compareExecutable", new BooleanType(true))
);
Parameters returnedParams = getClient().operation()
.onServer()
.named("$artifact-diff")
.withParameters(diffParams)
.returnResourceType(Parameters.class)
.execute();
List<Parameters> nestedChanges = returnedParams.getParameter().stream()
.filter(p -> !p.getName().equals("operation"))
.map(p -> (Parameters)p.getResource())
.filter(p -> p != null)
.collect(Collectors.toList());
assertTrue(nestedChanges.size() == 3);
Parameters grouperChanges = returnedParams.getParameter().stream().filter(p -> p.getName().contains("/dxtc")).map(p-> (Parameters)p.getResource()).findFirst().get();
List<ParametersParameterComponent> deleteOperations = getOperationsByType(grouperChanges.getParameter(), "delete");
List<ParametersParameterComponent> insertOperations = getOperationsByType(grouperChanges.getParameter(), "insert");
// old codes removed
assertTrue(deleteOperations.size() == 23);
// new codes added
assertTrue(insertOperations.size() == 32);
}
private List<ParametersParameterComponent> getOperationsByType(List<ParametersParameterComponent> parameters, String type) {
return parameters.stream().filter(
p -> p.getName().equals("operation")
&& p.getPart().stream().anyMatch(part -> part.getName().equals("type") && ((CodeType)part.getValue()).getCode().equals(type))
).collect(Collectors.toList());
}
}
Loading

0 comments on commit cef0efe

Please sign in to comment.