Skip to content

Commit d84c47d

Browse files
authored
Merge pull request #1331 from b2ihealthcare/feature/SO-6088-ecl22-evaluation
Allow evaluation of "top of set" and "bottom of set" ECL expressions
2 parents 7cca5aa + 668c889 commit d84c47d

File tree

6 files changed

+184
-50
lines changed

6 files changed

+184
-50
lines changed

core/com.b2international.snowowl.core/META-INF/MANIFEST.MF

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ Require-Bundle: org.eclipse.core.runtime;bundle-version="3.9.0",
2121
org.apache.commons.commons-io;bundle-version="2.15.0",
2222
io.github.classgraph.classgraph;bundle-version="4.8.90",
2323
com.b2international.groovy;bundle-version="3.0.9",
24-
com.b2international.snomed.ecl;bundle-version="1.6.0";visibility:=reexport,
24+
com.b2international.snomed.ecl;bundle-version="[2.2.0,2.3.0)";visibility:=reexport,
2525
com.github.f4b6a3.uuid;bundle-version="4.6.1";visibility:=reexport
2626
Bundle-RequiredExecutionEnvironment: JavaSE-17
2727
Bundle-ActivationPolicy: lazy

core/com.b2international.snowowl.core/src/com/b2international/snowowl/core/request/ecl/EclEvaluationRequest.java

Lines changed: 80 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import static com.b2international.snomed.ecl.Ecl.isEclConceptReference;
2323
import static com.google.common.collect.Sets.newHashSet;
2424

25+
import java.util.Collection;
2526
import java.util.Collections;
2627
import java.util.List;
2728
import java.util.Set;
@@ -32,6 +33,7 @@
3233

3334
import org.eclipse.emf.common.util.TreeIterator;
3435
import org.eclipse.emf.ecore.EObject;
36+
import org.eclipse.emf.ecore.util.EcoreUtil;
3537
import org.eclipse.xtext.util.PolymorphicDispatcher;
3638

3739
import com.b2international.commons.CompareUtils;
@@ -148,6 +150,10 @@ protected Promise<Expression> eval(C context, EclConceptReferenceSet conceptSet)
148150
return Promise.immediate(ids(conceptIds));
149151
}
150152

