diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/measure/MeasureRefreshProcessor.java b/tooling/src/main/java/org/opencds/cqf/tooling/measure/MeasureRefreshProcessor.java index d0b1e2ae8..bafca000a 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/measure/MeasureRefreshProcessor.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/measure/MeasureRefreshProcessor.java @@ -20,6 +20,7 @@ import org.hl7.fhir.r5.model.RelatedArtifact; import org.hl7.fhir.r5.model.Resource; import org.hl7.fhir.r5.model.StringType; +import org.opencds.cqf.tooling.utilities.LogicDefinitionUtils; import org.opencds.cqf.tooling.utilities.constants.CrmiConstants; public class MeasureRefreshProcessor { @@ -39,6 +40,7 @@ public Measure refreshMeasure(Measure measureToUse, LibraryManager libraryManage Library moduleDefinitionLibrary = getModuleDefinitionLibrary(measureToUse, libraryManager, compiledLibrary, options); removeModelInfoDependencies(moduleDefinitionLibrary); + LogicDefinitionUtils.deduplicate(moduleDefinitionLibrary.getExtension()); measureToUse.setDate(new Date()); // http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/measure-cqfm setMeta(measureToUse, moduleDefinitionLibrary); diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/operation/ig/Refresh.java b/tooling/src/main/java/org/opencds/cqf/tooling/operation/ig/Refresh.java index 74d75ce35..5d275cde1 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/operation/ig/Refresh.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/operation/ig/Refresh.java @@ -9,6 +9,7 @@ import org.hl7.fhir.r5.model.*; import org.opencds.cqf.tooling.parameter.RefreshIGParameters; import org.opencds.cqf.tooling.utilities.BundleUtils; +import org.opencds.cqf.tooling.utilities.LogicDefinitionUtils; import org.opencds.cqf.tooling.utilities.constants.CqfmConstants; import org.opencds.cqf.tooling.utilities.constants.CrmiConstants; import org.opencds.cqf.tooling.utilities.converters.ResourceAndTypeConverter; @@ -66,10 +67,17 @@ public void refreshCqfmExtensions(MetadataResource resource, Library moduleDefin resource.getExtension().removeAll(resource.getExtensionsByUrl(CqfmConstants.LOGIC_DEFINITION_EXT_URL)); resource.getExtension().removeAll(resource.getExtensionsByUrl(CqfmConstants.EFFECTIVE_DATA_REQS_EXT_URL)); + Set logicDefinitionKeys = new HashSet<>(); for (Extension extension : moduleDefinitionLibrary.getExtension()) { if (extension.hasUrl() && extension.getUrl().equals(CqfmConstants.DIRECT_REF_CODE_EXT_URL)) { continue; } + if (LogicDefinitionUtils.isLogicDefinition(extension)) { + String key = LogicDefinitionUtils.getLogicDefinitionKey(extension); + if (key != null && !logicDefinitionKeys.add(key)) { + continue; + } + } resource.addExtension(extension); } } diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/plandefinition/PlanDefinitionRefreshProcessor.java b/tooling/src/main/java/org/opencds/cqf/tooling/plandefinition/PlanDefinitionRefreshProcessor.java index 19730f9ff..0c74bf6c3 100644 --- a/tooling/src/main/java/org/opencds/cqf/tooling/plandefinition/PlanDefinitionRefreshProcessor.java +++ b/tooling/src/main/java/org/opencds/cqf/tooling/plandefinition/PlanDefinitionRefreshProcessor.java @@ -5,6 +5,7 @@ import org.cqframework.cql.cql2elm.model.CompiledLibrary; import org.cqframework.cql.elm.requirements.fhir.DataRequirementsProcessor; import org.hl7.fhir.r5.model.*; +import org.opencds.cqf.tooling.utilities.LogicDefinitionUtils; import org.opencds.cqf.tooling.utilities.constants.CqfConstants; import org.opencds.cqf.tooling.utilities.constants.CqfmConstants; import org.opencds.cqf.tooling.utilities.constants.CrmiConstants; @@ -23,6 +24,7 @@ public PlanDefinition refreshPlanDefinition(PlanDefinition planToUse, LibraryMan var dqReqTrans = new DataRequirementsProcessor(); var moduleDefinitionLibrary = dqReqTrans.gatherDataRequirements(libraryManager, compiledLibrary, options, expressions, true); + LogicDefinitionUtils.deduplicate(moduleDefinitionLibrary.getExtension()); // Clear all existing CQFM extensions // These extensions are now deprecated, but may be in use for older artifacts diff --git a/tooling/src/main/java/org/opencds/cqf/tooling/utilities/LogicDefinitionUtils.java b/tooling/src/main/java/org/opencds/cqf/tooling/utilities/LogicDefinitionUtils.java new file mode 100644 index 000000000..9b23d3af0 --- /dev/null +++ b/tooling/src/main/java/org/opencds/cqf/tooling/utilities/LogicDefinitionUtils.java @@ -0,0 +1,45 @@ +package org.opencds.cqf.tooling.utilities; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.hl7.fhir.r5.model.Extension; +import org.opencds.cqf.tooling.utilities.constants.CqfConstants; +import org.opencds.cqf.tooling.utilities.constants.CqfmConstants; + +public class LogicDefinitionUtils { + + private LogicDefinitionUtils() { + } + + public static String getLogicDefinitionKey(Extension logicDefinition) { + String libraryName = null; + String name = null; + for (Extension sub : logicDefinition.getExtension()) { + if ("libraryName".equals(sub.getUrl()) && sub.hasValue()) { + libraryName = sub.getValue().primitiveValue(); + } else if ("name".equals(sub.getUrl()) && sub.hasValue()) { + name = sub.getValue().primitiveValue(); + } + } + return (libraryName != null && name != null) ? libraryName + "|" + name : null; + } + + public static boolean isLogicDefinition(Extension extension) { + return extension.hasUrl() + && (CqfmConstants.LOGIC_DEFINITION_EXT_URL.equals(extension.getUrl()) + || CqfConstants.LOGIC_DEFINITION_EXT_URL.equals(extension.getUrl())); + } + + public static void deduplicate(List extensions) { + Set seen = new HashSet<>(); + extensions.removeIf(ext -> { + if (isLogicDefinition(ext)) { + String key = getLogicDefinitionKey(ext); + return key != null && !seen.add(key); + } + return false; + }); + } +} diff --git a/tooling/src/test/java/org/opencds/cqf/tooling/utilities/LogicDefinitionUtilsTests.java b/tooling/src/test/java/org/opencds/cqf/tooling/utilities/LogicDefinitionUtilsTests.java new file mode 100644 index 000000000..fb7525844 --- /dev/null +++ b/tooling/src/test/java/org/opencds/cqf/tooling/utilities/LogicDefinitionUtilsTests.java @@ -0,0 +1,90 @@ +package org.opencds.cqf.tooling.utilities; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNull; + +import java.util.ArrayList; +import java.util.List; + +import org.hl7.fhir.r5.model.Extension; +import org.hl7.fhir.r5.model.IntegerType; +import org.hl7.fhir.r5.model.StringType; +import org.opencds.cqf.tooling.utilities.constants.CqfConstants; +import org.opencds.cqf.tooling.utilities.constants.CqfmConstants; +import org.testng.annotations.Test; + +public class LogicDefinitionUtilsTests { + + private static Extension logicDefinition(String url, String libraryName, String name, int displaySequence) { + Extension ext = new Extension().setUrl(url); + ext.addExtension(new Extension().setUrl("libraryName").setValue(new StringType(libraryName))); + ext.addExtension(new Extension().setUrl("name").setValue(new StringType(name))); + ext.addExtension(new Extension().setUrl("displaySequence").setValue(new IntegerType(displaySequence))); + return ext; + } + + @Test + public void TestKeyUsesLibraryNameAndName() { + Extension ext = logicDefinition(CqfConstants.LOGIC_DEFINITION_EXT_URL, "HRDMeasure", "Inpatient Beds Initial Population", 38); + assertEquals(LogicDefinitionUtils.getLogicDefinitionKey(ext), "HRDMeasure|Inpatient Beds Initial Population"); + } + + @Test + public void TestKeyIsNullWhenLibraryNameMissing() { + Extension ext = new Extension().setUrl(CqfConstants.LOGIC_DEFINITION_EXT_URL); + ext.addExtension(new Extension().setUrl("name").setValue(new StringType("X"))); + assertNull(LogicDefinitionUtils.getLogicDefinitionKey(ext)); + } + + @Test + public void TestIsLogicDefinitionRecognizesBothUrls() { + Extension cqf = new Extension().setUrl(CqfConstants.LOGIC_DEFINITION_EXT_URL); + Extension cqfm = new Extension().setUrl(CqfmConstants.LOGIC_DEFINITION_EXT_URL); + Extension other = new Extension().setUrl("http://example.org/other"); + assertEquals(LogicDefinitionUtils.isLogicDefinition(cqf), true); + assertEquals(LogicDefinitionUtils.isLogicDefinition(cqfm), true); + assertEquals(LogicDefinitionUtils.isLogicDefinition(other), false); + } + + @Test + public void TestDeduplicateRemovesDuplicatesKeepingFirst() { + List extensions = new ArrayList<>(); + extensions.add(logicDefinition(CqfConstants.LOGIC_DEFINITION_EXT_URL, "HRDMeasure", "Inpatient Beds Initial Population", 38)); + extensions.add(logicDefinition(CqfConstants.LOGIC_DEFINITION_EXT_URL, "HRDMeasure", "Adult Inpatient Beds Initial Population", 40)); + extensions.add(logicDefinition(CqfConstants.LOGIC_DEFINITION_EXT_URL, "HRDMeasure", "Inpatient Beds Initial Population", 82)); + + LogicDefinitionUtils.deduplicate(extensions); + + assertEquals(extensions.size(), 2); + assertEquals(LogicDefinitionUtils.getLogicDefinitionKey(extensions.get(0)), "HRDMeasure|Inpatient Beds Initial Population"); + // The first-encountered entry is kept, so the displaySequence of the survivor is 38, not 82. + Extension kept = extensions.get(0); + int displaySequence = ((IntegerType) kept.getExtensionByUrl("displaySequence").getValue()).getValue(); + assertEquals(displaySequence, 38); + } + + @Test + public void TestDeduplicatePreservesNonLogicDefinitionExtensions() { + List extensions = new ArrayList<>(); + extensions.add(new Extension().setUrl("http://example.org/other").setValue(new StringType("a"))); + extensions.add(logicDefinition(CqfConstants.LOGIC_DEFINITION_EXT_URL, "Lib", "Def", 1)); + extensions.add(logicDefinition(CqfConstants.LOGIC_DEFINITION_EXT_URL, "Lib", "Def", 2)); + extensions.add(new Extension().setUrl("http://example.org/other").setValue(new StringType("b"))); + + LogicDefinitionUtils.deduplicate(extensions); + + // One logicDefinition removed; both unrelated extensions retained. + assertEquals(extensions.size(), 3); + } + + @Test + public void TestDeduplicateDedupesAcrossCqfmAndCqfUrls() { + List extensions = new ArrayList<>(); + extensions.add(logicDefinition(CqfmConstants.LOGIC_DEFINITION_EXT_URL, "Lib", "Def", 1)); + extensions.add(logicDefinition(CqfConstants.LOGIC_DEFINITION_EXT_URL, "Lib", "Def", 2)); + + LogicDefinitionUtils.deduplicate(extensions); + + assertEquals(extensions.size(), 1); + } +}