diff --git a/pom.xml b/pom.xml index 47679406..ac71d7bd 100644 --- a/pom.xml +++ b/pom.xml @@ -267,7 +267,7 @@ gov.cms.madie madie-java-models - 0.6.30-SNAPSHOT + 0.6.42-SNAPSHOT jackson-core diff --git a/src/main/java/gov/cms/mat/cql_elm_translation/controllers/CqlToolsController.java b/src/main/java/gov/cms/mat/cql_elm_translation/controllers/CqlToolsController.java index 35b4d502..7ac3ffd7 100644 --- a/src/main/java/gov/cms/mat/cql_elm_translation/controllers/CqlToolsController.java +++ b/src/main/java/gov/cms/mat/cql_elm_translation/controllers/CqlToolsController.java @@ -2,6 +2,7 @@ import gov.cms.madie.models.dto.TranslatedLibrary; import gov.cms.madie.models.measure.Measure; +import gov.cms.mat.cql_elm_translation.dto.CqlBuilderLookup; import gov.cms.mat.cql_elm_translation.dto.CqlLookupRequest; import gov.cms.mat.cql_elm_translation.dto.CqlLookups; import gov.cms.mat.cql_elm_translation.dto.SourceDataCriteria; @@ -103,4 +104,14 @@ public ResponseEntity getCqlLookups( cqlParsingService.getCqlLookups( lookupRequest.getCql(), lookupRequest.getMeasureExpressions(), accessToken)); } + + @PutMapping( + value = "/cql-builder-lookups", + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getCqlBuilderLookups( + @RequestBody String cql, @RequestHeader("Authorization") String accessToken) { + log.info("Preparing CqlBuilder Lookups"); + return ResponseEntity.ok(cqlParsingService.getCqlBuilderLookups(cql, accessToken)); + } } diff --git a/src/main/java/gov/cms/mat/cql_elm_translation/dto/CqlBuilderLookup.java b/src/main/java/gov/cms/mat/cql_elm_translation/dto/CqlBuilderLookup.java new file mode 100644 index 00000000..edef860f --- /dev/null +++ b/src/main/java/gov/cms/mat/cql_elm_translation/dto/CqlBuilderLookup.java @@ -0,0 +1,23 @@ +package gov.cms.mat.cql_elm_translation.dto; + +import lombok.Builder; +import lombok.Data; + +import java.util.Set; + +@Data +@Builder +public class CqlBuilderLookup { + private Set parameters; + private Set definitions; + private Set functions; + private Set fluentFunctions; + + @Builder + public static class Lookup { + private String name; + private String libraryName; + private String libraryAlias; + private String logic; + } +} diff --git a/src/main/java/gov/cms/mat/cql_elm_translation/service/CqlParsingService.java b/src/main/java/gov/cms/mat/cql_elm_translation/service/CqlParsingService.java index 695fa2ce..f3f7dd0b 100644 --- a/src/main/java/gov/cms/mat/cql_elm_translation/service/CqlParsingService.java +++ b/src/main/java/gov/cms/mat/cql_elm_translation/service/CqlParsingService.java @@ -1,5 +1,6 @@ package gov.cms.mat.cql_elm_translation.service; +import gov.cms.mat.cql_elm_translation.dto.CqlBuilderLookup; import gov.cms.mat.cql_elm_translation.dto.CqlLookups; import gov.cms.mat.cql_elm_translation.dto.ElementLookup; import gov.cms.mat.cql_elm_translation.utils.cql.CQLTools; @@ -45,13 +46,13 @@ public Set getAllDefinitions(String cql, String accessToken) { } /** - * Parses the CQL and generates cql artifacts(including for the CQL of the included Libraries). - * refer CQL artifacts- gov.cms.mat.cql_elm_translation.dto.CQLLookups + * Parses the CQL and generates only used cql artifacts(including for the CQL of the included + * Libraries). refer CQL artifacts- gov.cms.mat.cql_elm_translation.dto.CQLLookups * * @param cql- measure cql * @param measureExpressions- set of cql definitions used in measure groups, SDEs & RAVs * @param accessToken Requesting User's Okta Bearer token - * @return CQLLookups + * @return CQLLookups -> building blocks for HQMF and Human Readable generation */ public CqlLookups getCqlLookups(String cql, Set measureExpressions, String accessToken) { if (StringUtils.isBlank(cql) || CollectionUtils.isEmpty(measureExpressions)) { @@ -104,6 +105,78 @@ public CqlLookups getCqlLookups(String cql, Set measureExpressions, Stri .build(); } + /** + * Parses the CQL and collect all CQL building blocks irrespective of used or unused(including for + * the CQL of the included Libraries) + * + * @param cql- measure cql + * @param accessToken Requesting User's Okta Bearer token + * @return CqlBuilderLookup -> building blocks for CQL Definition UI builder + */ + public CqlBuilderLookup getCqlBuilderLookups(String cql, String accessToken) { + if (StringUtils.isBlank(cql)) { + return null; + } + + CQLTools cqlTools = parseCql(cql, accessToken, cqlLibraryService, null); + // all parameters + Set parameters = + cqlTools.getAllParameters().stream().map(this::buildParameterLookup).collect(toSet()); + + // get all CQLDefinitions including functions + Set allCqlDefinitions = buildCqlDefinitions(cqlTools); + // prepare lookups for definitions, functions and fluent functions from CQLDefinitions + Set definitions = new HashSet<>(); + Set functions = new HashSet<>(); + Set fluentFunctions = new HashSet<>(); + for (CQLDefinition cqlDefinition : allCqlDefinitions) { + CqlBuilderLookup.Lookup lookup = + buildCqlBuilderLookup( + cqlDefinition.getName(), + cqlDefinition.getLogic(), + cqlDefinition.getParentLibrary(), + cqlDefinition.getLibraryDisplayName()); + if (cqlDefinition.isFunction()) { + if (StringUtils.startsWith(cqlDefinition.getLogic(), "define fluent function")) { + fluentFunctions.add(lookup); + } else { + functions.add(lookup); + } + } else { + definitions.add(lookup); + } + } + return CqlBuilderLookup.builder() + .parameters(parameters) + .definitions(definitions) + .functions(functions) + .fluentFunctions(fluentFunctions) + .build(); + } + + private CqlBuilderLookup.Lookup buildParameterLookup(CQLParameter parameter) { + String[] parts = parameter.getParameterName().split("\\|"); + String name = parameter.getParameterName(); + String libraryName = null; + String libraryAlias = null; + if (parts.length == 3) { + libraryName = parts[0].split("-")[0]; + libraryAlias = parts[1]; + name = parts[2]; + } + return buildCqlBuilderLookup(name, parameter.getParameterLogic(), libraryName, libraryAlias); + } + + private CqlBuilderLookup.Lookup buildCqlBuilderLookup( + String name, String logic, String libraryName, String libraryAlias) { + return CqlBuilderLookup.Lookup.builder() + .name(name) + .logic(logic) + .libraryName(libraryName) + .libraryAlias(libraryAlias) + .build(); + } + /** * Maps the references between CQL Definitions. In other words, which CQL Definitions and * Functions are called by which other CQL Definition. @@ -328,13 +401,7 @@ private CQLDefinition buildCqlDefinition(DefinitionContent definitionContent) { definition.setLibraryVersion(libraryParts[1]); } } - // TODO could use a stronger comparator for determining if node is Definition or Function - definition.setFunction(definition.getDefinitionLogic().startsWith("define function")); + definition.setFunction(definitionContent.isFunction()); return definition; } - - public Map> getUsedFunctions(String cql, String accessToken) { - CQLTools cqlTools = parseCql(cql, accessToken, cqlLibraryService, null); - return cqlTools.getUsedFunctions(); - } } diff --git a/src/main/java/gov/cms/mat/cql_elm_translation/service/DataCriteriaService.java b/src/main/java/gov/cms/mat/cql_elm_translation/service/DataCriteriaService.java index e3cc43f7..4e8c6fba 100644 --- a/src/main/java/gov/cms/mat/cql_elm_translation/service/DataCriteriaService.java +++ b/src/main/java/gov/cms/mat/cql_elm_translation/service/DataCriteriaService.java @@ -26,10 +26,6 @@ public class DataCriteriaService extends CqlTooling { private final CqlLibraryService cqlLibraryService; - public DataCriteria parseDataCriteriaFromCql(String cql, String accessToken) { - return parseCql(cql, accessToken, cqlLibraryService, null).getDataCriteria(); - } - public Set getRelevantElements(Measure measure, String accessToken) { if (StringUtils.isBlank(measure.getCql())) { log.info("Data criteria not found as cql is blank"); diff --git a/src/main/java/gov/cms/mat/cql_elm_translation/utils/cql/CQLTools.java b/src/main/java/gov/cms/mat/cql_elm_translation/utils/cql/CQLTools.java index 3dacfb46..72675959 100644 --- a/src/main/java/gov/cms/mat/cql_elm_translation/utils/cql/CQLTools.java +++ b/src/main/java/gov/cms/mat/cql_elm_translation/utils/cql/CQLTools.java @@ -84,6 +84,7 @@ public class CQLTools { private Set usedCodeSystems = new HashSet<>(); private DataCriteria dataCriteria = new DataCriteria(); private Set definitionContents = new HashSet<>(); + private Set allParameters = new HashSet<>(); private Map> callstack = new HashMap<>(); private UsingProperties usingProperties; @@ -140,6 +141,7 @@ public void generate() throws IOException { walker.walk(listener, tree); definitionContents.addAll(listener.getDefinitionContents()); + allParameters.addAll(listener.getParameters()); callstack = graph.getAdjacencyList(); Set librariesSet = new HashSet<>(listener.getLibraries()); diff --git a/src/main/java/gov/cms/mat/cql_elm_translation/utils/cql/parsing/Cql2ElmListener.java b/src/main/java/gov/cms/mat/cql_elm_translation/utils/cql/parsing/Cql2ElmListener.java index 1d15ced9..1911c73c 100644 --- a/src/main/java/gov/cms/mat/cql_elm_translation/utils/cql/parsing/Cql2ElmListener.java +++ b/src/main/java/gov/cms/mat/cql_elm_translation/utils/cql/parsing/Cql2ElmListener.java @@ -163,12 +163,20 @@ public void enterCodesystemDefinition(cqlParser.CodesystemDefinitionContext ctx) CQLCodeSystem codeSystem = new CQLCodeSystem(); codeSystem.setId(csDef.getId()); codeSystem.setOID(csDef.getId()); + codeSystem.setCodeSystemVersion(getParsedVersion(csDef.getVersion())); codeSystem.setCodeSystemName(csDef.getName()); codeSystemMap.putIfAbsent(identifier, codeSystem); } } + private String getParsedVersion(String version) { + if (version != null && version.startsWith("urn:hl7:version:")) { + return version.substring("urn:hl7:version:".length()); + } + return version; + } + @Override public void enterQualifiedFunction(QualifiedFunctionContext ctx) { if (ctx.identifierOrFunctionIdentifier() != null @@ -201,6 +209,14 @@ public void enterQualifiedIdentifierExpression(QualifiedIdentifierExpressionCont } } + @Override + public void enterLocalIdentifier(cqlParser.LocalIdentifierContext ctx) { + String identifier = parseString(ctx.identifier().getText()); + if (shouldResolve(identifier)) { + resolve(identifier, getCurrentLibraryContext()); + } + } + @Override public void enterReferentialIdentifier(ReferentialIdentifierContext ctx) { String identifier = parseString(ctx.getText()); @@ -275,6 +291,7 @@ public void enterFunctionDefinition(@NotNull cqlParser.FunctionDefinitionContext .name(currentContext) .content(content) .functionArguments(functionArguments) + .function(true) .build()); graph.addNode(currentContext); } @@ -335,6 +352,9 @@ public void enterParameterDefinition(@NotNull cqlParser.ParameterDefinitionConte String identifier = parseString(ctx.identifier().getText()); this.currentContext = libraryIdentifier + identifier; graph.addNode(currentContext); + if (shouldResolve(identifier)) { + resolve(identifier, getCurrentLibraryContext()); + } } @Override @@ -555,18 +575,29 @@ private Element resolve(String identifier, CompiledLibrary library) { currentContext, def.getPath() + "-" + def.getVersion() + "|" + def.getLocalIdentifier()); libraryAccessor = def; try { - parseChildLibraries(def); - libraries.add( - CQLIncludeLibrary.builder() - .cqlLibraryName(def.getPath()) - .aliasName(def.getLocalIdentifier()) - .version(def.getVersion()) - // TODO: should be taken from librarySetId - .id(def.getTrackerId().toString()) - .setId(def.getTrackerId().toString()) - .build()); + var parsedLibrary = + libraries.stream() + .filter( + l -> + l.getCqlLibraryName().equalsIgnoreCase(def.getPath()) + && l.getVersion().equalsIgnoreCase(def.getVersion())) + .findFirst(); + if (parsedLibrary.isEmpty()) { + parseChildLibraries(def); + libraries.add( + CQLIncludeLibrary.builder() + .cqlLibraryName(def.getPath()) + .aliasName(def.getLocalIdentifier()) + .version(def.getVersion()) + // TODO: should be taken from librarySetId + .id(def.getTrackerId().toString()) + .setId(def.getTrackerId().toString()) + .build()); + } } catch (IOException e) { - e.printStackTrace(); + log.error( + "IOException while parsing child library [{}] " + e.getMessage(), + def.getPath() + "-" + def.getVersion()); } } else if (element instanceof CodeDef codeDef) { codes.add(formattedIdentifier); @@ -577,6 +608,8 @@ private Element resolve(String identifier, CompiledLibrary library) { .codeName(codeDef.getDisplay()) .codeSystemName(codeDef.getCodeSystem().getName()) .codeSystemOID(cqlCodeSystem == null ? null : cqlCodeSystem.getOID()) + .codeSystemVersion( + cqlCodeSystem == null ? null : cqlCodeSystem.getCodeSystemVersion()) .codeIdentifier(formattedIdentifier) .build(); declaredCodes.add(declaredCode); @@ -633,9 +666,6 @@ private void parseChildLibraries(IncludeDef def) throws IOException { cqlLexer lexer = new cqlLexer(CharStreams.fromStream(stream)); CommonTokenStream tokens = new CommonTokenStream(lexer); cqlParser parser = new cqlParser(tokens); - - // CompiledLibrary childLibrary = this.translatedLibraryMap.get(def.getPath() + "-" + - // def.getVersion()); CompiledLibrary childLibrary = this.translatedLibraryMap.get(def.getPath()); Cql2ElmListener listener = new Cql2ElmListener( diff --git a/src/main/java/gov/cms/mat/cql_elm_translation/utils/cql/parsing/model/CQLGraph.java b/src/main/java/gov/cms/mat/cql_elm_translation/utils/cql/parsing/model/CQLGraph.java index ad97e0f3..0c0fbee1 100644 --- a/src/main/java/gov/cms/mat/cql_elm_translation/utils/cql/parsing/model/CQLGraph.java +++ b/src/main/java/gov/cms/mat/cql_elm_translation/utils/cql/parsing/model/CQLGraph.java @@ -9,6 +9,9 @@ import java.util.Queue; import java.util.Set; +import lombok.extern.slf4j.Slf4j; + +@Slf4j public class CQLGraph { private final Map> graph = new HashMap<>(); @@ -49,20 +52,32 @@ public boolean isPath(String source, String destination) { while (!queue.isEmpty()) { String currentNode = queue.remove(); - List adjacentVertices = new ArrayList<>(this.graph.get(currentNode)); - - for (String adjacentNode : adjacentVertices) { - // we've found the destination node that we were looking for, so return true. - if (adjacentNode.equals(destination)) { - return true; - } - // if it's not the destination node and the node hasn't been visited yet, add it to the - // queue to be visited. - if (!visited.contains(adjacentNode)) { - visited.add(adjacentNode); - queue.add(adjacentNode); + if (this.graph.get(currentNode) != null) { + List adjacentVertices = new ArrayList<>(this.graph.get(currentNode)); + + for (String adjacentNode : adjacentVertices) { + // we've found the destination node that we were looking for, so return true. + if (adjacentNode.equals(destination)) { + return true; + } + + // if it's not the destination node and the node hasn't been visited yet, add it to the + // queue to be visited. + if (!visited.contains(adjacentNode)) { + visited.add(adjacentNode); + queue.add(adjacentNode); + } } + } else { + log.error( + "source = " + + source + + " destination = " + + destination + + " this.graph.get currentNode: " + + currentNode + + " is null"); } } diff --git a/src/main/java/gov/cms/mat/cql_elm_translation/utils/cql/parsing/model/DefinitionContent.java b/src/main/java/gov/cms/mat/cql_elm_translation/utils/cql/parsing/model/DefinitionContent.java index 860f8355..49d49370 100644 --- a/src/main/java/gov/cms/mat/cql_elm_translation/utils/cql/parsing/model/DefinitionContent.java +++ b/src/main/java/gov/cms/mat/cql_elm_translation/utils/cql/parsing/model/DefinitionContent.java @@ -11,4 +11,5 @@ public class DefinitionContent { private String name; private String content; private List functionArguments; + private boolean function; } diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 0edbe337..706532a5 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -1,8 +1,7 @@ server: port: 8083 servlet: - context-path: /api/fhir - + context-path: /api/qdm spring: profiles: active: local @@ -29,8 +28,8 @@ madie: cql: uri: /cql-libraries/cql translatorVersion: - currentVersion: ${CURRENT_TRANSLATOR_VERSION:3.3.2} - mostRecentVersion: ${MOST_RECENT_TRANSLATOR_VERSION:3.3.2} + currentVersion: ${CURRENT_TRANSLATOR_VERSION:3.11.0} + mostRecentVersion: ${MOST_RECENT_TRANSLATOR_VERSION:3.11.0} springdoc: swagger-ui: diff --git a/src/test/java/gov/cms/mat/cql_elm_translation/controllers/CqlToolsControllerTest.java b/src/test/java/gov/cms/mat/cql_elm_translation/controllers/CqlToolsControllerTest.java index 99738fba..58cff8c3 100644 --- a/src/test/java/gov/cms/mat/cql_elm_translation/controllers/CqlToolsControllerTest.java +++ b/src/test/java/gov/cms/mat/cql_elm_translation/controllers/CqlToolsControllerTest.java @@ -2,6 +2,7 @@ import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; @@ -25,6 +26,7 @@ import gov.cms.madie.models.dto.TranslatedLibrary; +import gov.cms.mat.cql_elm_translation.dto.CqlBuilderLookup; import gov.cms.mat.cql_elm_translation.dto.CqlLookupRequest; import gov.cms.mat.cql_elm_translation.dto.CqlLookups; import org.cqframework.cql.tools.formatter.CqlFormatterVisitor; @@ -211,4 +213,27 @@ void testGetCqlLookups() { assertThat(cqlLookups.getLibrary(), is(equalTo("Test"))); assertThat(cqlLookups.getVersion(), is(equalTo("0.0.001"))); } + + @Test + void testGetCqlBuilderLookups() { + var p = CqlBuilderLookup.Lookup.builder().name("Parameter").logic("abc").build(); + var d = CqlBuilderLookup.Lookup.builder().name("Definition").logic("abcd").build(); + var f = CqlBuilderLookup.Lookup.builder().name("Function").logic("abcdef").build(); + when(cqlParsingService.getCqlBuilderLookups(anyString(), anyString())) + .thenReturn( + CqlBuilderLookup.builder() + .parameters(Set.of(p)) + .definitions(Set.of(d)) + .functions(Set.of(f)) + .build()); + + ResponseEntity result = + cqlToolsController.getCqlBuilderLookups("CQL", "accessToken"); + CqlBuilderLookup cqlBuilderLookups = result.getBody(); + assertNotNull(cqlBuilderLookups); + assertThat(cqlBuilderLookups.getParameters().size(), is(1)); + assertThat(cqlBuilderLookups.getDefinitions().size(), is(1)); + assertThat(cqlBuilderLookups.getFunctions().size(), is(1)); + assertThat(cqlBuilderLookups.getFluentFunctions(), is(nullValue())); + } } diff --git a/src/test/java/gov/cms/mat/cql_elm_translation/service/CqlParsingServiceTest.java b/src/test/java/gov/cms/mat/cql_elm_translation/service/CqlParsingServiceTest.java index 6ea01897..f5a81327 100644 --- a/src/test/java/gov/cms/mat/cql_elm_translation/service/CqlParsingServiceTest.java +++ b/src/test/java/gov/cms/mat/cql_elm_translation/service/CqlParsingServiceTest.java @@ -3,11 +3,13 @@ import gov.cms.mat.cql.CqlTextParser; import gov.cms.mat.cql_elm_translation.ResourceFileUtil; import gov.cms.mat.cql_elm_translation.cql_translator.MadieLibrarySourceProvider; +import gov.cms.mat.cql_elm_translation.dto.CqlBuilderLookup; import gov.cms.mat.cql_elm_translation.dto.CqlLookups; import gov.cms.mat.cql_elm_translation.dto.ElementLookup; import gov.cms.mat.cql_elm_translation.utils.cql.parsing.model.CQLDefinition; import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.nullValue; import static org.junit.jupiter.api.Assertions.assertNull; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -140,6 +142,15 @@ void testAllDefinitions() { .definitionLogic("define function \"func\":\n" + " true") .build(); + CQLDefinition fluentFunction = + CQLDefinition.builder() + .id("HelperLibrary-0.0.000|Helper|Null Abatement") + .definitionName("Null Abatement") + .definitionLogic( + "define fluent function \"Null Abatement\"(Conditions List):\n" + + " Conditions C where C.abatement is null") + .build(); + CQLDefinition helperDefine = CQLDefinition.builder() .id("HelperLibrary-0.0.000|Helper|Inpatient Encounter") @@ -155,7 +166,9 @@ void testAllDefinitions() { .build(); assertThat( - allDefs, containsInAnyOrder(define1, define2, define3, define4, helperDefine, function)); + allDefs, + containsInAnyOrder( + define1, define2, define3, define4, helperDefine, function, fluentFunction)); } @Test @@ -202,4 +215,23 @@ void testGetCqlLookups() { List oids = cqlLookup.getElementLookups().stream().map(ElementLookup::getOid).toList(); assertThat(oids, containsInAnyOrder("204504", "197604", "2.16.840.1.113883.3.464.1003.1065")); } + + @Test + void testGetCqlBuilderLookups() { + MadieLibrarySourceProvider.setUsing(new CqlTextParser(qiCoreMeasureCql).getUsing()); + MadieLibrarySourceProvider.setCqlLibraryService(cqlLibraryService); + doReturn(qiCoreHelperCql).when(cqlLibraryService).getLibraryCql(any(), any(), any()); + doNothing().when(cqlLibraryService).setUpLibrarySourceProvider(anyString(), anyString()); + CqlBuilderLookup lookup = cqlParsingService.getCqlBuilderLookups(qiCoreMeasureCql, "token"); + assertThat(lookup.getParameters().size(), is(2)); + assertThat(lookup.getDefinitions().size(), is(5)); + assertThat(lookup.getFunctions().size(), is(1)); + assertThat(lookup.getFluentFunctions().size(), is(1)); + } + + @Test + void testGetCqlBuilderLookupsForEmptyCql() { + CqlBuilderLookup lookup = cqlParsingService.getCqlBuilderLookups(null, "token"); + assertThat(lookup, is(nullValue())); + } } diff --git a/src/test/resources/qicore_included_lib.cql b/src/test/resources/qicore_included_lib.cql index db10b150..3b52dc61 100644 --- a/src/test/resources/qicore_included_lib.cql +++ b/src/test/resources/qicore_included_lib.cql @@ -5,6 +5,7 @@ using QICore version '4.1.1' valueset "Encounter Inpatient": 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.666.5.307' parameter "Measurement Period" Interval +parameter "test date" Interval context Patient @@ -12,3 +13,6 @@ define "Inpatient Encounter": [Encounter: "Encounter Inpatient"] EncounterInpatient where EncounterInpatient.status = 'finished' and EncounterInpatient.period ends during day of "Measurement Period" + +define fluent function "Null Abatement"(Conditions List): + Conditions C where C.abatement is null \ No newline at end of file