All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.obolibrary.oboformat.writer.OBOFormatWriter Maven / Gradle / Ivy

There is a newer version: 5.5.0
Show newest version
package org.obolibrary.oboformat.writer;

import static org.semanticweb.owlapi.model.parameters.Navigation.IN_SUB_POSITION;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Serializable;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;

import org.obolibrary.obo2owl.OWLAPIObo2Owl;
import org.obolibrary.oboformat.model.Clause;
import org.obolibrary.oboformat.model.Frame;
import org.obolibrary.oboformat.model.Frame.FrameType;
import org.obolibrary.oboformat.model.OBODoc;
import org.obolibrary.oboformat.model.QualifierValue;
import org.obolibrary.oboformat.model.Xref;
import org.obolibrary.oboformat.parser.OBOFormatConstants;
import org.obolibrary.oboformat.parser.OBOFormatConstants.OboFormatTag;
import org.obolibrary.oboformat.parser.OBOFormatParser;
import org.obolibrary.oboformat.parser.OBOFormatParserException;
import org.semanticweb.owlapi.model.IRI;
import org.semanticweb.owlapi.model.OWLAnnotationAssertionAxiom;
import org.semanticweb.owlapi.model.OWLAnnotationSubject;
import org.semanticweb.owlapi.model.OWLAnnotationValue;
import org.semanticweb.owlapi.model.OWLLiteral;
import org.semanticweb.owlapi.model.OWLOntology;
import org.semanticweb.owlapi.model.parameters.Imports;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * The Class OBOFormatWriter.
 * 
 * @author Shahid Manzoor
 */
public class OBOFormatWriter {

    private static final Logger LOG = LoggerFactory.getLogger(OBOFormatWriter.class);
    @Nonnull
    private static final Set TAGSINFORMATIVE = buildTagsInformative();
    private boolean isCheckStructure = true;

    /**
     * @return true, if is check structure
     */
    public boolean isCheckStructure() {
        return isCheckStructure;
    }

    /**
     * @param isCheckStructure the new check structure
     */
    public void setCheckStructure(boolean isCheckStructure) {
        this.isCheckStructure = isCheckStructure;
    }

    @Nonnull
    private static Set buildTagsInformative() {
        Set set = new HashSet<>();
        set.add(OboFormatTag.TAG_IS_A.getTag());
        set.add(OboFormatTag.TAG_RELATIONSHIP.getTag());
        set.add(OboFormatTag.TAG_DISJOINT_FROM.getTag());
        set.add(OboFormatTag.TAG_INTERSECTION_OF.getTag());
        set.add(OboFormatTag.TAG_UNION_OF.getTag());
        set.add(OboFormatTag.TAG_EQUIVALENT_TO.getTag());
        // removed to be compatible with OBO-Edit
        // set.add( OboFormatTag.TAG_REPLACED_BY.getTag());
        set.add(OboFormatTag.TAG_PROPERTY_VALUE.getTag());
        set.add(OboFormatTag.TAG_DOMAIN.getTag());
        set.add(OboFormatTag.TAG_RANGE.getTag());
        set.add(OboFormatTag.TAG_INVERSE_OF.getTag());
        set.add(OboFormatTag.TAG_TRANSITIVE_OVER.getTag());
        // removed to be compatible with OBO-Edit
        // set.add( OboFormatTag.TAG_HOLDS_OVER_CHAIN.getTag());
        set.add(OboFormatTag.TAG_EQUIVALENT_TO_CHAIN.getTag());
        set.add(OboFormatTag.TAG_DISJOINT_OVER.getTag());
        return set;
    }

    /**
     * @param fn the file name to read in
     * @param writer the writer
     * @throws IOException Signals that an I/O exception has occurred.
     * @throws OBOFormatParserException the OBO format parser exception
     */
    public void write(@Nonnull String fn, @Nonnull BufferedWriter writer) throws IOException {
        if (fn.startsWith("http:")) {
            write(new URL(fn), writer);
        } else {
            BufferedReader reader = new BufferedReader(new FileReader(new File(fn)));
            try {
                write(reader, writer);
            } finally {
                reader.close();
            }
        }
    }

