You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@uima.apache.org by re...@apache.org on 2021/07/02 13:28:49 UTC
[uima-uimaj] branch
feature/UIMA-6374-Create-CAS-de-serialization-test-suite created (now
dff84ec)
This is an automated email from the ASF dual-hosted git repository.
rec pushed a change to branch feature/UIMA-6374-Create-CAS-de-serialization-test-suite
in repository https://gitbox.apache.org/repos/asf/uima-uimaj.git.
at dff84ec [UIMA-6374] Create CAS (de)serialization test suite
This branch includes the following new commits:
new dff84ec [UIMA-6374] Create CAS (de)serialization test suite
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
[uima-uimaj] 01/01: [UIMA-6374] Create CAS (de)serialization test
suite
Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
rec pushed a commit to branch feature/UIMA-6374-Create-CAS-de-serialization-test-suite
in repository https://gitbox.apache.org/repos/asf/uima-uimaj.git
commit dff84ec2254c644a9096a5eaf1ea79c4ec7a6ce5
Author: Richard Eckart de Castilho <re...@apache.org>
AuthorDate: Fri Jul 2 15:29:14 2021 +0200
[UIMA-6374] Create CAS (de)serialization test suite
- First bunch of test classes setting up the framework
- No test data yet except some very limited programmatically created data
---
uimaj-core/pom.xml | 221 +++---
.../main/java/org/apache/uima/util/CasIOUtils.java | 6 +-
.../org/apache/uima/cas/impl/SelectFsAssert.java | 70 +-
...rializationDeserialization_BINARY_TSI_Test.java | 173 ++++
...asSerializationDeserialization_BINARY_Test.java | 173 ++++
...izationDeserialization_XMI_1_1_PRETTY_Test.java | 175 +++++
.../uima/cas/serdes/CasToComparableText.java | 866 +++++++++++++++++++++
.../apache/uima/cas/serdes/SerDesCasIOUtils.java | 90 +++
.../cas/serdes/datasuites/RandomCasDataSuite.java | 57 ++
.../datasuites/TestSuiteData_CasBornInMemory.java | 65 ++
.../serdes/datasuites/TestSuiteData_XmiFiles.java | 101 +++
.../cas/serdes/generators/RandomCasGenerator.java | 139 ++++
.../cas/serdes/scenario/DesSerTestScenario.java | 146 ++++
.../cas/serdes/scenario/SerDesTestScenario.java | 109 +++
.../cas/serdes/scenario/SerRefTestScenario.java | 183 +++++
.../transitions/CasDesSerCycleConfiguration.java | 48 ++
.../transitions/CasSerDesCycleConfiguration.java | 47 ++
.../transitions/CasSourceTargetConfiguration.java | 104 +++
18 files changed, 2610 insertions(+), 163 deletions(-)
diff --git a/uimaj-core/pom.xml b/uimaj-core/pom.xml
index 7009edc..44ebe29 100644
--- a/uimaj-core/pom.xml
+++ b/uimaj-core/pom.xml
@@ -17,19 +17,21 @@
specific language governing permissions and limitations
under the License.
-->
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
- <modelVersion>4.0.0</modelVersion>
-
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
<parent>
<groupId>org.apache.uima</groupId>
<artifactId>uimaj-parent</artifactId>
<version>3.3.0-SNAPSHOT</version>
<relativePath>../uimaj-parent/pom.xml</relativePath>
</parent>
-
- <artifactId>uimaj-core</artifactId>
- <packaging>jar</packaging>
- <name>Apache UIMA Base: ${project.artifactId}</name>
+
+ <artifactId>uimaj-core</artifactId>
+ <packaging>jar</packaging>
+ <name>Apache UIMA Base: ${project.artifactId}</name>
<description>The core implementation of the UIMA Java Framework</description>
<url>${uimaWebsiteUrl}</url>
@@ -40,7 +42,7 @@
from the chain of parent poms, if this is omitted.
Keeping this a bit factored allows cutting/pasting the <scm>
- element, and just changing the following two properties -->
+ element, and just changing the following two properties -->
<scm>
<connection>
scm:git:https://github.com/apache/uima-uimaj/
@@ -53,14 +55,14 @@
</url>
<tag>uimaj-3.2.0</tag>
</scm>
-
+
<properties>
<uimaScmProject>${project.artifactId}</uimaScmProject>
<postNoticeText>${ibmNoticeText}</postNoticeText>
<maven.surefire.heap>650M</maven.surefire.heap>
</properties>
-
- <dependencies>
+
+ <dependencies>
<!-- Loggers -->
<dependency>
@@ -87,7 +89,7 @@
<version>${log4j.version}</version>
<!-- "provided" fails in maven test - says cannot initialize class that makes ref to classes in this jar
"test" fails in maven compile - cannot compile -->
- <scope>provided</scope>
+ <scope>provided</scope>
</dependency>
<!-- dependency> moved to surefire configuration
@@ -96,20 +98,20 @@
<version>${log4j.version}</version>
<scope>test</scope>
</dependency-->
-
+
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<!-- needed to convert slf4j markers into log4j markers
- and for running the test -->
+ and for running the test -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>${log4j.version}</version>
<!-- scope provided: put in compile classpath, excluded from test classpath, not passed along -->
- <scope>provided</scope>
+ <scope>provided</scope>
</dependency>
<!-- dependency> moved to surefire configuration
@@ -127,64 +129,76 @@
<scope>test</scope>
</dependency>
-->
-
- <dependency>
- <groupId>org.apache.uima</groupId>
- <artifactId>uimaj-test-util</artifactId>
- <version>${project.parent.version}</version>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>org.junit.jupiter</groupId>
- <artifactId>junit-jupiter-engine</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>org.junit.jupiter</groupId>
- <artifactId>junit-jupiter-params</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>org.assertj</groupId>
- <artifactId>assertj-core</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>commons-io</groupId>
- <artifactId>commons-io</artifactId>
- <version>2.7</version>
- <scope>test</scope>
- </dependency>
- <!-- Uncomment one of these to add Saxon8 or 9 to the classpath for JUnit tests.
- 1 fails due to different formatting and a dropped xmlns attribute,
- 5 work when run separately or by mvn, but fail when running all tests from Eclipse:
- Caused by: org.xml.sax.SAXParseException: cvc-elt.1: Cannot find the declaration of element '....'
- -->
+ <dependency>
+ <groupId>org.apache.uima</groupId>
+ <artifactId>uimaj-test-util</artifactId>
+ <version>${project.parent.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.junit.jupiter</groupId>
+ <artifactId>junit-jupiter-engine</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.junit.jupiter</groupId>
+ <artifactId>junit-jupiter-params</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.assertj</groupId>
+ <artifactId>assertj-core</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>commons-io</groupId>
+ <artifactId>commons-io</artifactId>
+ <version>2.7</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.commons</groupId>
+ <artifactId>commons-csv</artifactId>
+ <version>1.8</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.commons</groupId>
+ <artifactId>commons-lang3</artifactId>
+ <version>3.12.0</version>
+ <scope>test</scope>
+ </dependency>
+
+ <!-- Uncomment one of these to add Saxon8 or 9 to the classpath for JUnit tests.
+ 1 fails due to different formatting and a dropped xmlns attribute,
+ 5 work when run separately or by mvn, but fail when running all tests from Eclipse:
+ Caused by: org.xml.sax.SAXParseException: cvc-elt.1: Cannot find the declaration of element '....'
+ -->
<!--
- <dependency>
- <groupId>net.sf.saxon</groupId>
- <artifactId>Saxon-HE</artifactId>
- <version>9.7.0-14</version>
- <scope>test</scope>
- </dependency>
-
+ <dependency>
+ <groupId>net.sf.saxon</groupId>
+ <artifactId>Saxon-HE</artifactId>
+ <version>9.7.0-14</version>
+ <scope>test</scope>
+ </dependency>
+
- <dependency>
- <groupId>net.sf.saxon</groupId>
- <artifactId>saxon</artifactId>
- <version>8.7</version>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>net.sf.saxon</groupId>
- <artifactId>saxon-dom</artifactId>
- <version>8.7</version>
- <scope>test</scope>
- </dependency>
+ <dependency>
+ <groupId>net.sf.saxon</groupId>
+ <artifactId>saxon</artifactId>
+ <version>8.7</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>net.sf.saxon</groupId>
+ <artifactId>saxon-dom</artifactId>
+ <version>8.7</version>
+ <scope>test</scope>
+ </dependency>
-->
-
+
<!-- For decompiling recompiling jcas experiments -->
<!--
<dependency>
@@ -203,7 +217,7 @@
<!-- Apache v2 license 2016 checked. Also transitive include from above,
listed here so that binary assembly for distribution includes the jar -->
- <dependency>
+ <dependency>
<groupId>org.bitbucket.mstrobel</groupId>
<artifactId>procyon-core</artifactId>
<version>0.5.32</version>
@@ -222,10 +236,10 @@
<version>5.0.4</version>
</dependency>
-->
- </dependencies>
-
- <build>
- <finalName>uima-core</finalName>
+ </dependencies>
+
+ <build>
+ <finalName>uima-core</finalName>
<pluginManagement>
<plugins>
@@ -237,7 +251,7 @@
<compilerArgument>-Xlint:unchecked</compilerArgument>
</configuration>
</plugin-->
-
+
<plugin>
<groupId>org.apache.rat</groupId>
<artifactId>apache-rat-plugin</artifactId>
@@ -266,20 +280,20 @@
<exclude>src/test/java/org/apache/lang/LanguagePair.java</exclude> <!-- jcas classes generated -->
<exclude>src/test/java/sofa/test/CrossAnnotation.java</exclude> <!-- jcas classes generated -->
<exclude>src/test/java/x/y/z/*.java</exclude> <!-- jcas classes generated -->
- </excludes>
+ </excludes>
</configuration>
</execution>
</executions>
- </plugin>
+ </plugin>
</plugins>
</pluginManagement>
-
- <plugins>
- <!-- experiment with CLIRR
- gets fatal error:
-
- net.sf.clirr.core.CheckerException: Unable to find information in class org.apache.uima.cas.impl.AnnotationBaseImpl
- referring back to nested class org.apache.uima.cas.impl.AnnotationBaseImpl$1
+
+ <plugins>
+ <!-- experiment with CLIRR
+ gets fatal error:
+
+ net.sf.clirr.core.CheckerException: Unable to find information in class org.apache.uima.cas.impl.AnnotationBaseImpl
+ referring back to nested class org.apache.uima.cas.impl.AnnotationBaseImpl$1
at net.sf.clirr.core.internal.bcel.BcelScopeHelper.getClassScope(BcelScopeHelper.java:127)
at net.sf.clirr.core.internal.bcel.BcelJavaType.getDeclaredScope(BcelJavaType.java:141)
at net.sf.clirr.core.internal.bcel.BcelJavaType.getEffectiveScope(BcelJavaType.java:145)
@@ -287,13 +301,13 @@
at net.sf.clirr.core.Checker.runClassChecks(Checker.java:190)
at net.sf.clirr.core.Checker.reportDiffs(Checker.java:136)
at org.codehaus.mojo.clirr.AbstractClirrMojo.executeClirr(AbstractClirrMojo.java:236)
- -->
- <!--plugin>
- <artifactId>clirr-maven-plugin</artifactId>
- <groupId>org.codehaus.mojo</groupId>
- <version>2.2.3-SNAPSHOT</version>
-
- </plugin-->
+ -->
+ <!--plugin>
+ <artifactId>clirr-maven-plugin</artifactId>
+ <groupId>org.codehaus.mojo</groupId>
+ <version>2.2.3-SNAPSHOT</version>
+
+ </plugin-->
<!-- filter the template for the version java code
to insert the version from properties -->
@@ -302,7 +316,9 @@
<executions>
<execution>
<id>setVersions</id>
- <goals><goal>copy-resources</goal></goals>
+ <goals>
+ <goal>copy-resources</goal>
+ </goals>
<phase>generate-sources</phase>
<configuration>
<outputDirectory>${project.build.directory}/generated-sources/releaseVersion</outputDirectory>
@@ -328,7 +344,9 @@
<executions>
<execution>
<id>addVersionSrc</id>
- <goals><goal>add-source</goal></goals>
+ <goals>
+ <goal>add-source</goal>
+ </goals>
<configuration>
<sources>
<source>${project.build.directory}/generated-sources/releaseVersion</source>
@@ -337,11 +355,11 @@
</execution>
</executions>
</plugin>
-
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-surefire-plugin</artifactId>
- <configuration>
+
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-surefire-plugin</artifactId>
+ <configuration>
<systemProperties>
<uima.enable_strict_type_source_check>true</uima.enable_strict_type_source_check>
</systemProperties>
@@ -355,9 +373,8 @@
<classpathDependencyExcludes>
<classpathDependencyExclude>org.apache.logging.log4j:log4j-slf4j-impl</classpathDependencyExclude>
</classpathDependencyExcludes> -->
- </configuration>
- </plugin>
- </plugins>
- </build>
-
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
</project>
\ No newline at end of file
diff --git a/uimaj-core/src/main/java/org/apache/uima/util/CasIOUtils.java b/uimaj-core/src/main/java/org/apache/uima/util/CasIOUtils.java
index 99511d4..5d60cbe 100644
--- a/uimaj-core/src/main/java/org/apache/uima/util/CasIOUtils.java
+++ b/uimaj-core/src/main/java/org/apache/uima/util/CasIOUtils.java
@@ -427,14 +427,14 @@ public class CasIOUtils {
BinaryCasSerDes bcsd = casImpl.getBinaryCasSerDes();
// scan the first part of the file for known formats
- casInputStream.mark(6);
- byte[] firstPartOfFile = new byte[6];
+ casInputStream.mark(9);
+ byte[] firstPartOfFile = new byte[9];
int bytesReadCount = casInputStream.read(firstPartOfFile);
casInputStream.reset();
String start = new String(firstPartOfFile, 0, bytesReadCount, StandardCharsets.UTF_8)
.toLowerCase();
- if (start.startsWith("<?xml ")) { // could be XCAS or XMI
+ if (start.startsWith("<?xml ") || start.startsWith("<xmi:xmi ")) { // could be XCAS or XMI
try {
bcsd.setupCasFromCasMgrSerializer(readCasManager(tsiInputStream));
// next call decides on XMI or XCAS via content
diff --git a/uimaj-core/src/test/java/org/apache/uima/cas/impl/SelectFsAssert.java b/uimaj-core/src/test/java/org/apache/uima/cas/impl/SelectFsAssert.java
index 354bd8a..296d198 100644
--- a/uimaj-core/src/test/java/org/apache/uima/cas/impl/SelectFsAssert.java
+++ b/uimaj-core/src/test/java/org/apache/uima/cas/impl/SelectFsAssert.java
@@ -29,7 +29,6 @@ import static java.util.Comparator.comparing;
import static java.util.Comparator.reverseOrder;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;
-import static org.apache.uima.UIMAFramework.getResourceSpecifierFactory;
import static org.apache.uima.cas.text.AnnotationPredicateTestData.RelativePosition.COVERED_BY;
import static org.apache.uima.cas.text.AnnotationPredicateTestData.RelativePosition.FOLLOWING;
import static org.apache.uima.cas.text.AnnotationPredicateTestData.RelativePosition.PRECEDING;
@@ -50,6 +49,7 @@ import org.apache.uima.cas.CAS;
import org.apache.uima.cas.FSIterator;
import org.apache.uima.cas.SelectFSs;
import org.apache.uima.cas.Type;
+import org.apache.uima.cas.serdes.generators.RandomCasGenerator;
import org.apache.uima.cas.text.AnnotationFS;
import org.apache.uima.cas.text.AnnotationPredicateAssert.TestCase;
import org.apache.uima.cas.text.AnnotationPredicateTestData.RelativePosition;
@@ -143,33 +143,19 @@ public class SelectFsAssert {
System.out.print(".");
}
- TypeSystemDescription tsd = getResourceSpecifierFactory().createTypeSystemDescription();
- Map<String, Type> types = new LinkedHashMap<>();
- if (logAnnotationCreation) {
- System.out.println();
- }
- for (int ti = 0; ti < aTypes; ti++) {
- String typeName = "test.Type" + (ti + 1);
- TypeDescription newType;
- if (rnd.nextInt() % 2 == 0 || types.size() == 0) {
- newType = tsd.addType(typeName, "", CAS.TYPE_NAME_ANNOTATION);
- } else {
- newType = tsd.addType(typeName, "",
- new ArrayList<>(types.keySet()).get(rnd.nextInt(types.size())));
- }
-
- if (logAnnotationCreation) {
- System.out.printf("tsd.addType(\"%s\", \"\", \"%s\");%n", newType.getName(),
- newType.getSupertypeName());
- }
+ RandomCasGenerator casRandomizer = new RandomCasGenerator() //
+ .randomGenerator(rnd) //
+ .annotationsToGenerate(annotationsPerIteration.apply(i + 1)) //
+ .minimumAnnotationLength(0) //
+ .logAnnotationCreation(logAnnotationCreation) //
+ .typeCount(aTypes);
- types.put(typeName, null);
- }
-
- CAS randomCas = CasCreationUtils.createCas(tsd, null, null, null);
+ TypeSystemDescription tsd = casRandomizer.generateRandomTypeSystem();
+ CAS randomCas = casRandomizer.generateRandomCas(tsd);
- for (String typeName : types.keySet()) {
- types.put(typeName, randomCas.getTypeSystem().getType(typeName));
+ Map<String, Type> types = new LinkedHashMap<>();
+ for (TypeDescription td : tsd.getTypes()) {
+ types.put(td.getName(), randomCas.getTypeSystem().getType(td.getName()));
}
Iterator<Type> ti = types.values().iterator();
@@ -177,10 +163,6 @@ public class SelectFsAssert {
Type typeX = ti.hasNext() ? ti.next() : typeY;
Type[] typeList = types.values().toArray(new Type[types.size()]);
- randomCas.reset();
-
- initRandomCas(rnd, randomCas, annotationsPerIteration.apply(i), 0, typeList);
-
for (Annotation y : randomCas.<Annotation> select(typeY)) {
switch (rnd.nextInt(3)) {
case 0:
@@ -726,34 +708,6 @@ public class SelectFsAssert {
return sb.toString();
}
- private static void initRandomCas(Random rnd, CAS aCas, int aSize, int aMinimumWidth,
- Type... aTypes) {
- 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
- if (logAnnotationCreation) {
- System.out.println();
- }
- for (int n = 0; n < aSize; n++) {
- for (Type t : types) {
- int begin = rnd.nextInt(100);
- int end = begin + rnd.nextInt(30) + aMinimumWidth;
- AnnotationFS ann = aCas.createAnnotation(t, begin, end);
- if (logAnnotationCreation) {
- System.out.printf("cas.createAnnotation(%s, %d, %d)\t[%d]%n",
- t.getShortName().toLowerCase(), begin, end, identityHashCode(ann));
- }
- aCas.addFsToIndexes(ann);
- }
- }
- }
-
private static boolean nonOverlappingSupported(RelativePosition aXRelToY) {
return asList(COVERED_BY, FOLLOWING, PRECEDING).contains(aXRelToY);
}
diff --git a/uimaj-core/src/test/java/org/apache/uima/cas/serdes/CasSerializationDeserialization_BINARY_TSI_Test.java b/uimaj-core/src/test/java/org/apache/uima/cas/serdes/CasSerializationDeserialization_BINARY_TSI_Test.java
new file mode 100644
index 0000000..61a099f
--- /dev/null
+++ b/uimaj-core/src/test/java/org/apache/uima/cas/serdes/CasSerializationDeserialization_BINARY_TSI_Test.java
@@ -0,0 +1,173 @@
+/*
+ * 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.cas.serdes;
+
+import static java.nio.file.Files.walk;
+import static java.util.Arrays.asList;
+import static org.apache.uima.cas.SerialFormat.BINARY_TSI;
+import static org.apache.uima.cas.serdes.SerDesCasIOUtils.desser;
+import static org.apache.uima.cas.serdes.SerDesCasIOUtils.ser;
+import static org.apache.uima.cas.serdes.SerDesCasIOUtils.serdes;
+import static org.apache.uima.util.CasLoadMode.DEFAULT;
+import static org.apache.uima.util.CasLoadMode.REINIT;
+
+import java.lang.invoke.MethodHandles;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Stream;
+
+import org.apache.uima.cas.SerialFormat;
+import org.apache.uima.cas.serdes.datasuites.TestSuiteData_CasBornInMemory;
+import org.apache.uima.cas.serdes.datasuites.TestSuiteData_XmiFiles;
+import org.apache.uima.cas.serdes.scenario.DesSerTestScenario;
+import org.apache.uima.cas.serdes.scenario.SerDesTestScenario;
+import org.apache.uima.cas.serdes.scenario.SerRefTestScenario;
+import org.apache.uima.cas.serdes.transitions.CasSerDesCycleConfiguration;
+import org.apache.uima.cas.serdes.transitions.CasSourceTargetConfiguration;
+import org.apache.uima.util.CasCreationUtils;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+public class CasSerializationDeserialization_BINARY_TSI_Test {
+
+ private static final String CLASSNAME = MethodHandles.lookup().lookupClass().getSimpleName();
+
+ private static final Path TEST_RESOURCE_PATH = Paths.get("src", "test", "resources", CLASSNAME);
+ private static final Path SER_REF_BASE_PATH = TEST_RESOURCE_PATH.resolve("ser-ref");
+ private static final Path ONE_WAY_REFERENCE_BASE_PATH = TEST_RESOURCE_PATH.resolve("one-way");
+
+ private static final Path TEST_OUTPUT_PATH = Paths.get("target", "test-output", CLASSNAME);
+ private static final Path ONE_WAY_TARGET_BASE_PATH = TEST_OUTPUT_PATH.resolve("one-way");
+ private static final Path ROUND_TRIP_TARGET_BASE_PATH = TEST_OUTPUT_PATH.resolve("round-trip");
+ private static final Path SER_REF_TARGET_BASE_PATH = TEST_OUTPUT_PATH.resolve("ser-ref");
+
+ private static final SerialFormat FORMAT = BINARY_TSI;
+ private static final String CAS_FILE_NAME = "cas.bin";
+
+ /**
+ * SERIALIZE -> COMARE-TO-REFERENCE scenarios using the example CASes provided by
+ * {@link TestSuiteData_CasBornInMemory}.
+ */
+ private static List<SerRefTestScenario> serRefScenarios() {
+ List<SerRefTestScenario> confs = new ArrayList<>();
+
+ for (CasSourceTargetConfiguration data : TestSuiteData_CasBornInMemory.configurations()) {
+ confs.add(new SerRefTestScenario( //
+ SER_REF_BASE_PATH, //
+ SER_REF_TARGET_BASE_PATH, //
+ data, //
+ CAS_FILE_NAME, //
+ (cas, path) -> ser(cas, path, FORMAT)));
+ }
+
+ return confs;
+ }
+
+ /**
+ * SERIALIZE -> DESERIALIZE scenarios using the example CASes provided by
+ * {@link TestSuiteData_CasBornInMemory} and applying them to each of the configured
+ * serialization/deserialization cycles.
+ */
+ private static List<SerDesTestScenario> serDesScenarios() {
+ List<CasSerDesCycleConfiguration> cycles = asList( //
+ new CasSerDesCycleConfiguration(FORMAT + " / DEFAULT", //
+ (a, b) -> serdes(a, b, FORMAT, DEFAULT)),
+ new CasSerDesCycleConfiguration(FORMAT + " / REINIT", //
+ (a, b) -> serdes(a, b, FORMAT, REINIT)));
+
+ List<SerDesTestScenario> confs = new ArrayList<>();
+
+ for (CasSerDesCycleConfiguration cycle : cycles) {
+ for (CasSourceTargetConfiguration data : TestSuiteData_CasBornInMemory.configurations()) {
+ confs.add(new SerDesTestScenario(data, cycle));
+ }
+ }
+
+ return confs;
+ }
+
+ /**
+ * DESERIALIZE -> SERIALIZE scenarios using the reference data from the
+ * serialize/compare-to-reference data.
+ */
+ private static List<DesSerTestScenario> roundTripDesSerScenarios() throws Exception {
+ List<DesSerTestScenario> confs = new ArrayList<>();
+
+ try (Stream<Path> fileStream = walk(SER_REF_BASE_PATH, 2)
+ .filter(p -> p.getFileName().toString().equals(CAS_FILE_NAME))) {
+
+ fileStream.forEach(dataFile -> confs.add(DesSerTestScenario.builder() //
+ .withTitle(dataFile.getParent().getFileName().toString())
+ .withTargetBasePath(ROUND_TRIP_TARGET_BASE_PATH) //
+ .withCasFile(dataFile) // source / reference (round-trip)
+ .withCycle((a, b) -> desser(CasCreationUtils.createCas(), a, b, FORMAT, REINIT))
+ .build()));
+ }
+
+ return confs;
+ }
+
+ /**
+ * DESERIALIZE -> SERIALIZE scenarios using the reference data from the
+ * serialize/compare-to-reference data.
+ */
+ private static List<SerRefTestScenario> oneWayDesSerScenarios() throws Exception {
+ List<SerRefTestScenario> confs = new ArrayList<>();
+
+ for (CasSourceTargetConfiguration conf : TestSuiteData_XmiFiles.configurations()) {
+ confs.add(SerRefTestScenario.builder() //
+ .withTitle(conf.getTitle()) //
+ .withSourceCasSupplier(conf::createSourceCas) //
+ .withTargetCasFile(
+ ONE_WAY_TARGET_BASE_PATH.resolve(conf.getTitle()).resolve(CAS_FILE_NAME))
+ .withReferenceCasFile(
+ ONE_WAY_REFERENCE_BASE_PATH.resolve(conf.getTitle()).resolve(CAS_FILE_NAME)) //
+ .withSerializer((cas, path) -> ser(cas, path, FORMAT)) //
+ .build());
+ }
+
+ return confs;
+ }
+
+ @ParameterizedTest
+ @MethodSource("serRefScenarios")
+ public void serializeAndCompareToReferenceTest(Runnable aScenario) throws Exception {
+ aScenario.run();
+ }
+
+ @ParameterizedTest
+ @MethodSource("serDesScenarios")
+ public void serializeDeserializeTest(Runnable aScenario) throws Exception {
+ aScenario.run();
+ }
+
+ @ParameterizedTest
+ @MethodSource("roundTripDesSerScenarios")
+ public void roundTripDeserializeSerializeTest(Runnable aScenario) throws Exception {
+ aScenario.run();
+ }
+
+ @ParameterizedTest
+ @MethodSource("oneWayDesSerScenarios")
+ public void oneWayDeserializeSerializeTest(Runnable aScenario) throws Exception {
+ aScenario.run();
+ }
+}
diff --git a/uimaj-core/src/test/java/org/apache/uima/cas/serdes/CasSerializationDeserialization_BINARY_Test.java b/uimaj-core/src/test/java/org/apache/uima/cas/serdes/CasSerializationDeserialization_BINARY_Test.java
new file mode 100644
index 0000000..7286c81
--- /dev/null
+++ b/uimaj-core/src/test/java/org/apache/uima/cas/serdes/CasSerializationDeserialization_BINARY_Test.java
@@ -0,0 +1,173 @@
+/*
+ * 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.cas.serdes;
+
+import static java.nio.file.Files.walk;
+import static java.util.Arrays.asList;
+import static org.apache.uima.cas.SerialFormat.BINARY;
+import static org.apache.uima.cas.serdes.SerDesCasIOUtils.desser;
+import static org.apache.uima.cas.serdes.SerDesCasIOUtils.ser;
+import static org.apache.uima.cas.serdes.SerDesCasIOUtils.serdes;
+import static org.apache.uima.util.CasLoadMode.DEFAULT;
+import static org.apache.uima.util.CasLoadMode.REINIT;
+
+import java.lang.invoke.MethodHandles;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Stream;
+
+import org.apache.uima.cas.SerialFormat;
+import org.apache.uima.cas.serdes.datasuites.TestSuiteData_CasBornInMemory;
+import org.apache.uima.cas.serdes.datasuites.TestSuiteData_XmiFiles;
+import org.apache.uima.cas.serdes.scenario.DesSerTestScenario;
+import org.apache.uima.cas.serdes.scenario.SerDesTestScenario;
+import org.apache.uima.cas.serdes.scenario.SerRefTestScenario;
+import org.apache.uima.cas.serdes.transitions.CasSerDesCycleConfiguration;
+import org.apache.uima.cas.serdes.transitions.CasSourceTargetConfiguration;
+import org.apache.uima.util.CasCreationUtils;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+public class CasSerializationDeserialization_BINARY_Test {
+
+ private static final String CLASSNAME = MethodHandles.lookup().lookupClass().getSimpleName();
+
+ private static final Path TEST_RESOURCE_PATH = Paths.get("src", "test", "resources", CLASSNAME);
+ private static final Path SER_REF_BASE_PATH = TEST_RESOURCE_PATH.resolve("ser-ref");
+ private static final Path ONE_WAY_REFERENCE_BASE_PATH = TEST_RESOURCE_PATH.resolve("one-way");
+
+ private static final Path TEST_OUTPUT_PATH = Paths.get("target", "test-output", CLASSNAME);
+ private static final Path ONE_WAY_TARGET_BASE_PATH = TEST_OUTPUT_PATH.resolve("one-way");
+ private static final Path ROUND_TRIP_TARGET_BASE_PATH = TEST_OUTPUT_PATH.resolve("round-trip");
+ private static final Path SER_REF_TARGET_BASE_PATH = TEST_OUTPUT_PATH.resolve("ser-ref");
+
+ private static final SerialFormat FORMAT = BINARY;
+ private static final String CAS_FILE_NAME = "cas.bin";
+
+ /**
+ * SERIALIZE -> COMARE-TO-REFERENCE scenarios using the example CASes provided by
+ * {@link TestSuiteData_CasBornInMemory}.
+ */
+ private static List<SerRefTestScenario> serRefScenarios() {
+ List<SerRefTestScenario> confs = new ArrayList<>();
+
+ for (CasSourceTargetConfiguration data : TestSuiteData_CasBornInMemory.configurations()) {
+ confs.add(new SerRefTestScenario( //
+ SER_REF_BASE_PATH, //
+ SER_REF_TARGET_BASE_PATH, //
+ data, //
+ CAS_FILE_NAME, //
+ (cas, path) -> ser(cas, path, FORMAT)));
+ }
+
+ return confs;
+ }
+
+ /**
+ * SERIALIZE -> DESERIALIZE scenarios using the example CASes provided by
+ * {@link TestSuiteData_CasBornInMemory} and applying them to each of the configured
+ * serialization/deserialization cycles.
+ */
+ private static List<SerDesTestScenario> serDesScenarios() {
+ List<CasSerDesCycleConfiguration> cycles = asList( //
+ new CasSerDesCycleConfiguration(FORMAT + " / DEFAULT", //
+ (a, b) -> serdes(a, b, FORMAT, DEFAULT)),
+ new CasSerDesCycleConfiguration(FORMAT + " / REINIT", //
+ (a, b) -> serdes(a, b, FORMAT, REINIT)));
+
+ List<SerDesTestScenario> confs = new ArrayList<>();
+
+ for (CasSerDesCycleConfiguration cycle : cycles) {
+ for (CasSourceTargetConfiguration data : TestSuiteData_CasBornInMemory.configurations()) {
+ confs.add(new SerDesTestScenario(data, cycle));
+ }
+ }
+
+ return confs;
+ }
+
+ /**
+ * DESERIALIZE -> SERIALIZE scenarios using the reference data from the
+ * serialize/compare-to-reference data.
+ */
+ private static List<DesSerTestScenario> roundTripDesSerScenarios() throws Exception {
+ List<DesSerTestScenario> confs = new ArrayList<>();
+
+ try (Stream<Path> fileStream = walk(SER_REF_BASE_PATH, 2)
+ .filter(p -> p.getFileName().toString().equals(CAS_FILE_NAME))) {
+
+ fileStream.forEach(dataFile -> confs.add(DesSerTestScenario.builder() //
+ .withTitle(dataFile.getParent().getFileName().toString())
+ .withTargetBasePath(ROUND_TRIP_TARGET_BASE_PATH) //
+ .withCasFile(dataFile) // source / reference (round-trip)
+ .withCycle((a, b) -> desser(CasCreationUtils.createCas(), a, b, FORMAT, REINIT))
+ .build()));
+ }
+
+ return confs;
+ }
+
+ /**
+ * DESERIALIZE -> SERIALIZE scenarios using the reference data from the
+ * serialize/compare-to-reference data.
+ */
+ private static List<SerRefTestScenario> oneWayDesSerScenarios() throws Exception {
+ List<SerRefTestScenario> confs = new ArrayList<>();
+
+ for (CasSourceTargetConfiguration conf : TestSuiteData_XmiFiles.configurations()) {
+ confs.add(SerRefTestScenario.builder() //
+ .withTitle(conf.getTitle()) //
+ .withSourceCasSupplier(conf::createSourceCas) //
+ .withTargetCasFile(
+ ONE_WAY_TARGET_BASE_PATH.resolve(conf.getTitle()).resolve(CAS_FILE_NAME))
+ .withReferenceCasFile(
+ ONE_WAY_REFERENCE_BASE_PATH.resolve(conf.getTitle()).resolve(CAS_FILE_NAME)) //
+ .withSerializer((cas, path) -> ser(cas, path, FORMAT)) //
+ .build());
+ }
+
+ return confs;
+ }
+
+ @ParameterizedTest
+ @MethodSource("serRefScenarios")
+ public void serializeAndCompareToReferenceTest(Runnable aScenario) throws Exception {
+ aScenario.run();
+ }
+
+ @ParameterizedTest
+ @MethodSource("serDesScenarios")
+ public void serializeDeserializeTest(Runnable aScenario) throws Exception {
+ aScenario.run();
+ }
+
+ @ParameterizedTest
+ @MethodSource("roundTripDesSerScenarios")
+ public void roundTripDeserializeSerializeTest(Runnable aScenario) throws Exception {
+ aScenario.run();
+ }
+
+ @ParameterizedTest
+ @MethodSource("oneWayDesSerScenarios")
+ public void oneWayDeserializeSerializeTest(Runnable aScenario) throws Exception {
+ aScenario.run();
+ }
+}
diff --git a/uimaj-core/src/test/java/org/apache/uima/cas/serdes/CasSerializationDeserialization_XMI_1_1_PRETTY_Test.java b/uimaj-core/src/test/java/org/apache/uima/cas/serdes/CasSerializationDeserialization_XMI_1_1_PRETTY_Test.java
new file mode 100644
index 0000000..67b9b6a
--- /dev/null
+++ b/uimaj-core/src/test/java/org/apache/uima/cas/serdes/CasSerializationDeserialization_XMI_1_1_PRETTY_Test.java
@@ -0,0 +1,175 @@
+/*
+ * 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.cas.serdes;
+
+import static java.nio.file.Files.isDirectory;
+import static java.util.Arrays.asList;
+import static org.apache.uima.cas.SerialFormat.XMI_1_1_PRETTY;
+import static org.apache.uima.cas.serdes.SerDesCasIOUtils.desser;
+import static org.apache.uima.cas.serdes.SerDesCasIOUtils.ser;
+import static org.apache.uima.cas.serdes.SerDesCasIOUtils.serdes;
+import static org.apache.uima.cas.serdes.datasuites.TestSuiteData_XmiFiles.DATA_XMI;
+import static org.apache.uima.util.CasLoadMode.DEFAULT;
+import static org.apache.uima.util.CasLoadMode.LENIENT;
+
+import java.lang.invoke.MethodHandles;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Stream;
+
+import org.apache.uima.cas.SerialFormat;
+import org.apache.uima.cas.serdes.datasuites.TestSuiteData_CasBornInMemory;
+import org.apache.uima.cas.serdes.datasuites.TestSuiteData_XmiFiles;
+import org.apache.uima.cas.serdes.scenario.DesSerTestScenario;
+import org.apache.uima.cas.serdes.scenario.SerDesTestScenario;
+import org.apache.uima.cas.serdes.scenario.SerRefTestScenario;
+import org.apache.uima.cas.serdes.transitions.CasSerDesCycleConfiguration;
+import org.apache.uima.cas.serdes.transitions.CasSourceTargetConfiguration;
+import org.apache.uima.util.CasCreationUtils;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+public class CasSerializationDeserialization_XMI_1_1_PRETTY_Test {
+
+ private static final String CLASSNAME = MethodHandles.lookup().lookupClass().getSimpleName();
+
+ private static final Path TEST_RESOURCE_PATH = Paths.get("src", "test", "resources", CLASSNAME);
+ private static final Path SER_REF_BASE_PATH = TEST_RESOURCE_PATH.resolve("ser-ref");
+ private static final Path ONE_WAY_REFERENCE_BASE_PATH = TEST_RESOURCE_PATH.resolve("one-way");
+
+ private static final Path TEST_OUTPUT_PATH = Paths.get("target", "test-output", CLASSNAME);
+ private static final Path ONE_WAY_TARGET_BASE_PATH = TEST_OUTPUT_PATH.resolve("one-way");
+ private static final Path ROUND_TRIP_TARGET_BASE_PATH = TEST_OUTPUT_PATH.resolve("round-trip");
+ private static final Path SER_REF_TARGET_BASE_PATH = TEST_OUTPUT_PATH.resolve("ser-ref");
+
+ private static final SerialFormat FORMAT = XMI_1_1_PRETTY;
+ private static final String CAS_FILE_NAME = DATA_XMI;
+
+ /**
+ * SERIALIZE -> COMARE-TO-REFERENCE scenarios using the example CASes provided by
+ * {@link TestSuiteData_CasBornInMemory}.
+ */
+ private static List<SerRefTestScenario> serRefScenarios() {
+ List<SerRefTestScenario> confs = new ArrayList<>();
+
+ for (CasSourceTargetConfiguration data : TestSuiteData_CasBornInMemory.configurations()) {
+ confs.add(new SerRefTestScenario( //
+ SER_REF_BASE_PATH, //
+ SER_REF_TARGET_BASE_PATH, //
+ data, //
+ CAS_FILE_NAME, //
+ (cas, path) -> ser(cas, path, FORMAT)));
+ }
+
+ return confs;
+ }
+
+ /**
+ * SERIALIZE -> DESERIALIZE scenarios using the example CASes provided by
+ * {@link TestSuiteData_CasBornInMemory} and applying them to each of the configured
+ * serialization/deserialization cycles.
+ */
+ private static List<SerDesTestScenario> serDesScenarios() {
+ List<CasSerDesCycleConfiguration> cycles = asList( //
+ new CasSerDesCycleConfiguration(FORMAT + " / DEFAULT", //
+ (a, b) -> serdes(a, b, FORMAT, DEFAULT)),
+ new CasSerDesCycleConfiguration(FORMAT + " / LENIENT", //
+ (a, b) -> serdes(a, b, FORMAT, LENIENT)));
+
+ List<SerDesTestScenario> confs = new ArrayList<>();
+
+ for (CasSerDesCycleConfiguration cycle : cycles) {
+ for (CasSourceTargetConfiguration data : TestSuiteData_CasBornInMemory.configurations()) {
+ confs.add(new SerDesTestScenario(data, cycle));
+ }
+ }
+
+ return confs;
+ }
+
+ /**
+ * DESERIALIZE -> SERIALIZE scenarios using the reference data from the
+ * serialize/compare-to-reference data.
+ */
+ private static List<DesSerTestScenario> roundTripDesSerScenarios() throws Exception {
+ List<DesSerTestScenario> confs = new ArrayList<>();
+
+ try (Stream<Path> fileStream = Files.list(SER_REF_BASE_PATH)
+ .filter(p -> isDirectory(p) && !p.toFile().isHidden())) {
+
+ fileStream.forEach(caseFolder -> confs.add(DesSerTestScenario.builder() //
+ .withTitle(caseFolder.getFileName().toString())
+ .withTargetBasePath(ROUND_TRIP_TARGET_BASE_PATH) //
+ .withCasFile(caseFolder.resolve(CAS_FILE_NAME)) // source / reference (round-trip)
+ .withCycle((a, b) -> desser(CasCreationUtils.createCas(), a, b, FORMAT, DEFAULT))
+ .build()));
+ }
+
+ return confs;
+ }
+
+ /**
+ * DESERIALIZE -> SERIALIZE scenarios using the reference data from the
+ * serialize/compare-to-reference data.
+ */
+ private static List<SerRefTestScenario> oneWayDesSerScenarios() throws Exception {
+ List<SerRefTestScenario> confs = new ArrayList<>();
+
+ for (CasSourceTargetConfiguration conf : TestSuiteData_XmiFiles.configurations()) {
+ confs.add(SerRefTestScenario.builder() //
+ .withTitle(conf.getTitle()) //
+ .withSourceCasSupplier(conf::createSourceCas) //
+ .withTargetCasFile(
+ ONE_WAY_TARGET_BASE_PATH.resolve(conf.getTitle()).resolve(CAS_FILE_NAME))
+ .withReferenceCasFile(
+ ONE_WAY_REFERENCE_BASE_PATH.resolve(conf.getTitle()).resolve(CAS_FILE_NAME))
+ .withSerializer((cas, path) -> ser(cas, path, FORMAT)) //
+ .build());
+ }
+
+ return confs;
+ }
+
+ @ParameterizedTest
+ @MethodSource("serRefScenarios")
+ public void serializeAndCompareToReferenceTest(Runnable aScenario) throws Exception {
+ aScenario.run();
+ }
+
+ @ParameterizedTest
+ @MethodSource("serDesScenarios")
+ public void serializeDeserializeTest(Runnable aScenario) throws Exception {
+ aScenario.run();
+ }
+
+ @ParameterizedTest
+ @MethodSource("roundTripDesSerScenarios")
+ public void roundTripDeserializeSerializeTest(Runnable aScenario) throws Exception {
+ aScenario.run();
+ }
+
+ @ParameterizedTest
+ @MethodSource("oneWayDesSerScenarios")
+ public void oneWayDeserializeSerializeTest(Runnable aScenario) throws Exception {
+ aScenario.run();
+ }
+}
diff --git a/uimaj-core/src/test/java/org/apache/uima/cas/serdes/CasToComparableText.java b/uimaj-core/src/test/java/org/apache/uima/cas/serdes/CasToComparableText.java
new file mode 100644
index 0000000..d9e4447
--- /dev/null
+++ b/uimaj-core/src/test/java/org/apache/uima/cas/serdes/CasToComparableText.java
@@ -0,0 +1,866 @@
+/*
+ * 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.cas.serdes;
+
+import static java.util.Arrays.asList;
+import static java.util.Collections.unmodifiableSet;
+import static java.util.Comparator.comparing;
+import static java.util.Comparator.comparingInt;
+import static java.util.Comparator.reverseOrder;
+import static java.util.stream.Collectors.joining;
+import static java.util.stream.Collectors.toList;
+import static org.apache.commons.csv.CSVFormat.DEFAULT;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.lang.reflect.Array;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+import org.apache.commons.csv.CSVPrinter;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.tuple.Pair;
+import org.apache.uima.cas.ArrayFS;
+import org.apache.uima.cas.BooleanArrayFS;
+import org.apache.uima.cas.ByteArrayFS;
+import org.apache.uima.cas.CAS;
+import org.apache.uima.cas.CommonArrayFS;
+import org.apache.uima.cas.DoubleArrayFS;
+import org.apache.uima.cas.Feature;
+import org.apache.uima.cas.FeatureStructure;
+import org.apache.uima.cas.FloatArrayFS;
+import org.apache.uima.cas.IntArrayFS;
+import org.apache.uima.cas.LongArrayFS;
+import org.apache.uima.cas.ShortArrayFS;
+import org.apache.uima.cas.StringArrayFS;
+import org.apache.uima.cas.Type;
+import org.apache.uima.cas.TypeSystem;
+import org.apache.uima.cas.text.AnnotationFS;
+import org.apache.uima.jcas.JCas;
+import org.apache.uima.jcas.cas.AnnotationBase;
+
+/**
+ * @author entwicklerteam
+ */
+public class CasToComparableText {
+
+ // Parameters
+ private boolean markIndexed = true;
+ private boolean markView = true;
+ private boolean coveredTextColumnEnabled = true;
+ private boolean indexedColumnEnabled = false;
+ private int maxLengthCoveredText = 30;
+ private boolean sortAnnotationsInMultiValuedFeatures = true;
+ private boolean uniqueAnchors = true;
+ private Set<String> excludeFeaturePatterns = new HashSet<>();
+ private Set<String> excludeTypePatterns = new HashSet<>();
+ private String nullValue = "<NULL>";
+
+ // State
+ private final CAS cas;
+ private Set<FeatureStructure> _indexedFses;
+ private Map<String, Pattern> regexCache = new HashMap<>();
+ private Map<String, Boolean> exclusionCache;
+
+ public CasToComparableText(CAS aCas) {
+
+ cas = aCas;
+ }
+
+ public CasToComparableText(JCas jCas) {
+
+ this(jCas.getCas());
+ }
+
+ public void addExcludeTypePatterns(String... aPatterns) {
+
+ resetExclusionCache();
+ excludeTypePatterns.addAll(asList(aPatterns));
+ }
+
+ public Set<String> getExcludeTypePatterns() {
+
+ return unmodifiableSet(excludeTypePatterns);
+ }
+
+ public void setExcludeTypePatterns(Collection<String> aExcludeFeaturePatterns) {
+
+ resetExclusionCache();
+ excludeTypePatterns.clear();
+
+ if (aExcludeFeaturePatterns != null) {
+ excludeTypePatterns.addAll(aExcludeFeaturePatterns);
+ }
+ }
+
+ public void addExcludeFeaturePatterns(String... aPatterns) {
+
+ excludeFeaturePatterns.addAll(asList(aPatterns));
+ }
+
+ public Set<String> getExcludeFeaturePatterns() {
+
+ return unmodifiableSet(excludeFeaturePatterns);
+ }
+
+ public void setExcludeFeaturePatterns(Collection<String> aExcludeFeaturePatterns) {
+
+ resetExclusionCache();
+ excludeFeaturePatterns.clear();
+
+ if (aExcludeFeaturePatterns != null) {
+ excludeFeaturePatterns.addAll(aExcludeFeaturePatterns);
+ }
+ }
+
+ public void setUniqueAnchors(boolean aUniqueAnchors) {
+
+ uniqueAnchors = aUniqueAnchors;
+ }
+
+ public boolean isUniqueAnchors() {
+
+ return uniqueAnchors;
+ }
+
+ public void setSortAnnotationsInMultiValuedFeatures(
+ boolean aSortAnnotationsInMultiValuedFeatures) {
+
+ sortAnnotationsInMultiValuedFeatures = aSortAnnotationsInMultiValuedFeatures;
+ }
+
+ public boolean isSortAnnotationsInMultiValuedFeatures() {
+
+ return sortAnnotationsInMultiValuedFeatures;
+ }
+
+ public boolean isMarkIndexed() {
+
+ return markIndexed;
+ }
+
+ /**
+ * @param aMarkIndexed
+ * whether to mark indexed feature structures with an asterisk in the anchor and to add a
+ * column indicating the indexing status.
+ */
+ public void setMarkIndexed(boolean aMarkIndexed) {
+
+ markIndexed = aMarkIndexed;
+ }
+
+ public boolean isCoveredTextColumnEnabled() {
+
+ return coveredTextColumnEnabled;
+ }
+
+ public void setCoveredTextColumnEnabled(boolean aCoveredTextColumnEnabled) {
+
+ coveredTextColumnEnabled = aCoveredTextColumnEnabled;
+ }
+
+ public boolean isIndexedColumnEnabled() {
+
+ return indexedColumnEnabled;
+ }
+
+ public void setIndexedColumnEnabled(boolean aIndexedColumnEnabled) {
+
+ indexedColumnEnabled = aIndexedColumnEnabled;
+ }
+
+ /**
+ * @return to add the view name to the anchor. Should be disabled when this class is used to
+ * compare feature structures across views.
+ */
+ public boolean isMarkView() {
+
+ return markView;
+ }
+
+ public void setMarkView(boolean aMarkView) {
+
+ markView = aMarkView;
+ }
+
+ public int getMaxLengthCoveredText() {
+
+ return maxLengthCoveredText;
+ }
+
+ public void setMaxLengthCoveredText(int aMaxLengthCoveredText) {
+
+ maxLengthCoveredText = aMaxLengthCoveredText;
+ }
+
+ public String getNullValue() {
+
+ return nullValue;
+ }
+
+ public void setNullValue(String aNullValue) {
+
+ nullValue = aNullValue;
+ }
+
+ private Pattern pattern(String aRegex) {
+
+ return regexCache.computeIfAbsent(aRegex, ex -> Pattern.compile(ex));
+ }
+
+ public String toString(FeatureStructure aFS) {
+
+ if (aFS.getCAS() != cas) {
+ throw new IllegalArgumentException("FeatureStructure does not belong to CAS");
+ }
+
+ return toString(asList(aFS));
+ }
+
+ public String toString(Collection<? extends FeatureStructure> aSeeds) {
+
+ try (StringWriter out = new StringWriter()) {
+ write(out, aSeeds);
+ return out.toString();
+ } catch (IOException e) {
+ // The StringWriter shouldn't be throwing any IOExceptions, so if something goes wrong,
+ // it must be the fault of the rendering code / feature structure *caugh*
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ public void write(Writer out) throws IOException {
+
+ write(out, cas.getIndexedFSs());
+ }
+
+ private Set<FeatureStructure> getIndexedFses() {
+
+ if (!markIndexed && !indexedColumnEnabled) {
+ return null;
+ }
+
+ if (_indexedFses == null) {
+ _indexedFses = new HashSet<>(cas.getIndexedFSs());
+ }
+
+ return _indexedFses;
+ }
+
+ public void write(Writer out, Collection<? extends FeatureStructure> aSeeds) throws IOException {
+
+ if (aSeeds.isEmpty()) {
+ return;
+ }
+
+ if (aSeeds.stream().anyMatch(fs -> fs.getCAS() != cas)) {
+ throw new IllegalArgumentException("FeatureStructure does not belong to CAS");
+ }
+
+ Set<FeatureStructure> reachableFses = findReachableFeatureStructures(aSeeds);
+
+ // First group by type so we have per-type sections in the output
+ Map<Type, List<FeatureStructure>> indexByType = reachableFses.stream()
+ .collect(Collectors.groupingBy(fs -> fs.getType()));
+
+ // Ensure that the type sections have a stable order
+ List<Type> typesSorted = indexByType.keySet().stream()
+ .filter(type -> excludeTypePatterns.stream()
+ .noneMatch(p -> pattern(p).matcher(type.getName()).matches()))
+ .sorted(comparing(Type::getName)).collect(Collectors.toList());
+
+ // Build an anchor for every feature structure
+ Map<FeatureStructure, Anchor> fsToAnchor = generateAnchors(typesSorted, indexByType);
+
+ // Process the feature structures in each type section
+ PrintWriter pout = new PrintWriter(out);
+ for (Type type : typesSorted) {
+ try (CSVPrinter csv = new CSVPrinter(new CloseShieldAppendable(pout), DEFAULT)) {
+ renderHeader(csv, type);
+
+ // Generate all the rows for this type and then we sort them - this is necessary
+ // because there can be multiple annotations of the same type at the same location
+ // and we need to have a semantically stable ordering - so we take the actual
+ // row content into consideration
+ // @formatter:off
+ List<Pair<FeatureStructure, List<String>>> rows = indexByType.get(type).stream()
+ .map(fs -> renderFS(fsToAnchor, fs))
+ .sorted(comparing(
+ // Compare by type name and offsets
+ Pair<FeatureStructure, List<String>>::getKey, new FSComparator())
+ // ... then (if necessary) compare by the actual data
+ .thenComparing(p -> p.getValue().stream().collect(joining("\0"))))
+ .collect(toList());
+ // @formatter:on
+
+ for (Pair<FeatureStructure, List<String>> row : rows) {
+ csv.printRecord(row.getValue());
+ }
+ }
+
+ pout.print("\n");
+ }
+ }
+
+ private String escape(String aString) {
+
+ return StringUtils.replaceEach(aString, new String[] { "\t", "\n", "\r", "[", "]", ",", "\\" },
+ new String[] { "\\t", "\\n", "\\r", "\\[", "\\]", "\\,", "\\\\" });
+ }
+
+ private void renderHeader(CSVPrinter aCSV, Type aType) throws IOException {
+
+ TypeSystem ts = cas.getTypeSystem();
+ Type annotationType = ts.getType(CAS.TYPE_NAME_ANNOTATION);
+
+ // Type as comment
+ aCSV.printRecord(aType.getName());
+
+ List<String> sectionHeader = new ArrayList<>();
+ sectionHeader.add("<ANCHOR>");
+
+ if (indexedColumnEnabled) {
+ sectionHeader.add("<INDEXED>");
+ }
+
+ if (coveredTextColumnEnabled && ts.subsumes(annotationType, aType)) {
+ sectionHeader.add("<COVERED_TEXT>");
+ }
+
+ listFeatures(aType).stream().filter(f -> !isExcluded(f)).map(f -> f.getShortName())
+ .forEachOrdered(sectionHeader::add);
+ aCSV.printRecord(sectionHeader);
+ }
+
+ /**
+ * Build an anchor for every feature structure.
+ */
+ private Map<FeatureStructure, Anchor> generateAnchors(List<Type> aTypesSorted,
+ Map<Type, List<FeatureStructure>> aIndexByType) {
+
+ Set<FeatureStructure> indexedFses = getIndexedFses();
+ Map<FeatureStructure, Anchor> fsToAnchor = new HashMap<>();
+ Map<String, Integer> disambiguationByPrefix = new HashMap<>();
+ for (Type type : aTypesSorted) {
+ List<FeatureStructure> fses = new ArrayList<>(aIndexByType.get(type));
+ fses.sort(new FSComparator());
+
+ for (FeatureStructure fs : fses) {
+ Anchor anchor = new Anchor(fs, markIndexed && indexedFses.contains(fs),
+ disambiguationByPrefix);
+ fsToAnchor.put(fs, anchor);
+ }
+ }
+
+ return fsToAnchor;
+ }
+
+ private List<Feature> listFeatures(Type aType) {
+
+ // Determine which feature to show in which column
+ List<Feature> features = new ArrayList<>(aType.getFeatures());
+ features.sort(comparing(Feature::getShortName));
+
+ // Features going into the anchor column are suppressed
+ features.removeIf(f -> CAS.FEATURE_BASE_NAME_SOFA.equals(f.getShortName()));
+ features.removeIf(f -> CAS.FEATURE_BASE_NAME_BEGIN.equals(f.getShortName()));
+ features.removeIf(f -> CAS.FEATURE_BASE_NAME_END.equals(f.getShortName()));
+
+ return features;
+ }
+
+ private Pair<FeatureStructure, List<String>> renderFS(Map<FeatureStructure, Anchor> aFsToAnchor,
+ FeatureStructure aFS) {
+
+ List<String> data = new ArrayList<>();
+
+ // First column is always the anchor
+ data.add(aFsToAnchor.get(aFS).toString());
+
+ // Then add if the FS was in the index
+ if (indexedColumnEnabled) {
+ data.add(String.valueOf(getIndexedFses().contains(aFS)));
+ }
+
+ if (coveredTextColumnEnabled && aFS instanceof AnnotationFS) {
+ String coveredText = ((AnnotationFS) aFS).getCoveredText();
+ if (maxLengthCoveredText > 0) {
+ coveredText = StringUtils.abbreviateMiddle(coveredText, "...", maxLengthCoveredText);
+ }
+ data.add(escape(coveredText));
+ }
+
+ // Process the rest of the features
+ nextFeature: for (Feature feature : listFeatures(aFS.getType())) {
+
+ // Check if the feature is excluded
+ if (isExcluded(feature)) {
+ continue;
+ }
+
+ // Primitive features can be rendered as strings
+ if (feature.getRange().isPrimitive()) {
+ data.add(escape(aFS.getFeatureValueAsString(feature)));
+ continue;
+ }
+
+ // For multi-valued features, we need to dive into the list/array
+ if (isMultiValuedFeature(aFS, feature)) {
+ data.add(renderMultiValuedFeatureStructure(aFS.getFeatureValue(feature), aFsToAnchor));
+ continue nextFeature;
+ }
+
+ // So once we get here, it must be a feature structure or null
+ FeatureStructure value = aFS.getFeatureValue(feature);
+ if (value == null) {
+ data.add(nullValue);
+ continue nextFeature;
+ }
+
+ // Ok, so it's a feature structure
+ Anchor anchor = aFsToAnchor.get(value);
+ if (anchor == null) {
+ throw new IllegalStateException("No anchor - bug - should not happen");
+ }
+ data.add(anchor.toString());
+ }
+
+ return Pair.of(aFS, data);
+ }
+
+ private void resetExclusionCache() {
+
+ exclusionCache = null;
+ }
+
+ private boolean isExcluded(Feature aFeature) {
+
+ if (exclusionCache == null) {
+ exclusionCache = new HashMap<>();
+ }
+
+ return exclusionCache.computeIfAbsent(aFeature.getName(),
+ f -> excludeFeaturePatterns.stream().anyMatch(p -> pattern(p).matcher(f).matches()));
+ }
+
+ private static boolean isMultiValuedFeature(FeatureStructure aFS, Feature aFeature) {
+ if (aFeature == null) {
+ return false;
+ }
+
+ TypeSystem aTS = aFS.getCAS().getTypeSystem();
+
+ return aFeature.getRange().isArray()
+ || aTS.subsumes(aTS.getType(CAS.TYPE_NAME_LIST_BASE), aFeature.getRange());
+ }
+
+ private boolean isMultiValued(FeatureStructure fs) {
+ TypeSystem ts = fs.getCAS().getTypeSystem();
+ return fs.getType().isArray() || ts.subsumes(ts.getType(CAS.TYPE_NAME_LIST_BASE), fs.getType());
+ }
+
+ private String renderMultiValuedFeatureStructure(FeatureStructure aFS,
+ Map<FeatureStructure, Anchor> aFsToAnchor) {
+ List<Object> values = multiValuedFeatureStructureToList(aFS);
+
+ if (values == null) {
+ return nullValue;
+ }
+
+ // Optionally sort multi-valued feature that consist only of annotations. This essentially
+ // means that annotation-typed multi-valued features are treated as sets.
+ boolean allValuesAreAnnotations = values.stream().allMatch(v -> v instanceof AnnotationFS);
+ if (sortAnnotationsInMultiValuedFeatures && allValuesAreAnnotations) {
+ values = values.stream().map(v -> (AnnotationFS) v)
+ .sorted(comparingInt(AnnotationFS::getBegin)
+ .thenComparing(AnnotationFS::getEnd, reverseOrder())
+ .thenComparing(r -> r.getType().getName()))
+ .collect(Collectors.toList());
+ }
+
+ List<String> items = new ArrayList<>();
+ nextItem: for (Object item : values) {
+ if (item == null) {
+ items.add(nullValue);
+ continue nextItem;
+ }
+
+ if (item instanceof String || item.getClass().isPrimitive()) {
+ items.add(String.valueOf(item));
+ continue nextItem;
+ }
+
+ if (item instanceof FeatureStructure) {
+ FeatureStructure fsItem = (FeatureStructure) item;
+ if (isMultiValued(fsItem)) {
+ items.add(renderMultiValuedFeatureStructure(fsItem, aFsToAnchor));
+ } else {
+ Anchor anchor = aFsToAnchor.get(fsItem);
+ if (anchor == null) {
+ throw new IllegalStateException("No anchor - bug - should not happen");
+ }
+ items.add(anchor.toString());
+ }
+ continue nextItem;
+ }
+ }
+
+ return items.stream().collect(joining(",", "[", "]"));
+ }
+
+ // This method was derived from uimaFIT FSUtil.getFeature()
+ private List<Object> multiValuedFeatureStructureToList(FeatureStructure aValue) {
+
+ if (aValue == null) {
+ return null;
+ }
+
+ // Handle case where feature is an array
+ TypeSystem ts = aValue.getCAS().getTypeSystem();
+ Object target = null;
+ int length = -1;
+ if (aValue instanceof CommonArrayFS) {
+ CommonArrayFS<?> source = (CommonArrayFS<?>) aValue;
+ length = source.size();
+ if (aValue instanceof BooleanArrayFS) {
+ target = new boolean[length];
+ ((BooleanArrayFS) source).copyToArray(0, (boolean[]) target, 0, length);
+ } else if (aValue instanceof ByteArrayFS) {
+ target = new byte[length];
+ ((ByteArrayFS) source).copyToArray(0, (byte[]) target, 0, length);
+ } else if (aValue instanceof DoubleArrayFS) {
+ target = new double[length];
+ ((DoubleArrayFS) source).copyToArray(0, (double[]) target, 0, length);
+ } else if (aValue instanceof FloatArrayFS) {
+ target = new float[length];
+ ((FloatArrayFS) source).copyToArray(0, (float[]) target, 0, length);
+ } else if (aValue instanceof IntArrayFS) {
+ target = new int[length];
+ ((IntArrayFS) source).copyToArray(0, (int[]) target, 0, length);
+ } else if (aValue instanceof LongArrayFS) {
+ target = new long[length];
+ ((LongArrayFS) source).copyToArray(0, (long[]) target, 0, length);
+ } else if (aValue instanceof ShortArrayFS) {
+ target = new short[length];
+ ((ShortArrayFS) source).copyToArray(0, (short[]) target, 0, length);
+ } else if (aValue instanceof StringArrayFS) {
+ target = new String[length];
+ ((StringArrayFS) source).copyToArray(0, (String[]) target, 0, length);
+ } else {
+ target = new FeatureStructure[length];
+ ((ArrayFS<?>) source).copyToArray(0, (FeatureStructure[]) target, 0, length);
+ }
+ }
+ // Handle case where feature is a list
+ else if (ts.subsumes(ts.getType(CAS.TYPE_NAME_LIST_BASE), aValue.getType())) {
+ // Get length of list
+ length = 0;
+ {
+ FeatureStructure cur = aValue;
+ // We assume to by facing a non-empty element if it has a "head" feature
+ while (cur.getType().getFeatureByBaseName(CAS.FEATURE_BASE_NAME_HEAD) != null) {
+ length++;
+ cur = cur.getFeatureValue(cur.getType().getFeatureByBaseName(CAS.FEATURE_BASE_NAME_TAIL));
+ }
+ }
+
+ switch (aValue.getType().getName()) {
+ case CAS.TYPE_NAME_FLOAT_LIST: {
+ float[] floatTarget = new float[length];
+ int i = 0;
+ FeatureStructure cur = aValue;
+ // We assume to by facing a non-empty element if it has a "head" feature
+ while (cur.getType().getFeatureByBaseName(CAS.FEATURE_BASE_NAME_HEAD) != null) {
+ floatTarget[i] = cur
+ .getFloatValue(cur.getType().getFeatureByBaseName(CAS.FEATURE_BASE_NAME_HEAD));
+ cur = cur.getFeatureValue(
+ cur.getType().getFeatureByBaseName(CAS.FEATURE_BASE_NAME_TAIL));
+ }
+ target = floatTarget;
+ break;
+ }
+ case CAS.TYPE_NAME_INTEGER_LIST: {
+ int[] intTarget = new int[length];
+ int i = 0;
+ FeatureStructure cur = aValue;
+ // We assume to by facing a non-empty element if it has a "head" feature
+ while (cur.getType().getFeatureByBaseName(CAS.FEATURE_BASE_NAME_HEAD) != null) {
+ intTarget[i] = cur
+ .getIntValue(cur.getType().getFeatureByBaseName(CAS.FEATURE_BASE_NAME_HEAD));
+ cur = cur.getFeatureValue(
+ cur.getType().getFeatureByBaseName(CAS.FEATURE_BASE_NAME_TAIL));
+ }
+ target = intTarget;
+ break;
+ }
+ case CAS.TYPE_NAME_STRING_LIST: {
+ String[] stringTarget = new String[length];
+ int i = 0;
+ FeatureStructure cur = aValue;
+ // We assume to by facing a non-empty element if it has a "head" feature
+ while (cur.getType().getFeatureByBaseName(CAS.FEATURE_BASE_NAME_HEAD) != null) {
+ stringTarget[i] = cur
+ .getStringValue(cur.getType().getFeatureByBaseName(CAS.FEATURE_BASE_NAME_HEAD));
+ cur = cur.getFeatureValue(
+ cur.getType().getFeatureByBaseName(CAS.FEATURE_BASE_NAME_TAIL));
+ }
+ target = stringTarget;
+ break;
+ }
+ default: {
+ target = new FeatureStructure[length];
+ int i = 0;
+ FeatureStructure cur = aValue;
+ // We assume to by facing a non-empty element if it has a "head" feature
+ while (cur.getType().getFeatureByBaseName(CAS.FEATURE_BASE_NAME_HEAD) != null) {
+ Array.set(target, i, cur.getFeatureValue(
+ cur.getType().getFeatureByBaseName(CAS.FEATURE_BASE_NAME_HEAD)));
+ i++;
+ cur = cur.getFeatureValue(
+ cur.getType().getFeatureByBaseName(CAS.FEATURE_BASE_NAME_TAIL));
+ }
+ break;
+ }
+ }
+ }
+
+ if (length == -1) {
+ throw new IllegalStateException("Unable to extract values");
+ }
+
+ List<Object> targetCollection = new ArrayList<>();
+ for (int i = 0; i < length; i++) {
+ targetCollection.add(Array.get(target, i));
+ }
+
+ return targetCollection;
+ }
+
+ private Set<FeatureStructure> findReachableFeatureStructures(
+ Collection<? extends FeatureStructure> aSeeds) {
+
+ // Collect the seed points for the reachability tracking. We use a set here instead of a
+ // Deque because we use the contains() method on this queue and it is slow for typical
+ // Deques (e.g. LinkedList) but fast for sets.
+ Set<FeatureStructure> toProcess = new LinkedHashSet<>(aSeeds);
+
+ // Collect all feature structures that are reachable via the seed points
+ Set<FeatureStructure> seen = new HashSet<>();
+ while (!toProcess.isEmpty()) {
+ // Poll the next element from the processing queue
+ FeatureStructure fs = toProcess.iterator().next();
+ toProcess.remove(fs);
+
+ if (seen.contains(fs)) {
+ continue;
+ }
+
+ seen.add(fs);
+
+ if (isMultiValued(fs)) {
+ List<Object> values = multiValuedFeatureStructureToList(fs);
+ if (values != null) {
+ values.stream().filter(v -> v instanceof FeatureStructure).filter(v -> !seen.contains(v))
+ .forEach(v -> toProcess.add((FeatureStructure) v));
+ }
+
+ for (Feature feature : fs.getType().getFeatures()) {
+ if (feature.getRange().isPrimitive()) {
+ continue;
+ }
+
+ // Check if the feature is excluded
+ if (isExcluded(feature)) {
+ continue;
+ }
+
+ if (CAS.FEATURE_BASE_NAME_SOFA.equals(feature.getShortName())) {
+ continue;
+ }
+
+ if (isMultiValuedFeature(fs, feature)) {
+ List<Object> featureValues = multiValuedFeatureStructureToList(
+ fs.getFeatureValue(feature));
+
+ if (values != null) {
+ for (Object value : featureValues) {
+ if (value instanceof FeatureStructure && !seen.contains(value)) {
+ toProcess.add((FeatureStructure) value);
+ }
+ }
+ }
+ } else {
+ FeatureStructure value = fs.getFeatureValue(feature);
+ if (value != null && !seen.contains(value)) {
+ toProcess.add(value);
+ }
+ }
+ }
+ }
+ }
+
+ return seen;
+ }
+
+ private static class CloseShieldAppendable implements Appendable, Closeable {
+
+ private final Appendable delegate;
+
+ public CloseShieldAppendable(Appendable aDelegate) {
+
+ delegate = aDelegate;
+ }
+
+ @Override
+ public Appendable append(CharSequence aSequence) throws IOException {
+
+ return delegate.append(aSequence);
+ }
+
+ @Override
+ public Appendable append(CharSequence aSequence, int aStart, int aEnd) throws IOException {
+
+ return delegate.append(aSequence, aStart, aEnd);
+ }
+
+ @Override
+ public Appendable append(char aCharacter) throws IOException {
+
+ return delegate.append(aCharacter);
+ }
+
+ @Override
+ public void close() throws IOException {
+
+ // Do not forward close
+ }
+ }
+
+ private static class FSComparator implements Comparator<FeatureStructure> {
+
+ @Override
+ public int compare(FeatureStructure aFS1, FeatureStructure aFS2) {
+
+ // Same name?
+ int nameCmp = aFS2.getType().getName().compareTo(aFS2.getType().getName());
+ if (nameCmp != 0) {
+ return nameCmp;
+ }
+
+ // Annotation? Then sort by offsets
+ if (aFS1 instanceof AnnotationFS && aFS2 instanceof AnnotationFS) {
+ AnnotationFS ann1 = (AnnotationFS) aFS1;
+ AnnotationFS ann2 = (AnnotationFS) aFS2;
+
+ // Ascending by begin
+ int beginCmp = ann1.getBegin() - ann2.getBegin();
+ if (beginCmp != 0) {
+ return beginCmp;
+ }
+
+ // Descending by end
+ int endCmp = ann1.getEnd() - ann2.getEnd();
+ if (endCmp != 0) {
+ return endCmp;
+ }
+ }
+
+ // Giving up here. Hopefully the caller has a good idea to further sort FSes which
+ // have the same type and location
+ return 0;
+ }
+ }
+
+ private class Anchor {
+
+ private final String stringValue;
+ private final int disambiguationId;
+
+ public Anchor(FeatureStructure aFS, boolean aIndexed,
+ Map<String, Integer> aDisambiguationByPrefix) {
+
+ StringBuilder anchor = new StringBuilder();
+
+ anchor.append(aFS.getType().getShortName());
+
+ // Special handling for AnnotationFS
+ if (aFS instanceof AnnotationFS) {
+ AnnotationFS ann = (AnnotationFS) aFS;
+ anchor.append("[");
+ anchor.append(ann.getBegin());
+ anchor.append("-");
+ anchor.append(ann.getEnd());
+ anchor.append("]");
+ }
+
+ if (markIndexed && aIndexed) {
+ anchor.append("*");
+ }
+
+ // Special handling for AnnotationBase
+ if (markView && aFS instanceof AnnotationBase) {
+ AnnotationBase annBase = (AnnotationBase) aFS;
+ anchor.append('@');
+ anchor.append(String.valueOf(annBase.getSofa().getSofaID()));
+ }
+
+ // If we have the same anchor multiple times, then we need to disambiguate
+ String prefix = anchor.toString();
+ disambiguationId = aDisambiguationByPrefix.computeIfAbsent(prefix, key -> 0);
+
+ if (uniqueAnchors && disambiguationId > 0) {
+ anchor.append("(");
+ anchor.append(disambiguationId);
+ anchor.append(")");
+ }
+
+ aDisambiguationByPrefix.put(prefix, disambiguationId + 1);
+
+ stringValue = anchor.toString();
+ }
+
+ @Override
+ public String toString() {
+
+ return stringValue;
+ }
+ }
+
+ public static String toComparableString(CAS aCas) {
+ try (StringWriter sourceCasRepresentationBuffer = new StringWriter()) {
+ new CasToComparableText(aCas).write(sourceCasRepresentationBuffer);
+ return sourceCasRepresentationBuffer.toString();
+ } catch (IOException e) {
+ // This should normally never happen, so it should be ok to not throw a checked exception here
+ throw new IllegalStateException("Unable to serialize CAS", e);
+ }
+ }
+}
diff --git a/uimaj-core/src/test/java/org/apache/uima/cas/serdes/SerDesCasIOUtils.java b/uimaj-core/src/test/java/org/apache/uima/cas/serdes/SerDesCasIOUtils.java
new file mode 100644
index 0000000..a91cfa6
--- /dev/null
+++ b/uimaj-core/src/test/java/org/apache/uima/cas/serdes/SerDesCasIOUtils.java
@@ -0,0 +1,90 @@
+/*
+ * 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.cas.serdes;
+
+import static java.util.Arrays.asList;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import org.apache.commons.lang3.NotImplementedException;
+import org.apache.uima.cas.CAS;
+import org.apache.uima.cas.SerialFormat;
+import org.apache.uima.util.CasIOUtils;
+import org.apache.uima.util.CasLoadMode;
+
+public class SerDesCasIOUtils {
+ public static void ser(CAS aSourceCas, Path aTargetCasFile, SerialFormat aFormat)
+ throws Exception {
+ try (OutputStream casTarget = Files.newOutputStream(aTargetCasFile)) {
+ CasIOUtils.save(aSourceCas, casTarget, aFormat);
+ }
+ }
+
+ public static void desser(CAS aBufferCas, Path aSourceCasPath, Path aTargetCasPath,
+ SerialFormat aFormat, CasLoadMode aMode, CasLoadOptions... aOptions) throws Exception {
+ // Deserialize the file into the buffer CAS
+ try (InputStream casSource = Files.newInputStream(aSourceCasPath)) {
+ if (asList(aOptions).contains(CasLoadOptions.WITH_TSI)) {
+ throw new NotImplementedException("Not implemented yet...");
+ // try (InputStream tsiSource = Files.newInputStream(aSourceCasPath)) {
+ // CasIOUtils.load(casSource, tsiSource, aBufferCas, aMode);
+ // }
+ } else {
+ CasIOUtils.load(casSource, null, aBufferCas, aMode);
+ }
+ }
+
+ // Serialize the buffer CAS to the target file
+ try (OutputStream casTarget = Files.newOutputStream(aTargetCasPath)) {
+ CasIOUtils.save(aBufferCas, casTarget, aFormat);
+ }
+ }
+
+ public static void serdes(CAS aSourceCas, CAS aTargetCas, SerialFormat aFormat, CasLoadMode aMode,
+ CasLoadOptions... aOptions) throws Exception {
+ // Serialize the CAS
+ byte[] casBuffer;
+ byte[] tsiBuffer;
+ try (ByteArrayOutputStream casTarget = new ByteArrayOutputStream();
+ ByteArrayOutputStream tsiTarget = new ByteArrayOutputStream()) {
+ CasIOUtils.save(aSourceCas, casTarget, tsiTarget, aFormat);
+ casBuffer = casTarget.toByteArray();
+ tsiBuffer = tsiTarget.toByteArray();
+ }
+
+ // Deserialize the CAS
+ try (ByteArrayInputStream casSource = new ByteArrayInputStream(casBuffer);
+ ByteArrayInputStream tsiSource = new ByteArrayInputStream(tsiBuffer)) {
+ if (asList(aOptions).contains(CasLoadOptions.WITH_TSI)) {
+ CasIOUtils.load(casSource, tsiSource, aTargetCas, aMode);
+ } else {
+ CasIOUtils.load(casSource, null, aTargetCas, aMode);
+ }
+ }
+ }
+
+ enum CasLoadOptions {
+ WITH_TSI
+ }
+}
diff --git a/uimaj-core/src/test/java/org/apache/uima/cas/serdes/datasuites/RandomCasDataSuite.java b/uimaj-core/src/test/java/org/apache/uima/cas/serdes/datasuites/RandomCasDataSuite.java
new file mode 100644
index 0000000..487a235
--- /dev/null
+++ b/uimaj-core/src/test/java/org/apache/uima/cas/serdes/datasuites/RandomCasDataSuite.java
@@ -0,0 +1,57 @@
+/*
+ * 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.cas.serdes.datasuites;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.uima.cas.CAS;
+import org.apache.uima.cas.serdes.generators.RandomCasGenerator;
+import org.apache.uima.cas.serdes.transitions.CasSourceTargetConfiguration;
+import org.apache.uima.resource.metadata.TypeSystemDescription;
+import org.apache.uima.util.CasCreationUtils;
+
+public class RandomCasDataSuite {
+
+ public static List<CasSourceTargetConfiguration> configurations(int aCount) {
+ List<CasSourceTargetConfiguration> confs = new ArrayList<>();
+
+ for (int n = 0; n < aCount; n++) {
+ confs.add(CasSourceTargetConfiguration.builder() //
+ .withTitle("randomCas-" + (n + 1)) //
+ .withSourceCasSupplier(() -> randomCas(aCount + 1, (aCount + 1) * 10)) //
+ .withTargetCasSupplier(CasCreationUtils::createCas) //
+ .build());
+ }
+
+ return confs;
+ }
+
+ public static CAS emptyCas() throws Exception {
+ return CasCreationUtils.createCas();
+ }
+
+ public static CAS randomCas(int aTypeCount, int aAnnotationCount) throws Exception {
+ RandomCasGenerator randomizer = new RandomCasGenerator().typeCount(aTypeCount)
+ .minimumAnnotationLength(0).annotationsToGenerate(aAnnotationCount);
+
+ TypeSystemDescription tsd = randomizer.generateRandomTypeSystem();
+ return randomizer.generateRandomCas(tsd);
+ }
+}
diff --git a/uimaj-core/src/test/java/org/apache/uima/cas/serdes/datasuites/TestSuiteData_CasBornInMemory.java b/uimaj-core/src/test/java/org/apache/uima/cas/serdes/datasuites/TestSuiteData_CasBornInMemory.java
new file mode 100644
index 0000000..6f196d8
--- /dev/null
+++ b/uimaj-core/src/test/java/org/apache/uima/cas/serdes/datasuites/TestSuiteData_CasBornInMemory.java
@@ -0,0 +1,65 @@
+/*
+ * 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.cas.serdes.datasuites;
+
+import static java.util.Arrays.asList;
+
+import java.util.List;
+
+import org.apache.uima.cas.CAS;
+import org.apache.uima.cas.serdes.transitions.CasSourceTargetConfiguration;
+import org.apache.uima.jcas.tcas.Annotation;
+import org.apache.uima.util.CasCreationUtils;
+
+public class TestSuiteData_CasBornInMemory {
+
+ public static List<CasSourceTargetConfiguration> configurations() {
+ return asList( //
+ CasSourceTargetConfiguration.builder() //
+ .withTitle("casWithText") //
+ .withSourceCasSupplier(TestSuiteData_CasBornInMemory::casWithText) //
+ .withTargetCasSupplier(CasCreationUtils::createCas) //
+ .build(),
+ CasSourceTargetConfiguration.builder() //
+ .withTitle("casWithTextAndAnnotation") //
+ .withSourceCasSupplier(TestSuiteData_CasBornInMemory::casWithTextAndAnnotation)
+ .withTargetCasSupplier(CasCreationUtils::createCas) //
+ .build());
+ }
+
+ public static CAS emptyCas() throws Exception {
+ return CasCreationUtils.createCas();
+ }
+
+ public static CAS casWithText() throws Exception {
+ CAS cas = CasCreationUtils.createCas();
+ cas.setDocumentText("This is a test.");
+ return cas;
+ }
+
+ public static CAS casWithTextAndAnnotation() throws Exception {
+ CAS cas = CasCreationUtils.createCas();
+ cas.setDocumentText("This is a test.");
+
+ Annotation a = new Annotation(cas.getJCas(), 0, cas.getDocumentText().length());
+ a.addToIndexes();
+
+ return cas;
+ }
+}
diff --git a/uimaj-core/src/test/java/org/apache/uima/cas/serdes/datasuites/TestSuiteData_XmiFiles.java b/uimaj-core/src/test/java/org/apache/uima/cas/serdes/datasuites/TestSuiteData_XmiFiles.java
new file mode 100644
index 0000000..e6020a2
--- /dev/null
+++ b/uimaj-core/src/test/java/org/apache/uima/cas/serdes/datasuites/TestSuiteData_XmiFiles.java
@@ -0,0 +1,101 @@
+/*
+ * 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.cas.serdes.datasuites;
+
+import static java.nio.file.Files.isDirectory;
+import static java.nio.file.Files.newInputStream;
+import static org.apache.uima.UIMAFramework.getXMLParser;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Stream;
+
+import org.apache.uima.cas.CAS;
+import org.apache.uima.cas.serdes.transitions.CasSourceTargetConfiguration;
+import org.apache.uima.resource.ResourceInitializationException;
+import org.apache.uima.resource.metadata.TypeSystemDescription;
+import org.apache.uima.util.CasCreationUtils;
+import org.apache.uima.util.CasIOUtils;
+import org.apache.uima.util.InvalidXMLException;
+import org.apache.uima.util.XMLInputSource;
+
+public class TestSuiteData_XmiFiles {
+ public static final String TYPESYSTEM_XML = "typesystem.xml";
+ public static final String DATA_XMI = "data.xmi";
+
+ private static final Path XMI_SUITE_BASE_PATH = Paths.get("src", "test", "resources",
+ "CasSerializationDeserialization_XMI_TestSuiteData");
+
+ public static List<CasSourceTargetConfiguration> configurations() throws IOException {
+ List<CasSourceTargetConfiguration> confs = new ArrayList<>();
+
+ try (Stream<Path> fileStream = Files.list(XMI_SUITE_BASE_PATH)
+ .filter(p -> isDirectory(p) && !p.toFile().isHidden())) {
+
+ fileStream.forEach(testSuiteFolder -> confs.add(buildConfiguration(testSuiteFolder)));
+ }
+
+ return confs;
+ }
+
+ private static CasSourceTargetConfiguration buildConfiguration(Path aTestSuiteFolder) {
+ try {
+ Path casFile = aTestSuiteFolder.resolve(DATA_XMI);
+ Path tsFile = aTestSuiteFolder.resolve(TYPESYSTEM_XML);
+
+ TypeSystemDescription typeSystemDescription = loadTypeSystem(tsFile);
+
+ return CasSourceTargetConfiguration.builder() //
+ .withTitle(aTestSuiteFolder.getFileName().toString()) //
+ .withSourceCasSupplier(() -> loadXmi(casFile, typeSystemDescription))
+ .withTargetCasSupplier(() -> createCas(typeSystemDescription)) //
+ .build();
+ } catch (InvalidXMLException | IOException e) {
+ throw new Error("Unable to build test configuration for " + aTestSuiteFolder, e);
+ }
+ }
+
+ private static CAS loadXmi(Path aXmiFile, TypeSystemDescription aTsd)
+ throws IOException, InvalidXMLException, ResourceInitializationException {
+ CAS cas = createCas(aTsd);
+
+ try (InputStream is = newInputStream(aXmiFile)) {
+ CasIOUtils.load(is, cas);
+ }
+
+ return cas;
+ }
+
+ private static CAS createCas(TypeSystemDescription aTsd)
+ throws InvalidXMLException, IOException, ResourceInitializationException {
+ return CasCreationUtils.createCas(aTsd, null, null, null);
+ }
+
+ private static TypeSystemDescription loadTypeSystem(Path aTypeSystemFile)
+ throws IOException, InvalidXMLException {
+ try (InputStream is = newInputStream(aTypeSystemFile)) {
+ return getXMLParser().parseTypeSystemDescription(new XMLInputSource(is, null));
+ }
+ }
+}
diff --git a/uimaj-core/src/test/java/org/apache/uima/cas/serdes/generators/RandomCasGenerator.java b/uimaj-core/src/test/java/org/apache/uima/cas/serdes/generators/RandomCasGenerator.java
new file mode 100644
index 0000000..20931d9
--- /dev/null
+++ b/uimaj-core/src/test/java/org/apache/uima/cas/serdes/generators/RandomCasGenerator.java
@@ -0,0 +1,139 @@
+/*
+ * 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.cas.serdes.generators;
+
+import static java.lang.System.identityHashCode;
+import static org.apache.uima.UIMAFramework.getResourceSpecifierFactory;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+
+import org.apache.uima.cas.CAS;
+import org.apache.uima.cas.Type;
+import org.apache.uima.cas.text.AnnotationFS;
+import org.apache.uima.resource.ResourceInitializationException;
+import org.apache.uima.resource.metadata.TypeDescription;
+import org.apache.uima.resource.metadata.TypeSystemDescription;
+import org.apache.uima.util.CasCreationUtils;
+
+public class RandomCasGenerator {
+ private Random rnd = new Random();
+ private int size;
+ private int minimumWidth;
+ private boolean writeLog = false;
+ private int typeCount;
+
+ public RandomCasGenerator randomSeed(long aRandomSeed) {
+ rnd = new Random(aRandomSeed);
+ return this;
+ }
+
+ public RandomCasGenerator randomGenerator(Random aRandom) {
+ rnd = aRandom;
+ return this;
+ }
+
+ public RandomCasGenerator typeCount(int aTypeCount) {
+ typeCount = aTypeCount;
+ return this;
+ }
+
+ public RandomCasGenerator annotationsToGenerate(int aSize) {
+ size = aSize;
+ return this;
+ }
+
+ public RandomCasGenerator minimumAnnotationLength(int aMinimumWidth) {
+ minimumWidth = aMinimumWidth;
+ return this;
+ }
+
+ public RandomCasGenerator logAnnotationCreation(boolean aLogAnnotationCreation) {
+ writeLog = aLogAnnotationCreation;
+ return this;
+ }
+
+ public TypeSystemDescription generateRandomTypeSystem() {
+ TypeSystemDescription tsd = getResourceSpecifierFactory().createTypeSystemDescription();
+ Map<String, Type> types = new LinkedHashMap<>();
+
+ if (writeLog) {
+ System.out.println();
+ }
+
+ for (int ti = 0; ti < typeCount; ti++) {
+ String typeName = "test.Type" + (ti + 1);
+ TypeDescription newType;
+ if (rnd.nextInt() % 2 == 0 || types.size() == 0) {
+ newType = tsd.addType(typeName, "", CAS.TYPE_NAME_ANNOTATION);
+ } else {
+ newType = tsd.addType(typeName, "",
+ new ArrayList<>(types.keySet()).get(rnd.nextInt(types.size())));
+ }
+
+ if (writeLog) {
+ System.out.printf("tsd.addType(\"%s\", \"\", \"%s\");%n", newType.getName(),
+ newType.getSupertypeName());
+ }
+
+ types.put(typeName, null);
+ }
+
+ return tsd;
+ }
+
+ public CAS generateRandomCas(TypeSystemDescription aTsd) throws ResourceInitializationException {
+ CAS cas = CasCreationUtils.createCas(aTsd, null, null, null);
+ cas.reset();
+
+ List<Type> types = new ArrayList<>();
+ for (TypeDescription td : aTsd.getTypes()) {
+ types.add(cas.getTypeSystem().getType(td.getName()));
+ }
+
+ // Shuffle the types
+ for (int n = 0; n < 10; n++) {
+ Type t = types.remove(rnd.nextInt(types.size()));
+ types.add(t);
+ }
+
+ if (writeLog) {
+ System.out.println();
+ }
+
+ // Randomly generate annotations
+ for (int n = 0; n < size; n++) {
+ for (Type t : types) {
+ int begin = rnd.nextInt(100);
+ int end = begin + rnd.nextInt(30) + minimumWidth;
+ AnnotationFS ann = cas.createAnnotation(t, begin, end);
+ if (writeLog) {
+ System.out.printf("cas.createAnnotation(%s, %d, %d)\t[%d]%n",
+ t.getShortName().toLowerCase(), begin, end, identityHashCode(ann));
+ }
+ cas.addFsToIndexes(ann);
+ }
+ }
+
+ return cas;
+ }
+}
diff --git a/uimaj-core/src/test/java/org/apache/uima/cas/serdes/scenario/DesSerTestScenario.java b/uimaj-core/src/test/java/org/apache/uima/cas/serdes/scenario/DesSerTestScenario.java
new file mode 100644
index 0000000..ddc4783
--- /dev/null
+++ b/uimaj-core/src/test/java/org/apache/uima/cas/serdes/scenario/DesSerTestScenario.java
@@ -0,0 +1,146 @@
+/*
+ * 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.cas.serdes.scenario;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.contentOf;
+
+import java.nio.file.Path;
+
+import javax.annotation.Generated;
+
+import org.apache.commons.lang3.function.FailableBiConsumer;
+import org.assertj.core.internal.Failures;
+
+public class DesSerTestScenario implements Runnable {
+ private final String title;
+ private final Path sourceCasFile;
+ private final Path referenceCasFile;
+ private final Path targetBasePath;
+ private final FailableBiConsumer<Path, Path, ?> cycle;
+
+ @Generated("SparkTools")
+ private DesSerTestScenario(Builder builder) {
+ this.title = builder.title;
+ this.sourceCasFile = builder.sourceCasFile;
+ this.referenceCasFile = builder.referenceCasFile;
+ this.targetBasePath = builder.targetBasePath;
+ this.cycle = builder.cycle;
+ }
+
+ public Path getSourceCasFile() {
+ return sourceCasFile;
+ }
+
+ public Path getReferenceCasFile() {
+ return referenceCasFile;
+ }
+
+ @Override
+ public void run() {
+ Path targetCasFolder = targetBasePath.resolve(getTitle());
+ targetCasFolder.toFile().mkdirs();
+ Path targetCasFile = targetCasFolder.resolve(referenceCasFile.getFileName().toString());
+
+ deserializationSerializationCycle(getSourceCasFile(), targetCasFile);
+
+ // Compare the serialized CAS file against the reference
+ assertThat(contentOf(targetCasFile.toFile())) //
+ .isEqualTo(contentOf(referenceCasFile.toFile()));
+ }
+
+ public void deserializationSerializationCycle(Path aSourceCas, Path aTargetCas) {
+ try {
+ cycle.accept(aSourceCas, aTargetCas);
+ } catch (Throwable e) {
+ AssertionError error = Failures.instance().failure("Unable to (de)serialize CAS");
+ error.initCause(e);
+ throw error;
+ }
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ @Override
+ public String toString() {
+ return title;
+ }
+
+ /**
+ * Creates builder to build {@link DesSerTestScenario}.
+ *
+ * @return created builder
+ */
+ @Generated("SparkTools")
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * Builder to build {@link DesSerTestScenario}.
+ */
+ @Generated("SparkTools")
+ public static final class Builder {
+ private String title;
+ private Path sourceCasFile;
+ private Path referenceCasFile;
+ private Path targetBasePath;
+ private FailableBiConsumer<Path, Path, ?> cycle;
+
+ private Builder() {
+ }
+
+ public Builder withTitle(String title) {
+ this.title = title;
+ return this;
+ }
+
+ public Builder withCasFile(Path sourceCasFile) {
+ this.sourceCasFile = sourceCasFile;
+ this.referenceCasFile = sourceCasFile;
+ return this;
+ }
+
+ public Builder withSourceCasFile(Path sourceCasFile) {
+ this.sourceCasFile = sourceCasFile;
+ return this;
+ }
+
+ public Builder withReferenceCasFile(Path referenceCasFile) {
+ this.referenceCasFile = referenceCasFile;
+ return this;
+ }
+
+ public Builder withTargetBasePath(Path targetBasePath) {
+ this.targetBasePath = targetBasePath;
+ return this;
+ }
+
+ public Builder withCycle(FailableBiConsumer<Path, Path, ?> cycle) {
+ this.cycle = cycle;
+ return this;
+ }
+
+ public DesSerTestScenario build() {
+ return new DesSerTestScenario(this);
+ }
+ }
+}
diff --git a/uimaj-core/src/test/java/org/apache/uima/cas/serdes/scenario/SerDesTestScenario.java b/uimaj-core/src/test/java/org/apache/uima/cas/serdes/scenario/SerDesTestScenario.java
new file mode 100644
index 0000000..84e4912
--- /dev/null
+++ b/uimaj-core/src/test/java/org/apache/uima/cas/serdes/scenario/SerDesTestScenario.java
@@ -0,0 +1,109 @@
+/*
+ * 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.cas.serdes.scenario;
+
+import static java.util.Comparator.comparing;
+import static org.apache.uima.cas.serdes.CasToComparableText.toComparableString;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.apache.commons.lang3.function.FailableBiConsumer;
+import org.apache.commons.lang3.function.FailableSupplier;
+import org.apache.uima.cas.CAS;
+import org.apache.uima.cas.impl.CASImpl;
+import org.apache.uima.cas.impl.CasCompare;
+import org.apache.uima.cas.serdes.transitions.CasSerDesCycleConfiguration;
+import org.apache.uima.cas.serdes.transitions.CasSourceTargetConfiguration;
+import org.assertj.core.internal.Failures;
+
+public class SerDesTestScenario implements Runnable {
+ private final String title;
+ private final FailableSupplier<CAS, ?> sourceCasSupplier;
+ private final FailableSupplier<CAS, ?> targetCasSupplier;
+ private final FailableBiConsumer<CAS, CAS, ?> cycle;
+
+ public SerDesTestScenario(CasSourceTargetConfiguration aSourceTargetConfig,
+ CasSerDesCycleConfiguration aCycle) {
+ title = aCycle.getTitle() + " - " + aSourceTargetConfig.getTitle();
+ sourceCasSupplier = aSourceTargetConfig::createSourceCas;
+ targetCasSupplier = aSourceTargetConfig::createTargetCas;
+ cycle = aCycle::performCycle;
+ }
+
+ public SerDesTestScenario(String aTitle, FailableSupplier<CAS, ?> aSourceCasSupplier,
+ FailableSupplier<CAS, ?> aTargetCasSupplier, FailableBiConsumer<CAS, CAS, ?> aCycle) {
+ title = aTitle;
+ sourceCasSupplier = aSourceCasSupplier;
+ targetCasSupplier = aTargetCasSupplier;
+ cycle = aCycle;
+ }
+
+ public CAS createSourceCas() {
+ try {
+ return sourceCasSupplier.get();
+ } catch (Throwable e) {
+ AssertionError error = Failures.instance().failure("Unable to create source CAS");
+ error.initCause(e);
+ throw error;
+ }
+ }
+
+ public CAS createTargetCas() {
+ try {
+ return targetCasSupplier.get();
+ } catch (Throwable e) {
+ AssertionError error = Failures.instance().failure("Unable to create target CAS");
+ error.initCause(e);
+ throw error;
+ }
+ }
+
+ @Override
+ public void run() {
+ // Fetch the source and target CASes
+ CAS sourceCas = createSourceCas();
+ CAS targetCas = createTargetCas();
+
+ // Perform a serialization/de-serialization cycle
+ serializationDeserializationCycle(sourceCas, targetCas);
+
+ // Compare the de-serialized CAS against the original CAS
+ assertThat(targetCas) //
+ .usingComparator(comparing(cas -> toComparableString(cas))) //
+ .isEqualTo(sourceCas);
+
+ assertThat(targetCas) //
+ .usingComparator((a, b) -> CasCompare.compareCASes((CASImpl) a, (CASImpl) b) ? 0 : 1) //
+ .isEqualTo(sourceCas);
+ }
+
+ public void serializationDeserializationCycle(CAS aSourceCas, CAS aTargetCas) {
+ try {
+ cycle.accept(aSourceCas, aTargetCas);
+ } catch (Throwable e) {
+ AssertionError error = Failures.instance().failure("Unable to (de)serialize CAS");
+ error.initCause(e);
+ throw error;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return title;
+ }
+}
\ No newline at end of file
diff --git a/uimaj-core/src/test/java/org/apache/uima/cas/serdes/scenario/SerRefTestScenario.java b/uimaj-core/src/test/java/org/apache/uima/cas/serdes/scenario/SerRefTestScenario.java
new file mode 100644
index 0000000..bfa6aed
--- /dev/null
+++ b/uimaj-core/src/test/java/org/apache/uima/cas/serdes/scenario/SerRefTestScenario.java
@@ -0,0 +1,183 @@
+/*
+ * 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.cas.serdes.scenario;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.contentOf;
+
+import java.nio.file.Path;
+
+import javax.annotation.Generated;
+
+import org.apache.commons.lang3.function.FailableBiConsumer;
+import org.apache.commons.lang3.function.FailableSupplier;
+import org.apache.uima.cas.CAS;
+import org.apache.uima.cas.serdes.transitions.CasSourceTargetConfiguration;
+import org.assertj.core.internal.Failures;
+
+public class SerRefTestScenario implements Runnable {
+ private final String title;
+ private final FailableSupplier<CAS, ?> sourceCasSupplier;
+ private final Path referenceCasFile;
+ private final Path targetCasFile;
+ private final FailableBiConsumer<CAS, Path, ?> serializer;
+
+ @Generated("SparkTools")
+ private SerRefTestScenario(Builder builder) {
+ this.title = builder.title;
+ this.sourceCasSupplier = builder.sourceCasSupplier;
+ this.referenceCasFile = builder.referenceCasFile;
+ this.targetCasFile = builder.targetCasFile;
+ this.serializer = builder.serializer;
+ }
+
+ public SerRefTestScenario(Path aReferenceBasePath, Path aTargetBasePath,
+ CasSourceTargetConfiguration aSourceTargetConfiguration, String aTargetFileName,
+ FailableBiConsumer<CAS, Path, ?> aSerializer) {
+ title = aSourceTargetConfiguration.getTitle();
+ sourceCasSupplier = aSourceTargetConfiguration::createSourceCas;
+ referenceCasFile = aReferenceBasePath.resolve(title).resolve(aTargetFileName);
+ targetCasFile = aTargetBasePath.resolve(title).resolve(aTargetFileName);
+ serializer = aSerializer;
+ }
+
+ // public SerRefTestScenario(Path aReferenceBasePath, Path aTargetBasePath, String aTitle,
+ // FailableSupplier<CAS, ?> aSourceCasSupplier, String aTargetFileName,
+ // FailableBiConsumer<CAS, Path, ?> aSerializer) {
+ // title = aTitle;
+ // sourceCasSupplier = aSourceCasSupplier;
+ // referenceCasFile = aReferenceBasePath.resolve(title).resolve(aTargetFileName);
+ // targetCasFile = aTargetBasePath.resolve(title).resolve(aTargetFileName);
+ // serializer = aSerializer;
+ // }
+
+ // public SerRefTestScenario(String aTitle, FailableSupplier<CAS, ?> aSourceCasSupplier,
+ // Path aReferenceCasFile, Path aTargetCasFile,
+ // FailableBiConsumer<CAS, Path, ?> aSerializer) {
+ // title = aTitle;
+ // sourceCasSupplier = aSourceCasSupplier;
+ // referenceCasFile = aReferenceCasFile;
+ // targetCasFile = aTargetCasFile;
+ // serializer = aSerializer;
+ // }
+
+ public CAS createSourceCas() {
+ try {
+ return sourceCasSupplier.get();
+ } catch (Throwable e) {
+ AssertionError error = Failures.instance().failure("Unable to create source CAS");
+ error.initCause(e);
+ throw error;
+ }
+ }
+
+ public Path getReferenceCasFile() {
+ return referenceCasFile;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ @Override
+ public void run() {
+ // Fetch the source and target CASes
+ CAS sourceCas = createSourceCas();
+
+ // Serialize CAS to target file
+ serialize(sourceCas, targetCasFile);
+
+ // Compare the serialized CAS file against the reference
+ assertThat(referenceCasFile.toFile()) //
+ .as("Reference file must exists at %s", referenceCasFile) //
+ .exists();
+ assertThat(contentOf(targetCasFile.toFile())) //
+ .isEqualTo(contentOf(referenceCasFile.toFile()));
+ }
+
+ private void serialize(CAS aSourceCas, Path aTargetCasFile) {
+ try {
+ aTargetCasFile.getParent().toFile().mkdirs();
+ serializer.accept(aSourceCas, aTargetCasFile);
+ } catch (Throwable e) {
+ AssertionError error = Failures.instance().failure("Unable to serialize CAS");
+ error.initCause(e);
+ throw error;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return title;
+ }
+
+ /**
+ * Creates builder to build {@link SerRefTestScenario}.
+ *
+ * @return created builder
+ */
+ @Generated("SparkTools")
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * Builder to build {@link SerRefTestScenario}.
+ */
+ @Generated("SparkTools")
+ public static final class Builder {
+ private String title;
+ private FailableSupplier<CAS, ?> sourceCasSupplier;
+ private Path referenceCasFile;
+ private Path targetCasFile;
+ private FailableBiConsumer<CAS, Path, ?> serializer;
+
+ private Builder() {
+ }
+
+ public Builder withTitle(String title) {
+ this.title = title;
+ return this;
+ }
+
+ public Builder withSourceCasSupplier(FailableSupplier<CAS, ?> sourceCasSupplier) {
+ this.sourceCasSupplier = sourceCasSupplier;
+ return this;
+ }
+
+ public Builder withReferenceCasFile(Path referenceCasFile) {
+ this.referenceCasFile = referenceCasFile;
+ return this;
+ }
+
+ public Builder withTargetCasFile(Path targetCasFile) {
+ this.targetCasFile = targetCasFile;
+ return this;
+ }
+
+ public Builder withSerializer(FailableBiConsumer<CAS, Path, ?> serializer) {
+ this.serializer = serializer;
+ return this;
+ }
+
+ public SerRefTestScenario build() {
+ return new SerRefTestScenario(this);
+ }
+ }
+}
diff --git a/uimaj-core/src/test/java/org/apache/uima/cas/serdes/transitions/CasDesSerCycleConfiguration.java b/uimaj-core/src/test/java/org/apache/uima/cas/serdes/transitions/CasDesSerCycleConfiguration.java
new file mode 100644
index 0000000..54c47d5
--- /dev/null
+++ b/uimaj-core/src/test/java/org/apache/uima/cas/serdes/transitions/CasDesSerCycleConfiguration.java
@@ -0,0 +1,48 @@
+/*
+ * 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.cas.serdes.transitions;
+
+import java.nio.file.Path;
+
+import org.apache.commons.lang3.function.FailableBiConsumer;
+import org.assertj.core.internal.Failures;
+
+public class CasDesSerCycleConfiguration {
+ private final String title;
+ private final FailableBiConsumer<Path, Path, ?> cycle;
+
+ public CasDesSerCycleConfiguration(String aTitle, FailableBiConsumer<Path, Path, ?> aCycle) {
+ title = aTitle;
+ cycle = aCycle;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void performCycle(Path aSourceFile, Path aTargetFile) {
+ try {
+ cycle.accept(aSourceFile, aTargetFile);
+ } catch (Throwable e) {
+ AssertionError error = Failures.instance().failure("Unable to (de)serialize CAS");
+ error.initCause(e);
+ throw error;
+ }
+ }
+}
\ No newline at end of file
diff --git a/uimaj-core/src/test/java/org/apache/uima/cas/serdes/transitions/CasSerDesCycleConfiguration.java b/uimaj-core/src/test/java/org/apache/uima/cas/serdes/transitions/CasSerDesCycleConfiguration.java
new file mode 100644
index 0000000..5ed6cee
--- /dev/null
+++ b/uimaj-core/src/test/java/org/apache/uima/cas/serdes/transitions/CasSerDesCycleConfiguration.java
@@ -0,0 +1,47 @@
+/*
+ * 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.cas.serdes.transitions;
+
+import org.apache.commons.lang3.function.FailableBiConsumer;
+import org.apache.uima.cas.CAS;
+import org.assertj.core.internal.Failures;
+
+public class CasSerDesCycleConfiguration {
+ private final String title;
+ private final FailableBiConsumer<CAS, CAS, ?> cycle;
+
+ public CasSerDesCycleConfiguration(String aTitle, FailableBiConsumer<CAS, CAS, ?> aCycle) {
+ title = aTitle;
+ cycle = aCycle;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void performCycle(CAS aSourceCas, CAS aTargetCas) {
+ try {
+ cycle.accept(aSourceCas, aTargetCas);
+ } catch (Throwable e) {
+ AssertionError error = Failures.instance().failure("Unable to (de)serialize CAS");
+ error.initCause(e);
+ throw error;
+ }
+ }
+}
\ No newline at end of file
diff --git a/uimaj-core/src/test/java/org/apache/uima/cas/serdes/transitions/CasSourceTargetConfiguration.java b/uimaj-core/src/test/java/org/apache/uima/cas/serdes/transitions/CasSourceTargetConfiguration.java
new file mode 100644
index 0000000..1f1f011
--- /dev/null
+++ b/uimaj-core/src/test/java/org/apache/uima/cas/serdes/transitions/CasSourceTargetConfiguration.java
@@ -0,0 +1,104 @@
+/*
+ * 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.cas.serdes.transitions;
+
+import javax.annotation.Generated;
+
+import org.apache.commons.lang3.function.FailableSupplier;
+import org.apache.uima.cas.CAS;
+import org.assertj.core.internal.Failures;
+
+public class CasSourceTargetConfiguration {
+ private final String title;
+ private final FailableSupplier<CAS, ?> sourceCasSupplier;
+ private final FailableSupplier<CAS, ?> targetCasSupplier;
+
+ @Generated("SparkTools")
+ private CasSourceTargetConfiguration(Builder builder) {
+ this.title = builder.title;
+ this.sourceCasSupplier = builder.sourceCasSupplier;
+ this.targetCasSupplier = builder.targetCasSupplier;
+ }
+
+ public CAS createSourceCas() {
+ try {
+ return sourceCasSupplier.get();
+ } catch (Throwable e) {
+ AssertionError error = Failures.instance().failure("Unable to create source CAS");
+ error.initCause(e);
+ throw error;
+ }
+ }
+
+ public CAS createTargetCas() {
+ try {
+ return targetCasSupplier.get();
+ } catch (Throwable e) {
+ AssertionError error = Failures.instance().failure("Unable to create target CAS");
+ error.initCause(e);
+ throw error;
+ }
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ /**
+ * Creates builder to build {@link CasSourceTargetConfiguration}.
+ *
+ * @return created builder
+ */
+ @Generated("SparkTools")
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * Builder to build {@link CasSourceTargetConfiguration}.
+ */
+ @Generated("SparkTools")
+ public static final class Builder {
+ private String title;
+ private FailableSupplier<CAS, ?> sourceCasSupplier;
+ private FailableSupplier<CAS, ?> targetCasSupplier;
+
+ private Builder() {
+ }
+
+ public Builder withTitle(String aTitle) {
+ title = aTitle;
+ return this;
+ }
+
+ public Builder withSourceCasSupplier(FailableSupplier<CAS, ?> aSourceCasSupplier) {
+ sourceCasSupplier = aSourceCasSupplier;
+ return this;
+ }
+
+ public Builder withTargetCasSupplier(FailableSupplier<CAS, ?> aTargetCasSupplier) {
+ targetCasSupplier = aTargetCasSupplier;
+ return this;
+ }
+
+ public CasSourceTargetConfiguration build() {
+ return new CasSourceTargetConfiguration(this);
+ }
+ }
+}
\ No newline at end of file