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