    /**
     * Write.
     * 
     * @param url the url
     * @param writer the writer
     * @throws IOException Signals that an I/O exception has occurred.
     * @throws OBOFormatParserException the OBO format parser exception
     */
    public void write(@Nonnull URL url, @Nonnull BufferedWriter writer) throws IOException {
        BufferedReader reader = new BufferedReader(new InputStreamReader(url.openStream()));
        write(reader, writer);
    }

    /**
     * @param reader the reader
     * @param writer the writer
     * @throws IOException Signals that an I/O exception has occurred.
     * @throws OBOFormatParserException the OBO format parser exception
     */
    public void write(BufferedReader reader, @Nonnull BufferedWriter writer) throws IOException {
        OBOFormatParser parser = new OBOFormatParser();
        OBODoc doc = parser.parse(reader);
        write(doc, writer);
    }

    /**
     * @param doc the doc
     * @param outFilename the out file name
     * @throws IOException Signals that an I/O exception has occurred.
     */
    public void write(@Nonnull OBODoc doc, @Nonnull String outFilename) throws IOException {
        write(doc, new File(outFilename));
    }

    /**
     * @param doc the doc
     * @param outFile the out file
     * @throws IOException Signals that an I/O exception has occurred.
     */
    public void write(@Nonnull OBODoc doc, @Nonnull File outFile) throws IOException {
        FileOutputStream os = new FileOutputStream(outFile);
        OutputStreamWriter osw = new OutputStreamWriter(os, "UTF-8");
        BufferedWriter bw = new BufferedWriter(osw);
        write(doc, bw);
        bw.close();
    }

    /**
     * @param doc the doc
     * @param writer the writer
     * @throws IOException Signals that an I/O exception has occurred.
     */
    public void write(@Nonnull OBODoc doc, @Nonnull BufferedWriter writer) throws IOException {
        NameProvider nameProvider = new OBODocNameProvider(doc);
        write(doc, writer, nameProvider);
    }

    /**
     * @param doc the doc
     * @param writer the writer
     * @param nameProvider the name provider
     * @throws IOException Signals that an I/O exception has occurred.
     */
    public void write(@Nonnull OBODoc doc, @Nonnull BufferedWriter writer,
        NameProvider nameProvider) throws IOException {
        if (isCheckStructure) {
            doc.check();
        }
        Frame headerFrame = doc.getHeaderFrame();
        writeHeader(headerFrame, writer, nameProvider);
        List termFrames = new ArrayList<>();
        termFrames.addAll(doc.getTermFrames());
        Collections.sort(termFrames, FramesComparator.INSTANCE);
        List typeDefFrames = new ArrayList<>();
        typeDefFrames.addAll(doc.getTypedefFrames());
        Collections.sort(typeDefFrames, FramesComparator.INSTANCE);
        List instanceFrames = new ArrayList<>();
        typeDefFrames.addAll(doc.getInstanceFrames());
        Collections.sort(instanceFrames, FramesComparator.INSTANCE);
        for (Frame f : termFrames) {
            write(f, writer, nameProvider);
        }
        for (Frame f : typeDefFrames) {
            write(f, writer, nameProvider);
        }
        for (Frame f : instanceFrames) {
            write(f, writer, nameProvider);
        }
        // to be save always flush writer
        writer.flush();
    }

    private static void writeLine(@Nonnull StringBuilder ln, @Nonnull BufferedWriter writer)
        throws IOException {
        ln.append('\n');
        writer.write(ln.toString());
    }

    private static void writeLine(String ln, @Nonnull BufferedWriter writer) throws IOException {
        writer.write(ln + '\n');
    }

    private static void writeEmptyLine(@Nonnull BufferedWriter writer) throws IOException {
        writer.write("\n");
    }

    @Nonnull
    private static List duplicateTags(@Nonnull Set src) {
        List tags = new ArrayList<>(src.size());
        for (String tag : src) {
            tags.add(tag);
        }
        return tags;
    }

