From cef0efe3498eed5700ba3433081eb376ccc785f5 Mon Sep 17 00:00:00 2001 From: Taha Date: Fri, 5 Jan 2024 11:08:27 -0800 Subject: [PATCH] [APHL-780] initial implementation (#751) * [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 Co-authored-by: Adam Stevenson --- .../ruler/cr/KnowledgeArtifactAdapter.java | 10 +- .../ruler/cr/KnowledgeArtifactProcessor.java | 540 +++++++++++++++- .../cqf/ruler/cr/RepositoryService.java | 27 + .../ruler/cr/r4/RepositoryServiceTest.java | 221 ++++++- .../resources/ersd-small-active-bundle.json | 156 ++--- .../ersd-small-approved-draft-bundle.json | 611 +++++++++--------- .../resources/small-drafted-ersd-bundle.json | 465 +++++++++++++ 7 files changed, 1568 insertions(+), 462 deletions(-) create mode 100644 plugin/cr/src/test/resources/small-drafted-ersd-bundle.json diff --git a/plugin/cr/src/main/java/org/opencds/cqf/ruler/cr/KnowledgeArtifactAdapter.java b/plugin/cr/src/main/java/org/opencds/cqf/ruler/cr/KnowledgeArtifactAdapter.java index 7039dc2e5..a986e886e 100644 --- a/plugin/cr/src/main/java/org/opencds/cqf/ruler/cr/KnowledgeArtifactAdapter.java +++ b/plugin/cr/src/main/java/org/opencds/cqf/ruler/cr/KnowledgeArtifactAdapter.java @@ -21,13 +21,15 @@ import org.hl7.fhir.r4.model.ValueSet; import org.opencds.cqf.ruler.utility.SemanticVersion; -public class KnowledgeArtifactAdapter { +public class KnowledgeArtifactAdapter implements Cloneable { protected T resource; public KnowledgeArtifactAdapter(T resource) { this.resource = resource; } - + public KnowledgeArtifactAdapter clone() { + return new KnowledgeArtifactAdapter(this.copy()); + } public Date getApprovalDate() { switch (resource.getClass().getSimpleName()) { case "ActivityDefinition": @@ -142,10 +144,10 @@ public List getOwnedRelatedArtifacts(){ } private List 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")) diff --git a/plugin/cr/src/main/java/org/opencds/cqf/ruler/cr/KnowledgeArtifactProcessor.java b/plugin/cr/src/main/java/org/opencds/cqf/ruler/cr/KnowledgeArtifactProcessor.java index ec7de7df2..ddc7f85f2 100644 --- a/plugin/cr/src/main/java/org/opencds/cqf/ruler/cr/KnowledgeArtifactProcessor.java +++ b/plugin/cr/src/main/java/org/opencds/cqf/ruler/cr/KnowledgeArtifactProcessor.java @@ -8,6 +8,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.TreeSet; import java.util.UUID; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -34,10 +35,14 @@ import org.hl7.fhir.r4.model.Enumerations; import org.hl7.fhir.r4.model.Extension; import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.IntegerType; import org.hl7.fhir.r4.model.Library; import org.hl7.fhir.r4.model.MarkdownType; import org.hl7.fhir.r4.model.MetadataResource; +import org.hl7.fhir.r4.model.Parameters; +import org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent; import org.hl7.fhir.r4.model.Period; +import org.hl7.fhir.r4.model.PlanDefinition; import org.hl7.fhir.r4.model.Reference; import org.hl7.fhir.r4.model.RelatedArtifact; import org.hl7.fhir.r4.model.Resource; @@ -46,6 +51,10 @@ import org.hl7.fhir.r4.model.StructureDefinition; import org.hl7.fhir.r4.model.UsageContext; import org.hl7.fhir.r4.model.ValueSet; +import org.hl7.fhir.r4.model.ValueSet.ConceptSetComponent; +import org.hl7.fhir.r4.model.ValueSet.ValueSetComposeComponent; +import org.hl7.fhir.r4.model.ValueSet.ValueSetExpansionComponent; +import org.hl7.fhir.r4.model.ValueSet.ValueSetExpansionContainsComponent; import org.opencds.cqf.cql.evaluator.fhir.util.Canonicals; import org.opencds.cqf.ruler.cr.r4.ArtifactAssessment; import org.opencds.cqf.ruler.cr.r4.ArtifactAssessment.ArtifactAssessmentContentInformationType; @@ -55,8 +64,13 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Configurable; +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.support.ValueSetExpansionOptions; +import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoValueSet; +import ca.uhn.fhir.jpa.patch.FhirPatch; import ca.uhn.fhir.model.api.IQueryParameterType; import ca.uhn.fhir.model.api.TemporalPrecisionEnum; +import ca.uhn.fhir.parser.path.EncodeContextPath; import ca.uhn.fhir.rest.param.ReferenceParam; import ca.uhn.fhir.rest.param.TokenParam; import ca.uhn.fhir.rest.param.UriParam; @@ -190,7 +204,12 @@ private BundleEntryRequestComponent createRequest(IBaseResource theResource) { } return request; } - +/** + * search by versioned Canonical URL + * @param url canonical URL of the form www.example.com/Patient/123|0.1 + * @param fhirDal to do the searching + * @return a bundle of results + */ private Bundle searchResourceByUrl(String url, FhirDal fhirDal) { Map>> searchParams = new HashMap<>(); @@ -341,17 +360,27 @@ public Bundle createDraftBundle(IdType baseArtifactId, FhirDal fhirDal, String v Bundle transactionBundle = new Bundle() .setType(Bundle.BundleType.TRANSACTION); List urnList = resourcesToCreate.stream().map(res -> new IdType("urn:uuid:" + UUID.randomUUID().toString())).collect(Collectors.toList()); + TreeSet ownedResourceUrls = createOwnedResourceUrlCache(resourcesToCreate); for (int i = 0; i < resourcesToCreate.size(); i++) { KnowledgeArtifactAdapter newResourceAdapter = new KnowledgeArtifactAdapter(resourcesToCreate.get(i)); updateUsageContextReferencesWithUrns(resourcesToCreate.get(i), resourcesToCreate, urnList); - updateRelatedArtifactUrlsWithNewVersions(newResourceAdapter.getOwnedRelatedArtifacts(), draftVersion); + updateRelatedArtifactUrlsWithNewVersions(combineComponentsAndDependencies(newResourceAdapter), draftVersion, ownedResourceUrls); MetadataResource updateIdForBundle = newResourceAdapter.copy(); updateIdForBundle.setId(urnList.get(i)); transactionBundle.addEntry(createEntry(updateIdForBundle)); } return transactionBundle; } - + private TreeSet createOwnedResourceUrlCache(List resources) { + TreeSet retval = new TreeSet(); + resources.stream() + .map(KnowledgeArtifactAdapter::new) + .map(KnowledgeArtifactAdapter::getOwnedRelatedArtifacts).flatMap(List::stream) + .map(RelatedArtifact::getResource) + .map(Canonicals::getUrl) + .forEach(retval::add); + return retval; + } private void updateUsageContextReferencesWithUrns(MetadataResource newResource, List resourceListWithOriginalIds, List idListForTransactionBundle) { List useContexts = newResource.getUseContext(); for (UsageContext useContext : useContexts) { @@ -371,10 +400,12 @@ private void updateUsageContextReferencesWithUrns(MetadataResource newResource, } } - private void updateRelatedArtifactUrlsWithNewVersions(List relatedArtifactList, String updatedVersion){ + private void updateRelatedArtifactUrlsWithNewVersions(List relatedArtifactList, String updatedVersion, TreeSet ownedUrlCache){ // For each relatedArtifact, update the version of the reference. relatedArtifactList.stream() - .filter(ra -> ra.hasResource()) + .filter(RelatedArtifact::hasResource) + // only update the references to owned resources (including dependencies) + .filter(ra -> ownedUrlCache.contains(Canonicals.getUrl(ra.getResource()))) .collect(Collectors.toList()) .replaceAll(ra -> ra.setResource(Canonicals.getUrl(ra.getResource()) + "|" + updatedVersion)); } @@ -984,15 +1015,15 @@ void recursivePackage( entry.getRequest().setIfNoneExist("url="+resource.getUrl()+"&version="+resource.getVersion()); bundle.addEntry(entry); } - List components = adapter.getComponents(); - List dependencies = adapter.getDependencies(); - Stream.concat(components.stream(), dependencies.stream()) + combineComponentsAndDependencies(adapter).stream() .map(ra -> searchResourceByUrl(ra.getResource(), fhirDal)) .map(searchBundle -> searchBundle.getEntry().stream().findFirst().orElseGet(()-> new BundleEntryComponent()).getResource()) .forEach(component -> recursivePackage((MetadataResource)component, bundle, fhirDal, capability, include, canonicalVersion, checkCanonicalVersion, forceCanonicalVersion)); } } - + private List combineComponentsAndDependencies(KnowledgeArtifactAdapter adapter) { + return Stream.concat(adapter.getComponents().stream(), adapter.getDependencies().stream()).collect(Collectors.toList()); + } private Optional findVersionInListMatchingResource(List list, MetadataResource resource){ return list.stream() .filter((canonical) -> Canonicals.getUrl(canonical).equals(resource.getUrl())) @@ -1113,7 +1144,424 @@ private List findUnsupportedInclude(List dao) throws UnprocessableEntityException { + // setup + FhirPatch patch = new FhirPatch(theContext); + patch.setIncludePreviousValueInDiff(true); + // ignore meta changes + patch.addIgnorePath("*.meta"); + Parameters libraryDiff = handleRelatedArtifactArrayElementsDiff(theSourceLibrary,theTargetLibrary,patch); + + // then check for references and add those to the base Parameters object + diffCache cache = new diffCache(); + cache.addDiff(theSourceLibrary.getUrl()+"|"+theSourceLibrary.getVersion(), theTargetLibrary.getUrl()+"|"+theTargetLibrary.getVersion(), libraryDiff); + checkForChangesInChildren(libraryDiff, theSourceLibrary, theTargetLibrary, fhirDal, patch, cache, theContext, compareComputable, compareExecutable,dao); + return libraryDiff; + } + private Parameters handleRelatedArtifactArrayElementsDiff(MetadataResource theSourceLibrary,MetadataResource theTargetLibrary, FhirPatch patch) { + KnowledgeArtifactAdapter updateSource = new KnowledgeArtifactAdapter(theSourceLibrary.copy()); + KnowledgeArtifactAdapter updateTarget = new KnowledgeArtifactAdapter(theTargetLibrary.copy()); + additionsAndDeletions processedRelatedArtifacts = extractAdditionsAndDeletions(updateSource.getRelatedArtifact(), updateTarget.getRelatedArtifact(), RelatedArtifact.class); + updateSource.setRelatedArtifact(processedRelatedArtifacts.getSourceMatches()); + updateTarget.setRelatedArtifact(processedRelatedArtifacts.getTargetMatches()); + Parameters updateOperations = (Parameters) patch.diff(updateSource.copy(),updateTarget.copy()); + processedRelatedArtifacts.appendInsertOperations(updateOperations, patch, updateSource.getRelatedArtifact().size()); + processedRelatedArtifacts.appendDeleteOperations(updateOperations, patch, updateSource.getRelatedArtifact().size()); + return updateOperations; + } + private Parameters advancedValueSetDiff(MetadataResource theSourceValueSet,MetadataResource theTargetValueSet, FhirPatch patch, boolean compareComputable, boolean compareExecutable) { + ValueSet updateSource = (ValueSet)theSourceValueSet.copy(); + ValueSet updateTarget = (ValueSet)theTargetValueSet.copy(); + additionsAndDeletions composeIncludeProcessed = extractAdditionsAndDeletions(updateSource.getCompose().getInclude(), updateTarget.getCompose().getInclude(), ConceptSetComponent.class); + additionsAndDeletions expansionContainsProcessed = extractAdditionsAndDeletions(updateSource.getExpansion().getContains(), updateTarget.getExpansion().getContains(), ValueSetExpansionContainsComponent.class); + if (compareComputable) { + updateSource.getCompose().setInclude(composeIncludeProcessed.getSourceMatches()); + updateTarget.getCompose().setInclude(composeIncludeProcessed.getTargetMatches()); + } else { + // don't generate any Parameters + updateSource.getCompose().setInclude(new ArrayList<>()); + updateTarget.getCompose().setInclude(new ArrayList<>()); + } + if (compareExecutable) { + updateSource.getExpansion().setContains(expansionContainsProcessed.getSourceMatches()); + updateTarget.getExpansion().setContains(expansionContainsProcessed.getTargetMatches()); + } else { + // don't generate any Parameters + updateSource.getExpansion().setContains(new ArrayList<>()); + updateTarget.getExpansion().setContains(new ArrayList<>()); + } + // first match the ones which are just updated + Parameters vsDiff = (Parameters) patch.diff(updateSource,updateTarget); + // then get all the delete entries + if (compareComputable) { + composeIncludeProcessed.appendInsertOperations(vsDiff, patch, updateTarget.getCompose().getInclude().size()); + composeIncludeProcessed.appendDeleteOperations(vsDiff, patch, updateTarget.getCompose().getInclude().size()); + } + if (compareExecutable) { + expansionContainsProcessed.appendInsertOperations(vsDiff, patch, updateTarget.getExpansion().getContains().size()); + expansionContainsProcessed.appendDeleteOperations(vsDiff, patch, updateTarget.getExpansion().getContains().size()); + } + return vsDiff; + } + private void doesValueSetNeedExpansion(ValueSet vset, IFhirResourceDaoValueSet dao) { + Optional lastExpanded = Optional.ofNullable(vset.getExpansion()).map(e -> e.getTimestamp()); + Optional lastUpdated = Optional.ofNullable(vset.getMeta()).map(m -> m.getLastUpdated()); + if (lastExpanded.isPresent() && lastUpdated.isPresent() && lastExpanded.get().equals(lastUpdated.get())) { + // ValueSet was not changed after last expansion, don't need to update + return; + } else { + // clear obsolete expansion + vset.setExpansion(null); + ValueSetExpansionOptions options = new ValueSetExpansionOptions(); + options.setIncludeHierarchy(true); + + ValueSet e = dao.expand(vset,options); + // we need to do this because dao.expand sets the expansion to a subclass and then that breaks the FhirPatch + // `copy` creates the superclass again + vset.setExpansion(e.getExpansion().copy()); + return; + } + } + private class diffCache { + private final Map diffs = new HashMap(); + private final Map resources = new HashMap(); + public diffCache() { + super(); + } + public void addDiff(String sourceUrl, String targetUrl, Parameters diff) { + this.diffs.put(sourceUrl+"-"+targetUrl, diff); + } + public Parameters getDiff(String sourceUrl, String targetUrl) { + return this.diffs.get(sourceUrl+"-"+targetUrl); + } + public void addResource(String url, MetadataResource resource) { + this.resources.put(url, resource); + } + public MetadataResource getResource(String url) { + return this.resources.get(url); + } + } + private void fixDeletePathIndexes(List parameters, int newStart) { + for (int i = 0; i < parameters.size(); i++) { + ParametersParameterComponent parameter = parameters.get(i); + Optional path = parameter.getPart().stream() + .filter(part -> part.getName().equals("path")) + .findFirst(); + if (path.isPresent()) { + String pathString = ((StringType)path.get().getValue()).getValue(); + EncodeContextPath e = new EncodeContextPath(pathString); + String newIndex = "[" + String.valueOf(i + newStart) + "]"; // Replace with your desired string + String result = pathString.replaceAll("\\[([^\\]]+)\\]", newIndex); + path.get().setValue(new StringType(result)); + } + }; + } + private void fixInsertPathIndexes (List parameters, int newStart) { + int opCounter = 0; + for (ParametersParameterComponent parameter:parameters) { + // ParametersParameterComponent parameter = parameters.get(i); + // need to check for more than index here + /** + * { + "name": "operation", + "part": [ + { + "name": "type", + "valueCode": "insert" + }, + { + "name": "path", + "valueString": "ValueSet.expansion" + }, + { + "name": "index", + "valueInteger": 64 + } + ] + }, + { + "name": "operation", + "part": [ + { + "name": "type", + "valueCode": "insert" + }, + { + "name": "path", + "valueString": "ValueSet.expansion.contains" + }, + { + "name": "index", + "valueInteger": 65 + } + ] + }, + { + "name": "operation", + "part": [ + { + "name": "type", + "valueCode": "insert" + }, + { + "name": "path", + "valueString": "ValueSet.expansion.contains[0].system" + }, + { + "name": "index", + "valueInteger": 66 + }, + { + "name": "value", + "valueUri": "http://loinc.org" + } + ] + }, + { + "name": "operation", + "part": [ + { + "name": "type", + "valueCode": "insert" + }, + { + "name": "path", + "valueString": "ValueSet.expansion.contains[0].code" + }, + { + "name": "index", + "valueInteger": 67 + }, + { + "name": "value", + "valueCode": "39297-7" + } + ] + }, + { + "name": "operation", + "part": [ + { + "name": "type", + "valueCode": "insert" + }, + { + "name": "path", + "valueString": "ValueSet.expansion.contains[0].display" + }, + { + "name": "index", + "valueInteger": 68 + }, + { + "name": "value", + "valueString": "Influenza virus A H10 Ab [Titer] in Serum by Hemagglutination inhibition" + } + ] + } + */ + + Optional index = parameter.getPart().stream() + .filter(part -> part.getName().equals("index")) + .findFirst(); + Optional value = parameter.getPart().stream() + .filter(part -> part.getName().equals("value")) + .findFirst(); + Optional path = parameter.getPart().stream() + .filter(part -> part.getName().equals("path")) + .findFirst(); + if (path.isPresent()) { + String pathString = ((StringType)path.get().getValue()).getValue(); + EncodeContextPath e = new EncodeContextPath(pathString); + String elementName = e.getLeafElementName(); + // for contains / include, we want to update the second last index and the + if (elementName.equals("contains") + || elementName.equals("include") + || elementName.equals("relatedArtifact")) { + if ((index.isPresent() && !value.isPresent()) + || (elementName.equals("relatedArtifact") && index.isPresent())) { + index.get().setValue(new IntegerType(opCounter + newStart)); + opCounter+=1; + } + } + if (pathString.contains("expansion.contains") || pathString.contains("compose.include")) { + if(value.isPresent()) { + // subtract 1 here because the opcounter has already been incremented + // maybe separate out the contains / include / relatedartifact rules a little more? + // refactor into specific methods linked to specific signatures? + String newIndex = "[" + String.valueOf(opCounter - 1 + newStart) + "]"; // Replace with your desired string + String result = pathString.replaceAll("\\[([^\\]]+)\\]", newIndex); + path.get().setValue(new StringType(result)); + } + } + } + }; + } + private void checkForChangesInChildren(Parameters baseDiff, MetadataResource theSourceBase, MetadataResource theTargetBase, FhirDal fhirDal, FhirPatch patch, diffCache cache, FhirContext ctx, boolean compareComputable, boolean compareExecutable,IFhirResourceDaoValueSet dao) throws UnprocessableEntityException { + // get the references in both the source and target + List targetRefs = combineComponentsAndDependencies(new KnowledgeArtifactAdapter(theTargetBase)); + List sourceRefs = combineComponentsAndDependencies(new KnowledgeArtifactAdapter(theSourceBase)); + additionsAndDeletions fixed = extractAdditionsAndDeletions(sourceRefs, targetRefs, RelatedArtifact.class); + if (fixed.getSourceMatches().size() > 0) { + for(int i = 0; i < fixed.getSourceMatches().size(); i++) { + String sourceCanonical = fixed.getSourceMatches().get(i).getResource(); + String targetCanonical = fixed.getTargetMatches().get(i).getResource(); + boolean diffNotAlreadyComputedAndPresent = baseDiff.getParameter(Canonicals.getUrl(targetCanonical)) == null; + if (diffNotAlreadyComputedAndPresent) { + MetadataResource source = checkOrUpdateResourceCache(sourceCanonical, cache, fhirDal, dao); + MetadataResource target = checkOrUpdateResourceCache(targetCanonical, cache, fhirDal, dao); + // need to do something smart here to expand the executable or computable resources + checkOrUpdateDiffCache(sourceCanonical, targetCanonical, source, target, patch, cache, ctx, compareComputable, compareExecutable, dao) + .ifPresentOrElse(diffToAppend -> { + ParametersParameterComponent component = baseDiff.addParameter(); + component.setName(Canonicals.getUrl(sourceCanonical)); + component.setResource(diffToAppend); + // check for changes in the children of those as well + checkForChangesInChildren(diffToAppend, source, target, fhirDal, patch, cache, ctx, compareComputable, compareExecutable, dao); + }, + () -> { + if (target == null) { + ParametersParameterComponent component = baseDiff.addParameter(); + component.setName(Canonicals.getUrl(sourceCanonical)); + component.setValue(new StringType("Target could not be retrieved")); + } else if (source == null) { + ParametersParameterComponent component = baseDiff.addParameter(); + component.setName(Canonicals.getUrl(targetCanonical)); + component.setValue(new StringType("Source could not be retrieved")); + } + }); + } + } + } + for (RelatedArtifact addition : fixed.getInsertions() ) { + if (addition.hasResource()) { + boolean diffNotAlreadyComputedAndPresent = baseDiff.getParameter(Canonicals.getUrl(addition.getResource())) == null; + if (diffNotAlreadyComputedAndPresent) { + ParametersParameterComponent component = baseDiff.addParameter(); + component.setName(Canonicals.getUrl(addition.getResource())); + component.setValue(new StringType("Related artifact was inserted")); + } + } + } + for (RelatedArtifact deletion : fixed.getDeletions() ) { + if (deletion.hasResource()) { + boolean diffNotAlreadyComputedAndPresent = baseDiff.getParameter(Canonicals.getUrl(deletion.getResource())) == null; + if (diffNotAlreadyComputedAndPresent) { + ParametersParameterComponent component = baseDiff.addParameter(); + component.setName(Canonicals.getUrl(deletion.getResource())); + component.setValue(new StringType("Related artifact was deleted")); + } + } + } + } + private additionsAndDeletions extractAdditionsAndDeletions(List source, List target, Class t) { + List sourceCopy = new ArrayList(source); + List targetCopy = new ArrayList(target); + // this is n^2 with Lists but can be nlog(n) if we use TreeSets + // check for matches and additions + List insertions = new ArrayList(); + List deletions = new ArrayList(); + List sourceMatches = new ArrayList(); + List targetMatches = new ArrayList(); + targetCopy.forEach(targetObj -> { + Optional isInSource = sourceCopy.stream().filter(sourceObj -> { + if (sourceObj instanceof RelatedArtifact && targetObj instanceof RelatedArtifact) { + return relatedArtifactEquals((RelatedArtifact) sourceObj, (RelatedArtifact) targetObj); + } else if (sourceObj instanceof ConceptSetComponent && targetObj instanceof ConceptSetComponent) { + return conceptSetEquals((ConceptSetComponent)sourceObj, (ConceptSetComponent)targetObj); + } else if (sourceObj instanceof ValueSetExpansionContainsComponent && targetObj instanceof ValueSetExpansionContainsComponent) { + return ValueSetContainsEquals((ValueSetExpansionContainsComponent) sourceObj,(ValueSetExpansionContainsComponent) targetObj); + } else { + return false; + } + }).findAny(); + if (isInSource.isPresent()) { + sourceMatches.add(isInSource.get()); + targetMatches.add(targetObj); + sourceCopy.remove(isInSource.get()); + } else { + insertions.add(targetObj); + } + }); + // check for deletions + sourceCopy.forEach(sourceObj -> { + boolean isInTarget = targetCopy.stream().anyMatch(targetObj -> { + if (sourceObj instanceof RelatedArtifact && targetObj instanceof RelatedArtifact) { + return relatedArtifactEquals((RelatedArtifact) sourceObj, (RelatedArtifact) targetObj); + } else if (sourceObj instanceof ConceptSetComponent && targetObj instanceof ConceptSetComponent) { + return conceptSetEquals((ConceptSetComponent)sourceObj, (ConceptSetComponent)targetObj); + } else if (sourceObj instanceof ValueSetExpansionContainsComponent && targetObj instanceof ValueSetExpansionContainsComponent) { + return ValueSetContainsEquals((ValueSetExpansionContainsComponent) sourceObj,(ValueSetExpansionContainsComponent) targetObj); + } else { + return false; + } + }); + if (!isInTarget) { + deletions.add(sourceObj); + } + }); + return new additionsAndDeletions(sourceMatches,targetMatches,insertions,deletions,t); + } + private boolean relatedArtifactEquals(RelatedArtifact ref1, RelatedArtifact ref2) { + return Canonicals.getUrl(ref1.getResource()).equals(Canonicals.getUrl(ref2.getResource())) && ref1.getType() == ref2.getType(); + } + // RelatedArtifact extensions should diff nicely too....eventually + private boolean extensionEquals(Extension ref1, Extension ref2) { + return ref1.getUrl().equals(ref2.getUrl()); + } + private boolean conceptSetEquals(ConceptSetComponent ref1, ConceptSetComponent ref2) { + // consider any includes which share at least 1 URL + if (ref1.hasValueSet() && ref2.hasValueSet()) { + List ref1Urls = ref1.getValueSet().stream().map(CanonicalType::getValue).collect(Collectors.toList()); + List intersect = ref2.getValueSet().stream().map(CanonicalType::getValue).filter(ref1Urls::contains).collect(Collectors.toList()); + return intersect.size() > 0; + } else if (!ref1.hasValueSet() && !ref2.hasValueSet()) { + return ref1.getSystem().equals(ref2.getSystem()); + } else { + // if one conceptSet has a value set but not the other then they can't be updates of each other + return false; + } + } + private boolean ValueSetContainsEquals(ValueSetExpansionContainsComponent ref1, ValueSetExpansionContainsComponent ref2) { + return ref1.getSystem().equals(ref2.getSystem()) && ref1.getCode().equals(ref2.getCode()); + } + private MetadataResource checkOrUpdateResourceCache(String url, diffCache cache, FhirDal fhirDal, IFhirResourceDaoValueSet dao) throws UnprocessableEntityException { + MetadataResource resource = cache.getResource(url); + if (resource == null) { + try { + resource = retrieveResourcesByCanonical(url, fhirDal); + } catch (ResourceNotFoundException e) { + // ignore + } + if (resource != null) { + if (resource instanceof ValueSet) { + try { + doesValueSetNeedExpansion((ValueSet)resource, dao); + } catch (Exception e) { + throw new UnprocessableEntityException("Could not expand ValueSet: " + e.getMessage()); + } + } + cache.addResource(url, resource); + } + } + return resource; + } + private Optional checkOrUpdateDiffCache(String sourceCanonical, String targetCanonical, MetadataResource source, MetadataResource target, FhirPatch patch, diffCache cache, FhirContext ctx, boolean compareComputable, boolean compareExecutable,IFhirResourceDaoValueSet dao) { + Parameters retval = cache.getDiff(sourceCanonical, targetCanonical); + if (retval == null) { + if (target != null) { + if (source instanceof Library || source instanceof PlanDefinition) { + retval = handleRelatedArtifactArrayElementsDiff(source, target, patch); + } else if (source instanceof ValueSet) { + retval = advancedValueSetDiff(source, target, patch, compareComputable, compareExecutable); + } else { + retval = (Parameters) patch.diff(source, target); + } + cache.addDiff(sourceCanonical, targetCanonical, retval); + } + } + return Optional.ofNullable(retval); + } /* $revise */ public MetadataResource revise(FhirDal fhirDal, MetadataResource resource) { MetadataResource existingResource = (MetadataResource) fhirDal.read(resource.getIdElement()); @@ -1133,4 +1581,76 @@ public MetadataResource revise(FhirDal fhirDal, MetadataResource resource) { return resource; } + private class additionsAndDeletions { + private List mySourceMatches; + private List myTargetMatches; + private List myInsertions; + private List myDeletions; + private Class t; + public additionsAndDeletions(List sourceMatches,List targetMatches,List additions,List deletions, Class t) { + this.mySourceMatches = sourceMatches; + this.myTargetMatches = targetMatches; + this.myInsertions = additions; + this.myDeletions = deletions; + this.t = t; + } + public List getSourceMatches() { return this.mySourceMatches; } + public List getTargetMatches() { return this.myTargetMatches; } + public List getInsertions() { return this.myInsertions; } + public List getDeletions() { return this.myDeletions; } + public void appendInsertOperations (Parameters theBase, FhirPatch thePatch, int theStartIndex) throws UnprocessableEntityException { + prepareForComparison ( theBase, thePatch, theStartIndex, true, this.myInsertions); + } + public void appendDeleteOperations (Parameters theBase, FhirPatch thePatch, int theStartIndex) throws UnprocessableEntityException { + prepareForComparison ( theBase, thePatch, theStartIndex, false, this.myDeletions); + } + /** + * + * @param theBase base diff to append to + * @param thePatch patch instance which performs the diff + * @param theStartIndex where the start numbering the operations + * @param theInsertOrDelete true = insert, false = delete + * @param theResourcesToAdd list of insertions or deletions + * @throws UnprocessableEntityException + */ + private void prepareForComparison (Parameters theBase, FhirPatch thePatch, int theStartIndex, boolean theInsertOrDelete, List theResourcesToAdd) throws UnprocessableEntityException { + if (this.myInsertions.size() > 0) { + MetadataResource empty; + MetadataResource hasNewResources; + if (this.t.isAssignableFrom(RelatedArtifact.class)) { + empty = new Library(); + hasNewResources = new Library(); + ((Library)hasNewResources).setRelatedArtifact((List)theResourcesToAdd); + } else if (this.t.isAssignableFrom(ConceptSetComponent.class)) { + empty = new ValueSet(); + ((ValueSet)empty).setCompose(new ValueSetComposeComponent().setInclude(new ArrayList<>())); + hasNewResources = new ValueSet(); + ((ValueSet)hasNewResources).setCompose(new ValueSetComposeComponent().setInclude((List)theResourcesToAdd)); + } else if (this.t.isAssignableFrom(ValueSetExpansionContainsComponent.class)) { + empty = new ValueSet(); + ((ValueSet)empty).setExpansion(new ValueSetExpansionComponent().setContains(new ArrayList<>())); + hasNewResources = new ValueSet(); + ((ValueSet)hasNewResources).setExpansion(new ValueSetExpansionComponent().setContains((List)theResourcesToAdd)); + } else { + throw new UnprocessableEntityException("Could not process object"); + } + if (theInsertOrDelete) { + appendInsertOperations(theBase, empty, hasNewResources, thePatch, theStartIndex); + } else { + // swap source and target for deletions + appendDeleteOperations(theBase, hasNewResources, empty, thePatch, theStartIndex); + } + } + } + private void appendInsertOperations(Parameters theBase, IBaseResource theSource,IBaseResource theTarget, FhirPatch thePatch, int theStartIndex) { + Parameters insertions = (Parameters) thePatch.diff(theSource,theTarget); + fixInsertPathIndexes(insertions.getParameter(), theStartIndex); + theBase.getParameter().addAll(insertions.getParameter()); + } + private void appendDeleteOperations(Parameters theBase, IBaseResource theSource,IBaseResource theTarget, FhirPatch thePatch, int theStartIndex) { + Parameters deletions = (Parameters) thePatch.diff(theSource,theTarget); + fixDeletePathIndexes(deletions.getParameter(), theStartIndex); + theBase.getParameter().addAll(deletions.getParameter()); + } + } } diff --git a/plugin/cr/src/main/java/org/opencds/cqf/ruler/cr/RepositoryService.java b/plugin/cr/src/main/java/org/opencds/cqf/ruler/cr/RepositoryService.java index 53df4a58e..1680f4b0e 100644 --- a/plugin/cr/src/main/java/org/opencds/cqf/ruler/cr/RepositoryService.java +++ b/plugin/cr/src/main/java/org/opencds/cqf/ruler/cr/RepositoryService.java @@ -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; @@ -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; @@ -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 compareExecutable, + @OperationParam(name = "compareComputable", typeName = "Boolean") IPrimitiveType 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 dao = (IFhirResourceDaoValueSet)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) diff --git a/plugin/cr/src/test/java/org/opencds/cqf/ruler/cr/r4/RepositoryServiceTest.java b/plugin/cr/src/test/java/org/opencds/cqf/ruler/cr/r4/RepositoryServiceTest.java index 19d0b6027..550739120 100644 --- a/plugin/cr/src/test/java/org/opencds/cqf/ruler/cr/r4/RepositoryServiceTest.java +++ b/plugin/cr/src/test/java/org/opencds/cqf/ruler/cr/r4/RepositoryServiceTest.java @@ -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; @@ -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; @@ -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)); @@ -125,8 +127,16 @@ void draftOperation_test() { assertFalse(lib.hasExtension(KnowledgeArtifactProcessor.releaseLabelUrl)); List 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 relatedArtifacts2 = new KnowledgeArtifactAdapter(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() { @@ -151,12 +161,11 @@ void draftOperation_no_effectivePeriod_test() { .withParameters(params) .returnResourceType(Bundle.class) .execute(); - getMetadataResourcesFromBundle(returnedBundle) - .stream() - .forEach(resource -> { - KnowledgeArtifactAdapter adapter = new KnowledgeArtifactAdapter(resource); - assertFalse(adapter.getEffectivePeriod().hasStart() || adapter.getEffectivePeriod().hasEnd()); - }); + + forEachMetadataResource(returnedBundle.getEntry(), resource -> { + KnowledgeArtifactAdapter adapter = new KnowledgeArtifactAdapter(resource); + assertFalse(adapter.getEffectivePeriod().hasStart() || adapter.getEffectivePeriod().hasEnd()); + }); } @Test void draftOperation_version_conflict_test() { @@ -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 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 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 getMetadataResourcesFromBundle(Bundle bundle) { - return bundle.getEntry() - .stream() + private void forEachMetadataResource(List entries, Consumer callback) { + entries.stream() .map(entry -> entry.getResponse().getLocation()) .map(location -> { switch (location.split("/")[0]) { @@ -508,7 +514,8 @@ List getMetadataResourcesFromBundle(Bundle bundle) { default: return null; } - }).collect(Collectors.toList()); + }) + .forEach(callback); } @Test void releaseResource_latestFromTx_NotSupported_test() { @@ -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 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 parameters = returnedParams.getParameter(); + List libraryReplaceOperations = getOperationsByType(parameters, "replace"); + List libraryInsertOperations = getOperationsByType(parameters, "insert"); + List libraryDeleteOperations = getOperationsByType(parameters, "delete"); + List 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 libraryDeletedPaths = List.of( + "Library.relatedArtifact[5]" // deleted DXTC leaf VS + ); + List 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 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 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 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 deleteOperations = getOperationsByType(grouperChanges.getParameter(), "delete"); + List 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 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 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 deleteOperations = getOperationsByType(grouperChanges.getParameter(), "delete"); + List insertOperations = getOperationsByType(grouperChanges.getParameter(), "insert"); + // old codes removed + assertTrue(deleteOperations.size() == 23); + // new codes added + assertTrue(insertOperations.size() == 32); + } + private List getOperationsByType(List 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()); + } } diff --git a/plugin/cr/src/test/resources/ersd-small-active-bundle.json b/plugin/cr/src/test/resources/ersd-small-active-bundle.json index e2d6675f0..3e62d73f0 100644 --- a/plugin/cr/src/test/resources/ersd-small-active-bundle.json +++ b/plugin/cr/src/test/resources/ersd-small-active-bundle.json @@ -12,6 +12,36 @@ "url": "http://ersd.aimsplatform.org/fhir/Library/SpecificationLibrary", "version": "2022-10-19", "status": "active", + "useContext": [ + { + "code": { + "system": "http://hl7.org/fhir/us/ecr/CodeSystem/us-ph-usage-context-type", + "code": "reporting" + }, + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/us/ecr/CodeSystem/us-ph-usage-context", + "code": "triggering" + } + ] + } + }, + { + "code": { + "system": "http://hl7.org/fhir/us/ecr/CodeSystem/us-ph-usage-context-type", + "code": "specification-type" + }, + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/us/ecr/CodeSystem/us-ph-usage-context", + "code": "program" + } + ] + } + } + ], "relatedArtifact": [ { "type": "composed-of", @@ -95,6 +125,10 @@ ], "type": "depends-on", "resource": "http://cts.nlm.nih.gov/fhir/ValueSet/123-this-will-be-routine|20210526" + }, + { + "type": "depends-on", + "resource": "http://ersd.aimsplatform.org/fhir/ValueSet/dxtc|2022-10-19" } ] }, @@ -170,7 +204,11 @@ "include": [ { "valueSet": [ - "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1146.6|20210526", + "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1146.6|20210526" + ] + }, + { + "valueSet": [ "http://cts.nlm.nih.gov/fhir/ValueSet/123-this-will-be-routine|20210526" ] } @@ -361,124 +399,20 @@ "compose": { "include": [ { - "system": "http://snomed.info/sct", - "version": "Provisional_2022-04-25", + "system": "http://hl7.org/fhir/sid/icd-10-cm", + "version": "Provisional_2022-01-12", "concept": [ { - "code": "1086051000119107", - "display": "Cardiomyopathy due to diphtheria (disorder)" - }, - { - "code": "1086061000119109", - "display": "Diphtheria radiculomyelitis (disorder)" - }, - { - "code": "1086071000119103", - "display": "Diphtheria tubulointerstitial nephropathy (disorder)" - }, - { - "code": "1090211000119102", - "display": "Pharyngeal diphtheria (disorder)" - }, - { - "code": "129667001", - "display": "Diphtheritic peripheral neuritis (disorder)" - }, - { - "code": "13596001", - "display": "Diphtheritic peritonitis (disorder)" - }, - { - "code": "15682004", - "display": "Anterior nasal diphtheria (disorder)" - }, - { - "code": "186347006", - "display": "Diphtheria of penis (disorder)" - }, - { - "code": "18901009", - "display": "Cutaneous diphtheria (disorder)" - }, - { - "code": "194945009", - "display": "Acute myocarditis - diphtheritic (disorder)" - }, - { - "code": "230596007", - "display": "Diphtheritic neuropathy (disorder)" - }, - { - "code": "240422004", - "display": "Tracheobronchial diphtheria (disorder)" - }, - { - "code": "26117009", - "display": "Diphtheritic myocarditis (disorder)" - }, - { - "code": "276197005", - "display": "Infection caused by Corynebacterium diphtheriae (disorder)" - }, - { - "code": "3419005", - "display": "Faucial diphtheria (disorder)" - }, - { - "code": "397428000", - "display": "Diphtheria (disorder)" + "code": "B60.12", + "display": "Conjunctivitis due to Acanthamoeba" }, { - "code": "397430003", - "display": "Diphtheria caused by Corynebacterium diphtheriae (disorder)" - }, - { - "code": "48278001", - "display": "Diphtheritic cystitis (disorder)" - }, - { - "code": "50215002", - "display": "Laryngeal diphtheria (disorder)" - }, - { - "code": "715659006", - "display": "Diphtheria of respiratory system (disorder)" - }, - { - "code": "75589004", - "display": "Nasopharyngeal diphtheria (disorder)" - }, - { - "code": "7773002", - "display": "Conjunctival diphtheria (disorder)" - }, - { - "code": "789005009", - "display": "Paralysis of uvula after diphtheria (disorder)" + "code": "B60.13", + "display": "Keratoconjunctivitis due to Acanthamoeba" } ] } ] - }, - "expansion": { - "timestamp": "2022-10-21T15:18:29-04:00", - "contains": [ - { - "system": "http://snomed.info/sct", - "version": "Provisional_2022-04-25", - "code": "1086051000119107" - }, - { - "system": "http://snomed.info/sct", - "version": "Provisional_2022-04-25", - "code": "1086061000119109" - }, - { - "system": "http://snomed.info/sct", - "version": "Provisional_2022-04-25", - "code": "1086071000119103" - } - ] } }, "request": { diff --git a/plugin/cr/src/test/resources/ersd-small-approved-draft-bundle.json b/plugin/cr/src/test/resources/ersd-small-approved-draft-bundle.json index 582aaaace..01ccc5990 100644 --- a/plugin/cr/src/test/resources/ersd-small-approved-draft-bundle.json +++ b/plugin/cr/src/test/resources/ersd-small-approved-draft-bundle.json @@ -1,305 +1,308 @@ { - "resourceType": "Bundle", - "id": "rctc-release-2022-10-19-Bundle-rctc", - "type": "transaction", - "timestamp": "2022-10-21T15:18:28.504-04:00", - "entry": [ - { - "fullUrl": "http://ersd.aimsplatform.org/fhir/Library/SpecificationLibrary", - "resource": { - "resourceType": "Library", - "id": "SpecificationLibrary", - "url": "http://ersd.aimsplatform.org/fhir/Library/SpecificationLibrary", - "version": "1.2.3-draft", - "status": "draft", - "date": "2023-06-30", - "approvalDate": "2023-06-30", - "relatedArtifact": [ - { - "type": "composed-of", - "resource": "http://ersd.aimsplatform.org/fhir/PlanDefinition/us-ecr-specification|1.2.3-draft", - "extension":[ - { - "url":"http://hl7.org/fhir/StructureDefinition/crmi-isOwned", - "valueBoolean": true - } - ] - }, - { - "type": "composed-of", - "resource": "http://ersd.aimsplatform.org/fhir/Library/rctc|1.2.3-draft", - "extension":[ - { - "url":"http://hl7.org/fhir/StructureDefinition/crmi-isOwned", - "valueBoolean": true - } - ] - },{ - "extension": [ - { - "url": "http://aphl.org/fhir/vsm/StructureDefinition/vsm-valueset-priority", - "valueCodeableConcept": { - "coding": [ - { - "system": "http://hl7.org/fhir/us/ecr/CodeSystem/us-ph-usage-context", - "code": "emergent" - } - ], - "text": "Emergent" - } - },{ - "url": "http://aphl.org/fhir/vsm/StructureDefinition/vsm-valueset-condition", - "valueCodeableConcept": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "49649001" - } - ], - "text": "Infection caused by Acanthamoeba (disorder)" - } - }], - "type": "depends-on", - "resource": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1146.6|20210526" - } - ] - }, - "request": { - "method": "PUT", - "url": "Library/SpecificationLibrary" - } - }, - { - "fullUrl": "http://ersd.aimsplatform.org/fhir/PlanDefinition/us-ecr-specification", - "resource": { - "resourceType": "PlanDefinition", - "id": "us-ecr-specification", - "url": "http://ersd.aimsplatform.org/fhir/PlanDefinition/us-ecr-specification", - "version": "1.2.3-draft", - "status": "draft", - "relatedArtifact": [ - { - "type": "depends-on", - "label": "RCTC Value Set Library of Trigger Codes", - "resource": "http://ersd.aimsplatform.org/fhir/Library/rctc|1.2.3-draft" - } - ] - }, - "request": { - "method": "PUT", - "url": "PlanDefinition/us-ecr-specification" - } - }, - { - "fullUrl": "http://ersd.aimsplatform.org/fhir/Library/rctc", - "resource": { - "resourceType": "Library", - "id": "rctc", - "url": "http://ersd.aimsplatform.org/fhir/Library/rctc", - "version": "1.2.3-draft", - "status": "draft", - "relatedArtifact": [ - { - "type": "composed-of", - "resource": "http://ersd.aimsplatform.org/fhir/ValueSet/dxtc|1.2.3-draft", - "extension":[ - { - "url":"http://hl7.org/fhir/StructureDefinition/crmi-isOwned", - "valueBoolean": true - } - ] - } - ] - }, - "request": { - "method": "PUT", - "url": "Library/rctc" - } - }, - { - "fullUrl": "http://ersd.aimsplatform.org/fhir/ValueSet/dxtc", - "resource": { - "resourceType": "ValueSet", - "id": "dxtc", - "url": "http://ersd.aimsplatform.org/fhir/ValueSet/dxtc", - "version": "1.2.3-draft", - "status": "draft", - "compose": { - "include": [ - { - "valueSet": [ - "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1146.6|20210526" - ] - } - ] - }, - "expansion": { - "timestamp": "2022-10-21T15:18:29-04:00", - "contains": [ - { - "system": "http://hl7.org/fhir/sid/icd-10-cm", - "version": "Provisional_2022-01-12", - "code": "T40.0X1A" - }, - { - "system": "http://hl7.org/fhir/sid/icd-10-cm", - "version": "Provisional_2022-01-12", - "code": "T40.0X2A" - }, - { - "system": "http://hl7.org/fhir/sid/icd-10-cm", - "version": "Provisional_2022-01-12", - "code": "T40.0X3A" - } - ] - } - }, - "request": { - "method": "PUT", - "url": "ValueSet/dxtc" - } - }, - { - "fullUrl": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1146.6", - "resource": { - "resourceType": "ValueSet", - "id": "2.16.840.1.113762.1.4.1146.6", - "url": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1146.6", - "identifier": [ - { - "system": "urn:ietf:rfc:3986", - "value": "urn:oid:2.16.840.1.113762.1.4.1146.6" - } - ], - "version": "20210526", - "status": "active", - "compose": { - "include": [ - { - "system": "http://snomed.info/sct", - "version": "Provisional_2022-04-25", - "concept": [ - { - "code": "1086051000119107", - "display": "Cardiomyopathy due to diphtheria (disorder)" - }, - { - "code": "1086061000119109", - "display": "Diphtheria radiculomyelitis (disorder)" - }, - { - "code": "1086071000119103", - "display": "Diphtheria tubulointerstitial nephropathy (disorder)" - }, - { - "code": "1090211000119102", - "display": "Pharyngeal diphtheria (disorder)" - }, - { - "code": "129667001", - "display": "Diphtheritic peripheral neuritis (disorder)" - }, - { - "code": "13596001", - "display": "Diphtheritic peritonitis (disorder)" - }, - { - "code": "15682004", - "display": "Anterior nasal diphtheria (disorder)" - }, - { - "code": "186347006", - "display": "Diphtheria of penis (disorder)" - }, - { - "code": "18901009", - "display": "Cutaneous diphtheria (disorder)" - }, - { - "code": "194945009", - "display": "Acute myocarditis - diphtheritic (disorder)" - }, - { - "code": "230596007", - "display": "Diphtheritic neuropathy (disorder)" - }, - { - "code": "240422004", - "display": "Tracheobronchial diphtheria (disorder)" - }, - { - "code": "26117009", - "display": "Diphtheritic myocarditis (disorder)" - }, - { - "code": "276197005", - "display": "Infection caused by Corynebacterium diphtheriae (disorder)" - }, - { - "code": "3419005", - "display": "Faucial diphtheria (disorder)" - }, - { - "code": "397428000", - "display": "Diphtheria (disorder)" - }, - { - "code": "397430003", - "display": "Diphtheria caused by Corynebacterium diphtheriae (disorder)" - }, - { - "code": "48278001", - "display": "Diphtheritic cystitis (disorder)" - }, - { - "code": "50215002", - "display": "Laryngeal diphtheria (disorder)" - }, - { - "code": "715659006", - "display": "Diphtheria of respiratory system (disorder)" - }, - { - "code": "75589004", - "display": "Nasopharyngeal diphtheria (disorder)" - }, - { - "code": "7773002", - "display": "Conjunctival diphtheria (disorder)" - }, - { - "code": "789005009", - "display": "Paralysis of uvula after diphtheria (disorder)" - } - ] - } - ] - }, - "expansion": { - "timestamp": "2022-10-21T15:18:29-04:00", - "contains": [ - { - "system": "http://snomed.info/sct", - "version": "Provisional_2022-04-25", - "code": "1086051000119107" - }, - { - "system": "http://snomed.info/sct", - "version": "Provisional_2022-04-25", - "code": "1086061000119109" - }, - { - "system": "http://snomed.info/sct", - "version": "Provisional_2022-04-25", - "code": "1086071000119103" - } - ] - } - }, - "request": { - "method": "PUT", - "url": "ValueSet/2.16.840.1.113762.1.4.1146.6" - } - } - ] -} \ No newline at end of file + "resourceType": "Bundle", + "id": "rctc-release-2022-10-19-Bundle-rctc", + "type": "transaction", + "timestamp": "2022-10-21T15:18:28.504-04:00", + "entry": [ + { + "fullUrl": "http://ersd.aimsplatform.org/fhir/Library/SpecificationLibrary", + "resource": { + "resourceType": "Library", + "id": "SpecificationLibrary", + "url": "http://ersd.aimsplatform.org/fhir/Library/SpecificationLibrary", + "version": "1.2.3-draft", + "status": "draft", + "date": "2023-06-30", + "approvalDate": "2023-06-30", + "relatedArtifact": [ + { + "type": "composed-of", + "resource": "http://ersd.aimsplatform.org/fhir/PlanDefinition/us-ecr-specification|1.2.3-draft", + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/crmi-isOwned", + "valueBoolean": true + } + ] + }, + { + "type": "composed-of", + "resource": "http://ersd.aimsplatform.org/fhir/Library/rctc|1.2.3-draft", + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/crmi-isOwned", + "valueBoolean": true + } + ] + }, + { + "extension": [ + { + "url": "http://aphl.org/fhir/vsm/StructureDefinition/vsm-valueset-priority", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/us/ecr/CodeSystem/us-ph-usage-context", + "code": "emergent" + } + ], + "text": "Emergent" + } + }, + { + "url": "http://aphl.org/fhir/vsm/StructureDefinition/vsm-valueset-condition", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "49649001" + } + ], + "text": "Infection caused by Acanthamoeba (disorder)" + } + } + ], + "type": "depends-on", + "resource": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1146.6|20210526" + } + ] + }, + "request": { + "method": "PUT", + "url": "Library/SpecificationLibrary" + } + }, + { + "fullUrl": "http://ersd.aimsplatform.org/fhir/PlanDefinition/us-ecr-specification", + "resource": { + "resourceType": "PlanDefinition", + "id": "us-ecr-specification", + "url": "http://ersd.aimsplatform.org/fhir/PlanDefinition/us-ecr-specification", + "version": "1.2.3-draft", + "status": "draft", + "relatedArtifact": [ + { + "type": "depends-on", + "label": "RCTC Value Set Library of Trigger Codes", + "resource": "http://ersd.aimsplatform.org/fhir/Library/rctc|1.2.3-draft" + } + ] + }, + "request": { + "method": "PUT", + "url": "PlanDefinition/us-ecr-specification" + } + }, + { + "fullUrl": "http://ersd.aimsplatform.org/fhir/Library/rctc", + "resource": { + "resourceType": "Library", + "id": "rctc", + "url": "http://ersd.aimsplatform.org/fhir/Library/rctc", + "version": "1.2.3-draft", + "status": "draft", + "relatedArtifact": [ + { + "type": "composed-of", + "resource": "http://ersd.aimsplatform.org/fhir/ValueSet/dxtc|1.2.3-draft", + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/crmi-isOwned", + "valueBoolean": true + } + ] + } + ] + }, + "request": { + "method": "PUT", + "url": "Library/rctc" + } + }, + { + "fullUrl": "http://ersd.aimsplatform.org/fhir/ValueSet/dxtc", + "resource": { + "resourceType": "ValueSet", + "id": "dxtc", + "url": "http://ersd.aimsplatform.org/fhir/ValueSet/dxtc", + "version": "1.2.3-draft", + "status": "draft", + "compose": { + "include": [ + { + "valueSet": [ + "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1146.6|20210526" + ] + } + ] + }, + "expansion": { + "timestamp": "2022-10-21T15:18:29-04:00", + "contains": [ + { + "system": "http://hl7.org/fhir/sid/icd-10-cm", + "version": "Provisional_2022-01-12", + "code": "T40.0X1A" + }, + { + "system": "http://hl7.org/fhir/sid/icd-10-cm", + "version": "Provisional_2022-01-12", + "code": "T40.0X2A" + }, + { + "system": "http://hl7.org/fhir/sid/icd-10-cm", + "version": "Provisional_2022-01-12", + "code": "T40.0X3A" + } + ] + } + }, + "request": { + "method": "PUT", + "url": "ValueSet/dxtc" + } + }, + { + "fullUrl": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1146.6", + "resource": { + "resourceType": "ValueSet", + "id": "2.16.840.1.113762.1.4.1146.6", + "url": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1146.6", + "identifier": [ + { + "system": "urn:ietf:rfc:3986", + "value": "urn:oid:2.16.840.1.113762.1.4.1146.6" + } + ], + "version": "20210526", + "status": "active", + "compose": { + "include": [ + { + "system": "http://snomed.info/sct", + "version": "Provisional_2022-04-25", + "concept": [ + { + "code": "1086051000119107", + "display": "Cardiomyopathy due to diphtheria (disorder)" + }, + { + "code": "1086061000119109", + "display": "Diphtheria radiculomyelitis (disorder)" + }, + { + "code": "1086071000119103", + "display": "Diphtheria tubulointerstitial nephropathy (disorder)" + }, + { + "code": "1090211000119102", + "display": "Pharyngeal diphtheria (disorder)" + }, + { + "code": "129667001", + "display": "Diphtheritic peripheral neuritis (disorder)" + }, + { + "code": "13596001", + "display": "Diphtheritic peritonitis (disorder)" + }, + { + "code": "15682004", + "display": "Anterior nasal diphtheria (disorder)" + }, + { + "code": "186347006", + "display": "Diphtheria of penis (disorder)" + }, + { + "code": "18901009", + "display": "Cutaneous diphtheria (disorder)" + }, + { + "code": "194945009", + "display": "Acute myocarditis - diphtheritic (disorder)" + }, + { + "code": "230596007", + "display": "Diphtheritic neuropathy (disorder)" + }, + { + "code": "240422004", + "display": "Tracheobronchial diphtheria (disorder)" + }, + { + "code": "26117009", + "display": "Diphtheritic myocarditis (disorder)" + }, + { + "code": "276197005", + "display": "Infection caused by Corynebacterium diphtheriae (disorder)" + }, + { + "code": "3419005", + "display": "Faucial diphtheria (disorder)" + }, + { + "code": "397428000", + "display": "Diphtheria (disorder)" + }, + { + "code": "397430003", + "display": "Diphtheria caused by Corynebacterium diphtheriae (disorder)" + }, + { + "code": "48278001", + "display": "Diphtheritic cystitis (disorder)" + }, + { + "code": "50215002", + "display": "Laryngeal diphtheria (disorder)" + }, + { + "code": "715659006", + "display": "Diphtheria of respiratory system (disorder)" + }, + { + "code": "75589004", + "display": "Nasopharyngeal diphtheria (disorder)" + }, + { + "code": "7773002", + "display": "Conjunctival diphtheria (disorder)" + }, + { + "code": "789005009", + "display": "Paralysis of uvula after diphtheria (disorder)" + } + ] + } + ] + }, + "expansion": { + "timestamp": "2022-10-21T15:18:29-04:00", + "contains": [ + { + "system": "http://snomed.info/sct", + "version": "Provisional_2022-04-25", + "code": "1086051000119107" + }, + { + "system": "http://snomed.info/sct", + "version": "Provisional_2022-04-25", + "code": "1086061000119109" + }, + { + "system": "http://snomed.info/sct", + "version": "Provisional_2022-04-25", + "code": "1086071000119103" + } + ] + } + }, + "request": { + "method": "PUT", + "url": "ValueSet/2.16.840.1.113762.1.4.1146.6" + } + } + ] +} diff --git a/plugin/cr/src/test/resources/small-drafted-ersd-bundle.json b/plugin/cr/src/test/resources/small-drafted-ersd-bundle.json new file mode 100644 index 000000000..7da01d4ea --- /dev/null +++ b/plugin/cr/src/test/resources/small-drafted-ersd-bundle.json @@ -0,0 +1,465 @@ +{ + "resourceType": "Bundle", + "type": "transaction", + "entry": [ + { + "fullUrl": "http://ersd.aimsplatform.org/fhir/Library/SpecificationLibrary|1.0.0.0-draft", + "resource": { + "resourceType": "Library", + "id": "7", + "meta": { + "versionId": "1", + "lastUpdated": "2023-12-14T15:38:56.845-05:00", + "source": "#FNgLVm21fIyZMxwE" + }, + "url": "http://ersd.aimsplatform.org/fhir/Library/SpecificationLibrary", + "version": "1.0.0.0-draft", + "status": "draft", + "useContext": [ + { + "code": { + "system": "http://hl7.org/fhir/us/ecr/CodeSystem/us-ph-usage-context-type", + "code": "reporting" + }, + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/us/ecr/CodeSystem/us-ph-usage-context", + "code": "triggering" + } + ] + } + }, + { + "code": { + "system": "http://hl7.org/fhir/us/ecr/CodeSystem/us-ph-usage-context-type", + "code": "specification-type" + }, + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/us/ecr/CodeSystem/us-ph-usage-context", + "code": "program" + } + ] + } + } + ], + "relatedArtifact": [ + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/crmi-isOwned", + "valueBoolean": true + } + ], + "type": "composed-of", + "resource": "http://ersd.aimsplatform.org/fhir/PlanDefinition/us-ecr-specification|1.0.0.0-draft" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/crmi-isOwned", + "valueBoolean": true + } + ], + "type": "composed-of", + "resource": "http://ersd.aimsplatform.org/fhir/Library/rctc|1.0.0.0-draft" + }, + { + "type": "composed-of", + "resource": "http://notOwnedTest.com/Library/notOwnedRoot|0.1.1" + }, + { + "extension": [ + { + "url": "http://aphl.org/fhir/vsm/StructureDefinition/vsm-valueset-priority", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/us/ecr/CodeSystem/us-ph-usage-context", + "code": "emergent" + } + ], + "text": "Emergent" + } + }, + { + "url": "http://aphl.org/fhir/vsm/StructureDefinition/vsm-valueset-condition", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "49649001" + } + ], + "text": "Infection caused by Acanthamoeba (disorder)" + } + }, + { + "url": "http://aphl.org/fhir/vsm/StructureDefinition/vsm-valueset-condition", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "767146004" + } + ] + } + } + ], + "type": "depends-on", + "resource": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1146.163|20220603" + }, + { + "extension": [ + { + "url": "http://aphl.org/fhir/vsm/StructureDefinition/vsm-valueset-condition", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "49649001" + } + ], + "text": "Infection caused by Acanthamoeba (disorder)" + } + } + ], + "type": "depends-on", + "resource": "http://cts.nlm.nih.gov/fhir/ValueSet/123-this-will-be-routine|20210526" + }, + { + "type": "depends-on", + "resource": "http://ersd.aimsplatform.org/fhir/ValueSet/dxtc|1.0.0.0-draft" + } + ] + }, + "request": { + "method": "POST", + "url": "Library/7", + "ifNoneExist": "url=http://ersd.aimsplatform.org/fhir/Library/SpecificationLibrary&version=1.0.0.0-draft" + } + }, + { + "fullUrl": "http://ersd.aimsplatform.org/fhir/PlanDefinition/us-ecr-specification|1.0.0.0-draft", + "resource": { + "resourceType": "PlanDefinition", + "id": "8", + "meta": { + "versionId": "1", + "lastUpdated": "2023-12-14T15:38:56.845-05:00", + "source": "#FNgLVm21fIyZMxwE" + }, + "url": "http://ersd.aimsplatform.org/fhir/PlanDefinition/us-ecr-specification", + "version": "1.0.0.0-draft", + "status": "draft", + "relatedArtifact": [ + { + "type": "depends-on", + "label": "RCTC Value Set Library of Trigger Codes", + "resource": "http://ersd.aimsplatform.org/fhir/Library/rctc|1.0.0.0-draft" + }, + { + "type": "composed-of", + "resource": "http://notOwnedTest.com/Library/notOwnedLeaf|0.1.1" + } + ] + }, + "request": { + "method": "POST", + "url": "PlanDefinition/8", + "ifNoneExist": "url=http://ersd.aimsplatform.org/fhir/PlanDefinition/us-ecr-specification&version=1.0.0.0-draft" + } + }, + { + "fullUrl": "http://ersd.aimsplatform.org/fhir/Library/rctc|1.0.0.0-draft", + "resource": { + "resourceType": "Library", + "id": "9", + "meta": { + "versionId": "1", + "lastUpdated": "2023-12-14T15:38:56.845-05:00", + "source": "#FNgLVm21fIyZMxwE" + }, + "url": "http://ersd.aimsplatform.org/fhir/Library/rctc", + "version": "1.0.0.0-draft", + "status": "draft", + "relatedArtifact": [ + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/crmi-isOwned", + "valueBoolean": true + } + ], + "type": "composed-of", + "resource": "http://ersd.aimsplatform.org/fhir/ValueSet/dxtc|1.0.0.0-draft" + }, + { + "type": "composed-of", + "resource": "http://notOwnedTest.com/Library/notOwnedLeaf2|0.1.1" + } + ] + }, + "request": { + "method": "POST", + "url": "Library/9", + "ifNoneExist": "url=http://ersd.aimsplatform.org/fhir/Library/rctc&version=1.0.0.0-draft" + } + }, + { + "fullUrl": "http://ersd.aimsplatform.org/fhir/ValueSet/dxtc|1.0.0.0-draft", + "resource": { + "resourceType": "ValueSet", + "id": "10", + "meta": { + "versionId": "2", + "lastUpdated": "2023-12-14T15:43:06.193-05:00", + "source": "#YvcttWKq2KbM0Igj" + }, + "url": "http://ersd.aimsplatform.org/fhir/ValueSet/dxtc", + "version": "1.0.0.0-draft", + "status": "draft", + "compose": { + "include": [ + { + "valueSet": [ + "http://cts.nlm.nih.gov/fhir/ValueSet/123-this-will-be-routine|20210526" + ] + }, + { + "valueSet": [ + "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1146.163" + ] + } + ] + }, + "expansion": { + "timestamp": "2022-10-21T15:18:29-04:00", + "contains": [ + { + "system": "http://hl7.org/fhir/sid/icd-10-cm", + "version": "Provisional_2022-01-12", + "code": "T40.0X1A" + }, + { + "system": "http://hl7.org/fhir/sid/icd-10-cm", + "version": "Provisional_2022-01-12", + "code": "T40.0X2A" + }, + { + "system": "http://hl7.org/fhir/sid/icd-10-cm", + "version": "Provisional_2022-01-12", + "code": "T40.0X3A" + } + ] + } + }, + "request": { + "method": "POST", + "url": "ValueSet/10", + "ifNoneExist": "url=http://ersd.aimsplatform.org/fhir/ValueSet/dxtc&version=1.0.0.0-draft" + } + }, + { + "fullUrl": "http://cts.nlm.nih.gov/fhir/ValueSet/123-this-will-be-routine|20210526", + "resource": { + "resourceType": "ValueSet", + "id": "123-this-will-be-routine", + "meta": { + "versionId": "1", + "lastUpdated": "2023-12-14T15:38:33.243-05:00", + "source": "#MT6tY32vbfEfzQmh" + }, + "url": "http://cts.nlm.nih.gov/fhir/ValueSet/123-this-will-be-routine", + "identifier": [ + { + "system": "urn:ietf:rfc:3986", + "value": "urn:oid:123-this-will-be-routine" + } + ], + "version": "20210526", + "status": "active", + "compose": { + "include": [ + { + "system": "http://hl7.org/fhir/sid/icd-10-cm", + "version": "Provisional_2022-01-12", + "concept": [ + { + "code": "B60.12", + "display": "Conjunctivitis due to Acanthamoeba" + }, + { + "code": "B60.13", + "display": "Keratoconjunctivitis due to Acanthamoeba" + } + ] + } + ] + } + }, + "request": { + "method": "POST", + "url": "ValueSet/123-this-will-be-routine", + "ifNoneExist": "url=http://cts.nlm.nih.gov/fhir/ValueSet/123-this-will-be-routine&version=20210526" + } + }, + { + "fullUrl": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1146.163|20220603", + "resource": { + "resourceType": "ValueSet", + "id": "11", + "meta": { + "versionId": "1", + "lastUpdated": "2023-12-14T15:38:56.845-05:00", + "source": "#FNgLVm21fIyZMxwE", + "profile": [ + "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/publishable-valueset-cqfm", + "http://hl7.org/fhir/StructureDefinition/shareablevalueset" + ] + }, + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/valueset-author", + "valueString": "CSTE Author" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/valueset-keyWord", + "valueString": "Cholera,G_Enteric,Trigger" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/resource-lastReviewDate", + "valueDate": "2022-12-15" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/valueset-effectiveDate", + "valueDate": "2022-06-03" + }, + { + "url": "http://hl7.org/fhir/StructureDefinition/valueset-authoritativeSource", + "valueUri": "https://cts.nlm.nih.gov/fhir" + } + ], + "url": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1146.163", + "identifier": [ + { + "system": "urn:ietf:rfc:3986", + "value": "urn:oid:2.16.840.1.113762.1.4.1146.163" + } + ], + "version": "20220603", + "name": "Cholera (Disorders) (SNOMED)", + "title": "Cholera (Disorders) (SNOMED)", + "status": "active", + "experimental": false, + "date": "2022-06-03T01:06:35-04:00", + "publisher": "CSTE Steward", + "useContext": [ + { + "code": { + "system": "http://terminology.hl7.org/CodeSystem/usage-context-type", + "code": "focus" + }, + "valueCodeableConcept": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "49649001" + } + ], + "text": "Infection caused by Acanthamoeba (disorder)" + } + }, + { + "code": { + "system": "http://terminology.hl7.org/CodeSystem/usage-context-type", + "code": "focus" + }, + "valueCodeableConcept": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "767146004" + } + ] + } + }, + { + "code": { + "system": "http://hl7.org/fhir/us/ecr/CodeSystem/us-ph-usage-context-type", + "code": "priority" + }, + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/us/ecr/CodeSystem/us-ph-usage-context", + "code": "emergent" + } + ], + "text": "Emergent" + } + } + ], + "jurisdiction": [ + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/data-absent-reason", + "valueCode": "unknown" + } + ] + } + ], + "purpose": "(Clinical Focus: This set of values contains diagnoses or problems that represent that the patient has Cholera regardless of the clinical presentation of the condition),(Data Element Scope: Diagnoses or problems documented in a clinical record.),(Inclusion Criteria: Root1 = Cholera (disorder); \nRoot1 children included = All;\n\nAdded leaf concepts: YES\n\nRoot2 = Intestinal infection due to Vibrio cholerae O1 (disorder); \nRoot2 children included = All;),(Exclusion Criteria: Cholera non-O159 or non-O1; Verner–Morrison syndrome; Cholera vaccine related disorders)", + "compose": { + "include": [ + { + "system": "http://snomed.info/sct", + "concept": [ + { + "code": "1193749009", + "display": "Inflammation of small intestine caused by Vibrio cholerae (disorder)" + }, + { + "code": "1193750009", + "display": "Inflammation of intestine caused by Vibrio cholerae (disorder)" + }, + { + "code": "240349003", + "display": "Cholera caused by Vibrio cholerae O1 Classical biotype (disorder)" + }, + { + "code": "240350003", + "display": "Cholera - non-O1 group vibrio (disorder)" + }, + { + "code": "240351004", + "display": "Cholera - O139 group Vibrio cholerae (disorder)" + }, + { + "code": "447282003", + "display": "Intestinal infection caused by Vibrio cholerae O1 (disorder)" + }, + { + "code": "63650001", + "display": "Cholera (disorder)" + }, + { + "code": "81020007", + "display": "Cholera caused by Vibrio cholerae El Tor (disorder)" + } + ] + } + ] + } + }, + "request": { + "method": "POST", + "url": "ValueSet/11", + "ifNoneExist": "url=http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1146.163&version=20220603" + } + } + ] +}