de.tudarmstadt.ukp.dkpro.core.io.brat.BratWriter.java Source code

Java tutorial

Introduction

Here is the source code for de.tudarmstadt.ukp.dkpro.core.io.brat.BratWriter.java

Source

/*
 * Copyright 2015
 * Ubiquitous Knowledge Processing (UKP) Lab
 * Technische Universitt 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);
        }
    }
}