    /**
     * Write header.
     * 
     * @param frame the frame
     * @param writer the writer
     * @param nameProvider the name provider
     * @throws IOException Signals that an I/O exception has occurred.
     */
    public void writeHeader(@Nonnull Frame frame, @Nonnull BufferedWriter writer,
        NameProvider nameProvider) throws IOException {
        List tags = duplicateTags(frame.getTags());
        Collections.sort(tags, OBOFormatConstants.headerPriority);
        // cmungall: Hardcoding 1.2 is deliberate.
        // obof1.2 never really had much of a spec to speak of, just a guide. We
        // initiated the attempt to make a proper spec and named this 1.4
        // (ignoring the abandoned common logic 1.3 spec...). Formally, 1.4 is
        // actually a subset of 1.2 (as formally as you can get with the 1.2
        // guide), because 1.2 allows open-ended tag values.
        // We opted to write 1.2 in the header because every document produced
        // should be a valid 1.2 doc, and we wanted people to be able to use it
        // without worrying about downstream underspecified ad-hoc parsers
        // throwing a wobbly when they see something other than 1.2 in the
        // header.
        write(new Clause(OboFormatTag.TAG_FORMAT_VERSION.getTag(), "1.2"), writer, nameProvider);
        for (String tag : tags) {
            if (tag.equals(OboFormatTag.TAG_FORMAT_VERSION.getTag())) {
                continue;
            }
            List clauses = new ArrayList<>(frame.getClauses(tag));
            Collections.sort(clauses, ClauseComparator.INSTANCE);
            for (Clause clause : clauses) {
                assert clause != null;
                if (tag.equals(OboFormatTag.TAG_SUBSETDEF.getTag())) {
                    writeSynonymtypedef(clause, writer);
                } else if (tag.equals(OboFormatTag.TAG_SYNONYMTYPEDEF.getTag())) {
                    writeSynonymtypedef(clause, writer);
                } else if (tag.equals(OboFormatTag.TAG_DATE.getTag())) {
                    writeHeaderDate(clause, writer);
                } else if (tag.equals(OboFormatTag.TAG_PROPERTY_VALUE.getTag())) {
                    writePropertyValue(clause, writer);
                } else if (tag.equals(OboFormatTag.TAG_IDSPACE.getTag())) {
                    writeIdSpace(clause, writer);
                } else {
                    write(clause, writer, nameProvider);
                }
            }
        }
        writeEmptyLine(writer);
    }

    /**
     * @param frame the frame
     * @param writer the writer
     * @param nameProvider the name provider
     * @throws IOException Signals that an I/O exception has occurred.
     */
    @SuppressWarnings("null")
    public void write(@Nonnull Frame frame, @Nonnull BufferedWriter writer,
        @Nullable NameProvider nameProvider) throws IOException {
        Comparator comparator = null;
        if (frame.getType() == FrameType.TERM) {
            writeLine("[Term]", writer);
            comparator = OBOFormatConstants.tagPriority;
        } else if (frame.getType() == FrameType.TYPEDEF) {
            writeLine("[Typedef]", writer);
            comparator = OBOFormatConstants.typeDefPriority;
        } else if (frame.getType() == FrameType.INSTANCE) {
            writeLine("[Instance]", writer);
            comparator = OBOFormatConstants.typeDefPriority;
        }
        if (frame.getId() != null) {
            Object label = frame.getTagValue(OboFormatTag.TAG_NAME);
            String extra = "";
            if (label == null && nameProvider != null) {
                // the name clause may not be present in this OBODoc - however,
                // the name provider may be able to provide one, in which case,
                // we
                // write it as a parser-invisible comment, thus preserving the
                // document structure but providing useful information for any
                // person that inspects the obo file
                label = nameProvider.getName(frame.getId());
                if (label != null) {
                    extra = " ! " + label;
                }
            }
            writeLine(OboFormatTag.TAG_ID.getTag() + ": " + frame.getId() + extra, writer);
        }
        List tags = duplicateTags(frame.getTags());
        Collections.sort(tags, comparator);
        String defaultOboNamespace = null;
        if (nameProvider != null) {
            defaultOboNamespace = nameProvider.getDefaultOboNamespace();
        }
        for (String tag : tags) {
            List clauses = new ArrayList<>(frame.getClauses(tag));
            Collections.sort(clauses, ClauseComparator.INSTANCE);
            for (Clause clause : clauses) {
                String clauseTag = clause.getTag();
                if (OboFormatTag.TAG_ID.getTag().equals(clauseTag)) {
                    continue;
                } else if (OboFormatTag.TAG_DEF.getTag().equals(clauseTag)) {
                    writeDef(clause, writer);
                } else if (OboFormatTag.TAG_SYNONYM.getTag().equals(clauseTag)) {
                    writeSynonym(clause, writer);
                } else if (OboFormatTag.TAG_PROPERTY_VALUE.getTag().equals(clauseTag)) {
                    writePropertyValue(clause, writer);
                } else if (OboFormatTag.TAG_EXPAND_EXPRESSION_TO.getTag().equals(clauseTag)
                    || OboFormatTag.TAG_EXPAND_ASSERTION_TO.getTag().equals(clauseTag)) {
                    writeClauseWithQuotedString(clause, writer);
                } else if (OboFormatTag.TAG_XREF.getTag().equals(clauseTag)) {
                    writeXRefClause(clause, writer);
                } else if (OboFormatTag.TAG_NAMESPACE.getTag().equals(clauseTag)) {
                    // only write OBO namespace,
                    // if it is different from the default OBO namespace
                    if (defaultOboNamespace == null
                        || !clause.getValue().equals(defaultOboNamespace)) {
                        write(clause, writer, nameProvider);
                    }
                } else {
                    write(clause, writer, nameProvider);
                }
            }
        }
        writeEmptyLine(writer);
    }

