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 2026/03/19 11:09:15 UTC
(uima-uimaj) branch feature/444-Improve-CasToComparableText created (now 764eeab27)
This is an automated email from the ASF dual-hosted git repository.
rec pushed a change to branch feature/444-Improve-CasToComparableText
in repository https://gitbox.apache.org/repos/asf/uima-uimaj.git
at 764eeab27 Issue #444: Improve CasToComparableText
This branch includes the following new commits:
new 764eeab27 Issue #444: Improve CasToComparableText
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(uima-uimaj) 01/01: Issue #444: Improve CasToComparableText
Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
rec pushed a commit to branch feature/444-Improve-CasToComparableText
in repository https://gitbox.apache.org/repos/asf/uima-uimaj.git
commit 764eeab2755f337798d54b45029ef9150aaebc20
Author: Richard Eckart de Castilho <re...@apache.org>
AuthorDate: Thu Mar 19 12:09:06 2026 +0100
Issue #444: Improve CasToComparableText
- Enhance `CasToComparableText`: add HTML renderer (in addition to CSV).
- Add configurable columns: `<ANCHOR>`, `<INDEXED>`, `<COVERED_TEXT>` and `setMaxLengthCoveredText`.
- Stable, disambiguated anchors: optional unique anchors, sofa id marker, indexed marker, optional anchor feature hash.
- Deterministic ordering: sort annotation-valued multi-valued features; feature-hash tie-breaker; indexed-first tie-break.
- Exclude features/types via regex patterns with compiled-pattern cache.
- Treat empty strings as null and configurable `nullValue`.
- Robust multi-valued support: arrays and list types rendered recursively with primitive-array handling.
- New configuration API (setters/getters) for rendering options (e.g., `setOmitXmlDeclaration`, `setAnchorFeatureHash`, `setUniqueAnchors`, etc.).
- Update `CasToComparableTextTest` to cover HTML output, exclusions, ordering, anchor hashing and array/list rendering.
---
.gitignore | 1 +
.../uima/cas/serdes/CasToComparableText.java | 923 +++++++++++++++------
.../uima/cas/serdes/CasToComparableTextTest.java | 252 +++++-
3 files changed, 893 insertions(+), 283 deletions(-)
diff --git a/.gitignore b/.gitignore
index 4f5c78a4f..992ed46aa 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,7 @@
.classpath
.project
.settings
+.vscode
target
checkpoint_synchPoint.xml
checkpoint_synchPoint.xml.prev
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
index 94dda1dc6..efa495bf9 100644
--- 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
@@ -6,9 +6,9 @@
* 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
@@ -18,14 +18,42 @@
*/
package org.apache.uima.cas.serdes;
+import static java.lang.Integer.toHexString;
+import static java.lang.String.join;
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.groupingBy;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;
+import static javax.xml.transform.OutputKeys.INDENT;
+import static javax.xml.transform.OutputKeys.METHOD;
+import static javax.xml.transform.OutputKeys.OMIT_XML_DECLARATION;
import static org.apache.commons.csv.CSVFormat.DEFAULT;
+import static org.apache.commons.lang3.StringUtils.repeat;
+import static org.apache.commons.lang3.StringUtils.replaceEach;
+import static org.apache.uima.cas.CAS.FEATURE_BASE_NAME_HEAD;
+import static org.apache.uima.cas.CAS.FEATURE_BASE_NAME_SOFA;
+import static org.apache.uima.cas.CAS.FEATURE_BASE_NAME_TAIL;
+import static org.apache.uima.cas.CAS.TYPE_NAME_BOOLEAN;
+import static org.apache.uima.cas.CAS.TYPE_NAME_BYTE;
+import static org.apache.uima.cas.CAS.TYPE_NAME_DOUBLE;
+import static org.apache.uima.cas.CAS.TYPE_NAME_FLOAT;
+import static org.apache.uima.cas.CAS.TYPE_NAME_FLOAT_LIST;
+import static org.apache.uima.cas.CAS.TYPE_NAME_FS_ARRAY;
+import static org.apache.uima.cas.CAS.TYPE_NAME_INTEGER;
+import static org.apache.uima.cas.CAS.TYPE_NAME_INTEGER_LIST;
+import static org.apache.uima.cas.CAS.TYPE_NAME_LIST_BASE;
+import static org.apache.uima.cas.CAS.TYPE_NAME_LONG;
+import static org.apache.uima.cas.CAS.TYPE_NAME_SHORT;
+import static org.apache.uima.cas.CAS.TYPE_NAME_STRING_LIST;
+import static org.apache.uima.cas.serdes.CasToComparableText.OutputFormat.CSV;
+import static org.apache.uima.cas.serdes.CasToComparableText.XmlEventType.CHARACTERS;
+import static org.apache.uima.cas.serdes.CasToComparableText.XmlEventType.END_ELEMENT;
+import static org.apache.uima.cas.serdes.CasToComparableText.XmlEventType.START_DOCUMENT;
+import static org.apache.uima.cas.serdes.CasToComparableText.XmlEventType.START_ELEMENT;
import java.io.Closeable;
import java.io.IOException;
@@ -34,6 +62,7 @@ import java.io.StringWriter;
import java.io.Writer;
import java.lang.reflect.Array;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
@@ -41,11 +70,20 @@ import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
+import javax.xml.XMLConstants;
+import javax.xml.stream.FactoryConfigurationError;
+import javax.xml.transform.TransformerConfigurationException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.sax.SAXTransformerFactory;
+import javax.xml.transform.stream.StreamResult;
+
import org.apache.commons.csv.CSVPrinter;
+import org.apache.commons.io.output.CloseShieldWriter;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.uima.cas.ArrayFS;
@@ -67,34 +105,60 @@ import org.apache.uima.cas.impl.CASImpl;
import org.apache.uima.cas.text.AnnotationFS;
import org.apache.uima.jcas.JCas;
import org.apache.uima.jcas.cas.AnnotationBase;
+import org.xml.sax.Attributes;
+import org.xml.sax.ContentHandler;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.AttributesImpl;
-/**
- * @author entwicklerteam
- */
public class CasToComparableText {
// Parameters
private boolean markIndexed = true;
private boolean markView = true;
private boolean coveredTextColumnEnabled = true;
+ private boolean anchorColumnEnabled = true;
+ private boolean typeSectionHeaderEnabled = true;
+
private boolean indexedColumnEnabled = false;
private boolean treatEmptyStringsAsNull = false;
private int maxLengthCoveredText = 30;
private boolean sortAnnotationsInMultiValuedFeatures = true;
private boolean uniqueAnchors = true;
+ private boolean anchorFeatureHash = false;
+ private boolean omitXmlDeclaration = true;
private Set<String> excludeFeaturePatterns = new HashSet<>();
private Set<String> excludeTypePatterns = new HashSet<>();
private String nullValue = "<NULL>";
// State
private final CAS cas;
+ private final OutputFormat format;
private Set<FeatureStructure> _indexedFses;
private Map<String, Pattern> regexCache = new HashMap<>();
private Map<String, Boolean> exclusionCache;
- public CasToComparableText(CAS aCas) {
+ public enum OutputFormat {
+ CSV, HTML;
+ }
+
+ enum XmlEventType {
+ START_DOCUMENT, START_ELEMENT, END_ELEMENT, CHARACTERS;
+ }
+
+ public CasToComparableText(CAS aCas, OutputFormat aFormat) {
cas = aCas;
+ format = aFormat;
+ }
+
+ public CasToComparableText(CAS aCas) {
+
+ this(aCas, CSV);
+ }
+
+ public CasToComparableText(JCas jCas, OutputFormat aFormat) {
+
+ this(jCas.getCas(), aFormat);
}
public CasToComparableText(JCas jCas) {
@@ -128,6 +192,16 @@ public class CasToComparableText {
excludeFeaturePatterns.addAll(asList(aPatterns));
}
+ public void setOmitXmlDeclaration(boolean aOmitXmlDeclaration) {
+
+ omitXmlDeclaration = aOmitXmlDeclaration;
+ }
+
+ public boolean isOmitXmlDeclaration() {
+
+ return omitXmlDeclaration;
+ }
+
public Set<String> getExcludeFeaturePatterns() {
return unmodifiableSet(excludeFeaturePatterns);
@@ -153,6 +227,16 @@ public class CasToComparableText {
return uniqueAnchors;
}
+ public void setAnchorFeatureHash(boolean anchorFeatureHash) {
+
+ this.anchorFeatureHash = anchorFeatureHash;
+ }
+
+ public boolean isAnchorFeatureHash() {
+
+ return anchorFeatureHash;
+ }
+
public void setSortAnnotationsInMultiValuedFeatures(
boolean aSortAnnotationsInMultiValuedFeatures) {
@@ -189,6 +273,26 @@ public class CasToComparableText {
coveredTextColumnEnabled = aCoveredTextColumnEnabled;
}
+ public boolean isAnchorColumnEnabled() {
+
+ return anchorColumnEnabled;
+ }
+
+ public void setAnchorColumnEnabled(boolean anchorColumnEnabled) {
+
+ this.anchorColumnEnabled = anchorColumnEnabled;
+ }
+
+ public boolean isTypeSectionHeaderEnabled() {
+
+ return typeSectionHeaderEnabled;
+ }
+
+ public void setTypeSectionHeaderEnabled(boolean typeSectionHeaderEnabled) {
+
+ this.typeSectionHeaderEnabled = typeSectionHeaderEnabled;
+ }
+
public boolean isIndexedColumnEnabled() {
return indexedColumnEnabled;
@@ -234,10 +338,12 @@ public class CasToComparableText {
}
public void setTreatEmptyStringsAsNull(boolean aTreatEmptyStringsAsNull) {
+
treatEmptyStringsAsNull = aTreatEmptyStringsAsNull;
}
public boolean isTreatEmptyStringsAsNull() {
+
return treatEmptyStringsAsNull;
}
@@ -255,14 +361,27 @@ public class CasToComparableText {
return toString(asList(aFS));
}
+ @Override
+ public String toString() {
+
+ try (var buffer = new StringWriter()) {
+ write(buffer);
+ return buffer.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);
+ }
+ }
+
public String toString(Collection<? extends FeatureStructure> aSeeds) {
- try (StringWriter out = new StringWriter()) {
+ try (var 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*
+ // it must be the fault of the rendering code / feature structure
throw new IllegalArgumentException(e);
}
}
@@ -285,38 +404,67 @@ public class CasToComparableText {
return _indexedFses;
}
+ private FormatRenderer createRenderer(Writer aWriter) throws IOException {
+
+ return switch (format) {
+ case CSV -> new CsvRenderer(new PrintWriter(aWriter));
+ case HTML -> new HtmlRenderer(aWriter);
+ default -> throw new IllegalArgumentException("Unsupported format: [" + format + "]");
+ };
+ }
+
public void write(Writer out, Collection<? extends FeatureStructure> aSeeds) throws IOException {
if (aSeeds.isEmpty()) {
return;
}
- for (FeatureStructure fs : aSeeds) {
+ for (var fs : aSeeds) {
if (fs.getCAS() != cas && fs.getCAS() != ((CASImpl) cas).getBaseCAS()) {
throw new IllegalArgumentException("FeatureStructure does not belong to CAS");
}
}
- Set<FeatureStructure> reachableFses = findReachableFeatureStructures(aSeeds);
+ var 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()));
+ var indexByType = reachableFses.stream().collect(groupingBy(FeatureStructure::getType));
// Ensure that the type sections have a stable order
- List<Type> typesSorted = indexByType.keySet().stream()
+ var typesSorted = indexByType.keySet().stream()
.filter(type -> excludeTypePatterns.stream()
.noneMatch(p -> pattern(p).matcher(type.getName()).matches()))
- .sorted(comparing(Type::getName)).collect(Collectors.toList());
+ .sorted(comparing(Type::getName)) //
+ .collect(toList());
// Build an anchor for every feature structure
- Map<FeatureStructure, Anchor> fsToAnchor = generateAnchors(typesSorted, indexByType);
+ var 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);
+ try (var fmt = createRenderer(CloseShieldWriter.wrap(out))) {
+ for (var type : typesSorted) {
+ var ts = cas.getTypeSystem();
+ var annotationType = ts.getType(CAS.TYPE_NAME_ANNOTATION);
+
+ var columnsTitles = new ArrayList<String>();
+ if (anchorColumnEnabled) {
+ columnsTitles.add("<ANCHOR>");
+ }
+
+ if (indexedColumnEnabled) {
+ columnsTitles.add("<INDEXED>");
+ }
+
+ if (coveredTextColumnEnabled && ts.subsumes(annotationType, type)) {
+ columnsTitles.add("<COVERED_TEXT>");
+ }
+
+ listFeatures(type).stream() //
+ .filter(f -> !isExcluded(f)) //
+ .map(Feature::getShortName) //
+ .forEachOrdered(columnsTitles::add);
+
+ fmt.renderTypeSectionHeader(type, columnsTitles);
// 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
@@ -327,53 +475,28 @@ public class CasToComparableText {
.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"))))
+ Pair<FeatureStructure, List<String>>::getKey, new FSComparator(fsToAnchor))
+ // ... then (if necessary) compare by the actual data (except the anchor)
+ .thenComparing(p -> p.getValue().stream().skip(1).collect(joining("\0"))))
.collect(toList());
// @formatter:on
- for (Pair<FeatureStructure, List<String>> row : rows) {
- csv.printRecord(row.getValue());
+ for (var row : rows) {
+ fmt.renderFeatureStructure(row.getKey(), row.getValue());
}
- }
- pout.print("\n");
+ fmt.renderTypeSectionFooter(type);
+ }
}
}
private String escape(String aString) {
- return StringUtils.replaceEach(aString, new String[] { "\t", "\n", "\r", "[", "]", ",", "\\" },
+ return 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.
*/
@@ -383,13 +506,16 @@ public class CasToComparableText {
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));
+ for (var type : aTypesSorted) {
+
+ var includeOffsets = includeOffsets(type);
+
+ var fses = new ArrayList<>(aIndexByType.get(type));
fses.sort(new FSComparator());
- for (FeatureStructure fs : fses) {
- Anchor anchor = new Anchor(fs, markIndexed && indexedFses.contains(fs),
- disambiguationByPrefix);
+ for (var fs : fses) {
+ var anchor = new Anchor(fs, markIndexed && indexedFses.contains(fs), disambiguationByPrefix,
+ includeOffsets);
fsToAnchor.put(fs, anchor);
}
}
@@ -397,14 +523,25 @@ public class CasToComparableText {
return fsToAnchor;
}
+ private boolean includeOffsets(Type type) {
+
+ var bothExcluded = excludeFeaturePatterns
+ .contains(type.getName() + ":" + CAS.FEATURE_BASE_NAME_BEGIN)
+ && excludeFeaturePatterns.contains(type.getName() + ":" + CAS.FEATURE_BASE_NAME_END);
+ if (bothExcluded) {
+ return false;
+ }
+ return true;
+ }
+
private List<Feature> listFeatures(Type aType) {
// Determine which feature to show in which column
- List<Feature> features = new ArrayList<>(aType.getFeatures());
+ var 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 -> 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()));
@@ -414,10 +551,12 @@ public class CasToComparableText {
private Pair<FeatureStructure, List<String>> renderFS(Map<FeatureStructure, Anchor> aFsToAnchor,
FeatureStructure aFS) {
- List<String> data = new ArrayList<>();
+ var data = new ArrayList<String>();
// First column is always the anchor
- data.add(aFsToAnchor.get(aFS).toString());
+ if (anchorColumnEnabled) {
+ data.add(aFsToAnchor.get(aFS).toString());
+ }
// Then add if the FS was in the index
if (indexedColumnEnabled) {
@@ -425,7 +564,7 @@ public class CasToComparableText {
}
if (coveredTextColumnEnabled && aFS instanceof AnnotationFS) {
- String coveredText = ((AnnotationFS) aFS).getCoveredText();
+ var coveredText = ((AnnotationFS) aFS).getCoveredText();
if (maxLengthCoveredText > 0) {
coveredText = StringUtils.abbreviateMiddle(coveredText, "...", maxLengthCoveredText);
}
@@ -458,16 +597,19 @@ public class CasToComparableText {
}
// So once we get here, it must be a feature structure or null
- FeatureStructure value = aFS.getFeatureValue(feature);
+ var value = aFS.getFeatureValue(feature);
if (value == null) {
data.add(nullValue);
continue nextFeature;
}
// Ok, so it's a feature structure
- Anchor anchor = aFsToAnchor.get(value);
+ var anchor = aFsToAnchor.get(value);
if (anchor == null) {
- throw new IllegalStateException("No anchor - bug - should not happen");
+ throw new IllegalStateException("No anchor - bug - should not happen.\n" + //
+ "Feature structure without anchor: \n" + value + "\n" + //
+ "Reached through feature [" + feature.getName() + "] of feature structure: \n"
+ + aFS);
}
data.add(anchor.toString());
}
@@ -491,6 +633,7 @@ public class CasToComparableText {
}
private static boolean isMultiValuedFeature(FeatureStructure aFS, Feature aFeature) {
+
if (aFeature == null) {
return false;
}
@@ -498,17 +641,19 @@ public class CasToComparableText {
TypeSystem aTS = aFS.getCAS().getTypeSystem();
return aFeature.getRange().isArray()
- || aTS.subsumes(aTS.getType(CAS.TYPE_NAME_LIST_BASE), aFeature.getRange());
+ || aTS.subsumes(aTS.getType(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());
+
+ var ts = fs.getCAS().getTypeSystem();
+ return fs.getType().isArray() || ts.subsumes(ts.getType(TYPE_NAME_LIST_BASE), fs.getType());
}
private String renderMultiValuedFeatureStructure(FeatureStructure aFS,
Map<FeatureStructure, Anchor> aFsToAnchor) {
- List<Object> values = multiValuedFeatureStructureToList(aFS);
+
+ var values = multiValuedFeatureStructureToList(aFS);
if (values == null) {
return nullValue;
@@ -516,7 +661,7 @@ public class CasToComparableText {
// 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);
+ var allValuesAreAnnotations = values.stream().allMatch(v -> v instanceof AnnotationFS);
if (sortAnnotationsInMultiValuedFeatures && allValuesAreAnnotations) {
values = values.stream().map(v -> (AnnotationFS) v)
.sorted(comparingInt(AnnotationFS::getBegin)
@@ -525,24 +670,23 @@ public class CasToComparableText {
.collect(Collectors.toList());
}
- List<String> items = new ArrayList<>();
+ var items = new ArrayList<String>();
nextItem: for (Object item : values) {
if (item == null) {
items.add(nullValue);
continue nextItem;
}
- if (item instanceof String) {
- items.add(escape(renderStringValue((String) item)));
+ if (item instanceof String stringItem) {
+ items.add(escape(renderStringValue(stringItem)));
continue nextItem;
}
- if (item instanceof FeatureStructure) {
- FeatureStructure fsItem = (FeatureStructure) item;
+ if (item instanceof FeatureStructure fsItem) {
if (isMultiValued(fsItem)) {
items.add(renderMultiValuedFeatureStructure(fsItem, aFsToAnchor));
} else {
- Anchor anchor = aFsToAnchor.get(fsItem);
+ var anchor = aFsToAnchor.get(fsItem);
if (anchor == null) {
throw new IllegalStateException("No anchor - bug - should not happen");
}
@@ -558,7 +702,12 @@ public class CasToComparableText {
}
private String renderStringValue(String aString) {
- if ((aString == null) || (treatEmptyStringsAsNull && aString.isEmpty())) {
+
+ if (aString == null) {
+ return nullValue;
+ }
+
+ if (treatEmptyStringsAsNull && aString.isEmpty()) {
return nullValue;
}
@@ -573,99 +722,101 @@ public class CasToComparableText {
}
// Handle case where feature is an array
- TypeSystem ts = aValue.getCAS().getTypeSystem();
+ var 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) {
+ var length = -1;
+ if (aValue instanceof CommonArrayFS commonArray) {
+ length = commonArray.size();
+ if (commonArray instanceof BooleanArrayFS booleanArray) {
target = new boolean[length];
- ((BooleanArrayFS) source).copyToArray(0, (boolean[]) target, 0, length);
- } else if (aValue instanceof ByteArrayFS) {
+ booleanArray.copyToArray(0, (boolean[]) target, 0, length);
+ } else if (aValue instanceof ByteArrayFS byteArray) {
target = new byte[length];
- ((ByteArrayFS) source).copyToArray(0, (byte[]) target, 0, length);
- } else if (aValue instanceof DoubleArrayFS) {
+ byteArray.copyToArray(0, (byte[]) target, 0, length);
+ } else if (aValue instanceof DoubleArrayFS doubleArray) {
target = new double[length];
- ((DoubleArrayFS) source).copyToArray(0, (double[]) target, 0, length);
- } else if (aValue instanceof FloatArrayFS) {
+ doubleArray.copyToArray(0, (double[]) target, 0, length);
+ } else if (aValue instanceof FloatArrayFS floatArray) {
target = new float[length];
- ((FloatArrayFS) source).copyToArray(0, (float[]) target, 0, length);
- } else if (aValue instanceof IntArrayFS) {
+ floatArray.copyToArray(0, (float[]) target, 0, length);
+ } else if (aValue instanceof IntArrayFS intArray) {
target = new int[length];
- ((IntArrayFS) source).copyToArray(0, (int[]) target, 0, length);
- } else if (aValue instanceof LongArrayFS) {
+ intArray.copyToArray(0, (int[]) target, 0, length);
+ } else if (aValue instanceof LongArrayFS longArray) {
target = new long[length];
- ((LongArrayFS) source).copyToArray(0, (long[]) target, 0, length);
- } else if (aValue instanceof ShortArrayFS) {
+ longArray.copyToArray(0, (long[]) target, 0, length);
+ } else if (aValue instanceof ShortArrayFS shortArray) {
target = new short[length];
- ((ShortArrayFS) source).copyToArray(0, (short[]) target, 0, length);
- } else if (aValue instanceof StringArrayFS) {
+ shortArray.copyToArray(0, (short[]) target, 0, length);
+ } else if (aValue instanceof StringArrayFS stringArray) {
target = new String[length];
- ((StringArrayFS) source).copyToArray(0, (String[]) target, 0, length);
- } else {
+ stringArray.copyToArray(0, (String[]) target, 0, length);
+ } else if (aValue instanceof ArrayFS<?> fsArray) {
target = new FeatureStructure[length];
- ((ArrayFS<?>) source).copyToArray(0, (FeatureStructure[]) target, 0, length);
+ fsArray.copyToArray(0, (FeatureStructure[]) target, 0, length);
+ } else {
+ throw new IllegalArgumentException(
+ "Unsupported feature value type [" + commonArray.getType().getName() + "]");
}
}
// Handle case where feature is a list
- else if (ts.subsumes(ts.getType(CAS.TYPE_NAME_LIST_BASE), aValue.getType())) {
+ else if (ts.subsumes(ts.getType(TYPE_NAME_LIST_BASE), aValue.getType())) {
// Get length of list
length = 0;
{
- FeatureStructure cur = aValue;
+ var 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) {
+ while (cur.getType().getFeatureByBaseName(FEATURE_BASE_NAME_HEAD) != null) {
length++;
- cur = cur.getFeatureValue(cur.getType().getFeatureByBaseName(CAS.FEATURE_BASE_NAME_TAIL));
+ cur = cur.getFeatureValue(cur.getType().getFeatureByBaseName(FEATURE_BASE_NAME_TAIL));
}
}
- if (ts.subsumes(ts.getType(CAS.TYPE_NAME_FLOAT_LIST), aValue.getType())) {
- float[] floatTarget = new float[length];
- FeatureStructure cur = aValue;
+ if (ts.subsumes(ts.getType(TYPE_NAME_FLOAT_LIST), aValue.getType())) {
+ var floatTarget = new float[length];
+ var cur = aValue;
// We assume to by facing a non-empty element if it has a "head" feature
int i = 0;
- while (cur.getType().getFeatureByBaseName(CAS.FEATURE_BASE_NAME_HEAD) != null) {
+ while (cur.getType().getFeatureByBaseName(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));
+ .getFloatValue(cur.getType().getFeatureByBaseName(FEATURE_BASE_NAME_HEAD));
+ cur = cur.getFeatureValue(cur.getType().getFeatureByBaseName(FEATURE_BASE_NAME_TAIL));
i++;
}
target = floatTarget;
- } else if (ts.subsumes(ts.getType(CAS.TYPE_NAME_INTEGER_LIST), aValue.getType())) {
- int[] intTarget = new int[length];
- FeatureStructure cur = aValue;
+ } else if (ts.subsumes(ts.getType(TYPE_NAME_INTEGER_LIST), aValue.getType())) {
+ var intTarget = new int[length];
+ var cur = aValue;
// We assume to by facing a non-empty element if it has a "head" feature
int i = 0;
- while (cur.getType().getFeatureByBaseName(CAS.FEATURE_BASE_NAME_HEAD) != null) {
+ while (cur.getType().getFeatureByBaseName(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));
+ .getIntValue(cur.getType().getFeatureByBaseName(FEATURE_BASE_NAME_HEAD));
+ cur = cur.getFeatureValue(cur.getType().getFeatureByBaseName(FEATURE_BASE_NAME_TAIL));
i++;
}
target = intTarget;
- } else if (ts.subsumes(ts.getType(CAS.TYPE_NAME_STRING_LIST), aValue.getType())) {
- String[] stringTarget = new String[length];
- FeatureStructure cur = aValue;
+ } else if (ts.subsumes(ts.getType(TYPE_NAME_STRING_LIST), aValue.getType())) {
+ var stringTarget = new String[length];
+ var cur = aValue;
// We assume to by facing a non-empty element if it has a "head" feature
- int i = 0;
- while (cur.getType().getFeatureByBaseName(CAS.FEATURE_BASE_NAME_HEAD) != null) {
+ var i = 0;
+ while (cur.getType().getFeatureByBaseName(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));
+ .getStringValue(cur.getType().getFeatureByBaseName(FEATURE_BASE_NAME_HEAD));
+ cur = cur.getFeatureValue(cur.getType().getFeatureByBaseName(FEATURE_BASE_NAME_TAIL));
i++;
}
target = stringTarget;
} else if (ts.subsumes(ts.getType(CAS.TYPE_NAME_FS_LIST), aValue.getType())) {
target = new FeatureStructure[length];
- FeatureStructure cur = aValue;
+ var cur = aValue;
// We assume to by facing a non-empty element if it has a "head" feature
- int i = 0;
- while (cur.getType().getFeatureByBaseName(CAS.FEATURE_BASE_NAME_HEAD) != null) {
- Array.set(target, i, cur
- .getFeatureValue(cur.getType().getFeatureByBaseName(CAS.FEATURE_BASE_NAME_HEAD)));
- cur = cur.getFeatureValue(cur.getType().getFeatureByBaseName(CAS.FEATURE_BASE_NAME_TAIL));
+ var i = 0;
+ while (cur.getType().getFeatureByBaseName(FEATURE_BASE_NAME_HEAD) != null) {
+ Array.set(target, i,
+ cur.getFeatureValue(cur.getType().getFeatureByBaseName(FEATURE_BASE_NAME_HEAD)));
+ cur = cur.getFeatureValue(cur.getType().getFeatureByBaseName(FEATURE_BASE_NAME_TAIL));
i++;
}
} else {
@@ -678,8 +829,8 @@ public class CasToComparableText {
throw new IllegalStateException("Unable to extract values");
}
- List<Object> targetCollection = new ArrayList<>();
- for (int i = 0; i < length; i++) {
+ var targetCollection = new ArrayList<Object>();
+ for (var i = 0; i < length; i++) {
targetCollection.add(Array.get(target, i));
}
@@ -692,13 +843,13 @@ public class CasToComparableText {
// 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);
+ var toProcess = new LinkedHashSet<FeatureStructure>(aSeeds);
// Collect all feature structures that are reachable via the seed points
- Set<FeatureStructure> seen = new HashSet<>();
+ var seen = new HashSet<FeatureStructure>();
while (!toProcess.isEmpty()) {
// Poll the next element from the processing queue
- FeatureStructure fs = toProcess.iterator().next();
+ var fs = toProcess.iterator().next();
toProcess.remove(fs);
if (seen.contains(fs)) {
@@ -708,33 +859,40 @@ public class CasToComparableText {
seen.add(fs);
if (isMultiValued(fs)) {
- List<Object> values = multiValuedFeatureStructureToList(fs);
+ var values = multiValuedFeatureStructureToList(fs);
if (values != null) {
- values.stream().filter(v -> v instanceof FeatureStructure).filter(v -> !seen.contains(v))
+ values.stream() //
+ .filter(v -> v instanceof FeatureStructure) //
+ .filter(v -> !seen.contains(v)) //
.forEach(v -> toProcess.add((FeatureStructure) v));
}
} else {
- for (Feature feature : fs.getType().getFeatures()) {
+ for (var feature : fs.getType().getFeatures()) {
+ if (feature.getRange().isPrimitive()) {
+ continue;
+ }
// Check if the feature is excluded
- if (feature.getRange().isPrimitive() || isExcluded(feature)
- || CAS.FEATURE_BASE_NAME_SOFA.equals(feature.getShortName())) {
+ if (isExcluded(feature)) {
+ continue;
+ }
+
+ if (FEATURE_BASE_NAME_SOFA.equals(feature.getShortName())) {
continue;
}
if (isMultiValuedFeature(fs, feature)) {
- List<Object> featureValues = multiValuedFeatureStructureToList(
- fs.getFeatureValue(feature));
+ var featureValues = multiValuedFeatureStructureToList(fs.getFeatureValue(feature));
if (featureValues != null) {
- for (Object value : featureValues) {
- if (value instanceof FeatureStructure && !seen.contains(value)) {
- toProcess.add((FeatureStructure) value);
+ for (var value : featureValues) {
+ if (value instanceof FeatureStructure fsValue && !seen.contains(fsValue)) {
+ toProcess.add(fsValue);
}
}
}
} else {
- FeatureStructure value = fs.getFeatureValue(feature);
+ var value = fs.getFeatureValue(feature);
if (value != null && !seen.contains(value)) {
toProcess.add(value);
}
@@ -746,143 +904,121 @@ public class CasToComparableText {
return seen;
}
- private int featureHash(FeatureStructure aFS) {
+ private int featureHash(FeatureStructure aFS, Map<FeatureStructure, Anchor> aFsToAnchor) {
+
int hash = 0;
- for (Feature f : aFS.getType().getFeatures()) {
+ var features = aFS.getType().getFeatures().stream() //
+ .filter(f -> !isExcluded(f)) //
+ .sorted(comparing(Feature::getShortName)) //
+ .toArray(Feature[]::new);
+ for (var f : features) {
if (f.getRange().isStringOrStringSubtype() || f.getRange().isPrimitive()) {
- String value = renderStringValue(aFS.getFeatureValueAsString(f));
+ var value = renderStringValue(aFS.getFeatureValueAsString(f));
hash += value != null ? value.hashCode() : 0;
continue;
}
if (f.getRange().isArray()) {
if (f.getRange().getComponentType().isStringOrStringSubtype()) {
- StringArrayFS array = ((StringArrayFS) aFS.getFeatureValue(f));
+ var array = ((StringArrayFS) aFS.getFeatureValue(f));
if (array != null) {
- for (int i = 0; i < array.size(); i++) {
- String v = renderStringValue(array.get(i));
- hash += v != null ? v.hashCode() : 0;
+ for (var i = 0; i < array.size(); i++) {
+ var v = renderStringValue(array.get(i));
+ hash += (31 * hash) + (v != null ? v.hashCode() : 0);
}
}
continue;
}
switch (f.getRange().getComponentType().getName()) {
- case CAS.TYPE_NAME_BOOLEAN: {
- BooleanArrayFS array = ((BooleanArrayFS) aFS.getFeatureValue(f));
+ case TYPE_NAME_BOOLEAN: {
+ var array = ((BooleanArrayFS) aFS.getFeatureValue(f));
if (array != null) {
- for (int i = 0; i < array.size(); i++) {
- hash += array.get(i) ? -(i + 1) : (i + 1);
- }
+ hash += Arrays.hashCode(array.toArray());
}
break;
}
- case CAS.TYPE_NAME_BYTE: {
- ByteArrayFS array = ((ByteArrayFS) aFS.getFeatureValue(f));
+ case TYPE_NAME_BYTE: {
+ var array = ((ByteArrayFS) aFS.getFeatureValue(f));
if (array != null) {
- for (int i = 0; i < array.size(); i++) {
- hash += array.get(i);
- }
+ hash += Arrays.hashCode(array.toArray());
}
break;
}
- case CAS.TYPE_NAME_DOUBLE: {
- DoubleArrayFS array = ((DoubleArrayFS) aFS.getFeatureValue(f));
+ case TYPE_NAME_DOUBLE: {
+ var array = ((DoubleArrayFS) aFS.getFeatureValue(f));
if (array != null) {
- for (int i = 0; i < array.size(); i++) {
- hash += Double.hashCode(array.get(i));
- }
+ hash += Arrays.hashCode(array.toArray());
}
break;
}
- case CAS.TYPE_NAME_FLOAT: {
- FloatArrayFS array = ((FloatArrayFS) aFS.getFeatureValue(f));
+ case TYPE_NAME_FLOAT: {
+ var array = ((FloatArrayFS) aFS.getFeatureValue(f));
if (array != null) {
- for (int i = 0; i < array.size(); i++) {
- hash += Float.hashCode(array.get(i));
- }
+ hash += Arrays.hashCode(array.toArray());
}
break;
}
- case CAS.TYPE_NAME_INTEGER: {
- IntArrayFS array = ((IntArrayFS) aFS.getFeatureValue(f));
+ case TYPE_NAME_INTEGER: {
+ var array = ((IntArrayFS) aFS.getFeatureValue(f));
if (array != null) {
- for (int i = 0; i < array.size(); i++) {
- hash += array.get(i);
- }
+ hash += Arrays.hashCode(array.toArray());
}
break;
}
- case CAS.TYPE_NAME_LONG: {
- LongArrayFS array = ((LongArrayFS) aFS.getFeatureValue(f));
+ case TYPE_NAME_LONG: {
+ var array = ((LongArrayFS) aFS.getFeatureValue(f));
if (array != null) {
- for (int i = 0; i < array.size(); i++) {
- hash += Long.hashCode(array.get(i));
- }
+ hash += Arrays.hashCode(array.toArray());
}
break;
}
- case CAS.TYPE_NAME_SHORT: {
- ShortArrayFS array = ((ShortArrayFS) aFS.getFeatureValue(f));
+ case TYPE_NAME_SHORT: {
+ var array = ((ShortArrayFS) aFS.getFeatureValue(f));
if (array != null) {
- for (int i = 0; i < array.size(); i++) {
- hash += array.get(i);
- }
+ hash += Arrays.hashCode(array.toArray());
}
break;
}
- case CAS.TYPE_NAME_FS_ARRAY:
- // We cannot really recursively calculate the hash... let's just use the array length
+ case TYPE_NAME_FS_ARRAY:
+ // We cannot really recursively calculate the hash... let's just use the
+ // array length
if (aFS.getFeatureValue(f) != null) {
- hash *= ((CommonArrayFS) aFS.getFeatureValue(f)).size() + 1;
+ hash += Integer.hashCode(((CommonArrayFS<?>) aFS.getFeatureValue(f)).size());
}
break;
}
}
- // If we get here, it is a feature structure reference... we cannot really recursively
- // go into it to calculate a recursive hash... so we just check if the value is non-null
- hash *= aFS.getFeatureValue(f) != null ? 1 : -1;
+ // If we get here, it is a feature structure reference...
+ if (aFsToAnchor != null) {
+ hash += Optional.ofNullable(aFS.getFeatureValue(f)) //
+ .map(aFsToAnchor::get) //
+ .map(Object::hashCode) //
+ .orElse(0);
+ } else {
+ // we cannot really recursively go into it to calculate a recursive hash... so we
+ // just check if the value is non-null
+ hash += Boolean.hashCode(aFS.getFeatureValue(f) != null);
+ }
}
return hash;
}
- 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 {
+ private class FSComparator implements Comparator<FeatureStructure> {
- return delegate.append(aSequence, aStart, aEnd);
- }
+ private final Map<FeatureStructure, Anchor> fsToAnchor;
- @Override
- public Appendable append(char aCharacter) throws IOException {
+ public FSComparator() {
- return delegate.append(aCharacter);
+ this(null);
}
- @Override
- public void close() throws IOException {
+ public FSComparator(Map<FeatureStructure, Anchor> aFsToAnchor) {
- // Do not forward close
+ fsToAnchor = aFsToAnchor;
}
- }
-
- private class FSComparator implements Comparator<FeatureStructure> {
@Override
public int compare(FeatureStructure aFS1, FeatureStructure aFS2) {
@@ -892,14 +1028,14 @@ public class CasToComparableText {
}
// Same name?
- int nameCmp = aFS2.getType().getName().compareTo(aFS2.getType().getName());
+ var nameCmp = aFS2.getType().getName().compareTo(aFS2.getType().getName());
if (nameCmp != 0) {
return nameCmp;
}
// Annotation? Then sort by offsets
- boolean fs1IsAnnotation = aFS1 instanceof AnnotationFS;
- boolean fs2IsAnnotation = aFS2 instanceof AnnotationFS;
+ var fs1IsAnnotation = aFS1 instanceof AnnotationFS;
+ var fs2IsAnnotation = aFS2 instanceof AnnotationFS;
if (fs1IsAnnotation != fs2IsAnnotation) {
return -1;
}
@@ -908,27 +1044,34 @@ public class CasToComparableText {
AnnotationFS ann2 = (AnnotationFS) aFS2;
// Ascending by begin
- int beginCmp = ann1.getBegin() - ann2.getBegin();
+ var beginCmp = ann1.getBegin() - ann2.getBegin();
if (beginCmp != 0) {
return beginCmp;
}
// Descending by end
- int endCmp = ann2.getEnd() - ann1.getEnd();
+ var endCmp = ann2.getEnd() - ann1.getEnd();
if (endCmp != 0) {
return endCmp;
}
}
// Ok, so let's calculate a hash over the features then...
- int fh1 = featureHash(aFS1);
- int fh2 = featureHash(aFS2);
+ var fh1 = featureHash(aFS1, fsToAnchor);
+ var fh2 = featureHash(aFS2, fsToAnchor);
if (fh1 < fh2) {
return -1;
}
if (fh1 > fh2) {
return 1;
}
+
+ // Finally, let's consider if the feature structure is on the index or not
+ if (fsToAnchor != null) {
+ // First indexed, then non-indexed
+ return Boolean.compare(fsToAnchor.get(aFS2).indexed, fsToAnchor.get(aFS1).indexed);
+ }
+
return 0;
}
}
@@ -937,17 +1080,19 @@ public class CasToComparableText {
private final String stringValue;
private final int disambiguationId;
+ private final boolean indexed;
public Anchor(FeatureStructure aFS, boolean aIndexed,
- Map<String, Integer> aDisambiguationByPrefix) {
+ Map<String, Integer> aDisambiguationByPrefix, boolean includeOffsets) {
+
+ indexed = aIndexed;
- StringBuilder anchor = new StringBuilder();
+ var anchor = new StringBuilder();
anchor.append(aFS.getType().getShortName());
// Special handling for AnnotationFS
- if (aFS instanceof AnnotationFS) {
- AnnotationFS ann = (AnnotationFS) aFS;
+ if (aFS instanceof AnnotationFS ann && includeOffsets) {
anchor.append("[");
anchor.append(ann.getBegin());
anchor.append("-");
@@ -960,14 +1105,13 @@ public class CasToComparableText {
}
// Special handling for AnnotationBase
- if (markView && aFS instanceof AnnotationBase) {
- AnnotationBase annBase = (AnnotationBase) aFS;
+ if (markView && aFS instanceof AnnotationBase annBase) {
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();
+ var prefix = anchor.toString();
disambiguationId = aDisambiguationByPrefix.computeIfAbsent(prefix, key -> 0);
if (uniqueAnchors && disambiguationId > 0) {
@@ -976,6 +1120,12 @@ public class CasToComparableText {
anchor.append(")");
}
+ if (anchorFeatureHash) {
+ anchor.append("[");
+ anchor.append(toHexString(featureHash(aFS, null)));
+ anchor.append("]");
+ }
+
aDisambiguationByPrefix.put(prefix, disambiguationId + 1);
stringValue = anchor.toString();
@@ -986,15 +1136,304 @@ public class CasToComparableText {
return stringValue;
}
+
+ @Override
+ public int hashCode() {
+
+ return stringValue.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+
+ return stringValue.equals(((Anchor) obj).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);
+
+ return new CasToComparableText(aCas).toString();
+ }
+
+ private interface FormatRenderer extends Closeable {
+
+ void renderTypeSectionHeader(Type type, List<String> aColumnTitles) throws IOException;
+
+ void renderTypeSectionFooter(Type type) throws IOException;
+
+ void renderFeatureStructure(FeatureStructure aFS, List<String> aFeatureValues)
+ throws IOException;
+ }
+
+ private class HtmlRenderer implements FormatRenderer {
+
+ private static final String A_CLASS = "class";
+ private static final String A_DATA_COL = "data-col";
+ private static final String A_STYLE = "style";
+ private static final String C_NUM = "num";
+ private static final String E_BODY = "body";
+ private static final String E_HEAD = "head";
+ private static final String E_HTML = "html";
+ private static final String E_TBODY = "tbody";
+ private static final String E_TD = "td";
+ private static final String E_TH = "th";
+ private static final String E_THEAD = "thead";
+ private static final String E_TR = "tr";
+ private static final String A_CAPTION = "caption";
+ private static final String E_TABLE = "table";
+ private static final String LINE_BREAK = "\n";
+ private static final String SPACE = " ";
+ private final Writer out;
+ private final ContentHandler xml;
+
+ private List<String> columnTitles;
+ private int indent = 2;
+ private int depth;
+ private XmlEventType lastEventType = START_DOCUMENT;
+
+ HtmlRenderer(Writer aOut) throws IOException {
+
+ try {
+ out = aOut;
+
+ SAXTransformerFactory tf = (SAXTransformerFactory) TransformerFactory.newInstance();
+ tf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
+ tf.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
+ tf.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, "");
+
+ var th = tf.newTransformerHandler();
+ th.getTransformer().setOutputProperty(OMIT_XML_DECLARATION,
+ omitXmlDeclaration ? "yes" : "no");
+ th.getTransformer().setOutputProperty(METHOD, "xml");
+ th.getTransformer().setOutputProperty(INDENT, "no");
+ th.setResult(new StreamResult(aOut));
+
+ xml = th;
+
+ xml.startDocument();
+ writeStartElement(E_HTML); // <html>
+
+ writeStartElement(E_HEAD); // <head>
+ writeStartElement(A_STYLE); // <head>
+ writeCharacters(join(SPACE, //
+ "body { font-family: sans-serif; }", //
+ "td, th { padding: 5px; }", //
+ "table, th, td { border: 1px solid lightgray; border-collapse: collapse; } ", //
+ "table { margin-bottom: 15px; }", //
+ "caption { text-align: left; background: lightgray; border-radius: 5px 5px 0px 0px; padding: 5px; }", //
+ ".num { text-align: right; } "));
+ writeEndElement(A_STYLE); // </style>
+ writeEndElement(E_HEAD); // </head>
+
+ writeStartElement(E_BODY); // <body>
+
+ } catch (FactoryConfigurationError | TransformerConfigurationException | SAXException e) {
+ throw new IOException(e);
+ }
+ }
+
+ private void _startElement(String aString, Attributes aAttributes) throws SAXException {
+
+ xml.startElement(null, null, aString, aAttributes); // <body>
+ }
+
+ private void _endElement(String aString) throws SAXException {
+
+ xml.endElement(null, null, aString); // <body>
+ }
+
+ private void _writeCharacters(String aString) throws SAXException {
+
+ var chars = aString.toCharArray();
+ xml.characters(chars, 0, chars.length);
+ }
+
+ private void writeCharacters(String aString) throws SAXException {
+
+ lastEventType = CHARACTERS;
+
+ if (aString == null || aString.isEmpty()) {
+ return;
+ }
+
+ _writeCharacters(aString);
+ }
+
+ private void writeStartElement(String aLocalName) throws SAXException {
+
+ writeStartElement(aLocalName, null);
+ }
+
+ private void writeStartElement(String aLocalName, Attributes aAttributes) throws SAXException {
+
+ if (lastEventType == START_ELEMENT
+ || (lastEventType == START_DOCUMENT && !omitXmlDeclaration)) {
+ _writeCharacters(LINE_BREAK);
+ }
+ _writeCharacters(repeat(SPACE, depth * indent));
+ _startElement(aLocalName, aAttributes);
+ depth++;
+ lastEventType = START_ELEMENT;
+ }
+
+ private void writeEndElement(String aLocalName) throws SAXException {
+
+ depth--;
+ if (lastEventType != CHARACTERS) {
+ _writeCharacters(repeat(SPACE, depth * indent));
+ }
+ _endElement(aLocalName);
+ _writeCharacters(LINE_BREAK);
+ lastEventType = END_ELEMENT;
+ }
+
+ @Override
+ public void renderTypeSectionHeader(Type type, List<String> aColumnTitles) throws IOException {
+
+ columnTitles = aColumnTitles;
+
+ try {
+ writeStartElement(E_TABLE); // <table>
+ if (typeSectionHeaderEnabled) {
+ writeStartElement(A_CAPTION); // <caption>
+ writeCharacters(type.getName());
+ writeEndElement(A_CAPTION); // </caption>
+ }
+ writeStartElement(E_THEAD); // <thead>
+ writeStartElement(E_TR); // <tr>
+
+ for (String columnTitle : aColumnTitles) {
+ columnTitle = sanitizeColumnTitle(columnTitle);
+ writeStartElement(E_TH); // <th>
+ writeCharacters(columnTitle);
+ writeEndElement(E_TH); // </th>
+ }
+
+ writeEndElement(E_TR); // </tr>
+ writeEndElement(E_THEAD); // </thead>
+ writeStartElement(E_TBODY); // <tbody>
+ } catch (SAXException e) {
+ throw new IOException(e);
+ }
+
+ }
+
+ private String sanitizeColumnTitle(String columnTitle) {
+
+ if (columnTitle.startsWith("<") && columnTitle.endsWith(">")) {
+ return columnTitle.substring(1, columnTitle.length() - 1);
+ }
+
+ return columnTitle;
+ }
+
+ @Override
+ public void renderTypeSectionFooter(Type type) throws IOException {
+
+ try {
+ writeEndElement(E_TBODY); // </tbody>
+ writeEndElement(E_TABLE); // </table>
+ } catch (SAXException e) {
+ throw new IOException(e);
+ }
+ }
+
+ @Override
+ public void renderFeatureStructure(FeatureStructure aFS, List<String> aColumnValues)
+ throws IOException {
+
+ try {
+ int i = 0;
+ writeStartElement(E_TR); // <td>
+ for (var value : aColumnValues) {
+ var cssClasses = new LinkedHashSet<String>();
+ var columnTitle = columnTitles.get(i);
+ if (isNumericFeature(aFS.getType().getFeatureByBaseName(columnTitle))) {
+ cssClasses.add(C_NUM);
+ }
+
+ var attributes = new AttributesImpl();
+ attributes.addAttribute(null, null, A_DATA_COL, null, sanitizeColumnTitle(columnTitle));
+
+ if (!cssClasses.isEmpty()) {
+ attributes.addAttribute(null, null, A_CLASS, null,
+ cssClasses.stream().collect(joining(SPACE)));
+ }
+ writeStartElement(E_TD, attributes); // <td>
+ writeCharacters(value);
+ writeEndElement(E_TD); // </td>
+ i++;
+ }
+ writeEndElement(E_TR); // </tr>
+ } catch (SAXException e) {
+ throw new IOException(e);
+ }
+ }
+
+ private boolean isNumericFeature(Feature feat) {
+
+ return feat != null && (feat.getRange().getName().equals(TYPE_NAME_INTEGER)
+ || feat.getRange().getName().equals(TYPE_NAME_SHORT)
+ || feat.getRange().getName().equals(TYPE_NAME_LONG)
+ || feat.getRange().getName().equals(TYPE_NAME_DOUBLE)
+ || feat.getRange().getName().equals(TYPE_NAME_FLOAT)
+
+ );
+ }
+
+ @Override
+ public void close() throws IOException {
+
+ try {
+ writeEndElement(E_BODY);
+ writeEndElement(E_HTML);
+ xml.endDocument();
+ out.close();
+ } catch (SAXException e) {
+ throw new IOException(e);
+ }
+ }
+ }
+
+ private class CsvRenderer implements FormatRenderer {
+
+ private final Writer out;
+ private final CSVPrinter csv;
+
+ CsvRenderer(Writer aOut) throws IOException {
+
+ out = aOut;
+ csv = new CSVPrinter(aOut, DEFAULT);
+ }
+
+ @Override
+ public void renderTypeSectionHeader(Type type, List<String> aColumnTitles) throws IOException {
+
+ if (typeSectionHeaderEnabled) {
+ csv.printRecord(type.getName());
+ }
+
+ csv.printRecord(aColumnTitles);
+ }
+
+ @Override
+ public void renderFeatureStructure(FeatureStructure aFS, List<String> aFeatureValues)
+ throws IOException {
+
+ csv.printRecord(aFeatureValues);
+ }
+
+ @Override
+ public void close() throws IOException {
+
+ csv.close();
+ }
+
+ @Override
+ public void renderTypeSectionFooter(Type type) throws IOException {
+
+ out.append("\n");
+ out.flush();
}
}
}
diff --git a/uimaj-core/src/test/java/org/apache/uima/cas/serdes/CasToComparableTextTest.java b/uimaj-core/src/test/java/org/apache/uima/cas/serdes/CasToComparableTextTest.java
index 02b738183..703ad2f3a 100644
--- a/uimaj-core/src/test/java/org/apache/uima/cas/serdes/CasToComparableTextTest.java
+++ b/uimaj-core/src/test/java/org/apache/uima/cas/serdes/CasToComparableTextTest.java
@@ -6,9 +6,9 @@
* 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
@@ -18,24 +18,24 @@
*/
package org.apache.uima.cas.serdes;
+import static java.util.Arrays.asList;
+import static org.apache.commons.lang3.ArrayUtils.shuffle;
+import static org.apache.uima.cas.CAS.TYPE_NAME_ANNOTATION;
+import static org.apache.uima.cas.CAS.TYPE_NAME_STRING;
+import static org.apache.uima.cas.serdes.CasToComparableText.OutputFormat.HTML;
import static org.assertj.core.api.Assertions.assertThat;
import org.apache.uima.UIMAFramework;
-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.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.resource.metadata.FeatureDescription;
import org.apache.uima.resource.metadata.TypeDescription;
import org.apache.uima.resource.metadata.TypeSystemDescription;
import org.apache.uima.util.CasCreationUtils;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
class CasToComparableTextTest {
@@ -61,10 +61,9 @@ class CasToComparableTextTest {
@Test
void thatPrimitiveValuesWork() throws Exception {
- TypeSystemDescription tsd = UIMAFramework.getResourceSpecifierFactory()
- .createTypeSystemDescription();
+ var tsd = UIMAFramework.getResourceSpecifierFactory().createTypeSystemDescription();
- TypeDescription akofTD = tsd.addType(TYPE_NAME_AKOF, null, CAS.TYPE_NAME_TOP);
+ var akofTD = tsd.addType(TYPE_NAME_AKOF, null, CAS.TYPE_NAME_TOP);
akofTD.addFeature(FEATURE_BASE_NAME_AKOF_INT, null, CAS.TYPE_NAME_INTEGER);
akofTD.addFeature(FEATURE_BASE_NAME_AKOF_FLOAT, null, CAS.TYPE_NAME_FLOAT);
@@ -73,15 +72,15 @@ class CasToComparableTextTest {
akofTD.addFeature(FEATURE_BASE_NAME_AKOF_SHORT, null, CAS.TYPE_NAME_SHORT);
akofTD.addFeature(FEATURE_BASE_NAME_AKOF_BYTE, null, CAS.TYPE_NAME_BYTE);
akofTD.addFeature(FEATURE_BASE_NAME_AKOF_BOOLEAN, null, CAS.TYPE_NAME_BOOLEAN);
- akofTD.addFeature(FEATURE_BASE_NAME_AKOF_STRING, null, CAS.TYPE_NAME_STRING);
+ akofTD.addFeature(FEATURE_BASE_NAME_AKOF_STRING, null, TYPE_NAME_STRING);
// akofTD.addFeature(FEATURE_BASE_NAME_AKOF_FS, null, CAS.TYPE_NAME_TOP);
// akofTD.addFeature(FEATURE_BASE_NAME_AKOF_FS_ARRAY, null, CAS.TYPE_NAME_FS_ARRAY);
- CAS cas = CasCreationUtils.createCas(tsd, null, null, null);
+ var cas = CasCreationUtils.createCas(tsd, null, null, null);
- Type t = cas.getTypeSystem().getType(TYPE_NAME_AKOF);
- FeatureStructure fs = cas.createFS(t);
+ var t = cas.getTypeSystem().getType(TYPE_NAME_AKOF);
+ var fs = cas.createFS(t);
fs.setIntValue(t.getFeatureByBaseName(FEATURE_BASE_NAME_AKOF_INT), 1);
fs.setFloatValue(t.getFeatureByBaseName(FEATURE_BASE_NAME_AKOF_FLOAT), 10.2321321f);
@@ -94,17 +93,16 @@ class CasToComparableTextTest {
cas.addFsToIndexes(fs);
- String result = CasToComparableText.toComparableString(cas);
+ var result = CasToComparableText.toComparableString(cas);
assertThat(result).contains("akof*,true,64,10.2321321,10.232132,1,10000000,31000,dummy");
}
@Test
void thatPrimitiveWork() throws Exception {
- TypeSystemDescription tsd = UIMAFramework.getResourceSpecifierFactory()
- .createTypeSystemDescription();
+ var tsd = UIMAFramework.getResourceSpecifierFactory().createTypeSystemDescription();
- TypeDescription akofTD = tsd.addType(TYPE_NAME_AKOF, null, CAS.TYPE_NAME_TOP);
+ var akofTD = tsd.addType(TYPE_NAME_AKOF, null, CAS.TYPE_NAME_TOP);
// akofTD.addFeature(FEATURE_BASE_NAME_AKOF_INT, null, CAS.TYPE_NAME_INTEGER);
// akofTD.addFeature(FEATURE_BASE_NAME_AKOF_FS, null, CAS.TYPE_NAME_TOP);
@@ -126,47 +124,47 @@ class CasToComparableTextTest {
akofTD.addFeature(FEATURE_BASE_NAME_AKOF_BOOLEAN_ARRAY, null, CAS.TYPE_NAME_BOOLEAN_ARRAY);
akofTD.addFeature(FEATURE_BASE_NAME_AKOF_STRING_ARRAY, null, CAS.TYPE_NAME_STRING_ARRAY);
- CAS cas = CasCreationUtils.createCas(tsd, null, null, null);
+ var cas = CasCreationUtils.createCas(tsd, null, null, null);
- Type t = cas.getTypeSystem().getType(TYPE_NAME_AKOF);
+ var t = cas.getTypeSystem().getType(TYPE_NAME_AKOF);
FeatureStructure fs = cas.createFS(t);
- IntArrayFS intArrayFs = cas.createIntArrayFS(1);
+ var intArrayFs = cas.createIntArrayFS(1);
intArrayFs.set(0, 1);
fs.setFeatureValue(t.getFeatureByBaseName(FEATURE_BASE_NAME_AKOF_INT_ARRAY), intArrayFs);
- FloatArrayFS floatArrayFs = cas.createFloatArrayFS(1);
+ var floatArrayFs = cas.createFloatArrayFS(1);
floatArrayFs.set(0, 10.2321321f);
fs.setFeatureValue(t.getFeatureByBaseName(FEATURE_BASE_NAME_AKOF_FLOAT_ARRAY), floatArrayFs);
- DoubleArrayFS doubleArrayFs = cas.createDoubleArrayFS(1);
+ var doubleArrayFs = cas.createDoubleArrayFS(1);
doubleArrayFs.set(0, 10.2321321);
fs.setFeatureValue(t.getFeatureByBaseName(FEATURE_BASE_NAME_AKOF_DOUBLE_ARRAY), doubleArrayFs);
- LongArrayFS longArrayFs = cas.createLongArrayFS(1);
+ var longArrayFs = cas.createLongArrayFS(1);
longArrayFs.set(0, 10_000_000);
fs.setFeatureValue(t.getFeatureByBaseName(FEATURE_BASE_NAME_AKOF_LONG_ARRAY), longArrayFs);
- ShortArrayFS shortArrayFs = cas.createShortArrayFS(1);
+ var shortArrayFs = cas.createShortArrayFS(1);
shortArrayFs.set(0, (short) 31000);
fs.setFeatureValue(t.getFeatureByBaseName(FEATURE_BASE_NAME_AKOF_SHORT_ARRAY), shortArrayFs);
- ByteArrayFS byteArrayFs = cas.createByteArrayFS(1);
+ var byteArrayFs = cas.createByteArrayFS(1);
byteArrayFs.set(0, (byte) 64);
fs.setFeatureValue(t.getFeatureByBaseName(FEATURE_BASE_NAME_AKOF_BYTE_ARRAY), byteArrayFs);
- BooleanArrayFS booleanArrayFs = cas.createBooleanArrayFS(1);
+ var booleanArrayFs = cas.createBooleanArrayFS(1);
booleanArrayFs.set(0, true);
fs.setFeatureValue(t.getFeatureByBaseName(FEATURE_BASE_NAME_AKOF_BOOLEAN_ARRAY),
booleanArrayFs);
- StringArrayFS stringArrayFs = cas.createStringArrayFS(1);
+ var stringArrayFs = cas.createStringArrayFS(1);
stringArrayFs.set(0, "dummy");
fs.setFeatureValue(t.getFeatureByBaseName(FEATURE_BASE_NAME_AKOF_STRING_ARRAY), stringArrayFs);
cas.addFsToIndexes(fs);
- String result = CasToComparableText.toComparableString(cas);
+ var result = CasToComparableText.toComparableString(cas);
assertThat(result)
.contains("akof*,[true],[64],[10.2321321],[10.232132],[1],[10000000],[31000],[dummy]");
@@ -174,29 +172,201 @@ class CasToComparableTextTest {
@Test
void thatReferenceValuesAndArraysWork() throws Exception {
- TypeSystemDescription tsd = UIMAFramework.getResourceSpecifierFactory()
- .createTypeSystemDescription();
+ var tsd = UIMAFramework.getResourceSpecifierFactory().createTypeSystemDescription();
- TypeDescription akofTD = tsd.addType(TYPE_NAME_AKOF, null, CAS.TYPE_NAME_TOP);
+ var akofTD = tsd.addType(TYPE_NAME_AKOF, null, CAS.TYPE_NAME_TOP);
akofTD.addFeature(FEATURE_BASE_NAME_AKOF_FS, null, CAS.TYPE_NAME_TOP);
akofTD.addFeature(FEATURE_BASE_NAME_AKOF_FS_ARRAY, null, CAS.TYPE_NAME_FS_ARRAY);
- CAS cas = CasCreationUtils.createCas(tsd, null, null, null);
+ var cas = CasCreationUtils.createCas(tsd, null, null, null);
- Type t = cas.getTypeSystem().getType(TYPE_NAME_AKOF);
- FeatureStructure fs = cas.createFS(t);
+ var t = cas.getTypeSystem().getType(TYPE_NAME_AKOF);
+ var fs = cas.createFS(t);
fs.setFeatureValue(t.getFeatureByBaseName(FEATURE_BASE_NAME_AKOF_FS), fs);
- ArrayFS<FeatureStructure> arrayFs = cas.createArrayFS(1);
+ var arrayFs = cas.createArrayFS(1);
arrayFs.set(0, fs);
fs.setFeatureValue(t.getFeatureByBaseName(FEATURE_BASE_NAME_AKOF_FS_ARRAY), arrayFs);
cas.addFsToIndexes(fs);
- String result = CasToComparableText.toComparableString(cas);
+ var result = CasToComparableText.toComparableString(cas);
assertThat(result).contains("akof*,[akof*],akof*");
}
+
+ @Nested
+ class TokenBasedTests {
+
+ private TypeSystemDescription tsd;
+ private TypeDescription t1TypeDesc;
+ private TypeDescription t2TypeDesc;
+ private FeatureDescription t2f1Desc;
+ private FeatureDescription t2f2Desc;
+ private FeatureDescription t2f3Desc;
+ private FeatureDescription t2f4Desc;
+ private FeatureDescription t1f1Desc;
+ private CAS cas;
+ private Type t1;
+ private Feature t1f1;
+ private Type t2;
+ private Feature t2f1;
+ private Feature t2f2;
+ private Feature t2f3;
+ private Feature t2f4;
+
+ @BeforeEach
+ void setup() throws Exception {
+ tsd = UIMAFramework.getResourceSpecifierFactory().createTypeSystemDescription();
+
+ t1TypeDesc = tsd.addType("T1", null, TYPE_NAME_ANNOTATION);
+ t1f1Desc = t1TypeDesc.addFeature("label", null, TYPE_NAME_STRING);
+
+ t2TypeDesc = tsd.addType("T2", null, TYPE_NAME_ANNOTATION);
+ t2f1Desc = t2TypeDesc.addFeature("f1", null, TYPE_NAME_STRING);
+ t2f2Desc = t2TypeDesc.addFeature("f2", null, TYPE_NAME_STRING);
+ t2f3Desc = t2TypeDesc.addFeature("f3", null, TYPE_NAME_STRING);
+ t2f4Desc = t2TypeDesc.addFeature("f4", null, t1TypeDesc.getName());
+
+ cas = CasCreationUtils.createCas(tsd, null, null, null);
+
+ t2 = cas.getTypeSystem().getType(t2TypeDesc.getName());
+ t2f1 = t2.getFeatureByBaseName(t2f1Desc.getName());
+ t2f2 = t2.getFeatureByBaseName(t2f2Desc.getName());
+ t2f3 = t2.getFeatureByBaseName(t2f3Desc.getName());
+ t2f4 = t2.getFeatureByBaseName(t2f4Desc.getName());
+
+ t1 = cas.getTypeSystem().getType(t1TypeDesc.getName());
+ t1f1 = t1.getFeatureByBaseName(t1f1Desc.getName());
+ }
+
+ @Test
+ void thatColumnsAndHeadersMatch() throws Exception {
+
+ cas.setDocumentText("document");
+
+ var token = cas.createAnnotation(t2, 0, 8);
+ token.setFeatureValueFromString(t2f1, "f1");
+ token.setFeatureValueFromString(t2f2, "f2");
+ token.setFeatureValueFromString(t2f3, "f3");
+ cas.addFsToIndexes(token);
+
+ var cas2Text = new CasToComparableText(cas);
+ var result = cas2Text.toString(token);
+
+ var lines = result.split("\n");
+ var headers = lines[1].trim().split(",");
+ var values = lines[2].trim().split(",");
+
+ assertThat(headers[0]).isEqualTo("<ANCHOR>");
+ assertThat(values[0]).isEqualTo("T2[0-8]*@_InitialView");
+
+ assertThat(headers[1]).isEqualTo("<COVERED_TEXT>");
+ assertThat(values[1]).isEqualTo(cas.getDocumentText());
+
+ var componentIdFeatureIndex = asList(headers).indexOf("f1");
+ assertThat(headers[componentIdFeatureIndex]).isEqualTo(values[componentIdFeatureIndex]);
+
+ var normalizedFeatureIndex = asList(headers).indexOf("f2");
+ assertThat(headers[normalizedFeatureIndex]).isEqualTo(values[normalizedFeatureIndex]);
+
+ var tokenClassFeatureIndex = asList(headers).indexOf("f3");
+ assertThat(headers[tokenClassFeatureIndex]).isEqualTo(values[tokenClassFeatureIndex]);
+ }
+
+ @Test
+ void thatColumnsAndHeadersMatchWithExclusions() throws Exception {
+
+ cas.setDocumentText("document");
+
+ var ann = cas.createAnnotation(t2, 0, 8);
+ ann.setFeatureValueFromString(t2f1, "f1");
+ ann.setFeatureValueFromString(t2f2, "f2");
+ cas.addFsToIndexes(ann);
+
+ var cas2Text = new CasToComparableText(cas);
+ cas2Text.addExcludeFeaturePatterns(".*:" + t2f1.getShortName());
+ var result = cas2Text.toString(ann);
+
+ var lines = result.split("\n");
+ var headers = lines[1].trim().split(",");
+ var values = lines[2].trim().split(",");
+
+ var componentIdFeatureIndex = asList(headers).indexOf("f1");
+ assertThat(componentIdFeatureIndex).isEqualTo(-1);
+
+ var tokenClassFeatureIndex = asList(headers).indexOf("f2");
+ assertThat(headers[tokenClassFeatureIndex]).isEqualTo(values[tokenClassFeatureIndex]);
+ }
+
+ @Test
+ void thatHtmlOutputWorks() throws Exception {
+
+ cas.setDocumentText("document");
+
+ var ann = cas.createAnnotation(t2, 0, 8);
+ ann.setFeatureValueFromString(t2f1, "f1");
+ ann.setFeatureValueFromString(t2f2, "f2");
+ cas.addFsToIndexes(ann);
+
+ var cas2Text = new CasToComparableText(cas, HTML);
+ cas2Text.setOmitXmlDeclaration(false);
+ var result = cas2Text.toString(ann);
+
+ assertThat(result.trim().replace("\r\n", "\n")) //
+ .startsWith("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<html>") //
+ .endsWith("</html>");
+ }
+
+ @Test
+ void thatHtmlOutputWorksWithoutXmlDeclaration() throws Exception {
+
+ cas.setDocumentText("document");
+
+ var ann = cas.createAnnotation(t2, 0, 8);
+ ann.setFeatureValueFromString(t2f1, "f1");
+ ann.setFeatureValueFromString(t2f2, "f2");
+ cas.addFsToIndexes(ann);
+
+ var cas2Text = new CasToComparableText(cas, HTML);
+ cas2Text.setOmitXmlDeclaration(true);
+ var result = cas2Text.toString(ann);
+
+ assertThat(result.trim().replace("\r\n", "\n")) //
+ .startsWith("<html>") //
+ .endsWith("</html>");
+ }
+
+ @Test
+ void thatOrderOfIndexedAndNonIndexedFeatureStructuresIsStable() throws Exception {
+
+ cas.setDocumentText("document");
+
+ var indexFlags = new boolean[] { //
+ false, false, false, false, false, true, true, true, true, true //
+ };
+ shuffle(indexFlags);
+
+ for (var indexFlag : indexFlags) {
+ var t1Ann = cas.createAnnotation(t1, 0, 8);
+ t1Ann.setFeatureValueFromString(t1f1, "N");
+ if (indexFlag) {
+ cas.addFsToIndexes(t1Ann);
+ }
+
+ var t2Ann = cas.createAnnotation(t2, 0, 8);
+ t2Ann.setFeatureValue(t2f4, t1Ann);
+ cas.addFsToIndexes(t2Ann);
+ }
+
+ var cas2Text = new CasToComparableText(cas);
+ cas2Text.setAnchorFeatureHash(true);
+ cas2Text.setUniqueAnchors(false);
+ assertThat(cas2Text.toString().lines().filter(s -> s.startsWith("T1[0-8]"))) //
+ .extracting(s -> s.charAt(7)) //
+ .containsExactly('*', '*', '*', '*', '*', '@', '@', '@', '@', '@');
+ }
+ }
}