153+
protected Promise<Expression> eval(C context, AlternateIdentifier alternateIdentifier) {
154+
return throwUnsupported(alternateIdentifier);
155+
}
156+
151157
/**
152158
* Handles DescendantsOf simple expression constraints
153159
* @see https://confluence.ihtsdotools.org/display/DOCECL/6.1+Simple+Expression+Constraints
@@ -300,6 +306,61 @@ protected Promise<Expression> eval(C context, final AncestorOrSelfOf ancestorOrS
300306
}
301307
}
302308

309+
/**
310+
* Handles "top of set" simple expression constraints
311+
* @see https://confluence.ihtsdotools.org/display/DOCECL/6.12+Top+and+Bottom
312+
*/
313+
protected Promise<Expression> eval(C context, final Top topOfSet) {
314+
final ExpressionConstraint innerConstraint = topOfSet.getConstraint();
315+
if (isAnyExpression(innerConstraint)) {
316+
// !!>* should eval to root concepts (direct descendants of the virtual root, -1)
317+
return Promise.immediate(parentsExpression(Collections.singleton(IComponent.ROOT_ID)));
318+
} else {
319+
// Otherwise evaluate the equivalent expression constraint: !!>X is changed to X MINUS <X
320+
final ExclusionExpressionConstraint minus = EclFactory.eINSTANCE.createExclusionExpressionConstraint();
321+
minus.setLeft(EcoreUtil.copy(innerConstraint));
322+
323+
final DescendantOf descendantsOfInner = EclFactory.eINSTANCE.createDescendantOf();
324+
descendantsOfInner.setConstraint(EcoreUtil.copy(innerConstraint));
325+
minus.setRight(descendantsOfInner);
326+
327+
return eval(context, minus);
328+
}
329+
}
330+
331+
/**
332+
* Handles "bottom of set" simple expression constraints
333+
* @see https://confluence.ihtsdotools.org/display/DOCECL/6.12+Top+and+Bottom
334+
*/
335+
protected Promise<Expression> eval(C context, final Bottom bottomOfSet) {
336+
final ExpressionConstraint innerConstraint = bottomOfSet.getConstraint();
337+
if (isAnyExpression(innerConstraint)) {
338+
// !!<* should eval to all leaf nodes in the dataset, which we don't have any means of retrieving in an efficient manner
339+
throw new TooCostlyException("ECL expression attempted to enumerate all leaf concepts.");
340+
} else {
341+
return resolveConcepts(context, innerConstraint)
342+
.then(concepts -> {
343+
final Set<String> results = newHashSet();
344+
345+
// Add all concepts in the result set first
346+
for (IComponent concept : concepts) {
347+
results.add(concept.getId());
348+
}
349+
350+
if (results.size() > 1) {
351+
// Then remove all parents and ancestors recorded on the concepts
352+
for (IComponent concept : concepts) {
353+
removeParentIds(concept, results);
354+
removeAncestorIds(concept, results);
355+
}
356+
}
357+
358+
return results;
359+
})
360+
.then(matchIdsOrNone());
361+
}
362+
}
363+
303364
/**
304365
* Handles conjunction binary operator expressions
305366
* @see https://confluence.ihtsdotools.org/display/DOCECL/6.4+Conjunction+and+Disjunction
@@ -631,12 +692,27 @@ public static Function<Set<String>, Expression> matchIdsOrNone() {
631692

632693
protected abstract Class<?> getDocumentType();
633694

634-
protected void addParentIds(IComponent concept, final Set<String> collection) {
635-
throw new UnsupportedOperationException();
695+
protected final void addParentIds(IComponent concept, final Set<String> collection) {
696+
collection.addAll(getParentIds(concept));
697+
}
698+
699+
protected final void removeParentIds(IComponent concept, final Set<String> collection) {
700+
collection.removeAll(getParentIds(concept));
701+
}
702+
703+
protected final void addAncestorIds(IComponent concept, final Set<String> collection) {
704+
collection.addAll(getAncestorIds(concept));
636705
}
637706

638-
protected void addAncestorIds(IComponent concept, final Set<String> collection) {
707+
protected final void removeAncestorIds(IComponent concept, final Set<String> collection) {
708+
collection.removeAll(getAncestorIds(concept));
709+
}
710+
711+
protected Collection<String> getParentIds(IComponent concept) {
712+
throw new UnsupportedOperationException();
713+
}
714+
715+
protected Collection<String> getAncestorIds(IComponent concept) {
639716
throw new UnsupportedOperationException();
640717
}
641-
642718
}

releng/target-platform/target-platform.target

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
<repository location="https://download.eclipse.org/modeling/emft/mwe/updates/releases/2.16.0/"/>
4040
</location>
4141
<location includeAllPlatforms="false" includeConfigurePhase="true" includeMode="planner" includeSource="true" type="InstallableUnit">
42-
<repository location="mvn:com.b2international:com.b2international.snomed.ecl.update:2.1.5:zip"/>
42+
<repository location="mvn:com.b2international:com.b2international.snomed.ecl.update:2.2.0:zip"/>
4343
<unit id="com.b2international.snomed.ecl.feature.feature.group" version="0.0.0"/>
4444
<unit id="com.b2international.snomed.ecl.ui.feature.feature.group" version="0.0.0"/>
4545
</location>

snomed/com.b2international.snowowl.snomed.datastore.tests/src/com/b2international/snowowl/snomed/core/ecl/SnomedEclEvaluationRequestTest.java

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,11 @@ public void self() throws Exception {
159159
assertEquals(expected, actual);
160160
}
161161

162+
@Test(expected = BadRequestException.class)
163+
public void alternateIdentifierUnsupported() throws Exception {
164+
eval("LOINC#12345-6");
165+
}
166+
162167
@Test
163168
public void selfWithTerm() throws Exception {
164169
final Expression actual = eval(String.format("%s|SNOMED CT Root|", ROOT_ID));
@@ -395,6 +400,73 @@ public void ancestorOrSelfOfAny() throws Exception {
395400
final Expression actual = eval(">> *");
396401
assertEquals(Expressions.matchAll(), actual);
397402
}
403+
404+
@Test
405+
public void topOf() throws Exception {
406+
// SCT core module and nested model component module
407+
indexRevision(MAIN, concept(Concepts.MODULE_SCT_CORE)
408+
.ancestors(Long.parseLong(Concepts.ROOT_CONCEPT))
409+
.parents(Long.parseLong(Concepts.MODULE_ROOT))
410+
.statedAncestors(Long.parseLong(Concepts.ROOT_CONCEPT))
411+
.statedParents(Long.parseLong(Concepts.MODULE_ROOT))
412+
.build());
413+
414+
indexRevision(MAIN, concept(Concepts.MODULE_SCT_MODEL_COMPONENT)
415+
.ancestors(Long.parseLong(Concepts.ROOT_CONCEPT), Long.parseLong(Concepts.MODULE_ROOT))
416+
.parents(Long.parseLong(Concepts.MODULE_SCT_CORE))
417+
.statedAncestors(Long.parseLong(Concepts.ROOT_CONCEPT), Long.parseLong(Concepts.MODULE_ROOT))
418+
.statedParents(Long.parseLong(Concepts.MODULE_SCT_CORE))
419+
.build());
420+
421+
final Expression actual = eval("!!> (< " + Concepts.MODULE_ROOT + ")");
422+
final Expression expected = Expressions.bool()
423+
.filter(descendantsOf(Concepts.MODULE_ROOT))
424+
.mustNot(descendantsOf(Concepts.MODULE_SCT_CORE, Concepts.MODULE_SCT_MODEL_COMPONENT))
425+
.build();
426+
427+
assertEquals(expected, actual);
428+
}
429+
430+
@Test
431+
public void topOfAny() throws Exception {
432+
final Expression actual = eval("!!> *");
433+
final Expression expected;
434+
435+
if (isInferred()) {
436+
expected = parents(Collections.singleton(IComponent.ROOT_ID));
437+
} else {
438+
expected = statedParents(Collections.singleton(IComponent.ROOT_ID));
439+
}
440+
441+
assertEquals(expected, actual);
442+
}
443+
444+
@Test
445+
public void bottomOf() throws Exception {
446+
// SCT core module and nested model component module
447+
indexRevision(MAIN, concept(Concepts.MODULE_SCT_CORE)
448+
.ancestors(Long.parseLong(Concepts.ROOT_CONCEPT))
449+
.parents(Long.parseLong(Concepts.MODULE_ROOT))
450+
.statedAncestors(Long.parseLong(Concepts.ROOT_CONCEPT))
451+
.statedParents(Long.parseLong(Concepts.MODULE_ROOT))
452+
.build());
453+
454+
indexRevision(MAIN, concept(Concepts.MODULE_SCT_MODEL_COMPONENT)
455+
.ancestors(Long.parseLong(Concepts.ROOT_CONCEPT), Long.parseLong(Concepts.MODULE_ROOT))
456+
.parents(Long.parseLong(Concepts.MODULE_SCT_CORE))
457+
.statedAncestors(Long.parseLong(Concepts.ROOT_CONCEPT), Long.parseLong(Concepts.MODULE_ROOT))
458+
.statedParents(Long.parseLong(Concepts.MODULE_SCT_CORE))
459+
.build());
460+
461+
final Expression actual = eval("!!< (< " + Concepts.MODULE_ROOT + ")");
462+
final Expression expected = ids(Set.of(Concepts.MODULE_SCT_MODEL_COMPONENT));
463+
assertEquals(expected, actual);
464+
}
465+
466+
@Test(expected = TooCostlyException.class)
467+
public void bottomOfAnyTooCostly() throws Exception {
468+
eval("!!< *");
469+
}
398470

399471
@Test
400472
public void selfAndSelf() throws Exception {

snomed/com.b2international.snowowl.snomed.datastore/META-INF/MANIFEST.MF

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ Require-Bundle: org.eclipse.core.runtime;bundle-version="3.9.0",
2525
com.b2international.snowowl.snomed.common;visibility:=reexport,
2626
com.b2international.snowowl.snomed.icons,
2727
com.b2international.snowowl.core;visibility:=reexport,
28-
com.b2international.snomed.ecl;bundle-version="[2.1.0,2.2.0)";visibility:=reexport
28+
com.b2international.snomed.ecl;bundle-version="[2.2.0,2.3.0)";visibility:=reexport
2929
Export-Package: com.b2international.snowowl.snomed.core,
3030
com.b2international.snowowl.snomed.core.domain,
3131
com.b2international.snowowl.snomed.core.domain.refset,

snomed/com.b2international.snowowl.snomed.datastore/src/com/b2international/snowowl/snomed/core/ecl/SnomedEclEvaluationRequest.java

Lines changed: 29 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import static com.b2international.snomed.ecl.Ecl.isAnyExpression;
1919
import static com.b2international.snowowl.snomed.datastore.index.entry.SnomedComponentDocument.Expressions.activeMemberOf;
2020
import static com.b2international.snowowl.snomed.datastore.index.entry.SnomedComponentDocument.Fields.ACTIVE_MEMBER_OF;
21+
import static com.google.common.collect.Sets.newHashSet;
2122

2223
import java.util.*;
2324
import java.util.concurrent.TimeUnit;
@@ -646,55 +647,40 @@ protected Class<?> getDocumentType() {
646647
}
647648

648649
@Override
649-
protected void addParentIds(IComponent concept, Set<String> collection) {
650-
if (concept instanceof SnomedConcept) {
651-
SnomedConcept snomedConcept = (SnomedConcept) concept;
652-
if (Trees.INFERRED_FORM.equals(expressionForm)) {
653-
if (snomedConcept.getParentIds() != null) {
654-
for (long parent : snomedConcept.getParentIds()) {
655-
if (IComponent.ROOT_IDL != parent) {
656-
collection.add(Long.toString(parent));
657-
}
658-
}
659-
}
660-
} else {
661-
if (snomedConcept.getStatedParentIds() != null) {
662-
for (long statedParent : snomedConcept.getStatedParentIds()) {
663-
if (IComponent.ROOT_IDL != statedParent) {
664-
collection.add(Long.toString(statedParent));
665-
}
666-
}
667-
}
668-
}
650+
protected Collection<String> getParentIds(IComponent concept) {
651+
if (!(concept instanceof SnomedConcept snomedConcept)) {
652+
return super.getParentIds(concept);
653+
}
654+
655+
if (Trees.INFERRED_FORM.equals(expressionForm)) {
656+
return toStringSet(snomedConcept.getParentIds());
669657
} else {
670-
super.addParentIds(concept, collection);
658+
return toStringSet(snomedConcept.getStatedParentIds());
671659
}
672660
}
673-
661+
674662
@Override
675-
protected void addAncestorIds(IComponent concept, Set<String> collection) {
676-
if (concept instanceof SnomedConcept) {
677-
SnomedConcept snomedConcept = (SnomedConcept) concept;
678-
if (Trees.INFERRED_FORM.equals(expressionForm)) {
679-
if (snomedConcept.getAncestorIds() != null) {
680-
for (long ancestor : snomedConcept.getAncestorIds()) {
681-
if (IComponent.ROOT_IDL != ancestor) {
682-
collection.add(Long.toString(ancestor));
683-
}
684-
}
685-
}
686-
} else {
687-
if (snomedConcept.getStatedAncestorIds() != null) {
688-
for (long statedAncestor : snomedConcept.getStatedAncestorIds()) {
689-
if (IComponent.ROOT_IDL != statedAncestor) {
690-
collection.add(Long.toString(statedAncestor));
691-
}
692-
}
693-
}
694-
}
663+
protected Collection<String> getAncestorIds(IComponent concept) {
664+
if (!(concept instanceof SnomedConcept snomedConcept)) {
665+
return super.getAncestorIds(concept);
666+
}
667+
668+
if (Trees.INFERRED_FORM.equals(expressionForm)) {
669+
return toStringSet(snomedConcept.getAncestorIds());
695670
} else {
696-
super.addAncestorIds(concept, collection);
671+
return toStringSet(snomedConcept.getStatedAncestorIds());
672+
}
673+
}
674+
675+
private Set<String> toStringSet(final long[] idArray) {
676+
if (idArray == null || idArray.length < 1) {
677+
return newHashSet();
697678
}
679+
680+
return Arrays.stream(idArray)
681+
.filter(parent -> IComponent.ROOT_IDL != parent)
682+
.mapToObj(Long::toString)
683+
.collect(Collectors.toSet());
698684
}
699685

700686
/*package*/ static String extractTerm(TypedSearchTermClause clause) {

0 commit comments

Comments
 (0)