    private static void writeXRefClause(@Nonnull Clause clause, @Nonnull BufferedWriter writer)
        throws IOException {
        Xref xref = clause.getValue(Xref.class);
        if (xref != null) {
            StringBuilder sb = new StringBuilder();
            sb.append(clause.getTag());
            sb.append(": ");
            String idref = xref.getIdref();
            int colonPos = idref.indexOf(':');
            if (colonPos > 0) {
                sb.append(escapeOboString(idref.substring(0, colonPos), EscapeMode.xref));
                sb.append(':');
                sb.append(escapeOboString(idref.substring(colonPos + 1), EscapeMode.xref));
            } else {
                sb.append(escapeOboString(idref, EscapeMode.xref));
            }
            String annotation = xref.getAnnotation();
            if (annotation != null) {
                sb.append(" \"");
                sb.append(escapeOboString(annotation, EscapeMode.quotes));
                sb.append('"');
            }
            appendQualifiers(sb, clause);
            writeLine(sb, writer);
        }
    }

    private static void writeSynonymtypedef(@Nonnull Clause clause, @Nonnull BufferedWriter writer)
        throws IOException {
        StringBuilder sb = new StringBuilder();
        sb.append(clause.getTag());
        sb.append(": ");
        Iterator valuesIterator = clause.getValues().iterator();
        Collection values = clause.getValues();
        for (int i = 0; i < values.size(); i++) {
            String value = valuesIterator.next().toString();
            assert value != null;
            if (i == 1) {
                sb.append('"');
            }
            sb.append(escapeOboString(value, EscapeMode.quotes));
            if (i == 1) {
                sb.append('"');
            }
            if (valuesIterator.hasNext()) {
                sb.append(' ');
            }
        }
        appendQualifiers(sb, clause);
        writeLine(sb, writer);
    }

    private static void writeHeaderDate(@Nonnull Clause clause, @Nonnull BufferedWriter writer)
        throws IOException {
        StringBuilder sb = new StringBuilder();
        sb.append(clause.getTag());
        sb.append(": ");
        Object value = clause.getValue();
        assert value != null;
        if (value instanceof Date) {
            sb.append(OBOFormatConstants.headerDateFormat().format((Date) value));
        } else if (value instanceof String) {
            sb.append(value);
        } else {
            if (LOG.isWarnEnabled()) {
                LOG.warn("Unknown datatype ('{}') for value in clause: {}",
                    value.getClass().getName(), clause);
                sb.append(value);
            }
        }
        writeLine(sb, writer);
    }

    private static void writeIdSpace(@Nonnull Clause cl, @Nonnull BufferedWriter writer)
        throws IOException {
        StringBuilder sb = new StringBuilder(cl.getTag());
        sb.append(": ");
        Collection values = cl.getValues();
        int i = 0;
        Iterator iterator = values.iterator();
        while (iterator.hasNext() && i < 3) {
            String value = iterator.next().toString();
            assert value != null;
            if (i == 2) {
                sb.append('"').append(escapeOboString(value, EscapeMode.quotes)).append('"');
            } else {
                sb.append(escapeOboString(value, EscapeMode.simple)).append(' ');
            }
            i++;
        }
        appendQualifiers(sb, cl);
        writeLine(sb, writer);
    }

