From a783fd711fd2e4ba6b2d77ddd8d86a5743dbde67 Mon Sep 17 00:00:00 2001 From: c-schuler Date: Tue, 7 Apr 2026 09:10:26 -0600 Subject: [PATCH 1/2] Deduplicate logicDefinition extensions on the moduleDefinitionLibrary --- .../measure/MeasureRefreshProcessor.java | 26 +++++++++++++++++++ .../cqf/tooling/operation/ig/Refresh.java | 20 ++++++++++++++ 2 files changed, 46 insertions(+) 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..64fd01920 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.constants.CqfConstants; 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); + deduplicateLogicDefinitions(moduleDefinitionLibrary); measureToUse.setDate(new Date()); // http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/measure-cqfm setMeta(measureToUse, moduleDefinitionLibrary); @@ -102,6 +104,30 @@ private Set getExpressions(Measure measureToUse) { return expressionSet; } + private void deduplicateLogicDefinitions(Library moduleDefinitionLibrary) { + Set seen = new HashSet<>(); + moduleDefinitionLibrary.getExtension().removeIf(ext -> { + if (ext.hasUrl() && ext.getUrl().equals(CqfConstants.LOGIC_DEFINITION_EXT_URL)) { + String key = getLogicDefinitionKey(ext); + return key != null && !seen.add(key); + } + return false; + }); + } + + private 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; + } + private void clearMeasureExtensions(Measure measure, String extensionUrl) { List extensionsToRemove = measure.getExtensionsByUrl(extensionUrl); measure.getExtension().removeAll(extensionsToRemove); 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..a0faf1f72 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 @@ -66,14 +66,34 @@ 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 (extension.hasUrl() && extension.getUrl().equals(CqfmConstants.LOGIC_DEFINITION_EXT_URL)) { + String key = getLogicDefinitionKey(extension); + if (key != null && !logicDefinitionKeys.add(key)) { + continue; + } + } resource.addExtension(extension); } } + private 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 void attachModuleDefinitionLibrary(MetadataResource resource, Library moduleDefinitionLibrary) { resource.getContained().removeIf(res -> res.getId() .equalsIgnoreCase("#" + CrmiConstants.EFFECTIVE_DATA_REQUIREMENTS_IDENTIFIER)); From 156af79ca1fc02935ba75e94a28e332d8ca4754c Mon Sep 17 00:00:00 2001 From: c-schuler Date: Thu, 16 Apr 2026 11:54:10 -0600 Subject: [PATCH 2/2] Created util for common code and added focused unit testing for new utility --- .../measure/MeasureRefreshProcessor.java | 28 +----- .../cqf/tooling/operation/ig/Refresh.java | 18 +--- .../PlanDefinitionRefreshProcessor.java | 2 + .../utilities/LogicDefinitionUtils.java | 45 ++++++++++ .../utilities/LogicDefinitionUtilsTests.java | 90 +++++++++++++++++++ 5 files changed, 142 insertions(+), 41 deletions(-) create mode 100644 tooling/src/main/java/org/opencds/cqf/tooling/utilities/LogicDefinitionUtils.java create mode 100644 tooling/src/test/java/org/opencds/cqf/tooling/utilities/LogicDefinitionUtilsTests.java 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 64fd01920..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,7 +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.constants.CqfConstants; +import org.opencds.cqf.tooling.utilities.LogicDefinitionUtils; import org.opencds.cqf.tooling.utilities.constants.CrmiConstants; public class MeasureRefreshProcessor { @@ -40,7 +40,7 @@ public Measure refreshMeasure(Measure measureToUse, LibraryManager libraryManage Library moduleDefinitionLibrary = getModuleDefinitionLibrary(measureToUse, libraryManager, compiledLibrary, options); removeModelInfoDependencies(moduleDefinitionLibrary); - deduplicateLogicDefinitions(moduleDefinitionLibrary); + LogicDefinitionUtils.deduplicate(moduleDefinitionLibrary.getExtension()); measureToUse.setDate(new Date()); // http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/measure-cqfm setMeta(measureToUse, moduleDefinitionLibrary); @@ -104,30 +104,6 @@ private Set getExpressions(Measure measureToUse) { return expressionSet; } - private void deduplicateLogicDefinitions(Library moduleDefinitionLibrary) { - Set seen = new HashSet<>(); - moduleDefinitionLibrary.getExtension().removeIf(ext -> { - if (ext.hasUrl() && ext.getUrl().equals(CqfConstants.LOGIC_DEFINITION_EXT_URL)) { - String key = getLogicDefinitionKey(ext); - return key != null && !seen.add(key); - } - return false; - }); - } - - private 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; - } - private void clearMeasureExtensions(Measure measure, String extensionUrl) { List extensionsToRemove = measure.getExtensionsByUrl(extensionUrl); measure.getExtension().removeAll(extensionsToRemove); 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 a0faf1f72..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; @@ -71,8 +72,8 @@ public void refreshCqfmExtensions(MetadataResource resource, Library moduleDefin if (extension.hasUrl() && extension.getUrl().equals(CqfmConstants.DIRECT_REF_CODE_EXT_URL)) { continue; } - if (extension.hasUrl() && extension.getUrl().equals(CqfmConstants.LOGIC_DEFINITION_EXT_URL)) { - String key = getLogicDefinitionKey(extension); + if (LogicDefinitionUtils.isLogicDefinition(extension)) { + String key = LogicDefinitionUtils.getLogicDefinitionKey(extension); if (key != null && !logicDefinitionKeys.add(key)) { continue; } @@ -81,19 +82,6 @@ public void refreshCqfmExtensions(MetadataResource resource, Library moduleDefin } } - private 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 void attachModuleDefinitionLibrary(MetadataResource resource, Library moduleDefinitionLibrary) { resource.getContained().removeIf(res -> res.getId() .equalsIgnoreCase("#" + CrmiConstants.EFFECTIVE_DATA_REQUIREMENTS_IDENTIFIER)); 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); + } +}