/* * Copyright 2015 * Ubiquitous Knowledge Processing (UKP) Lab * Technische Universität Darmstadt * * Licensed 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 de.tudarmstadt.ukp.dkpro.core.io.brat; import static org.apache.uima.fit.util.JCasUtil.selectAll; import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.StringWriter; import java.io.Writer; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.stream.Collectors; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.apache.uima.UimaContext; import org.apache.uima.analysis_engine.AnalysisEngineProcessException; import org.apache.uima.cas.CAS; import org.apache.uima.cas.Feature; import org.apache.uima.cas.FeatureStructure; import org.apache.uima.cas.Type; import org.apache.uima.cas.TypeSystem; import org.apache.uima.cas.text.AnnotationFS; import org.apache.uima.fit.descriptor.ConfigurationParameter; import org.apache.uima.fit.util.FSUtil; import org.apache.uima.jcas.JCas; import org.apache.uima.resource.ResourceInitializationException; import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonGenerator; import de.tudarmstadt.ukp.dkpro.core.api.io.JCasFileWriter_ImplBase; import de.tudarmstadt.ukp.dkpro.core.api.parameter.ComponentParameters; import de.tudarmstadt.ukp.dkpro.core.io.brat.internal.model.BratAnnotation; import de.tudarmstadt.ukp.dkpro.core.io.brat.internal.model.BratAnnotationDocument; import de.tudarmstadt.ukp.dkpro.core.io.brat.internal.model.BratAttributeDecl; import de.tudarmstadt.ukp.dkpro.core.io.brat.internal.model.BratConfiguration; import de.tudarmstadt.ukp.dkpro.core.io.brat.internal.model.BratConstants; import de.tudarmstadt.ukp.dkpro.core.io.brat.internal.model.BratEventAnnotation; import de.tudarmstadt.ukp.dkpro.core.io.brat.internal.model.BratEventAnnotationDecl; import de.tudarmstadt.ukp.dkpro.core.io.brat.internal.model.BratEventArgument; import de.tudarmstadt.ukp.dkpro.core.io.brat.internal.model.BratEventArgumentDecl; import de.tudarmstadt.ukp.dkpro.core.io.brat.internal.model.BratRelationAnnotation; import de.tudarmstadt.ukp.dkpro.core.io.brat.internal.model.BratTextAnnotation; import de.tudarmstadt.ukp.dkpro.core.io.brat.internal.model.BratTextAnnotationDrawingDecl; import de.tudarmstadt.ukp.dkpro.core.io.brat.internal.model.RelationParam; import de.tudarmstadt.ukp.dkpro.core.io.brat.internal.model.TypeMapping; /** * Writer for the brat annotation format. * * <p>Known issues:</p> * <ul> * <li><a href="https://github.com/nlplab/brat/issues/791">Brat is unable to read relation * attributes created by this writer.</a></li> * <li>PARAM_TYPE_MAPPINGS not implemented yet</li> * </ul> * * @see <a href="http://brat.nlplab.org/standoff.html">brat standoff format</a> * @see <a href="http://brat.nlplab.org/configuration.html">brat configuration format</a> */ public class BratWriter extends JCasFileWriter_ImplBase { /** * Specify the suffix of text output files. Default value <code>.txt</code>. If the suffix is not * needed, provide an empty string as value. */ public static final String PARAM_TEXT_FILENAME_EXTENSION = "textFilenameExtension"; @ConfigurationParameter(name = PARAM_TEXT_FILENAME_EXTENSION, mandatory = true, defaultValue = ".txt") private String textFilenameExtension; /** * Specify the suffix of output files. Default value <code>.ann</code>. If the suffix is not * needed, provide an empty string as value. */ public static final String PARAM_FILENAME_EXTENSION = ComponentParameters.PARAM_FILENAME_EXTENSION; @ConfigurationParameter(name = PARAM_FILENAME_EXTENSION, mandatory = true, defaultValue = ".ann") private String filenameSuffix; /** * Types that will not be written to the exported file. */ public static final String PARAM_EXCLUDE_TYPES = "excludeTypes"; @ConfigurationParameter(name = PARAM_EXCLUDE_TYPES, mandatory = true, defaultValue = { "de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.Sentence" }) private Set<String> excludeTypes; /** * Types that are text annotations (aka entities or spans). */ public static final String PARAM_TEXT_ANNOTATION_TYPES = "spanTypes"; @ConfigurationParameter(name = PARAM_TEXT_ANNOTATION_TYPES, mandatory = true, defaultValue = { // "de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.Sentence", // "de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.Token", // "de.tudarmstadt.ukp.dkpro.core.api.lexmorph.type.pos.POS", // "de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.Lemma", // "de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.Stem", // "de.tudarmstadt.ukp.dkpro.core.api.syntax.type.chunk.Chunk", // "de.tudarmstadt.ukp.dkpro.core.api.ner.type.NamedEntity", // "de.tudarmstadt.ukp.dkpro.core.api.semantics.type.SemArg", // "de.tudarmstadt.ukp.dkpro.core.api.semantics.type.SemPred" }) private Set<String> spanTypes; /** * Types that are relations. It is mandatory to provide the type name followed by two feature * names that represent Arg1 and Arg2 separated by colons, e.g. * <code>de.tudarmstadt.ukp.dkpro.core.api.syntax.type.dependency.Dependency:Governor:Dependent</code>. */ public static final String PARAM_RELATION_TYPES = "relationTypes"; @ConfigurationParameter(name = PARAM_RELATION_TYPES, mandatory = true, defaultValue = { "de.tudarmstadt.ukp.dkpro.core.api.syntax.type.dependency.Dependency:Governor:Dependent" }) private Set<String> relationTypes; private Map<String, RelationParam> parsedRelationTypes; // /** // * Types that are events. Optionally, multiple slot features can be specified. // * <code>my.type.Event:location:participant</code>. // */ // public static final String PARAM_EVENT_TYPES = "eventTypes"; // @ConfigurationParameter(name = PARAM_EVENT_TYPES, mandatory = true, defaultValue = { }) // private Set<String> eventTypes; // private Map<String, EventParam> parsedEventTypes; /** * Enable type mappings. */ public static final String PARAM_ENABLE_TYPE_MAPPINGS = "enableTypeMappings"; @ConfigurationParameter(name = PARAM_ENABLE_TYPE_MAPPINGS, mandatory = true, defaultValue = "false") private boolean enableTypeMappings; /** * FIXME */ public static final String PARAM_TYPE_MAPPINGS = "typeMappings"; @ConfigurationParameter(name = PARAM_TYPE_MAPPINGS, mandatory = false, defaultValue = { "de.tudarmstadt.ukp.dkpro.core.api.lexmorph.type.(\\w+) -> $1", "de.tudarmstadt.ukp.dkpro.core.api.lexmorph.type.pos.(\\w+) -> $1", "de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.(\\w+) -> $1", "de.tudarmstadt.ukp.dkpro.core.api.ner.type.(\\w+) -> $1" }) private String[] typeMappings; private TypeMapping typeMapping; /** * The brat web application can currently not handle attributes on relations, thus they are * disabled by default. Here they can be enabled again. */ public static final String PARAM_WRITE_RELATION_ATTRIBUTES = "writeRelationAttributes"; @ConfigurationParameter(name = PARAM_WRITE_RELATION_ATTRIBUTES, mandatory = true, defaultValue = "false") private boolean writeRelationAttributes; /** * Enable writing of features with null values. */ public static final String PARAM_WRITE_NULL_ATTRIBUTES = "writeNullAttributes"; @ConfigurationParameter(name = PARAM_WRITE_NULL_ATTRIBUTES, mandatory = true, defaultValue = "false") private boolean writeNullAttributes; /** * Colors to be used for the visual configuration that is generated for brat. */ public static final String PARAM_PALETTE = "palette"; @ConfigurationParameter(name = PARAM_PALETTE, mandatory = false, defaultValue = { "#8dd3c7", "#ffffb3", "#bebada", "#fb8072", "#80b1d3", "#fdb462", "#b3de69", "#fccde5", "#d9d9d9", "#bc80bd", "#ccebc5", "#ffed6f" }) private String[] palette; /** * Whether to render attributes by their short name or by their qualified name. */ public static final String PARAM_SHORT_ATTRIBUTE_NAMES = "shortAttributeNames"; @ConfigurationParameter(name = PARAM_SHORT_ATTRIBUTE_NAMES, mandatory = true, defaultValue = "false") private boolean shortAttributeNames; private int nextEventAnnotationId; private int nextTextAnnotationId; private int nextRelationAnnotationId; private int nextAttributeId; private int nextPaletteIndex; private Map<FeatureStructure, String> spanIdMap; private BratConfiguration conf; private Set<String> warnings; @Override public void initialize(UimaContext aContext) throws ResourceInitializationException { super.initialize(aContext); conf = new BratConfiguration(); warnings = new LinkedHashSet<String>(); parsedRelationTypes = new HashMap<>(); for (String rel : relationTypes) { RelationParam p = RelationParam.parse(rel); parsedRelationTypes.put(p.getType(), p); } // parsedEventTypes = new HashMap<>(); // for (String rel : eventTypes) { // EventParam p = EventParam.parse(rel); // parsedEventTypes.put(p.getType(), p); // } if (enableTypeMappings) { typeMapping = new TypeMapping(typeMappings); } } @Override public void process(JCas aJCas) throws AnalysisEngineProcessException { nextEventAnnotationId = 1; nextTextAnnotationId = 1; nextRelationAnnotationId = 1; nextAttributeId = 1; nextPaletteIndex = 0; spanIdMap = new HashMap<>(); try { if (".ann".equals(filenameSuffix)) { writeText(aJCas); } writeAnnotations(aJCas); } catch (IOException e) { throw new AnalysisEngineProcessException(e); } } @Override public void collectionProcessComplete() throws AnalysisEngineProcessException { if (!".ann".equals(filenameSuffix)) { return; } try { writeAnnotationConfiguration(); writeVisualConfiguration(); } catch (IOException e) { throw new AnalysisEngineProcessException(e); } for (String warning : warnings) { getLogger().warn(warning); } } private void writeAnnotationConfiguration() throws IOException { try (Writer out = new OutputStreamWriter(getOutputStream("annotation", ".conf"), "UTF-8")) { conf.writeAnnotationConfiguration(out); } } private void writeVisualConfiguration() throws IOException { try (Writer out = new OutputStreamWriter(getOutputStream("visual", ".conf"), "UTF-8")) { conf.writeVisualConfiguration(out); } } private void writeAnnotations(JCas aJCas) throws IOException { BratAnnotationDocument doc = new BratAnnotationDocument(); List<FeatureStructure> relationFS = new ArrayList<>(); Map<BratEventAnnotation, FeatureStructure> eventFS = new LinkedHashMap<>(); // Go through all the annotations but only handle the ones that have no references to // other annotations. for (FeatureStructure fs : selectAll(aJCas)) { // Skip document annotation if (fs == aJCas.getDocumentAnnotationFs()) { continue; } // Skip excluded types if (excludeTypes.contains(fs.getType().getName())) { getLogger().debug("Excluding [" + fs.getType().getName() + "]"); continue; } if (spanTypes.contains(fs.getType().getName())) { writeTextAnnotation(doc, (AnnotationFS) fs); } else if (parsedRelationTypes.containsKey(fs.getType().getName())) { relationFS.add(fs); } else if (hasNonPrimitiveFeatures(fs) && (fs instanceof AnnotationFS)) { // else if (parsedEventTypes.containsKey(fs.getType().getName())) { BratEventAnnotation event = writeEventAnnotation(doc, (AnnotationFS) fs); eventFS.put(event, fs); } else if (fs instanceof AnnotationFS) { warnings.add("Assuming annotation type ["+fs.getType().getName()+"] is span"); writeTextAnnotation(doc, (AnnotationFS) fs); } else { warnings.add("Skipping annotation with type ["+fs.getType().getName()+"]"); } } // Handle relations now since now we can resolve their targets to IDs. for (FeatureStructure fs : relationFS) { writeRelationAnnotation(doc, fs); } // Handle event slots now since now we can resolve their targets to IDs. for (Entry<BratEventAnnotation, FeatureStructure> e : eventFS.entrySet()) { writeSlots(doc, e.getKey(), e.getValue()); } switch (filenameSuffix) { case ".ann": try (Writer out = new OutputStreamWriter(getOutputStream(aJCas, filenameSuffix), "UTF-8")) { doc.write(out); break; } case ".html": case ".json": String template ; if (filenameSuffix.equals(".html")) { template = IOUtils.toString(getClass().getResource("html/template.html")); } else { template = "{ \"collData\" : ##COLL-DATA## , \"docData\" : ##DOC-DATA## }"; } JsonFactory jfactory = new JsonFactory(); try (Writer out = new OutputStreamWriter(getOutputStream(aJCas, filenameSuffix), "UTF-8")) { String docData; try (StringWriter buf = new StringWriter()) { try (JsonGenerator jg = jfactory.createGenerator(buf)) { jg.useDefaultPrettyPrinter(); doc.write(jg, aJCas.getDocumentText()); } docData = buf.toString(); } String collData; try (StringWriter buf = new StringWriter()) { try (JsonGenerator jg = jfactory.createGenerator(buf)) { jg.useDefaultPrettyPrinter(); conf.write(jg); } collData = buf.toString(); } template = StringUtils.replaceEach(template, new String[] {"##COLL-DATA##", "##DOC-DATA##"}, new String[] {collData, docData}); out.write(template); } conf = new BratConfiguration(); break; default: throw new IllegalArgumentException("Unknown file format: [" + filenameSuffix + "]"); } } /** * Checks if the feature structure has non-default non-primitive properties. */ private boolean hasNonPrimitiveFeatures(FeatureStructure aFS) { for (Feature f : aFS.getType().getFeatures()) { if (CAS.FEATURE_BASE_NAME_SOFA.equals(f.getShortName())) { continue; } if (!f.getRange().isPrimitive()) { return true; } } return false; } private String getBratType(Type aType) { if (enableTypeMappings) { return typeMapping.getBratType(aType); } else { return aType.getName().replace('.', '-'); } } private BratEventAnnotation writeEventAnnotation(BratAnnotationDocument aDoc, AnnotationFS aFS) { // Write trigger annotation BratTextAnnotation trigger = new BratTextAnnotation(nextTextAnnotationId, getBratType(aFS.getType()), aFS.getBegin(), aFS.getEnd(), aFS.getCoveredText()); nextTextAnnotationId++; // Write event annotation BratEventAnnotation event = new BratEventAnnotation(nextEventAnnotationId, getBratType(aFS.getType()), trigger.getId()); spanIdMap.put(aFS, event.getId()); nextEventAnnotationId++; // We do not add the trigger annotations to the document - they are owned by the event //aDoc.addAnnotation(trigger); event.setTriggerAnnotation(trigger); // Write attributes writeAttributes(event, aFS); // Slots are written later after we know all the span/event IDs conf.addLabelDecl(event.getType(), aFS.getType().getShortName(), aFS.getType() .getShortName().substring(0, 1)); if (!conf.hasDrawingDecl(event.getType())) { conf.addDrawingDecl(new BratTextAnnotationDrawingDecl(event.getType(), "black", palette[nextPaletteIndex % palette.length])); nextPaletteIndex++; } aDoc.addAnnotation(event); return event; } private void writeSlots(BratAnnotationDocument aDoc, BratEventAnnotation aEvent, FeatureStructure aFS) { String superType = getBratType(aFS.getCAS().getTypeSystem().getParent(aFS.getType())); String type = getBratType(aFS.getType()); assert type.equals(aEvent.getType()); BratEventAnnotationDecl decl = conf.getEventDecl(type); if (decl == null) { decl = new BratEventAnnotationDecl(superType, type); conf.addEventDecl(decl); } Map<String, List<BratEventArgument>> slots = new LinkedHashMap<>(); for (Feature feat : aFS.getType().getFeatures()) { if (!isSlotFeature(aFS, feat)) { continue; } String slot = feat.getShortName(); List<BratEventArgument> args = slots.get(slot); if (args == null) { args = new ArrayList<>(); slots.put(slot, args); } if ( FSUtil.isMultiValuedFeature(aFS, feat) && CAS.TYPE_NAME_TOP.equals(aFS.getCAS().getTypeSystem().getParent(feat.getRange().getComponentType()).getName()) && (feat.getRange().getComponentType().getFeatureByBaseName("target") != null) && (feat.getRange().getComponentType().getFeatureByBaseName("role") != null) ) { // Handle WebAnno-style slot links // FIXME It would be better if the link type could be configured, e.g. what // is the name of the link feature and what is the name of the role feature... // but right now we just keep it hard-coded to the values that are used // in the DKPro Core SemArgLink and that are also hard-coded in WebAnno BratEventArgumentDecl slotDecl = new BratEventArgumentDecl(slot, BratConstants.CARD_ZERO_OR_MORE); decl.addSlot(slotDecl); FeatureStructure[] links = FSUtil.getFeature(aFS, feat, FeatureStructure[].class); if (links != null) { for (FeatureStructure link : links) { FeatureStructure target = FSUtil.getFeature(link, "target", FeatureStructure.class); Feature roleFeat = link.getType().getFeatureByBaseName("role"); BratEventArgument arg = new BratEventArgument(slot, args.size(), spanIdMap.get(target)); args.add(arg); // Attach the role attribute to the target span BratAnnotation targetAnno = aDoc.getAnnotation(spanIdMap.get(target)); writePrimitiveAttribute(targetAnno, link, roleFeat); } } } else if (FSUtil.isMultiValuedFeature(aFS, feat)) { // Handle normal multi-valued features BratEventArgumentDecl slotDecl = new BratEventArgumentDecl(slot, BratConstants.CARD_ZERO_OR_MORE); decl.addSlot(slotDecl); FeatureStructure[] targets = FSUtil.getFeature(aFS, feat, FeatureStructure[].class); if (targets != null) { for (FeatureStructure target : targets) { BratEventArgument arg = new BratEventArgument(slot, args.size(), spanIdMap.get(target)); args.add(arg); } } } else { // Handle normal single-valued features BratEventArgumentDecl slotDecl = new BratEventArgumentDecl(slot, BratConstants.CARD_OPTIONAL); decl.addSlot(slotDecl); FeatureStructure target = FSUtil.getFeature(aFS, feat, FeatureStructure.class); if (target != null) { BratEventArgument arg = new BratEventArgument(slot, args.size(), spanIdMap.get(target)); args.add(arg); } } } aEvent.setArguments(slots.values().stream().flatMap(args -> args.stream()) .collect(Collectors.toList())); } private boolean isSlotFeature(FeatureStructure aFS, Feature aFeature) { return !isInternalFeature(aFeature) && (FSUtil.isMultiValuedFeature(aFS, aFeature) || !aFeature.getRange() .isPrimitive()); } private void writeRelationAnnotation(BratAnnotationDocument aDoc, FeatureStructure aFS) { RelationParam rel = parsedRelationTypes.get(aFS.getType().getName()); FeatureStructure arg1 = aFS.getFeatureValue(aFS.getType().getFeatureByBaseName( rel.getArg1())); FeatureStructure arg2 = aFS.getFeatureValue(aFS.getType().getFeatureByBaseName( rel.getArg2())); if (arg1 == null || arg2 == null) { throw new IllegalArgumentException("Dangling relation"); } String arg1Id = spanIdMap.get(arg1); String arg2Id = spanIdMap.get(arg2); if (arg1Id == null || arg2Id == null) { throw new IllegalArgumentException("Unknown targets!"); } String superType = getBratType(aFS.getCAS().getTypeSystem().getParent(aFS.getType())); String type = getBratType(aFS.getType()); BratRelationAnnotation anno = new BratRelationAnnotation(nextRelationAnnotationId, type, rel.getArg1(), arg1Id, rel.getArg2(), arg2Id); nextRelationAnnotationId++; conf.addRelationDecl(superType, type, rel.getArg1(), rel.getArg2()); conf.addLabelDecl(anno.getType(), aFS.getType().getShortName(), aFS.getType() .getShortName().substring(0, 1)); aDoc.addAnnotation(anno); // brat doesn't support attributes on relations // https://github.com/nlplab/brat/issues/791 if (writeRelationAttributes) { writeAttributes(anno, aFS); } } private void writeTextAnnotation(BratAnnotationDocument aDoc, AnnotationFS aFS) { String superType = getBratType(aFS.getCAS().getTypeSystem().getParent(aFS.getType())); String type = getBratType(aFS.getType()); BratTextAnnotation anno = new BratTextAnnotation(nextTextAnnotationId, type, aFS.getBegin(), aFS.getEnd(), aFS.getCoveredText()); nextTextAnnotationId++; conf.addEntityDecl(superType, type); conf.addLabelDecl(anno.getType(), aFS.getType().getShortName(), aFS.getType() .getShortName().substring(0, 1)); if (!conf.hasDrawingDecl(anno.getType())) { conf.addDrawingDecl(new BratTextAnnotationDrawingDecl(anno.getType(), "black", palette[nextPaletteIndex % palette.length])); nextPaletteIndex++; } aDoc.addAnnotation(anno); writeAttributes(anno, aFS); spanIdMap.put(aFS, anno.getId()); } private boolean isInternalFeature(Feature aFeature) { // https://issues.apache.org/jira/browse/UIMA-4565 return "uima.cas.AnnotationBase:sofa".equals(aFeature.getName()); // return CAS.FEATURE_FULL_NAME_SOFA.equals(aFeature.getName()); } private void writeAttributes(BratAnnotation aAnno, FeatureStructure aFS) { for (Feature feat : aFS.getType().getFeatures()) { // Skip Sofa feature if (isInternalFeature(feat)) { continue; } // No need to write begin / end, they are already on the text annotation if (CAS.FEATURE_FULL_NAME_BEGIN.equals(feat.getName()) || CAS.FEATURE_FULL_NAME_END.equals(feat.getName())) { continue; } // No need to write link endpoints again, they are already on the relation annotation RelationParam relParam = parsedRelationTypes.get(aFS.getType().getName()); if (relParam != null) { if (relParam.getArg1().equals(feat.getShortName()) || relParam.getArg2().equals(feat.getShortName())) { continue; } } if (feat.getRange().isPrimitive()) { writePrimitiveAttribute(aAnno, aFS, feat); } // The following warning is not relevant for event annotations because these render such // features as slots. else if (!(aAnno instanceof BratEventAnnotation)) { warnings.add( "Unable to render feature [" + feat.getName() + "] with range [" + feat.getRange().getName() + "] as attribute"); } } } private void writePrimitiveAttribute(BratAnnotation aAnno, FeatureStructure aFS, Feature feat) { String featureValue = aFS.getFeatureValueAsString(feat); // Do not write attributes with null values unless this is explicitly enabled if (featureValue == null && !writeNullAttributes) { return; } String attributeName = shortAttributeNames ? feat.getShortName() : aAnno.getType() + '_' + feat.getShortName(); aAnno.addAttribute(nextAttributeId, attributeName, featureValue); nextAttributeId++; // Do not write certain values to the visual/annotation configuration because // they are not compatible with the brat annotation file format. The values are // still maintained in the ann file. if (isValidFeatureValue(featureValue)) { // Features are inherited to subtypes in UIMA. By storing the attribute under // the name of the type that declares the feature (domain) instead of the name // of the actual instance we are processing, we make sure not to maintain // multiple value sets for the same feature. BratAttributeDecl attrDecl = conf.addAttributeDecl( aAnno.getType(), getAllSubtypes(aFS.getCAS().getTypeSystem(), feat.getDomain()), attributeName, featureValue); conf.addDrawingDecl(attrDecl); } } // This generates lots of types as well that we may not otherwise have in declared in the // brat configuration files, but brat doesn't seem to mind. private Set<String> getAllSubtypes(TypeSystem aTS, Type aType) { Set<String> types = new LinkedHashSet<>(); aTS.getProperlySubsumedTypes(aType).stream().forEach(t -> types.add(getBratType(t))); return types; } /** * Some feature values do not need to be registered or cannot be registered because brat does * not support them. */ private boolean isValidFeatureValue(String aFeatureValue) { // https://github.com/nlplab/brat/issues/1149 return !(aFeatureValue == null || aFeatureValue.length() == 0 || aFeatureValue.equals(",")); } private void writeText(JCas aJCas) throws IOException { try (OutputStream docOS = getOutputStream(aJCas, textFilenameExtension)) { IOUtils.write(aJCas.getDocumentText(), docOS); } } }