    private static void writeClauseWithQuotedString(@Nonnull Clause clause,
        @Nonnull BufferedWriter writer) throws IOException {
        StringBuilder sb = new StringBuilder();
        sb.append(clause.getTag());
        sb.append(": ");
        boolean first = true;
        Iterator valuesIterator = clause.getValues().iterator();
        while (valuesIterator.hasNext()) {
            if (first) {
                sb.append('"');
            }
            String value = valuesIterator.next().toString();
            assert value != null;
            sb.append(escapeOboString(value, EscapeMode.quotes));
            if (first) {
                sb.append('"');
            }
            if (valuesIterator.hasNext()) {
                sb.append(' ');
            }
            first = false;
        }
        Collection xrefs = clause.getXrefs();
        // If the xref list is null, then there should *never* be xref values at
        // this location.
        // Note that the value may be a non-null empty list - here we still want
        // to write []
        if (!xrefs.isEmpty()) {
            appendXrefs(sb, xrefs);
        } else if (OboFormatTag.TAG_DEF.getTag().equals(clause.getTag())
            || OboFormatTag.TAG_SYNONYM.getTag().equals(clause.getTag())
            || OboFormatTag.TAG_EXPAND_EXPRESSION_TO.getTag().equals(clause.getTag())
            || OboFormatTag.TAG_EXPAND_ASSERTION_TO.getTag().equals(clause.getTag())) {
            sb.append(" []");
        }
        appendQualifiers(sb, clause);
        writeLine(sb, writer);
    }

    private static void appendXrefs(@Nonnull StringBuilder sb, @Nonnull Collection xrefs) {
        List sortedXrefs = new ArrayList<>(xrefs);
        Collections.sort(sortedXrefs, XrefComparator.INSTANCE);
        sb.append(" [");
        Iterator xrefsIterator = sortedXrefs.iterator();
        while (xrefsIterator.hasNext()) {
            Xref current = xrefsIterator.next();
            String idref = current.getIdref();
            int colonPos = idref.indexOf(':');
            if (colonPos > 0) {
                sb.append(escapeOboString(idref.substring(0, colonPos), EscapeMode.xrefList));
                sb.append(':');
                sb.append(escapeOboString(idref.substring(colonPos + 1), EscapeMode.xrefList));
            } else {
                sb.append(escapeOboString(idref, EscapeMode.xrefList));
            }
            String annotation = current.getAnnotation();
            if (annotation != null) {
                sb.append(' ');
                sb.append('"');
                sb.append(escapeOboString(annotation, EscapeMode.quotes));
                sb.append('"');
            }
            if (xrefsIterator.hasNext()) {
                sb.append(", ");
            }
        }
        sb.append(']');
    }

    /**
     * Write def.
     * 
     * @param clause the clause
     * @param writer the writer
     * @throws IOException Signals that an I/O exception has occurred.
     */
    public static void writeDef(@Nonnull Clause clause, @Nonnull BufferedWriter writer)
        throws IOException {
        writeClauseWithQuotedString(clause, writer);
    }

    /**
     * Write property value.
     * 
     * @param clause the clause
     * @param writer the writer
     * @throws IOException Signals that an I/O exception has occurred.
     */
    public static void writePropertyValue(Clause clause, BufferedWriter writer) throws IOException {
        Collection cols = clause.getValues();
        if (cols.size() < 2) {
            LOG.error("The {} has incorrect number of values: {}",
                OboFormatTag.TAG_PROPERTY_VALUE.getTag(), clause);
            return;
        }
        Object v = clause.getValue();
        Object v2 = clause.getValue2();
        assert v != null;
        assert v2 != null;
        StringBuilder sb = new StringBuilder();
        sb.append(clause.getTag());
        sb.append(": ");
        sb.append(escapeOboString(v.toString(), EscapeMode.simple));
        sb.append(' ');
        if (cols.size() == 2) {
            sb.append(escapeOboString(v2.toString(), EscapeMode.simple));
        } else if (cols.size() == 3) {
            Iterator it = clause.getValues().iterator();
            it.next();
            it.next();
            String v3String = (String) it.next();
            sb.append('"');
            sb.append(escapeOboString(v2.toString(), EscapeMode.quotes));
            sb.append('"');
            sb.append(' ');
            sb.append(escapeOboString(v3String, EscapeMode.simple));
        }
        appendQualifiers(sb, clause);
        writeLine(sb, writer);
    }

