[UIMA-6336] Align select behaviors of uimaFITv2 with uimaFITv3
- Backport tests from v3
- Adjust behavior of several select* calls to align with the v3 behavior
diff --git a/uimafit-core/src/main/java/org/apache/uima/fit/util/CasUtil.java b/uimafit-core/src/main/java/org/apache/uima/fit/util/CasUtil.java
index c8f72b7..c2cbe36 100644
--- a/uimafit-core/src/main/java/org/apache/uima/fit/util/CasUtil.java
+++ b/uimafit-core/src/main/java/org/apache/uima/fit/util/CasUtil.java
@@ -1099,7 +1099,7 @@
throw new IllegalArgumentException("Type [" + type.getName() + "] is not an annotation type");
}
- List<AnnotationFS> precedingAnnotations = new LinkedList<AnnotationFS>();
+ List<AnnotationFS> precedingAnnotations = new ArrayList<AnnotationFS>();
// Seek annotation in index
// withSnapshotIterators() not needed here since we copy the FSes to a list anyway
@@ -1115,11 +1115,18 @@
}
int anchorBegin = anchor.getBegin();
- int anchorEnd = anchor.getEnd();
- // No need to do additional seeks here (as done in selectCovered) because the current method
- // does not have to worry about type priorities - it never returns annotations that have
- // the same offset as the reference annotation.
+ // Zero-width annotations are in the index *after* the wider annotations starting at the same
+ // location, but we would consider a zero-width annotation at the beginning of a larger
+ // reference annotation to be preceding the larger one. So we need to seek right
+ // for any relevant zero-with annotations.
+ while (itr.isValid() && itr.get().getBegin() == anchorBegin) {
+ itr.moveToNext();
+ if (!itr.isValid()) {
+ itr.moveToLast();
+ break;
+ }
+ }
// make sure we're past the beginning of the reference annotation
while (itr.isValid() && itr.get().getEnd() > anchorBegin) {
@@ -1133,11 +1140,11 @@
int curEnd = cur.getEnd();
if (
- curEnd <= anchorBegin &&
- (cur.getBegin() != curEnd || anchorBegin != curEnd) &&
- (anchorBegin != anchorEnd || curEnd != anchorBegin)
+ (curEnd <= anchorBegin
+ || (cur.getBegin() == curEnd && curEnd == anchorBegin))
+ && cur != anchor
) {
- precedingAnnotations.add(itr.get());
+ precedingAnnotations.add(cur);
i++;
}
}
@@ -1154,14 +1161,14 @@
* a CAS.
* @param type
* a UIMA type.
- * @param annotation
+ * @param anchor
* anchor annotation
* @param count
* number of annotations to collect
* @return List of aType annotations following anchor annotation
* @see <a href="package-summary.html#SortOrder">Order of selected feature structures</a>
*/
- public static List<AnnotationFS> selectFollowing(CAS cas, Type type, AnnotationFS annotation,
+ public static List<AnnotationFS> selectFollowing(CAS cas, Type type, AnnotationFS anchor,
int count) {
if (!cas.getTypeSystem().subsumes(cas.getAnnotationType(), type)) {
throw new IllegalArgumentException("Type [" + type.getName() + "] is not an annotation type");
@@ -1170,7 +1177,10 @@
// Seek annotation in index
// withSnapshotIterators() not needed here since we copy the FSes to a list anyway
FSIterator<AnnotationFS> itr = cas.getAnnotationIndex(type).iterator();
- itr.moveTo(annotation);
+ itr.moveTo(anchor);
+
+ int anchorBegin = anchor.getBegin();
+ int anchorEnd = anchor.getEnd();
// When seeking forward, there is no need to check if the insertion point is beyond the
// index. If it was, there would be nothing beyond it that could be found and returned.
@@ -1182,24 +1192,41 @@
// does not have to worry about type priorities - it never returns annotations that have
// the same offset as the reference annotation.
+ if (anchorBegin == anchorEnd) {
+ // zero-width annotations appear *after* larger annotations with the same start position in
+ // the index but the larger annotations are considered to be *following* the zero-width, so we
+ // have to look to the left for larger annotations...
+ if (itr.isValid()) {
+ itr.moveToPrevious();
+ while (itr.isValid() && itr.get().getBegin() == anchorBegin) {
+ itr.moveToPrevious();
+ }
+
+ if (!itr.isValid()) {
+ itr.moveToFirst();
+ }
+ else {
+ itr.moveToNext();
+ }
+ }
+ else {
+ itr.moveToFirst();
+ }
+ }
+
// make sure we're past the end of the reference annotation
- while (itr.isValid() && itr.get().getBegin() < annotation.getEnd()) {
+ while (itr.isValid() && itr.get().getBegin() < anchorEnd) {
itr.moveToNext();
}
// add annotations from the iterator into the result list
- int refEnd = annotation.getEnd();
- List<AnnotationFS> followingAnnotations = new LinkedList<AnnotationFS>();
- for (int i = 0; i < count && itr.isValid(); i++, itr.moveToNext()) {
- AnnotationFS fs = itr.get();
- int begin = fs.getBegin();
- int end = fs.getEnd();
- if (begin == end && refEnd == begin) {
- // Skip zero-width annotation at the end of the reference annotation. These are considered
- // to be "coveredBy" instead of following
- continue;
+ List<AnnotationFS> followingAnnotations = new ArrayList<AnnotationFS>();
+ for (int i = 0; i < count && itr.isValid(); itr.moveToNext()) {
+ AnnotationFS cur = itr.get();
+ if (cur != anchor && cur.getBegin() >= anchorEnd) {
+ followingAnnotations.add(cur);
+ i ++;
}
- followingAnnotations.add(itr.get());
}
return followingAnnotations;
diff --git a/uimafit-core/src/test/java/org/apache/uima/fit/util/AnnotationPredicateTestData.java b/uimafit-core/src/test/java/org/apache/uima/fit/util/AnnotationPredicateTestData.java
new file mode 100644
index 0000000..12dd85e
--- /dev/null
+++ b/uimafit-core/src/test/java/org/apache/uima/fit/util/AnnotationPredicateTestData.java
@@ -0,0 +1,137 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.uima.fit.util;
+
+import static java.lang.Integer.MAX_VALUE;
+import static java.util.Arrays.asList;
+import static org.apache.uima.fit.util.AnnotationPredicateTestData.RelativePosition.BEGINNING_WITH;
+import static org.apache.uima.fit.util.AnnotationPredicateTestData.RelativePosition.COLOCATED;
+import static org.apache.uima.fit.util.AnnotationPredicateTestData.RelativePosition.COVERED_BY;
+import static org.apache.uima.fit.util.AnnotationPredicateTestData.RelativePosition.COVERING;
+import static org.apache.uima.fit.util.AnnotationPredicateTestData.RelativePosition.ENDING_WITH;
+import static org.apache.uima.fit.util.AnnotationPredicateTestData.RelativePosition.FOLLOWING;
+import static org.apache.uima.fit.util.AnnotationPredicateTestData.RelativePosition.OVERLAPPING;
+import static org.apache.uima.fit.util.AnnotationPredicateTestData.RelativePosition.OVERLAPPING_AT_BEGIN;
+import static org.apache.uima.fit.util.AnnotationPredicateTestData.RelativePosition.OVERLAPPING_AT_END;
+import static org.apache.uima.fit.util.AnnotationPredicateTestData.RelativePosition.PRECEDING;
+
+import java.util.List;
+
+import org.apache.uima.fit.util.SelectionAssert.TestCase;
+
+public class AnnotationPredicateTestData {
+ public static enum RelativePosition {
+ COLOCATED,
+ OVERLAPPING,
+ OVERLAPPING_AT_BEGIN,
+ OVERLAPPING_AT_END,
+ COVERING,
+ COVERED_BY,
+ PRECEDING,
+ FOLLOWING,
+ BEGINNING_WITH,
+ ENDING_WITH
+ }
+
+ // Used as fixed references for the annotation relation cases.
+ private static final int BEGIN = 10;
+ private static final int END = 20;
+ private static final int Z_POS = 10;
+
+ public static final TestCase T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13;
+
+ public static final List<TestCase> NON_ZERO_WIDTH_TEST_CASES = asList(
+ T1 = new TestCase("1) Y begins and ends after X (### [---])",
+ p -> p.apply(BEGIN, END, END + 1, MAX_VALUE),
+ asList(PRECEDING)),
+ T2 = new TestCase("2) Y begins at X's end and ends after X (###[---])",
+ p -> p.apply(BEGIN, END, END, MAX_VALUE),
+ asList(PRECEDING)),
+ T3 = new TestCase("3) Y begins within and ends after X (##[#--])",
+ p -> p.apply(BEGIN, END, END - 1 , MAX_VALUE),
+ asList(OVERLAPPING, OVERLAPPING_AT_BEGIN)),
+ T4 = new TestCase("4) Y begins and ends at X's boundries ([###])",
+ p -> p.apply(BEGIN, END, BEGIN, END),
+ asList(OVERLAPPING, COLOCATED, COVERED_BY, COVERING, BEGINNING_WITH, ENDING_WITH)),
+ T5 = new TestCase("5) Y begins and ends within X (#[#]#)",
+ p -> p.apply(BEGIN, END, BEGIN + 1, END - 1),
+ asList(OVERLAPPING, COVERING)),
+ T6 = new TestCase("6) Y begins at and ends before X's boundries ([##]#)",
+ p -> p.apply(BEGIN, END, BEGIN, END - 1),
+ asList(OVERLAPPING, COVERING, BEGINNING_WITH, OVERLAPPING_AT_END)),
+ T7 = new TestCase("7) Y begins after and ends at X's boundries (#[##])",
+ p -> p.apply(BEGIN, END, BEGIN + 1, END),
+ asList(OVERLAPPING, COVERING, ENDING_WITH, OVERLAPPING_AT_BEGIN)),
+ T8 = new TestCase("8) Y begins before and ends after X's boundries ([-###-])",
+ p -> p.apply(BEGIN, END, BEGIN - 1, END + 1),
+ asList(OVERLAPPING, COVERED_BY)),
+ T9 = new TestCase("9) X starts where Y begins and ends within Y ([##-])",
+ p -> p.apply(BEGIN, END, BEGIN, END + 1),
+ asList(OVERLAPPING, COVERED_BY, BEGINNING_WITH)),
+ T10 = new TestCase("10) X starts within Y and ends where Y ends ([-##])",
+ p -> p.apply(BEGIN, END, BEGIN - 1, END),
+ asList(OVERLAPPING, COVERED_BY, ENDING_WITH)),
+ T11 = new TestCase("11) Y begins before and ends within X ([--#]##)",
+ p -> p.apply(BEGIN, END, 0, BEGIN + 1),
+ asList(OVERLAPPING, OVERLAPPING_AT_END)),
+ T12 = new TestCase("12) Y begins before and ends where X begins ([---]###)",
+ p -> p.apply(BEGIN, END, 0, BEGIN),
+ asList(FOLLOWING)),
+ T13 = new TestCase("13) Y begins and ends before X begins ([---] ###)",
+ p -> p.apply(BEGIN, END, 0, BEGIN - 1),
+ asList(FOLLOWING)));
+
+ public static final TestCase TZ1, TZ2, TZ3, TZ4, TZ5, TZ6, TZ7, TZ8, TZ9, TZ10, TZ11;
+
+ public static final List<TestCase> ZERO_WIDTH_TEST_CASES = asList(
+ TZ1 = new TestCase("Z1) Zero-width X before Y start (# [---])",
+ p -> p.apply(Z_POS, Z_POS, Z_POS + 10, Z_POS + 20),
+ asList(PRECEDING)),
+ TZ2 = new TestCase("Z2) Zero-width Y after X's end (### |)",
+ p -> p.apply(BEGIN, END, END + 1, END + 1),
+ asList(PRECEDING)),
+ TZ3 = new TestCase("Z3) Zero-width X at Y's start (#---])",
+ p -> p.apply(Z_POS, Z_POS, Z_POS, Z_POS + 10),
+ asList(PRECEDING, OVERLAPPING, COVERED_BY, BEGINNING_WITH)),
+ TZ4 = new TestCase("Z4) Zero-width X at Y's end ([---#)",
+ p -> p.apply(Z_POS, Z_POS, Z_POS-10, Z_POS),
+ asList(FOLLOWING, OVERLAPPING, COVERED_BY, ENDING_WITH)),
+ TZ5 = new TestCase("Z5) Zero-width Y where X begins (|###)",
+ p -> p.apply(BEGIN, END, BEGIN, BEGIN),
+ asList(FOLLOWING, OVERLAPPING, COVERING, BEGINNING_WITH)),
+ TZ6 = new TestCase("Z6) Zero-width Y within X (#|#)",
+ p -> p.apply(BEGIN, END, BEGIN + 1, BEGIN + 1),
+ asList(OVERLAPPING, COVERING)),
+ TZ7 = new TestCase("Z7) Zero-width Y at X's end (###|)",
+ p -> p.apply(BEGIN, END, END, END),
+ asList(PRECEDING, OVERLAPPING, COVERING, ENDING_WITH)),
+ TZ8 = new TestCase("Z8) Zero-width X with Y (-|-)",
+ p -> p.apply(Z_POS, Z_POS, Z_POS - 5, Z_POS + 5),
+ asList(OVERLAPPING, COVERED_BY)),
+ TZ9 = new TestCase("Z9) Zero-width X after Y's end ([---] #)",
+ p -> p.apply(Z_POS, Z_POS, Z_POS - 10, Z_POS - 5),
+ asList(FOLLOWING)),
+ TZ10 = new TestCase("Z10) Zero-width Y before X begins (| ###)",
+ p -> p.apply(BEGIN, END, BEGIN - 1, BEGIN - 1),
+ asList(FOLLOWING)),
+ TZ11 = new TestCase("Z11) Zero-width X matches zero-width Y start/end (#)",
+ p -> p.apply(Z_POS, Z_POS, Z_POS, Z_POS),
+ asList(FOLLOWING, PRECEDING, OVERLAPPING, COVERED_BY, COVERING, COLOCATED, BEGINNING_WITH,
+ ENDING_WITH)));
+}
diff --git a/uimafit-core/src/test/java/org/apache/uima/fit/util/AnnotationPredicates.java b/uimafit-core/src/test/java/org/apache/uima/fit/util/AnnotationPredicates.java
new file mode 100644
index 0000000..d503799
--- /dev/null
+++ b/uimafit-core/src/test/java/org/apache/uima/fit/util/AnnotationPredicates.java
@@ -0,0 +1,234 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.uima.fit.util;
+
+import org.apache.uima.cas.text.AnnotationFS;
+
+/**
+ * This class is back-ported for testing only from UIMAv3. It is used to check that the behavior
+ * of uimaFIT v2 and uimaFITv2 with respect to the select utilities is aligned.
+ */
+final class AnnotationPredicates {
+ public static boolean coveredBy(int aXBegin, int aXEnd, int aYBegin, int aYEnd) {
+ return aYBegin <= aXBegin && aXEnd <= aYEnd;
+ }
+
+ public static boolean coveredBy(AnnotationFS aX, int aYBegin, int aYEnd) {
+ return aYBegin <= aX.getBegin() && aX.getEnd() <= aYEnd;
+ }
+
+ /**
+ * Y is starting before or at the same position as A and ends after or at the
+ * same position as X.
+ *
+ * @param aX
+ * X
+ * @param aY
+ * Y
+ * @return whether X is covered by Y.
+ */
+ public static boolean coveredBy(AnnotationFS aX, AnnotationFS aY) {
+ return aY.getBegin() <= aX.getBegin() && aX.getEnd() <= aY.getEnd();
+ }
+
+ public static boolean covering(int aXBegin, int aXEnd, int aYBegin, int aYEnd) {
+ return aXBegin <= aYBegin && aYEnd <= aXEnd;
+ }
+
+ public static boolean covering(AnnotationFS aX, int aYBegin, int aYEnd) {
+ return aX.getBegin() <= aYBegin && aYEnd <= aX.getEnd();
+ }
+
+ /**
+ * X is starting before or at the same position as Y and ends after or at the
+ * same position as Y.
+ *
+ * @param aX
+ * X
+ * @param aY
+ * Y
+ * @return whether X is covering Y.
+ */
+ public static boolean covering(AnnotationFS aX, AnnotationFS aY) {
+ return aX.getBegin() <= aY.getBegin() && aY.getEnd() <= aX.getEnd();
+ }
+
+ public static boolean colocated(int aXBegin, int aXEnd, int aYBegin, int aYEnd) {
+ return aXBegin == aYBegin && aXEnd == aYEnd;
+ }
+
+ public static boolean colocated(AnnotationFS aX, int aYBegin, int aYEnd) {
+ return aX.getBegin() == aYBegin && aX.getEnd() == aYEnd;
+ }
+
+ /**
+ * X starts and ends at the same position as Y.
+ *
+ * @param aX
+ * X
+ * @param aY
+ * Y
+ * @return whether X is at the same location as Y.
+ */
+ public static boolean colocated(AnnotationFS aX, AnnotationFS aY) {
+ return aX.getBegin() == aY.getBegin() && aX.getEnd() == aY.getEnd();
+ }
+
+ public static boolean overlapping(int aXBegin, int aXEnd, int aYBegin, int aYEnd) {
+ return aYBegin == aXBegin || aYEnd == aXEnd || (aXBegin < aYEnd && aYBegin < aXEnd);
+ }
+
+ public static boolean overlapping(AnnotationFS aX, int aYBegin, int aYEnd) {
+ int xBegin = aX.getBegin();
+ int xEnd = aX.getEnd();
+ return aYBegin == xBegin || aYEnd == xEnd || (xBegin < aYEnd && aYBegin < xEnd);
+ }
+
+ /**
+ * The intersection of the spans X and Y is non-empty. If either X or Y have a
+ * zero-width, then the intersection is considered to be non-empty if the begin
+ * of X is either within Y or the same as the begin of Y - and vice versa.
+ *
+ * @param aX
+ * X
+ * @param aY
+ * Y
+ * @return whether X overlaps with Y in any way.
+ */
+ public static boolean overlapping(AnnotationFS aX, AnnotationFS aY) {
+ int xBegin = aX.getBegin();
+ int xEnd = aX.getEnd();
+ int yBegin = aY.getBegin();
+ int yEnd = aY.getEnd();
+ return yBegin == xBegin || yEnd == xEnd || (xBegin < yEnd && yBegin < xEnd);
+ }
+
+ public static boolean overlappingAtBegin(int aXBegin, int aXEnd, int aYBegin, int aYEnd) {
+ return aXBegin < aYBegin && aYBegin < aXEnd && aXEnd <= aYEnd;
+ }
+
+ public static boolean overlappingAtBegin(AnnotationFS aX, int aYBegin, int aYEnd) {
+ int xEnd = aX.getEnd();
+ return aYBegin < xEnd && xEnd <= aYEnd && aX.getBegin() < aYBegin;
+ }
+
+ /**
+ * X is starting before or at the same position as Y and ends before Y ends.
+ *
+ * @param aX
+ * X
+ * @param aY
+ * Y
+ * @return whether X overlaps Y on the left.
+ */
+ public static boolean overlappingAtBegin(AnnotationFS aX, AnnotationFS aY) {
+ int xEnd = aX.getEnd();
+ int yBegin = aY.getBegin();
+ return yBegin < xEnd && xEnd <= aY.getEnd() && aX.getBegin() < yBegin;
+ }
+
+ public static boolean overlappingAtEnd(int aXBegin, int aXEnd, int aYBegin, int aYEnd) {
+ return aYBegin <= aXBegin && aXBegin < aYEnd && aYEnd < aXEnd;
+ }
+
+ public static boolean overlappingAtEnd(AnnotationFS aX, int aYBegin, int aYEnd) {
+ int xBegin = aX.getBegin();
+ return aYBegin <= xBegin && xBegin < aYEnd && aYEnd < aX.getEnd();
+ }
+
+ /**
+ * X is starting after Y starts and ends after or at the same position as Y.
+ *
+ * @param aX
+ * X
+ * @param aY
+ * Y
+ * @return whether X overlaps Y on the right.
+ */
+ public static boolean overlappingAtEnd(AnnotationFS aX, AnnotationFS aY) {
+ int xBegin = aX.getBegin();
+ int yEnd = aY.getEnd();
+ return xBegin < yEnd && aY.getBegin() <= xBegin && yEnd < aX.getEnd();
+ }
+
+ public static boolean following(int aXBegin, int aXEnd, int aYBegin, int aYEnd) {
+ return aXBegin >= aYEnd;
+ }
+
+ public static boolean following(AnnotationFS aX, int aYBegin, int aYEnd) {
+ return aX.getBegin() >= aYEnd;
+ }
+
+ /**
+ * X starts at or after the position that Y ends.
+ *
+ * @param aX
+ * X
+ * @param aY
+ * Y
+ * @return whether X is right of Y.
+ */
+ public static boolean following(AnnotationFS aX, AnnotationFS aY) {
+ return aX.getBegin() >= aY.getEnd();
+ }
+
+ public static boolean preceding(int aXBegin, int aXEnd, int aYBegin, int aYEnd) {
+ return aYBegin >= aXEnd;
+ }
+
+ public static boolean preceding(AnnotationFS aX, int aYBegin, int aYEnd) {
+ return aYBegin >= aX.getEnd();
+ }
+
+ /**
+ * X ends before or at the position that Y starts.
+ *
+ * @param aX
+ * X
+ * @param aY
+ * Y
+ * @return whether X left of Y.
+ */
+ public static boolean preceding(AnnotationFS aX, AnnotationFS aY) {
+ return aY.getBegin() >= aX.getEnd();
+ }
+
+ public static boolean beginningWith(int aXBegin, int aXEnd, int aYBegin, int aYEnd) {
+ return aXBegin == aYBegin;
+ }
+
+ public static boolean beginningWith(AnnotationFS aX, int aYBegin, int aYEnd) {
+ return aX.getBegin() == aYBegin;
+ }
+
+ public static boolean beginningWith(AnnotationFS aX, AnnotationFS aY) {
+ return aX.getBegin() == aY.getBegin();
+ }
+
+ public static boolean endingWith(int aXBegin, int aXEnd, int aYBegin, int aYEnd) {
+ return aXEnd == aYEnd;
+ }
+ public static boolean endingWith(AnnotationFS aX, int aYBegin, int aYEnd) {
+ return aX.getEnd() == aYEnd;
+ }
+
+ public static boolean endingWith(AnnotationFS aX, AnnotationFS aY) {
+ return aX.getEnd() == aY.getEnd();
+ }
+}
diff --git a/uimafit-core/src/test/java/org/apache/uima/fit/util/CasUtilTest.java b/uimafit-core/src/test/java/org/apache/uima/fit/util/CasUtilTest.java
index 769502e..8589108 100644
--- a/uimafit-core/src/test/java/org/apache/uima/fit/util/CasUtilTest.java
+++ b/uimafit-core/src/test/java/org/apache/uima/fit/util/CasUtilTest.java
@@ -18,24 +18,48 @@
*/
package org.apache.uima.fit.util;
+import static java.lang.Integer.MAX_VALUE;
import static java.util.Arrays.asList;
+import static java.util.stream.Collectors.toList;
+import static java.util.stream.StreamSupport.stream;
import static org.apache.uima.fit.factory.TypeSystemDescriptionFactory.createTypeSystemDescription;
+import static org.apache.uima.fit.util.AnnotationPredicateTestData.NON_ZERO_WIDTH_TEST_CASES;
+import static org.apache.uima.fit.util.AnnotationPredicateTestData.ZERO_WIDTH_TEST_CASES;
+import static org.apache.uima.fit.util.AnnotationPredicateTestData.RelativePosition.COLOCATED;
+import static org.apache.uima.fit.util.AnnotationPredicateTestData.RelativePosition.COVERED_BY;
+import static org.apache.uima.fit.util.AnnotationPredicateTestData.RelativePosition.COVERING;
+import static org.apache.uima.fit.util.AnnotationPredicateTestData.RelativePosition.FOLLOWING;
+import static org.apache.uima.fit.util.AnnotationPredicateTestData.RelativePosition.PRECEDING;
+import static org.apache.uima.fit.util.AnnotationPredicates.colocated;
+import static org.apache.uima.fit.util.AnnotationPredicates.coveredBy;
+import static org.apache.uima.fit.util.AnnotationPredicates.covering;
+import static org.apache.uima.fit.util.AnnotationPredicates.following;
+import static org.apache.uima.fit.util.AnnotationPredicates.preceding;
+import static org.apache.uima.fit.util.CasUtil.exists;
import static org.apache.uima.fit.util.CasUtil.getAnnotationType;
import static org.apache.uima.fit.util.CasUtil.getType;
import static org.apache.uima.fit.util.CasUtil.iterator;
import static org.apache.uima.fit.util.CasUtil.iteratorFS;
import static org.apache.uima.fit.util.CasUtil.select;
+import static org.apache.uima.fit.util.CasUtil.selectAt;
import static org.apache.uima.fit.util.CasUtil.selectByIndex;
+import static org.apache.uima.fit.util.CasUtil.selectCovered;
+import static org.apache.uima.fit.util.CasUtil.selectCovering;
import static org.apache.uima.fit.util.CasUtil.selectFS;
+import static org.apache.uima.fit.util.CasUtil.selectFollowing;
+import static org.apache.uima.fit.util.CasUtil.selectPreceding;
import static org.apache.uima.fit.util.CasUtil.toText;
-import static org.apache.uima.fit.util.CasUtil.exists;
+import static org.apache.uima.fit.util.SelectionAssert.assertSelection;
+import static org.apache.uima.fit.util.SelectionAssert.assertSelectionIsEqualOnRandomData;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
+import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
+import java.util.List;
import org.apache.uima.UIMAException;
import org.apache.uima.cas.ArrayFS;
@@ -45,6 +69,7 @@
import org.apache.uima.cas.text.AnnotationFS;
import org.apache.uima.fit.ComponentTestBase;
import org.apache.uima.fit.type.Token;
+import org.apache.uima.fit.util.SelectionAssert.TestCase;
import org.apache.uima.jcas.cas.TOP;
import org.apache.uima.jcas.tcas.Annotation;
import org.apache.uima.util.CasCreationUtils;
@@ -55,6 +80,9 @@
*
*/
public class CasUtilTest extends ComponentTestBase {
+ private List<TestCase> defaultPredicatesTestCases = union(NON_ZERO_WIDTH_TEST_CASES,
+ ZERO_WIDTH_TEST_CASES);
+
@Test
public void testGetType() {
String text = "Rot wood cheeses dew?";
@@ -205,4 +233,111 @@
assertTrue(exists(cas, tokenType));
}
+
+ @Test
+ public void thatSelectFollowingBehaviorAlignsWithPrecedingPredicate() throws Exception {
+ // In order to find annotations that X is preceding, we select the following annotations
+ assertSelection(
+ PRECEDING,
+ (cas, type, x, y) -> selectFollowing(cas, type, x, MAX_VALUE).contains(y),
+ defaultPredicatesTestCases);
+ }
+
+ @Test
+ public void thatSelectPrecedingBehaviorAlignsWithPrecedingPredicateOnRandomData() throws Exception
+ {
+ assertSelectionIsEqualOnRandomData(
+ (cas, type, context) -> stream(cas.getAnnotationIndex(type).spliterator(), false)
+ .filter(candidate -> preceding(candidate, context))
+ .collect(toList()),
+ (cas, type, context) -> selectPreceding(cas, type, context, MAX_VALUE));
+ }
+
+ @Test
+ public void thatSelectPrecedingBehaviorAlignsWithFollowingPredicate() throws Exception {
+ // In order to find annotations that X is following, we select the preceding annotations
+ assertSelection(
+ FOLLOWING,
+ (cas, type, x, y) -> selectPreceding(cas, type, x, MAX_VALUE).contains(y),
+ defaultPredicatesTestCases);
+ }
+
+ @Test
+ public void thatSelectFollowingBehaviorAlignsWithFollowingPredicateOnRandomData() throws Exception
+ {
+ assertSelectionIsEqualOnRandomData(
+ (cas, type, context) -> stream(cas.getAnnotationIndex(type).spliterator(), false)
+ .filter(candidate -> following(candidate, context))
+ .collect(toList()),
+ (cas, type, context) -> selectFollowing(cas, type, context, MAX_VALUE));
+ }
+
+ @Test
+ public void thatSelectCoveringBehaviorAlignsWithCoveredByPredicate() throws Exception {
+ // X covered by Y means that Y is covering X, so we need to select the covering annotations
+ // below.
+ assertSelection(
+ COVERED_BY,
+ (cas, type, x, y) -> selectCovering(cas, type, x).contains(y),
+ defaultPredicatesTestCases);
+ }
+
+ @Test
+ public void thatSelectCoveredBehaviorAlignsWithCoveredByPredicateOnRandomData() throws Exception
+ {
+ assertSelectionIsEqualOnRandomData(
+ (cas, type, context) -> stream(cas.getAnnotationIndex(type).spliterator(), false)
+ .filter(candidate -> coveredBy(candidate, context))
+ .collect(toList()),
+ (cas, type, context) -> selectCovered(cas, type, context));
+ }
+
+ @Test
+ public void thatSelectCoveredBehaviorAlignsWithCoveringPredicate() throws Exception {
+ // X covering Y means that Y is covered by Y, so we need to select the covered by annotations
+ // below.
+ assertSelection(
+ COVERING,
+ (cas, type, x, y) -> selectCovered(cas, type, x).contains(y),
+ defaultPredicatesTestCases);
+ }
+
+ @Test
+ public void thatSelectFsBehaviorAlignsWithCoveringPredicateOnRandomData() throws Exception
+ {
+ assertSelectionIsEqualOnRandomData(
+ (cas, type, context) -> stream(cas.getAnnotationIndex(type).spliterator(), false)
+ .filter(candidate -> covering(candidate, context))
+ .collect(toList()),
+ (cas, type, context) -> selectCovering(cas, type, context));
+ }
+
+ @Test
+ public void thatSelectAtBehaviorAlignsWithColocatedPredicate() throws Exception {
+ // X covering Y means that Y is covered by Y, so we need to select the covered by annotations
+ // below.
+ assertSelection(
+ COLOCATED,
+ (cas, type, x, y) -> selectAt(cas, type, x.getBegin(), x.getEnd()).contains(y),
+ defaultPredicatesTestCases);
+ }
+
+ @Test
+ public void thatSelectAtBehaviorAlignsWithColocatedPredicateOnRandomData() throws Exception
+ {
+ assertSelectionIsEqualOnRandomData(
+ (cas, type, context) -> stream(cas.getAnnotationIndex(type).spliterator(), false)
+ .filter(candidate -> colocated(candidate, context))
+ .collect(toList()),
+ (cas, type, context) -> selectAt(cas, type, context.getBegin(), context.getEnd()));
+ }
+
+ @SafeVarargs
+ public static <T> List<T> union(List<T>... aLists) {
+ List<T> all = new ArrayList<>();
+ for (List<T> list : aLists) {
+ all.addAll(list);
+ }
+ return all;
+ }
}
diff --git a/uimafit-core/src/test/java/org/apache/uima/fit/util/JCasUtilTest.java b/uimafit-core/src/test/java/org/apache/uima/fit/util/JCasUtilTest.java
index 7094af4..091a6e1 100644
--- a/uimafit-core/src/test/java/org/apache/uima/fit/util/JCasUtilTest.java
+++ b/uimafit-core/src/test/java/org/apache/uima/fit/util/JCasUtilTest.java
@@ -318,7 +318,9 @@
// print(a1);
// System.out.println("--- Optimized");
// print(a2);
- assertEquals("Container: [" + t.getBegin() + ".." + t.getEnd() + "]", a1, a2);
+ assertThat(a2)
+ .as("Container: [" + t.getBegin() + ".." + t.getEnd() + "]")
+ .containsExactlyElementsOf((Iterable) a1);
}
@Test
@@ -668,7 +670,7 @@
}
@Test
- public void thatSelectFollowingDoesNotFindZeroWidthAnnotationAtEnd()
+ public void thatSelectFollowingDoesFindZeroWidthAnnotationAtEnd()
{
Annotation a1 = new Annotation(jCas, 10, 20);
Annotation a2 = new Annotation(jCas, 20, 20);
@@ -678,11 +680,11 @@
List<Annotation> selection = selectFollowing(Annotation.class, a1, MAX_VALUE);
assertThat(selection)
- .isEmpty();
+ .containsExactly(a2);
}
@Test
- public void thatSelectPrecedingDoesNotFindZeroWidthAnnotationAtStart()
+ public void thatSelectPrecedingDoesFindZeroWidthAnnotationAtStart()
{
Annotation a1 = new Annotation(jCas, 10, 20);
Annotation a2 = new Annotation(jCas, 10, 10);
@@ -692,11 +694,11 @@
List<Annotation> selection = selectPreceding(Annotation.class, a1, MAX_VALUE);
assertThat(selection)
- .isEmpty();
+ .containsExactly(a2);
}
@Test
- public void thatSelectPrecedingOnZeroWidthDoesNotFindAnnotationEndingAtSameLocation()
+ public void thatSelectPrecedingOnZeroWidthDoesFindAnnotationEndingAtSameLocation()
{
Annotation a1 = new Annotation(jCas, 10, 20);
Annotation a2 = new Annotation(jCas, 20, 20);
@@ -706,7 +708,7 @@
List<Annotation> selection = selectPreceding(Annotation.class, a2, MAX_VALUE);
assertThat(selection)
- .isEmpty();
+ .containsExactly(a1);
}
@Test
diff --git a/uimafit-core/src/test/java/org/apache/uima/fit/util/SelectionAssert.java b/uimafit-core/src/test/java/org/apache/uima/fit/util/SelectionAssert.java
new file mode 100644
index 0000000..22f21cd
--- /dev/null
+++ b/uimafit-core/src/test/java/org/apache/uima/fit/util/SelectionAssert.java
@@ -0,0 +1,187 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.uima.fit.util;
+
+import static java.util.Arrays.asList;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.function.Function;
+
+import org.apache.uima.UIMAFramework;
+import org.apache.uima.cas.CAS;
+import org.apache.uima.cas.Type;
+import org.apache.uima.cas.text.AnnotationFS;
+import org.apache.uima.fit.util.AnnotationPredicateTestData.RelativePosition;
+import org.apache.uima.resource.metadata.TypeSystemDescription;
+import org.apache.uima.util.CasCreationUtils;
+import org.assertj.core.api.AutoCloseableSoftAssertions;
+
+public class SelectionAssert {
+ public static void assertSelection(RelativePosition aCondition, RelativeAnnotationPredicate aPredicate,
+ List<TestCase> aTestCases)
+ throws Exception {
+ CAS cas = CasCreationUtils.createCas((TypeSystemDescription) null, null, null);
+ Type type = cas.getAnnotationType();
+
+ try (AutoCloseableSoftAssertions softly = new AutoCloseableSoftAssertions()) {
+ for (TestCase testCase : aTestCases) {
+ cas.reset();
+
+ // Create annotations
+ AnnotationFS x = cas.createAnnotation(type, 0, 0);
+ AnnotationFS y = cas.createAnnotation(type, 0, 0);
+
+ // Position the annotations according to the test data
+ testCase.getTest().apply((beginA, endA, beginB, endB) -> {
+ FSUtil.setFeature(x, CAS.FEATURE_BASE_NAME_BEGIN, beginA);
+ FSUtil.setFeature(x, CAS.FEATURE_BASE_NAME_END, endA);
+ FSUtil.setFeature(y, CAS.FEATURE_BASE_NAME_BEGIN, beginB);
+ FSUtil.setFeature(y, CAS.FEATURE_BASE_NAME_END, endB);
+ cas.addFsToIndexes(x);
+ cas.addFsToIndexes(y);
+ return true;
+ });
+
+ softly.assertThat(aPredicate.apply(cas, type, x, y)).as(testCase.getDescription())
+ .isEqualTo(testCase.getValidPositions().contains(aCondition));
+ }
+ }
+ }
+
+ public static void assertSelectionIsEqualOnRandomData(TypeByContextSelector aExpected, TypeByContextSelector aActual)
+ throws Exception {
+ final int ITERATIONS = 30;
+ final int TYPES = 5;
+
+ TypeSystemDescription tsd = UIMAFramework.getResourceSpecifierFactory().createTypeSystemDescription();
+
+ Map<String, Type> types = new LinkedHashMap<>();
+ for (int i = 0; i < TYPES; i++) {
+ String typeName = "test.Type" + (i + 1);
+ tsd.addType(typeName, "", CAS.TYPE_NAME_ANNOTATION);
+ types.put(typeName, null);
+ }
+
+ CAS randomCas = CasCreationUtils.createCas(tsd, null, null, null);
+
+ for (String typeName : types.keySet()) {
+ types.put(typeName, randomCas.getTypeSystem().getType(typeName));
+ }
+
+ System.out.print("Iteration: ");
+ try {
+ Iterator<Type> ti = types.values().iterator();
+ Type type1 = ti.next();
+ Type type2 = ti.next();
+
+ for (int i = 0; i < ITERATIONS; i++) {
+ if (i % 10 == 0) {
+ System.out.print(i);
+ }
+ else {
+ System.out.print(".");
+ }
+
+ initRandomCas(randomCas, 3 * i, 0, types.values().toArray(new Type[types.size()]));
+
+ for (AnnotationFS context : randomCas.getAnnotationIndex(type1)) {
+ List<AnnotationFS> expected = aExpected.select(randomCas, type2, context);
+ List<AnnotationFS> actual = aActual.select(randomCas, type2, context);
+
+ assertThat(actual)
+ .as("Selected [%s] with context [%s]@[%d..%d]", type2.getShortName(),
+ type1.getShortName(), context.getBegin(), context.getEnd())
+ .containsExactlyElementsOf(expected);
+ }
+ }
+ System.out.print(ITERATIONS);
+ }
+ finally {
+ System.out.println();
+ }
+ }
+
+ private static void initRandomCas(CAS aCas, int aSize, int aMinimumWidth, Type... aTypes) {
+ Random rnd = new Random();
+
+ List<Type> types = new ArrayList<>(asList(aTypes));
+
+ // Shuffle the types
+ for (int n = 0; n < 10; n++) {
+ Type t = types.remove(rnd.nextInt(types.size()));
+ types.add(t);
+ }
+
+ // Randomly generate annotations
+ for (int n = 0; n < aSize; n++) {
+ for (Type t : types) {
+ int begin = rnd.nextInt(100);
+ int end = begin + rnd.nextInt(30) + aMinimumWidth;
+ aCas.addFsToIndexes(aCas.createAnnotation(t, begin, end));
+ }
+ }
+ }
+
+ @FunctionalInterface
+ public static interface RelativeAnnotationPredicate {
+ boolean apply(CAS cas, Type type, AnnotationFS x, AnnotationFS y);
+ }
+
+ @FunctionalInterface
+ public static interface TypeByContextSelector {
+ List<AnnotationFS> select(CAS aCas, Type aType, AnnotationFS aContext);
+ }
+
+ @FunctionalInterface
+ public static interface RelativePositionPredicate {
+ boolean apply(int beginA, int endA, int beginB, int endB);
+ }
+
+ public static class TestCase {
+ private final String description;
+
+ private final Function<RelativePositionPredicate, Boolean> predicate;
+
+ private final List<RelativePosition> validPositions;
+
+ public TestCase(String aDescription, Function<RelativePositionPredicate, Boolean> aPredicate, List<RelativePosition> aValidPositions) {
+ description = aDescription;
+ predicate = aPredicate;
+ validPositions = aValidPositions;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public Function<RelativePositionPredicate, Boolean> getTest() {
+ return predicate;
+ }
+
+ public List<RelativePosition> getValidPositions() {
+ return validPositions;
+ }
+ }
+}