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:50 UTC
[uima-uimaj] 01/01: [UIMA-6374] Create CAS (de)serialization test
suite
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