    /**
     * Write synonym.
     * 
     * @param clause the clause
     * @param writer the writer
     * @throws IOException Signals that an I/O exception has occurred.
     */
    public static void writeSynonym(@Nonnull Clause clause, @Nonnull BufferedWriter writer)
        throws IOException {
        writeClauseWithQuotedString(clause, writer);
    }

    /**
     * Write.
     * 
     * @param clause the clause
     * @param writer the writer
     * @param nameProvider the name provider
     * @throws IOException Signals that an I/O exception has occurred.
     */
    public static void write(@Nonnull Clause clause, @Nonnull BufferedWriter writer,
        @Nullable NameProvider nameProvider) throws IOException {
        if (OboFormatTag.TAG_IS_OBSELETE.getTag().equals(clause.getTag())) {
            // only write the obsolete tag if the value is Boolean.TRUE or
            // "true"
            Object value = clause.getValue();
            if (value instanceof Boolean) {
                if (Boolean.FALSE.equals(value)) {
                    return;
                }
            } else {
                // also check for a String representation of Boolean.TRUE
                if (!Boolean.TRUE.toString().equals(value)) {
                    return;
                }
            }
        }
        StringBuilder sb = new StringBuilder();
        sb.append(clause.getTag());
        sb.append(": ");
        Iterator valuesIterator = clause.getValues().iterator();
        StringBuilder idsLabel = null;
        if (nameProvider != null && TAGSINFORMATIVE.contains(clause.getTag())) {
            idsLabel = new StringBuilder();
        }
        while (valuesIterator.hasNext()) {
            String value = valuesIterator.next().toString();
            assert value != null;
            if (idsLabel != null && nameProvider != null) {
                String label = nameProvider.getName(value);
                if (label != null && (isOpaqueIdentifier(value) || !valuesIterator.hasNext())) {
                    // only print label if the label exists
                    // and the label is different from the id
                    // relationships: ID part_of LABEL part_of
                    if (idsLabel.length() > 0) {
                        idsLabel.append(' ');
                    }
                    idsLabel.append(label);
                }
            }
            EscapeMode mode = EscapeMode.most;
            if (OboFormatTag.TAG_COMMENT.getTag().equals(clause.getTag())) {
                mode = EscapeMode.parenthesis;
            }
            sb.append(escapeOboString(value, mode));
            if (valuesIterator.hasNext()) {
                sb.append(' ');
            }
        }
        Collection xrefs = clause.getXrefs();
        if (!xrefs.isEmpty()) {
            appendXrefs(sb, xrefs);
        }
        appendQualifiers(sb, clause);
        if (idsLabel != null && idsLabel.length() > 0) {
            String trimmed = idsLabel.toString().trim();
            if (!trimmed.isEmpty()) {
                sb.append(" ! ");
                sb.append(trimmed);
            }
        }
        writeLine(sb, writer);
    }

    private static boolean isOpaqueIdentifier(@Nullable String value) {
        boolean result = false;
        if (value != null && !value.isEmpty()) {
            // check for colon
            int colonPos = value.indexOf(':');
            // check that the suffix after the colon contains only digits
            if (colonPos > 0 && value.length() > colonPos + 1) {
                result = true;
                for (int i = colonPos; i < value.length(); i++) {
                    char c = value.charAt(i);
                    if (!Character.isDigit(c) && c != ':') {
                        result = false;
                        break;
                    }
                }
            }
        }
        return result;
    }

    private static void appendQualifiers(@Nonnull StringBuilder sb, @Nonnull Clause clause) {
        Collection qvs = clause.getQualifierValues();
        if (!qvs.isEmpty()) {
            sb.append(" {");
            Iterator qvsIterator = qvs.iterator();
            while (qvsIterator.hasNext()) {
                QualifierValue qv = qvsIterator.next();
                sb.append(qv.getQualifier());
                sb.append("=\"");
                sb.append(escapeOboString(qv.getValue(), EscapeMode.quotes));
                sb.append('"');
                if (qvsIterator.hasNext()) {
                    sb.append(", ");
                }
            }
            sb.append('}');
        }
    }

    /** The Enum EscapeMode. */
    private enum EscapeMode {
        /** all except xref and xrefList. */
        most,
        /** simple + parenthesis. */
        parenthesis,
        /** simple + quotes. */
        quotes,
        /** simple + comma + colon. */
        xref,
        /** xref + closing brackets. */
        xrefList,
        /** newline and backslash. */
        simple
    }

    @Nonnull
    private static CharSequence escapeOboString(@Nonnull String in, EscapeMode mode) {
        boolean modfied = false;
        StringBuilder sb = new StringBuilder();
        int length = in.length();
        for (int i = 0; i < length; i++) {
            char c = in.charAt(i);
            if (c == '\n') {
                modfied = true;
                sb.append("\\n");
            } else if (c == '\\') {
                modfied = true;
                sb.append("\\\\");
            } else if (c == '"' && (mode == EscapeMode.most || mode == EscapeMode.quotes)) {
                modfied = true;
                sb.append("\\\"");
            } else if (c == '{' && (mode == EscapeMode.most || mode == EscapeMode.parenthesis)) {
                modfied = true;
                sb.append("\\{");
            } else if (c == '}' && (mode == EscapeMode.most || mode == EscapeMode.parenthesis)) {
                modfied = true;
                sb.append("\\}");
            } else if (c == ',' && (mode == EscapeMode.xref || mode == EscapeMode.xrefList)) {
                modfied = true;
                sb.append("\\,");
            } else if (c == ':' && (mode == EscapeMode.xref || mode == EscapeMode.xrefList)) {
                modfied = true;
                sb.append("\\:");
            } else if (c == ']' && mode == EscapeMode.xrefList) {
                modfied = true;
                sb.append("\\]");
            } else if (c == '[' && mode == EscapeMode.xrefList) {
                modfied = true;
                sb.append("\\[");
            } else {
                sb.append(c);
            }
        }
        if (modfied) {
            return sb;
        }
        return in;
    }


    /** The Class ClauseListComparator. */
    private static class ClauseListComparator implements Comparator, Serializable {

        protected static final ClauseListComparator INSTANCE = new ClauseListComparator();
        private static final long serialVersionUID = 40000L;

        @Override
        public int compare(Clause o1, Clause o2) {
            String t1 = o1.getTag();
            String t2 = o2.getTag();
            int compare = OBOFormatConstants.tagPriority.compare(t1, t2);
            if (compare == 0) {
                compare = ClauseComparator.INSTANCE.compare(o1, o2);
            }
            return compare;
        }
    }

    /**
     * Sort a list of term frame clauses according to in the OBO format specified tag and value
     * order.
     * 
     * @param clauses the clauses
     */
    public static void sortTermClauses(@Nonnull List clauses) {
        Collections.sort(clauses, ClauseListComparator.INSTANCE);
    }

    /** The Class FramesComparator. */
    private static class FramesComparator implements Comparator, Serializable {

        static final FramesComparator INSTANCE = new FramesComparator();
        private static final long serialVersionUID = 40000L;

        @Override
        public int compare(Frame o1, Frame o2) {
            return o1.getId().compareTo(o2.getId());
        }
    }

    /**
     * This comparator sorts clauses with the same tag in the specified write order.
     */
    private static class ClauseComparator implements Comparator, Serializable {

        static final ClauseComparator INSTANCE = new ClauseComparator();
        private static final long serialVersionUID = 40000L;

        @Override
        public int compare(Clause o1, Clause o2) {
            // special case for intersections
            String tag = o1.getTag();
            if (OboFormatTag.TAG_INTERSECTION_OF.getTag().equals(tag)) {
                // sort by values size, prefer short ones.
                int s1 = o1.getValues().size();
                int s2 = o2.getValues().size();
                if (s1 < s2) {
                    return -1;
                } else if (s1 > s2) {
                    return 1;
                }
            }
            // sort by value
            int comp = compareValues(o1.getValue(), o2.getValue());
            if (comp != 0) {
                return comp;
            }
            return compareValues(o1.getValue2(), o2.getValue2());
        }

        /**
         * Compare values.
         * 
         * @param o1 the first object
         * @param o2 the second
         * @return comparison value
         */
        @SuppressWarnings("null")
        private static int compareValues(@Nullable Object o1, @Nullable Object o2) {
            if (o1 == null && o2 == null) {
                return 0;
            }
            if (o1 == null) {
                return -1;
            }
            if (o2 == null) {
                return 1;
            }
            String s1 = toStringRepresentation(o1);
            String s2 = toStringRepresentation(o2);
            int comp = s1.compareToIgnoreCase(s2);
            if (comp == 0) {
                // normally ignore case, for sorting
                // but if the strings are equal,
                // try again with case check
                comp = s1.compareTo(s2);
            }
            return comp;
        }

        /**
         * @param obj the object
         * @return string representation
         */
        @Nullable
        private static String toStringRepresentation(@Nullable Object obj) {
            String s = null;
            if (obj != null) {
                if (obj instanceof Xref) {
                    Xref xref = (Xref) obj;
                    s = xref.getIdref() + ' ' + xref.getAnnotation();
                } else if (obj instanceof String) {
                    s = (String) obj;
                } else {
                    s = obj.toString();
                }
            }
            return s;
        }
    }

    /** The Class XrefComparator. */
    private static class XrefComparator implements Comparator, Serializable {

        static final XrefComparator INSTANCE = new XrefComparator();
        private static final long serialVersionUID = 40000L;

        @Override
        public int compare(Xref o1, Xref o2) {
            String idref1 = o1.getIdref();
            String idref2 = o2.getIdref();
            return idref1.compareToIgnoreCase(idref2);
        }
    }

    /**
     * Provide names for given OBO identifiers. This abstraction layer allows to find names from
     * different sources, including {@link OBODoc}.
     */
    public interface NameProvider {

        /**
         * Try to retrieve the valid name for the given identifier. If not available return null.
         * 
         * @param id identifier
         * @return name or null
         */
        @Nullable
        String getName(@Nonnull String id);

        /**
         * Retrieve the default OBO namespace.
         * 
         * @return default OBO namespace or null
         */
        @Nullable
        String getDefaultOboNamespace();
    }

    /**
     * Default implementation of a {@link NameProvider} using an underlying. {@link OBODoc}.
     */
    public static class OBODocNameProvider implements NameProvider {

        @Nonnull
        private final OBODoc oboDoc;
        @Nullable
        private final String defaultOboNamespace;

        /**
         * Instantiates a new OBO doc name provider.
         * 
         * @param oboDoc the obo doc
         */
        public OBODocNameProvider(@Nonnull OBODoc oboDoc) {
            this.oboDoc = oboDoc;
            Frame headerFrame = oboDoc.getHeaderFrame();
            if (headerFrame != null) {
                defaultOboNamespace =
                    headerFrame.getTagValue(OboFormatTag.TAG_DEFAULT_NAMESPACE, String.class);
            } else {
                defaultOboNamespace = null;
            }
        }

        @Nullable
        @Override
        public String getName(String id) {
            String name = null;
            Frame frame = oboDoc.getTermFrame(id);
            if (frame == null) {
                frame = oboDoc.getTypedefFrame(id);
            }
            if (frame != null) {
                Clause cl = frame.getClause(OboFormatTag.TAG_NAME);
                if (cl != null) {
                    name = cl.getValue(String.class);
                }
            }
            return name;
        }

        @Nullable
        @Override
        public String getDefaultOboNamespace() {
            return defaultOboNamespace;
        }
    }

    /**
     * Alternative implementation to lookup labels in an {@link OWLOntology}. 
* This implementation might be a bit slower as it involves additional id conversion back into * OWL. */ public static class OWLOntologyNameProvider implements NameProvider { @Nonnull private final OWLOntology ont; @Nullable private final String defaultOboNamespace; private final OBODoc result; /** * @param ont ontology * @param defaultOboNamespace default OBO namespace * @param result result */ public OWLOntologyNameProvider(@Nonnull OWLOntology ont, String defaultOboNamespace, OBODoc result) { this.ont = ont; this.defaultOboNamespace = defaultOboNamespace; this.result = result; } @Override public String getName(String id) { // convert OBO id to IRI OWLAPIObo2Owl obo2owl = new OWLAPIObo2Owl(ont.getOWLOntologyManager()); obo2owl.setObodoc(result); IRI iri = obo2owl.oboIdToIRI(id); // look for label of entity Set axioms = ont.getAxioms(OWLAnnotationAssertionAxiom.class, OWLAnnotationSubject.class, iri, Imports.INCLUDED, IN_SUB_POSITION); for (OWLAnnotationAssertionAxiom axiom : axioms) { if (axiom.getProperty().isLabel()) { OWLAnnotationValue value = axiom.getValue(); if (value instanceof OWLLiteral) { return ((OWLLiteral) value).getLiteral(); } } } return null; } @Override public String getDefaultOboNamespace() { return defaultOboNamespace; } } }