package org.obolibrary.robot;

import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nonnull;
import org.obolibrary.robot.exceptions.ColumnException;
import org.obolibrary.robot.exceptions.RowParseException;
import org.semanticweb.owlapi.apibinding.OWLManager;
import org.semanticweb.owlapi.manchestersyntax.parser.ManchesterOWLSyntaxClassExpressionParser;
import org.semanticweb.owlapi.model.*;
import org.semanticweb.owlapi.util.SimpleShortFormProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/** @author <a href="mailto:[email protected]">Becky Tauber</a> */
public class Template {

  /** Logger */
  private static final Logger logger = LoggerFactory.getLogger(Template.class);

  /** Template IOHelper to resolve prefixes. */
  private IOHelper ioHelper;

  /** Template QuotedEntityChecker to get entities and IRIs by label. */
  private QuotedEntityChecker checker;

  /** Manchester Syntax parser to parse class expressions. */
  private ManchesterOWLSyntaxClassExpressionParser parser;

  /** Set of axioms generated from template. */
  private Set<OWLAxiom> axioms;

  /** Name of the table. */
  private String name;

  /** Location of IDs (ID). */
  private int idColumn = -1;

  /** Location of labels (LABEL, A rdfs:label, A label). */
  private int labelColumn = -1;

  /** Location of entity types (TYPE). */
  private int typeColumn = -1;

  /** Location of class types (CLASS_TYPE). */
  private int classTypeColumn = -1;

  /** Location of property types (PROPERTY_TYPE). */
  private int propertyTypeColumn = -1;

  /** Location of property characteristic (CHARACTERISTIC). */
  private int characteristicColumn = -1;

  /** Location of individual types (INDIVIDUAL_TYPE). */
  private int individualTypeColumn = -1;

  /** Character to split property characteristics on. */
  private String characteristicSplit = null;

  /** Character to split generic types on. */
  private String typeSplit = null;

  /** List of human-readable template headers. */
  private List<String> headers;

  /** List of ROBOT template strings. */
  private List<String> templates;

  /** All other rows of the table (does not include headers and template strings). */
  private List<List<String>> tableRows;

  /** Row number tracker. Start with 2 to skip headers. */
  private int rowNum = 2;

  /** Shared data factory. */
  private final OWLDataFactory dataFactory = OWLManager.getOWLDataFactory();

  /** Namespace for error messages. */
  private static final String NS = "template#";

  /** Error message when an annotation property has a characteristic. */
  private static final String annotationPropertyCharacteristicError =
      NS
          + "ANNOTATION PROPERTY CHARACTERISTIC ERROR annotation property '%s' should not have any characteristics at line %d, column %d in table \"%s\"";

  /** Error message when an annotation property gets a property type other than subproperty. */
  private static final String annotationPropertyTypeError =
      NS
          + "ANNOTATION PROPERTY TYPE ERROR annotation property %s type '%s' must be 'subproperty' at row %d, column %d in table \"%s\".";

  /** Error message when an invalid class type is provided. */
  private static final String classTypeError =
      NS + "CLASS TYPE ERROR class %s has unknown type '%s' at row %d, column %d in table \"%s\".";

  /** Error message when CLASS_TYPE has a SPLIT. */
  private static final String classTypeSplitError =
      NS
          + "CLASS TYPE SPLIT ERROR the SPLIT functionality should not be used for CLASS_TYPE in column %d in table \"%s\".";

  /**
   * Error message when a template column does not have a header. Expects: column number, table name
   */
  private static final String columnMismatchError =
      NS
          + "COLUMN MISMATCH ERROR the template string in column %d must have a corresponding header in table \"%s\"";

  /** Error message when a data property has a characteristic other than 'functional'. */
  private static final String dataPropertyCharacteristicError =
      NS
          + "DATA PROPERTY CHARACTERISTIC ERROR data property '%s' can only have characteristic 'functional' at line %d, column %d in table \"%s\".";

  /** Error message when an invalid individual type is provided. */
  private static final String individualTypeError =
      NS
          + "INDIVIDUAL TYPE ERROR individual %s has unknown type '%s' at row %d, column %d in table \"%s\".";

  /** Error message when INDIVIDUAL_TYPE has a SPLIT. */
  private static final String individualTypeSplitError =
      NS
          + "INDIVIDUAL TYPE SPLIT ERROR the SPLIT functionality should not be used for INDIVIDUAL_TYPE in column %d in table \"%s\".";

  /** Error message when an invalid property type is provided. */
  private static final String propertyTypeError =
      NS
          + "PROPERTY TYPE ERROR property %s has unknown type '%s' at row %d, column %d in table \"%s\".";

  /** Error message when more than one logical type is used in PROPERTY_TYPE. */
  private static final String propertyTypeSplitError =
      NS
          + "PROPERTY TYPE SPLIT ERROR the SPLIT functionality should not be used for PROPERTY_TYPE in column %d in table \"%s\".";

  /** Error message when property characteristic not valid. */
  private static final String unknownCharacteristicError =
      NS
          + "UNKNOWN CHARACTERISTIC ERROR property '%s' has unknown characteristic '%s' at line %d, column %d in table \"%s\".";

  /**
   * Error message when a template cannot be understood. Expects: table name, column number, column
   * name, template.
   */
  private static final String unknownTemplateError =
      NS
          + "UNKNOWN TEMPLATE ERROR could not interpret template string \"%4$s\" for column %2$d (\"%3$s\") in table \"%1$s\".";

  private static final String unknownEntityError =
      NS
          + "UNKNOWN ENTITY ERROR could not interpret '%1$s' in row %2$d, column %3$d (\"%4$s\") in table \"%5$s\".";

  private static final List<String> validClassTypes =
      new ArrayList<>(Arrays.asList("subclass", "disjoint", "equivalent"));

  /**
   * Given a template name and a list of rows, create a template object with a new IOHelper and
   * QuotedEntityChecker. The rows are added to the object, new labels from the rows are added to
   * the checker, and a Manchester Syntax parser is created.
   *
   * @param name template name
   * @param rows list of rows (lists)
   * @throws Exception on issue creating IOHelper or adding table to template object
   */
  public Template(@Nonnull String name, @Nonnull List<List<String>> rows) throws Exception {
    this.name = name;
    this.ioHelper = new IOHelper();
    tableRows = new ArrayList<>();
    templates = new ArrayList<>();
    headers = new ArrayList<>();
    axioms = new HashSet<>();

    checker = new QuotedEntityChecker();
    checker.setIOHelper(this.ioHelper);
    checker.addProvider(new SimpleShortFormProvider());

    // Add the contents of the tableRows
    addTable(rows);
    addLabels();
    createParser();
  }

  /**
   * Given a template name, a list of rows, and an IOHelper, create a template object with a new
   * QuotedEntityChecker. The rows are added to the object, new labels from the rows are added to
   * the checker, and a Manchester Syntax parser is created.
   *
   * @param name template name
   * @param rows list of rows (lists)
   * @param ioHelper IOHelper to resolve prefixes
   * @throws Exception on issue adding table to template object
   */
  public Template(@Nonnull String name, @Nonnull List<List<String>> rows, IOHelper ioHelper)
      throws Exception {
    this.name = name;
    this.ioHelper = ioHelper;
    tableRows = new ArrayList<>();
    templates = new ArrayList<>();
    headers = new ArrayList<>();
    axioms = new HashSet<>();

    checker = new QuotedEntityChecker();
    checker.setIOHelper(this.ioHelper);
    checker.addProvider(new SimpleShortFormProvider());
    checker.addProperty(dataFactory.getRDFSLabel());

    // Add the contents of the tableRows
    addTable(rows);
    addLabels();
    createParser();
  }

  /**
   * Given a template name, a list of rows, and an input ontology, create a template object with a
   * new IOHelper and QuotedEntityChecker populated by the input ontology. The rows are added to the
   * object, new labels from the rows are added to the checker, and a Manchester Syntax parser is
   * created.
   *
   * @param name template name
   * @param rows list of rows (lists)
   * @param inputOntology OWLOntology to get labels of entities for QuotedEntityChecker
   * @throws Exception on issue creating IOHelper or adding table to template object
   */
  public Template(@Nonnull String name, @Nonnull List<List<String>> rows, OWLOntology inputOntology)
      throws Exception {
    this.name = name;
    ioHelper = new IOHelper();
    tableRows = new ArrayList<>();
    templates = new ArrayList<>();
    headers = new ArrayList<>();
    axioms = new HashSet<>();

    checker = new QuotedEntityChecker();
    checker.setIOHelper(this.ioHelper);
    checker.addProvider(new SimpleShortFormProvider());
    checker.addProperty(dataFactory.getRDFSLabel());
    if (inputOntology != null) {
      checker.addAll(inputOntology);
    }

    // Add the contents of the tableRows
    addTable(rows);
    addLabels();
    createParser();
  }

  /**
   * Given a template name, a list of rows, an input ontology, and an IOHelper, create a template
   * object with a new QuotedEntityChecker with the IOHelper populated by the input ontology. The
   * rows are added to the object, new labels from the rows are added to the checker, and a
   * Manchester Syntax parser is created.
   *
   * @param name template name
   * @param rows list of rows (lists)
   * @param inputOntology OWLOntology to get labels of entities for QuotedEntityChecker
   * @param ioHelper IOHelper to resolve prefixes
   * @throws Exception on issue adding table to template object
   */
  public Template(
      @Nonnull String name,
      @Nonnull List<List<String>> rows,
      OWLOntology inputOntology,
      IOHelper ioHelper)
      throws Exception {
    this.name = name;
    this.ioHelper = ioHelper;
    tableRows = new ArrayList<>();
    templates = new ArrayList<>();
    headers = new ArrayList<>();
    axioms = new HashSet<>();

    checker = new QuotedEntityChecker();
    checker.setIOHelper(this.ioHelper);
    checker.addProvider(new SimpleShortFormProvider());
    checker.addProperty(dataFactory.getRDFSLabel());
    if (inputOntology != null) {
      checker.addAll(inputOntology);
    }

    // Add the contents of the tableRows
    addTable(rows);
    addLabels();
    createParser();
  }

  /**
   * Given a template name, a list of rows, an IOHelper, and a QuotedEntityChecker, create a
   * template object. The rows are added to the object, new labels from the rows are added to the
   * checker, and a Manchester Syntax parser is created.
   *
   * @param name template name
   * @param rows list of rows (lists)
   * @param inputOntology OWLOntology to get labels of entities for QuotedEntityChecker
   * @param ioHelper IOHelper to resolve prefixes
   * @param checker QuotedEntityChecker to get entities by label
   * @throws Exception on issue adding table to template object
   */
  public Template(
      @Nonnull String name,
      @Nonnull List<List<String>> rows,
      OWLOntology inputOntology,
      IOHelper ioHelper,
      QuotedEntityChecker checker)
      throws Exception {
    this.name = name;
    this.ioHelper = ioHelper;
    if (checker == null) {
      this.checker = new QuotedEntityChecker();
      this.checker.setIOHelper(this.ioHelper);
      this.checker.addProvider(new SimpleShortFormProvider());
      this.checker.addProperty(dataFactory.getRDFSLabel());
    } else {
      this.checker = checker;
    }
    tableRows = new ArrayList<>();
    templates = new ArrayList<>();
    headers = new ArrayList<>();
    axioms = new HashSet<>();

    if (inputOntology != null) {
      this.checker.addAll(inputOntology);
    }

    // Add the contents of the tableRows
    addTable(rows);
    addLabels();
    createParser();
    parser.setOWLEntityChecker(this.checker);
  }

  /**
   * Return the QuotedEntityChecker.
   *
   * @return QuotedEntityChecker
   */
  public QuotedEntityChecker getChecker() {
    return checker;
  }

  /**
   * Generate an OWLOntology based on the rows of the template.
   *
   * @return new OWLOntology
   * @throws Exception on issue parsing rows to axioms or creating new ontology
   */
  public OWLOntology generateOutputOntology() throws Exception {
    return generateOutputOntology(null, false);
  }

  /**
   * Generate an OWLOntology with given IRI based on the rows of the template.
   *
   * @param outputIRI IRI for final ontology
   * @param force if true, do not exit on errors
   * @return new OWLOntology
   * @throws Exception on issue parsing rows to axioms or creating new ontology
   */
  public OWLOntology generateOutputOntology(String outputIRI, boolean force) throws Exception {
    // Set to true on first exception
    boolean hasException = false;

    for (List<String> row : tableRows) {
      try {
        processRow(row);
      } catch (RowParseException e) {
        // If force = false, fail on the first exception
        if (!force) {
          throw e;
        }
        // otherwise print exceptions as they show up
        hasException = true;
        logger.error(e.getMessage().substring(e.getMessage().indexOf("#") + 1));
      }
    }

    if (hasException) {
      logger.warn("Ontology created from template with errors");
    }

    // Create a new ontology object to add axioms to
    OWLOntologyManager manager = OWLManager.createOWLOntologyManager();
    OWLOntology outputOntology;
    if (outputIRI != null) {
      IRI iri = IRI.create(outputIRI);
      outputOntology = manager.createOntology(iri);
    } else {
      outputOntology = manager.createOntology();
    }

    manager.addAxioms(outputOntology, axioms);

    return outputOntology;
  }

  /**
   * Given a list of rows for a table, first validate the headers and template strings. Then, get
   * the location of important columns (e.g. IDs and labels). Finally, add all template rows to the
   * object.
   *
   * @param rows list of rows (lists)
   * @throws Exception on malformed template
   */
  private void addTable(List<List<String>> rows) throws Exception {
    // Get and validate headers
    headers = rows.get(0);
    templates = rows.get(1);

    for (int column = 0; column < templates.size(); column++) {
      String template = templates.get(column).trim();
      if (template.isEmpty()) {
        // If the template is empty, skip this column
        continue;
      }

      // Validate that there's a header in this column
      // It's OK if there's a header without a template
      String header;
      try {
        header = headers.get(column).trim();
      } catch (IndexOutOfBoundsException e) {
        // Template row is longer than header row
        // Which means there is at least one header missing
        throw new ColumnException(String.format(columnMismatchError, column + 1, name));
      }
      if (header.isEmpty()) {
        // Template string is not empty
        // Header string is empty
        throw new ColumnException(String.format(columnMismatchError, column + 1, name));
      }

      // Validate the template string
      if (!TemplateHelper.validateTemplateString(template)) {
        throw new ColumnException(
            String.format(unknownTemplateError, name, column + 1, headers.get(column), template));
      }

      // Get the location of important columns
      // If it is an annotation, check if it resolves to RDFS label
      if (template.startsWith("A ")) {
        String property = template.substring(2);
        maybeSetLabelColumn(property, column);
      } else if (template.startsWith("AT ")) {
        String property;
        if (template.contains("^^")) {
          property = template.substring(3, template.indexOf("^^")).trim();
        } else {
          property = template.substring(3).trim();
        }
        maybeSetLabelColumn(property, column);
      } else if (template.startsWith("AL ")) {
        String property;
        if (template.contains("@")) {
          property = template.substring(3, template.indexOf("@")).trim();
        } else {
          property = template.substring(3).trim();
        }
        maybeSetLabelColumn(property, column);
      } else if (template.startsWith("AI ")) {
        String property = template.substring(3);
        maybeSetLabelColumn(property, column);
      } else if (template.equals("ID")) {
        // Unique identifier (CURIE, IRI...)
        idColumn = column;
      } else if (template.equals("LABEL")) {
        // Label identifier
        labelColumn = column;
      } else if (template.startsWith("TYPE")) {
        // Entity type
        typeColumn = column;
        if (template.contains("SPLIT=")) {
          typeSplit = template.substring(template.indexOf("SPLIT=") + 6);
        }
      } else if (template.startsWith("CLASS_TYPE")) {
        // Class expression type
        classTypeColumn = column;
        if (template.contains("SPLIT=")) {
          // Classes should only have one class type
          throw new ColumnException(String.format(classTypeSplitError, column, name));
        }
      } else if (template.startsWith("PROPERTY_TYPE")) {
        // Property expression type
        propertyTypeColumn = column;
        if (template.contains("SPLIT=")) {
          // Instances should only have one individual type
          throw new ColumnException(String.format(propertyTypeSplitError, column, name));
        }
      } else if (template.startsWith("INDIVIDUAL_TYPE")) {
        // Individual expression type
        individualTypeColumn = column;
        if (template.contains("SPLIT=")) {
          // Instances should only have one individual type
          throw new ColumnException(String.format(individualTypeSplitError, column, name));
        }
      } else if (template.startsWith("CHARACTERISTIC")) {
        // Property characteristic
        characteristicColumn = column;
        if (template.contains("SPLIT=")) {
          characteristicSplit = template.substring(template.indexOf("SPLIT=") + 6);
        }
      }
    }

    // Each template needs a way to identify the entities
    // Without one, we cannot continue
    if (idColumn == -1 && labelColumn == -1) {
      throw new ColumnException(
          "Template row must include an \"ID\" or \"LABEL\" column in table: " + name);
    }

    // Add the rest of the tableRows to Template
    for (int row = 2; row < rows.size(); row++) {
      tableRows.add(rows.get(row));
    }
  }

  /** Add the labels from the rows of the template to the QuotedEntityChecker. */
  private void addLabels() {
    // If there's no label column, we can't add labels
    if (labelColumn == -1) {
      return;
    }
    for (List<String> row : tableRows) {
      String id = null;
      if (idColumn != -1) {
        try {
          id = row.get(idColumn);
        } catch (IndexOutOfBoundsException e) {
          // ignore
        }
      }

      String label = null;
      try {
        label = row.get(labelColumn);
      } catch (IndexOutOfBoundsException e) {
        // ignore
      }

      if (idColumn != -1 && id == null) {
        continue;
      }

      if (id == null || label == null) {
        continue;
      }

      String type = null;
      if (typeColumn != -1) {
        try {
          type = row.get(typeColumn);
        } catch (IndexOutOfBoundsException e) {
          // ignore
        }
      }
      if (type == null || type.trim().isEmpty()) {
        type = "class";
      }

      IRI iri = ioHelper.createIRI(id, true);
      if (iri == null) {
        iri = IRI.create(id);
      }

      // Try to resolve a CURIE
      IRI typeIRI = ioHelper.createIRI(type, true);

      // Set to IRI string or to type string
      String typeOrIRI = type;
      if (typeIRI != null) {
        typeOrIRI = typeIRI.toString();
      }

      OWLEntity entity;
      switch (typeOrIRI) {
        case "":
        case "http://www.w3.org/2002/07/owl#Class":
        case "class":
          entity = dataFactory.getOWLEntity(EntityType.CLASS, iri);
          break;

        case "http://www.w3.org/2002/07/owl#ObjectProperty":
        case "object property":
          entity = dataFactory.getOWLEntity(EntityType.OBJECT_PROPERTY, iri);
          break;

        case "http://www.w3.org/2002/07/owl#DataProperty":
        case "data property":
          entity = dataFactory.getOWLEntity(EntityType.DATA_PROPERTY, iri);
          break;

        case "http://www.w3.org/2002/07/owl#AnnotationProperty":
        case "annotation property":
          entity = dataFactory.getOWLEntity(EntityType.ANNOTATION_PROPERTY, iri);
          break;

        case "http://www.w3.org/2002/07/owl#Individual":
        case "individual":
        case "http://www.w3.org/2002/07/owl#NamedIndividual":
        case "named individual":
          entity = dataFactory.getOWLEntity(EntityType.NAMED_INDIVIDUAL, iri);
          break;

        case "http://www.w3.org/2002/07/owl#Datatype":
        case "datatype":
          entity = dataFactory.getOWLEntity(EntityType.DATATYPE, iri);
          break;

        default:
          // Assume type is an individual (checked later)
          entity = dataFactory.getOWLEntity(EntityType.NAMED_INDIVIDUAL, iri);
          break;
      }
      checker.add(entity, label);
    }
  }

  /** Create a Manchester Syntax parser from the OWLDataFactory and QuotedEntityChecker. */
  private void createParser() {
    this.parser = new ManchesterOWLSyntaxClassExpressionParser(dataFactory, checker);
  }

  /**
   * Process each of the table rows. First, get an entity based on ID or label. If the template
   * contains an ID column, but it is empty, skip that row. If it does not contain an ID column,
   * skip if the label is empty. Add axioms based on the entity type (class, object property, data
   * property, annotation property, datatype, or individual).
   *
   * @throws Exception on issue creating axioms from template
   */
  private void processRow(List<String> row) throws Exception {
    rowNum++;

    String id = null;
    try {
      id = row.get(idColumn);
      if (id.trim().isEmpty()) {
        id = null;
      }
    } catch (IndexOutOfBoundsException e) {
      // ignore
    }
    String label = null;
    try {
      label = row.get(labelColumn);
      if (label.trim().isEmpty()) {
        label = null;
      }
    } catch (IndexOutOfBoundsException e) {
      // ignore
    }
    String type = null;
    try {
      type = row.get(typeColumn);
      if (type.trim().isEmpty()) {
        type = null;
      }
    } catch (IndexOutOfBoundsException e) {
      // ignore
    }

    // Skip if no ID and no label
    if (id == null && label == null) {
      return;
    }

    if (type == null) {
      // Try to guess the type from already existing entities
      if (label != null) {
        OWLEntity e = checker.getOWLEntity(label);
        if (e != null) {
          type = e.getEntityType().getIRI().toString();
        }
      } else {
        OWLEntity e = checker.getOWLEntity(id);
        if (e != null) {
          type = e.getEntityType().getIRI().toString();
        }
      }
      // If the entity type is not defined
      // and the entity does not already exist
      // default to class
      if (type == null) {
        type = "class";
      }
    }

    // Create an IRI for the subject of this row
    IRI iri = getIRI(id, label);
    if (iri == null) {
      // Unable to create an IRI from the entity
      if (idColumn != -1) {
        if (id != null) {
          // Has an ID column with contents that could not be resolved
          throw new RowParseException(
              String.format(unknownEntityError, id, rowNum, idColumn, "ID", name));
        } else {
          // Has an ID column, but it's empty
          return;
        }
      } else {
        // No ID column and label could not be resolved
        throw new RowParseException(
            String.format(unknownEntityError, label, rowNum, labelColumn, "LABEL", name));
      }
    }

    // Try to resolve a CURIE
    IRI typeIRI = ioHelper.createIRI(type, true);

    // Set to IRI string or to type string
    String typeOrIRI = type;
    if (typeIRI != null) {
      typeOrIRI = typeIRI.toString();
    }

    switch (typeOrIRI) {
      case "http://www.w3.org/2002/07/owl#Class":
      case "class":
        addClassAxioms(iri, row);
        break;

      case "http://www.w3.org/2002/07/owl#ObjectProperty":
      case "object property":
        addObjectPropertyAxioms(iri, row);
        break;

      case "http://www.w3.org/2002/07/owl#DataProperty":
      case "data property":
        addDataPropertyAxioms(iri, row);
        break;

      case "http://www.w3.org/2002/07/owl#AnnotationProperty":
      case "annotation property":
        addAnnotationPropertyAxioms(iri, row);
        break;

      case "http://www.w3.org/2002/07/owl#Datatype":
      case "datatype":
        addDatatypeAxioms(iri, row);
        break;

      case "http://www.w3.org/2002/07/owl#Individual":
      case "individual":
      case "http://www.w3.org/2002/07/owl#NamedIndividual":
      case "named individual":
      default:
        addIndividualAxioms(iri, row);
        break;
    }
  }

  /* CLASS AXIOMS */

  /**
   * Given a class IRI and the row containing the class details, generate class axioms.
   *
   * @param iri class IRI
   * @param row list of template values for given class
   * @throws Exception on issue creating class axioms from template
   */
  private void addClassAxioms(IRI iri, List<String> row) throws Exception {
    if (iri == null) {
      return;
    }
    // Add the declaration
    OWLClass cls = dataFactory.getOWLClass(iri);
    OWLDeclarationAxiom ax = dataFactory.getOWLDeclarationAxiom(cls);
    axioms.add(ax);

    String classType = null;
    if (classTypeColumn != -1) {
      try {
        classType = row.get(classTypeColumn);
      } catch (IndexOutOfBoundsException e) {
        // do nothing
      }
    }
    if (classType == null || classType.trim().isEmpty()) {
      classType = "subclass";
    } else {
      classType = classType.trim().toLowerCase();
    }

    if (!validClassTypes.contains(classType)) {
      // Unknown class type
      throw new RowParseException(
          String.format(
              classTypeError,
              cls.getIRI().getShortForm(),
              classType,
              rowNum,
              classTypeColumn,
              name));
    }

    // Iterate through all columns and add annotations as we go
    // Also collect any class expressions that will be used in logical definitions
    // We collect all of these together so that equivalent expressions can be made into an
    // intersection
    // Instead of adding them in as we iterate through
    Map<Integer, Set<OWLClassExpression>> subclassExpressionColumns = new HashMap<>();
    Map<Integer, Set<OWLClassExpression>> equivalentExpressionColumns = new HashMap<>();
    Map<Integer, Set<OWLClassExpression>> intersectionEquivalentExpressionColumns = new HashMap<>();
    Map<Integer, Set<OWLClassExpression>> disjointExpressionColumns = new HashMap<>();
    for (int column = 0; column < templates.size(); column++) {
      String template = templates.get(column);
      String value = null;
      try {
        value = row.get(column);
      } catch (IndexOutOfBoundsException e) {
        // do nothing
      }

      if (value == null || value.trim().isEmpty()) {
        continue;
      }

      if (template.startsWith("A") || template.startsWith("LABEL")) {
        // Handle class annotations
        Set<OWLAnnotation> annotations = getAnnotations(template, value, row, column);
        for (OWLAnnotation annotation : annotations) {
          axioms.add(dataFactory.getOWLAnnotationAssertionAxiom(iri, annotation));
        }
      } else if (template.startsWith("SC")) {
        // Subclass expression
        subclassExpressionColumns.put(
            column,
            TemplateHelper.getClassExpressions(name, parser, template, value, rowNum, column + 1));
      } else if (template.startsWith("EC")) {
        // Equivalent expression
        equivalentExpressionColumns.put(
            column,
            TemplateHelper.getClassExpressions(name, parser, template, value, rowNum, column + 1));
      } else if (template.startsWith("DC")) {
        // Disjoint expression
        disjointExpressionColumns.put(
            column,
            TemplateHelper.getClassExpressions(name, parser, template, value, rowNum, column + 1));
      } else if (template.startsWith("C") && !template.startsWith("CLASS_TYPE")) {
        // Use class type to determine what to do with the expression
        switch (classType) {
          case "subclass":
            subclassExpressionColumns.put(
                column,
                TemplateHelper.getClassExpressions(
                    name, parser, template, value, rowNum, column + 1));
            break;
          case "equivalent":
            intersectionEquivalentExpressionColumns.put(
                column,
                TemplateHelper.getClassExpressions(
                    name, parser, template, value, rowNum, column + 1));
            break;
          case "disjoint":
            disjointExpressionColumns.put(
                column,
                TemplateHelper.getClassExpressions(
                    name, parser, template, value, rowNum, column + 1));
            break;
          default:
            break;
        }
      }
    }

    // Add the axioms
    if (!subclassExpressionColumns.isEmpty()) {
      addSubClassAxioms(cls, subclassExpressionColumns, row);
    }
    if (!equivalentExpressionColumns.isEmpty()) {
      addEquivalentClassesAxioms(cls, equivalentExpressionColumns, row);
    }
    if (!intersectionEquivalentExpressionColumns.isEmpty()) {
      // Special case to support legacy "C"/"equivalent" class type
      // Which is the intersection of all C columns
      addIntersectionEquivalentClassesAxioms(cls, intersectionEquivalentExpressionColumns, row);
    }
    if (!disjointExpressionColumns.isEmpty()) {
      addDisjointClassAxioms(cls, disjointExpressionColumns, row);
    }
  }

  /**
   * Given an OWLClass, a map of column number to class expressions, and the row containing the
   * class details, generate subClassOf axioms for the class where the parents are the class
   * expressions. Maybe annotate the axioms.
   *
   * @param cls OWLClass to create subClassOf axioms for
   * @param expressionColumns map of column numbers to sets of parent class expressions
   * @param row list of template values for given class
   * @throws Exception on issue getting axiom annotations
   */
  private void addSubClassAxioms(
      OWLClass cls, Map<Integer, Set<OWLClassExpression>> expressionColumns, List<String> row)
      throws Exception {
    // Generate axioms
    for (int column : expressionColumns.keySet()) {
      // Maybe get an annotation on the expression
      Set<OWLAnnotation> axiomAnnotations = maybeGetAxiomAnnotations(row, column);
      Set<OWLClassExpression> exprs = expressionColumns.get(column);
      // Each expression will be its own subclass statement
      for (OWLClassExpression expr : exprs) {
        axioms.add(dataFactory.getOWLSubClassOfAxiom(cls, expr, axiomAnnotations));
      }
    }
  }

  /**
   * Given an OWLClass, a map of column number to class expressions, and the row containing the
   * class details, generate equivalent class axioms for the class where the equivalents are the
   * class expressions. Maybe annotate the axioms.
   *
   * @param cls OWLClass to create equivalentClasses axiom for
   * @param expressionColumns map of column number to equivalent class expression
   * @param row list of template values for given class
   * @throws Exception on issue getting axiom annotations
   */
  private void addEquivalentClassesAxioms(
      OWLClass cls, Map<Integer, Set<OWLClassExpression>> expressionColumns, List<String> row)
      throws Exception {
    Set<OWLAnnotation> axiomAnnotations = new HashSet<>();
    Set<OWLClassExpression> expressions = new HashSet<>();
    expressions.add(cls);
    for (int column : expressionColumns.keySet()) {
      // Maybe get an annotation on the expression (all annotations will be on the one intersection)
      axiomAnnotations.addAll(maybeGetAxiomAnnotations(row, column));
      // Add all expressions to the set of expressions
      expressions.addAll(expressionColumns.get(column));
    }
    // Create the axiom as an intersection of the provided expressions
    axioms.add(dataFactory.getOWLEquivalentClassesAxiom(expressions, axiomAnnotations));
  }

  /**
   * Given an OWLClass, a map of column number to class expressions, and the row for this class,
   * generate an equivalentClasses axiom for the class where the equivalent is the intersection of
   * the provided class expressions. Maybe annotate the axioms.
   *
   * @param cls OWLClass to create equivalentClasses axiom for
   * @param expressionColumns map of column number to equivalent class expression
   * @param row list of template values for given class
   * @throws Exception on issue getting axiom annotations
   */
  private void addIntersectionEquivalentClassesAxioms(
      OWLClass cls, Map<Integer, Set<OWLClassExpression>> expressionColumns, List<String> row)
      throws Exception {
    Set<OWLAnnotation> axiomAnnotations = new HashSet<>();
    Set<OWLClassExpression> expressions = new HashSet<>();
    for (int column : expressionColumns.keySet()) {
      // Maybe get an annotation on the expression (all annotations will be on the one intersection)
      axiomAnnotations.addAll(maybeGetAxiomAnnotations(row, column));
      // Add all expressions to the set of expressions
      expressions.addAll(expressionColumns.get(column));
    }
    // Create the axiom as an intersection of the provided expressions
    OWLObjectIntersectionOf intersection = dataFactory.getOWLObjectIntersectionOf(expressions);
    axioms.add(dataFactory.getOWLEquivalentClassesAxiom(cls, intersection, axiomAnnotations));
  }

  /**
   * Given an OWLClass, a map of column number to class expressions, and the row containing the
   * class details, generate disjointClasses axioms for the class where the disjoints are the class
   * expressions. Maybe annotate the axioms.
   *
   * @param cls OWLClass to create disjointClasses axioms for
   * @param expressionColumns map of column number to equivalent class expression
   * @param row list of template values for given class
   * @throws Exception on issue getting axiom annotations
   */
  private void addDisjointClassAxioms(
      OWLClass cls, Map<Integer, Set<OWLClassExpression>> expressionColumns, List<String> row)
      throws Exception {
    for (int column : expressionColumns.keySet()) {
      // Maybe get an annotation on the expression
      Set<OWLAnnotation> axiomAnnotations = maybeGetAxiomAnnotations(row, column);
      Set<OWLClassExpression> exprs = expressionColumns.get(column);
      // Each expression will be its own disjoint statement
      for (OWLClassExpression expr : exprs) {
        Set<OWLClassExpression> disjoint = new HashSet<>(Arrays.asList(cls, expr));
        axioms.add(dataFactory.getOWLDisjointClassesAxiom(disjoint, axiomAnnotations));
      }
    }
  }

  /* OBJECT PROPERTY AXIOMS */

  /**
   * Given an object property IRI and the row containing the property details, generate property
   * axioms.
   *
   * @param iri object property IRI
   * @param row list of template values for given object property
   * @throws Exception on issue creating object property axioms from template
   */
  private void addObjectPropertyAxioms(IRI iri, List<String> row) throws Exception {
    // Add the declaration
    axioms.add(
        dataFactory.getOWLDeclarationAxiom(
            dataFactory.getOWLEntity(EntityType.OBJECT_PROPERTY, iri)));

    // Maybe get a property type (default subproperty)
    String propertyType = getPropertyType(row);

    // Maybe get characteristics (default none)
    List<String> characteristics = getCharacteristics(row);

    // Create the property object
    OWLObjectProperty property = dataFactory.getOWLObjectProperty(iri);

    // Handle special property types
    for (String c : characteristics) {
      switch (c.trim().toLowerCase()) {
        case "asymmetric":
          axioms.add(dataFactory.getOWLAsymmetricObjectPropertyAxiom(property));
          break;
        case "functional":
          axioms.add(dataFactory.getOWLFunctionalObjectPropertyAxiom(property));
          break;
        case "inversefunctional":
        case "inverse functional":
          axioms.add(dataFactory.getOWLInverseFunctionalObjectPropertyAxiom(property));
          break;
        case "irreflexive":
          axioms.add(dataFactory.getOWLIrreflexiveObjectPropertyAxiom(property));
          break;
        case "reflexive":
          axioms.add(dataFactory.getOWLReflexiveObjectPropertyAxiom(property));
          break;
        case "symmetric":
          axioms.add(dataFactory.getOWLSymmetricObjectPropertyAxiom(property));
          break;
        case "transitive":
          axioms.add(dataFactory.getOWLTransitiveObjectPropertyAxiom(property));
          break;
        default:
          throw new Exception(
              String.format(
                  unknownCharacteristicError,
                  property.getIRI().getShortForm(),
                  c,
                  rowNum,
                  characteristicColumn,
                  name));
      }
    }

    for (int column = 0; column < templates.size(); column++) {
      String template = templates.get(column);
      String value = null;
      try {
        value = row.get(column);
      } catch (IndexOutOfBoundsException e) {
        // do nothing
      }

      if (value == null || value.trim().isEmpty()) {
        continue;
      }

      if (template.startsWith("A") || template.startsWith("LABEL")) {
        // Handle annotations
        Set<OWLAnnotation> annotations = getAnnotations(template, value, row, column);
        for (OWLAnnotation annotation : annotations) {
          axioms.add(dataFactory.getOWLAnnotationAssertionAxiom(iri, annotation));
        }
      } else if (template.startsWith("SP")) {
        // Subproperty expressions
        Set<OWLObjectPropertyExpression> expressions =
            TemplateHelper.getObjectPropertyExpressions(
                name, checker, template, value, rowNum, column);
        addSubObjectPropertyAxioms(property, expressions, row, column);
      } else if (template.startsWith("EP")) {
        // Equivalent properties expressions
        Set<OWLObjectPropertyExpression> expressions =
            TemplateHelper.getObjectPropertyExpressions(
                name, checker, template, value, rowNum, column);
        addEquivalentObjectPropertiesAxioms(property, expressions, row, column);
      } else if (template.startsWith("DP")) {
        // Disjoint properties expressions
        Set<OWLObjectPropertyExpression> expressions =
            TemplateHelper.getObjectPropertyExpressions(
                name, checker, template, value, rowNum, column);
        addDisjointObjectPropertiesAxioms(property, expressions, row, column);
      } else if (template.startsWith("IP")) {
        // Inverse properties expressions
        Set<OWLObjectPropertyExpression> expressions =
            TemplateHelper.getObjectPropertyExpressions(
                name, checker, template, value, rowNum, column);
        addInverseObjectPropertiesAxioms(property, expressions, row, column);
      } else if (template.startsWith("P") && !template.startsWith("PROPERTY_TYPE")) {
        // Use the property type to determine what type of expression
        Set<OWLObjectPropertyExpression> expressions =
            TemplateHelper.getObjectPropertyExpressions(
                name, checker, template, value, rowNum, column);
        switch (propertyType) {
          case "subproperty":
            addSubObjectPropertyAxioms(property, expressions, row, column);
            break;
          case "equivalent":
            addEquivalentObjectPropertiesAxioms(property, expressions, row, column);
            break;
          case "disjoint":
            addDisjointObjectPropertiesAxioms(property, expressions, row, column);
            break;
          case "inverse":
            addInverseObjectPropertiesAxioms(property, expressions, row, column);
            break;
          default:
            // Unknown property type
            throw new RowParseException(
                String.format(
                    propertyTypeError, iri.getShortForm(), propertyType, rowNum, column + 1, name));
        }
      } else if (template.startsWith("DOMAIN")) {
        // Handle domains
        Set<OWLClassExpression> expressions =
            TemplateHelper.getClassExpressions(name, parser, template, value, rowNum, column);
        addObjectPropertyDomains(property, expressions, row, column);
      } else if (template.startsWith("RANGE")) {
        // Handle ranges
        Set<OWLClassExpression> expressions =
            TemplateHelper.getClassExpressions(name, parser, template, value, rowNum, column);
        addObjectPropertyRanges(property, expressions, row, column);
      }
    }
  }

  /**
   * Given an OWLObjectProperty, a set of OWLObjectPropertyExpressions, and the row containing the
   * property details, generate subPropertyOf axioms for the property where the parents are the
   * property expressions. Maybe annotate the axioms.
   *
   * @param property OWLObjectProperty to create subPropertyOf axioms for
   * @param expressions set of OWLObjectPropertyExpressions
   * @param row list of template values for given property
   * @param column column number of logical template string
   * @throws Exception on issue getting axiom annotations
   */
  private void addSubObjectPropertyAxioms(
      OWLObjectProperty property,
      Set<OWLObjectPropertyExpression> expressions,
      List<String> row,
      int column)
      throws Exception {
    // Maybe get an annotation on the expression
    Set<OWLAnnotation> axiomAnnotations = maybeGetAxiomAnnotations(row, column);

    // Generate axioms
    for (OWLObjectPropertyExpression expr : expressions) {
      axioms.add(dataFactory.getOWLSubObjectPropertyOfAxiom(property, expr, axiomAnnotations));
    }
  }

  /**
   * Given an OWLObjectProperty, a set of OWLObjectPropertyExpressions, and the row containing the
   * property details, generate equivalentProperties axioms for the property where the equivalents
   * are the property expressions. Maybe annotate the axioms.
   *
   * @param property OWLObjectProperty to create equivalentProperties axioms for
   * @param expressions set of equivalent OWLObjectPropertyExpressions
   * @param row list of template values for given property
   * @param column column number of logical template string
   * @throws Exception on issue getting axiom annotations
   */
  private void addEquivalentObjectPropertiesAxioms(
      OWLObjectProperty property,
      Set<OWLObjectPropertyExpression> expressions,
      List<String> row,
      int column)
      throws Exception {
    // Maybe get an annotation on the expression
    Set<OWLAnnotation> axiomAnnotations = maybeGetAxiomAnnotations(row, column);

    // Generate axioms
    for (OWLObjectPropertyExpression expr : expressions) {
      axioms.add(
          dataFactory.getOWLEquivalentObjectPropertiesAxiom(property, expr, axiomAnnotations));
    }
  }

  /**
   * Given an OWLObjectProperty, a set of OWLObjectPropertyExpressions, and the row containing the
   * property details, generate disjointProperties axioms for the property where the disjoints are
   * the property expressions. Maybe annotate the axioms.
   *
   * @param property OWLObjectProperty to create disjointProperties axioms for
   * @param expressions set of disjoint OWLObjectPropertyExpressions
   * @param row list of template values for given property
   * @param column column number of logical template string
   * @throws Exception on issue getting axiom annotations
   */
  private void addDisjointObjectPropertiesAxioms(
      OWLObjectProperty property,
      Set<OWLObjectPropertyExpression> expressions,
      List<String> row,
      int column)
      throws Exception {
    // Maybe get an annotation on the expression
    Set<OWLAnnotation> axiomAnnotations = maybeGetAxiomAnnotations(row, column);

    // Generate axioms
    expressions.add(property);
    axioms.add(dataFactory.getOWLDisjointObjectPropertiesAxiom(expressions, axiomAnnotations));
  }

  /**
   * Given an OWLObjectProperty, a set of OWLObjectPropertyExpressions, and the row containing the
   * property details, generate inverseProperties axioms for the property where the inverses are the
   * property expressions. Maybe annotate the axioms.
   *
   * @param property OWLObjectProperty to create inverseProperties axioms for
   * @param expressions set of inverse OWLObjectPropertyExpressions
   * @param row list of template values for given property
   * @param column column number of logical template string
   * @throws Exception on issue getting axiom annotations
   */
  private void addInverseObjectPropertiesAxioms(
      OWLObjectProperty property,
      Set<OWLObjectPropertyExpression> expressions,
      List<String> row,
      int column)
      throws Exception {
    // Maybe get an annotation on the expression
    Set<OWLAnnotation> axiomAnnotations = maybeGetAxiomAnnotations(row, column);

    // Generate axioms
    for (OWLObjectPropertyExpression expr : expressions) {
      axioms.add(dataFactory.getOWLInverseObjectPropertiesAxiom(property, expr, axiomAnnotations));
    }
  }

  /**
   * Given an OWLObjectProperty, a set of OWLClassExpressions, the row containing the property
   * details, and a column location, generate domain axioms where the domains are the class
   * expressions. Maybe annotation the axioms.
   *
   * @param property OWLObjectProperty to create domain axioms for
   * @param expressions set of domain OWLClassExpressions
   * @param row list of template values for given property
   * @param column column number of logical template string
   * @throws Exception on issue getting axiom annotations
   */
  private void addObjectPropertyDomains(
      OWLObjectProperty property, Set<OWLClassExpression> expressions, List<String> row, int column)
      throws Exception {
    // Maybe get an annotation on the expression
    Set<OWLAnnotation> axiomAnnotations = maybeGetAxiomAnnotations(row, column);

    // Generate axioms
    for (OWLClassExpression expr : expressions) {
      axioms.add(dataFactory.getOWLObjectPropertyDomainAxiom(property, expr, axiomAnnotations));
    }
  }

  /**
   * Given an OWLObjectProperty, a set of OWLClassExpressions, the row containing the property
   * details, and a column location, generate range axioms where the ranges are the class
   * expressions. Maybe annotation the axioms.
   *
   * @param property OWLObjectProperty to create range axioms for
   * @param expressions set of range OWLClassExpressions
   * @param row list of template values for given property
   * @param column column number of logical template string
   * @throws Exception on issue getting axiom annotations
   */
  private void addObjectPropertyRanges(
      OWLObjectProperty property, Set<OWLClassExpression> expressions, List<String> row, int column)
      throws Exception {
    // Maybe get an annotation on the expression
    Set<OWLAnnotation> axiomAnnotations = maybeGetAxiomAnnotations(row, column);

    // Generate axioms
    for (OWLClassExpression expr : expressions) {
      axioms.add(dataFactory.getOWLObjectPropertyRangeAxiom(property, expr, axiomAnnotations));
    }
  }

  /* DATA PROPERTY AXIOMS */

  /**
   * Given an data property IRI and the row containing the property details, generate property
   * axioms.
   *
   * @param iri data property IRI
   * @param row list of template values for given data property
   * @throws Exception on issue creating data property axioms from template
   */
  private void addDataPropertyAxioms(IRI iri, List<String> row) throws Exception {
    // Add the declaration
    axioms.add(
        dataFactory.getOWLDeclarationAxiom(
            dataFactory.getOWLEntity(EntityType.DATA_PROPERTY, iri)));

    OWLDataProperty property = dataFactory.getOWLDataProperty(iri);

    // Maybe get property type (default subproperty)
    String propertyType = getPropertyType(row);

    // Maybe get property characteristics (default empty list)
    List<String> characteristics = getCharacteristics(row);

    // Maybe add property characteristics
    for (String c : characteristics) {
      if (!c.equalsIgnoreCase("functional")) {
        throw new Exception(
            String.format(
                dataPropertyCharacteristicError,
                property.getIRI().getShortForm(),
                rowNum,
                characteristicColumn,
                name));
      }
      axioms.add(dataFactory.getOWLFunctionalDataPropertyAxiom(property));
    }

    for (int column = 0; column < templates.size(); column++) {
      String template = templates.get(column);
      String value = null;
      try {
        value = row.get(column);
      } catch (IndexOutOfBoundsException e) {
        // do nothing
      }

      if (value == null || value.trim().isEmpty()) {
        continue;
      }

      String split = null;
      if (template.contains("SPLIT=")) {
        split = template.substring(template.indexOf("SPLIT=") + 6).trim();
      }

      if (template.startsWith("A") || template.startsWith("LABEL")) {
        // Handle annotations
        Set<OWLAnnotation> annotations = getAnnotations(template, value, row, column);
        for (OWLAnnotation annotation : annotations) {
          axioms.add(dataFactory.getOWLAnnotationAssertionAxiom(iri, annotation));
        }
      } else if (template.startsWith("SP")) {
        // Subproperty expressions
        Set<OWLDataPropertyExpression> expressions =
            TemplateHelper.getDataPropertyExpressions(
                name, checker, template, value, rowNum, column);
        addSubDataPropertyAxioms(property, expressions, row, column);
      } else if (template.startsWith("EP")) {
        // Equivalent properties expressions
        Set<OWLDataPropertyExpression> expressions =
            TemplateHelper.getDataPropertyExpressions(
                name, checker, template, value, rowNum, column);
        addEquivalentDataPropertiesAxioms(property, expressions, row, column);
      } else if (template.startsWith("DP")) {
        // Disjoint properties expressions
        Set<OWLDataPropertyExpression> expressions =
            TemplateHelper.getDataPropertyExpressions(
                name, checker, template, value, rowNum, column);
        addDisjointDataPropertiesAxioms(property, expressions, row, column);
      } else if (template.startsWith("IP")) {
        // Cannot use inverse with data properties
        throw new RowParseException(
            String.format(
                propertyTypeError, iri.getShortForm(), propertyType, rowNum, column + 1, name));
      } else if (template.startsWith("P ") && !template.startsWith("PROPERTY_TYPE")) {
        // Use property type to handle expression type
        Set<OWLDataPropertyExpression> expressions =
            TemplateHelper.getDataPropertyExpressions(
                name, checker, template, value, rowNum, column);
        switch (propertyType) {
          case "subproperty":
            addSubDataPropertyAxioms(property, expressions, row, column);
            break;
          case "equivalent":
            addEquivalentDataPropertiesAxioms(property, expressions, row, column);
            break;
          case "disjoint":
            addDisjointDataPropertiesAxioms(property, expressions, row, column);
            break;
          default:
            // Unknown property type
            throw new RowParseException(
                String.format(
                    propertyTypeError, iri.getShortForm(), propertyType, rowNum, column + 1, name));
        }
      } else if (template.startsWith("DOMAIN")) {
        // Handle domains
        Set<OWLClassExpression> expressions =
            TemplateHelper.getClassExpressions(name, parser, template, value, rowNum, column);
        addDataPropertyDomains(property, expressions, row, column);
      } else if (template.startsWith("RANGE")) {
        // Handle ranges
        Set<OWLDatatype> datatypes =
            TemplateHelper.getDatatypes(name, checker, value, split, rowNum, column);
        addDataPropertyRanges(property, datatypes, row, column);
      }
    }
  }

  /**
   * Given an OWLDataProperty, a set of OWLDataPropertyExpressions, and the row containing the
   * property details, generate subPropertyOf axioms for the property where the parents are the
   * property expressions. Maybe annotate the axioms.
   *
   * @param property OWLDataProperty to create subPropertyOf axioms for
   * @param expressions set of parent OWLDataPropertyExpressions
   * @param row list of template values for given property
   * @param column column number of logical template string
   * @throws Exception on issue getting axiom annotations
   */
  private void addSubDataPropertyAxioms(
      OWLDataProperty property,
      Set<OWLDataPropertyExpression> expressions,
      List<String> row,
      int column)
      throws Exception {
    // Maybe get an annotation on the expression
    Set<OWLAnnotation> axiomAnnotations = maybeGetAxiomAnnotations(row, column);

    // Generate axioms
    for (OWLDataPropertyExpression expr : expressions) {
      if (expr != null) {
        axioms.add(dataFactory.getOWLSubDataPropertyOfAxiom(property, expr, axiomAnnotations));
      }
    }
  }

  /**
   * Given an OWLDataProperty, a set of OWLDataPropertyExpressions, and the row containing the
   * property details, generate equivalentProperties axioms for the property where the equivalents
   * are the property expressions. Maybe annotate the axioms.
   *
   * @param property OWLDataProperty to create equivalentProperties axioms for
   * @param expressions set of equivalent OWLDataPropertyExpressions
   * @param row list of template values for given property
   * @param column column number of logical template string
   * @throws Exception on issue getting axiom annotations
   */
  private void addEquivalentDataPropertiesAxioms(
      OWLDataProperty property,
      Set<OWLDataPropertyExpression> expressions,
      List<String> row,
      int column)
      throws Exception {
    // Maybe get an annotation on the expression
    Set<OWLAnnotation> axiomAnnotations = maybeGetAxiomAnnotations(row, column);

    // Generate axioms
    for (OWLDataPropertyExpression expr : expressions) {
      axioms.add(dataFactory.getOWLEquivalentDataPropertiesAxiom(property, expr, axiomAnnotations));
    }
  }

  /**
   * Given an OWLDataProperty, a set of OWLObjectPropertyExpressions, and the row containing the
   * property details, generate disjointProperties axioms for the property where the disjoints are
   * the property expressions. Maybe annotate the axioms.
   *
   * @param property OWLDataProperty to create disjointProperties axioms for
   * @param expressions set of disjoint OWLDataPropertyExpressions
   * @param row list of template values for given property
   * @param column column number of logical template string
   * @throws Exception on issue getting axiom annotations
   */
  private void addDisjointDataPropertiesAxioms(
      OWLDataProperty property,
      Set<OWLDataPropertyExpression> expressions,
      List<String> row,
      int column)
      throws Exception {
    // Maybe get an annotation on the expression
    Set<OWLAnnotation> axiomAnnotations = maybeGetAxiomAnnotations(row, column);

    // Generate axioms
    expressions.add(property);
    axioms.add(dataFactory.getOWLDisjointDataPropertiesAxiom(expressions, axiomAnnotations));
  }

  /**
   * Given an OWLDataProperty, a set of OWLClassExpressions, the row containing the property
   * details, and a column location, generate domain axioms where the domains are the class
   * expressions. Maybe annotation the axioms.
   *
   * @param property OWLDataProperty to create domain axioms for
   * @param expressions set of domain OWLClassExpressions
   * @param row list of template values for given property
   * @param column column number of logical template string
   * @throws Exception on issue getting axiom annotations
   */
  private void addDataPropertyDomains(
      OWLDataProperty property, Set<OWLClassExpression> expressions, List<String> row, int column)
      throws Exception {
    // Maybe get an annotation on the expression
    Set<OWLAnnotation> axiomAnnotations = maybeGetAxiomAnnotations(row, column);

    // Generate axioms
    for (OWLClassExpression expr : expressions) {
      axioms.add(dataFactory.getOWLDataPropertyDomainAxiom(property, expr, axiomAnnotations));
    }
  }

  /**
   * Given an OWLObjectProperty, a set of OWLDatatypes, the row containing the property details, and
   * a column location, generate range axioms where the ranges are the datatypes. Maybe annotation
   * the axioms.
   *
   * @param property OWLObjectProperty to create range axioms for
   * @param datatypes set of range OWLDatatypes
   * @param row list of template values for given property
   * @param column column number of logical template string
   * @throws Exception on issue getting axiom annotations
   */
  private void addDataPropertyRanges(
      OWLDataProperty property, Set<OWLDatatype> datatypes, List<String> row, int column)
      throws Exception {
    // Maybe get an annotation on the expression
    Set<OWLAnnotation> axiomAnnotations = maybeGetAxiomAnnotations(row, column);

    // Generate axioms
    for (OWLDatatype datatype : datatypes) {
      axioms.add(dataFactory.getOWLDataPropertyRangeAxiom(property, datatype, axiomAnnotations));
    }
  }

  /* ANNOTATION PROPERTY AXIOMS */

  /**
   * Given an annotation property IRI and the row containing the property details, generate property
   * axioms.
   *
   * @param iri annotation property IRI
   * @param row list of template values for given annotation property
   * @throws Exception on issue creating annotation property axioms from template
   */
  private void addAnnotationPropertyAxioms(IRI iri, List<String> row) throws Exception {
    // Add the declaration
    axioms.add(
        dataFactory.getOWLDeclarationAxiom(
            dataFactory.getOWLEntity(EntityType.ANNOTATION_PROPERTY, iri)));

    String propertyType = getPropertyType(row);

    if (!propertyType.equals("subproperty")) {
      // Annotation properties can only have type "subproperty"
      throw new RowParseException(
          String.format(
              annotationPropertyTypeError, iri, propertyType, rowNum, propertyTypeColumn, name));
    }

    // Annotation properties should not have characteristics
    if (characteristicColumn != -1) {
      String propertyCharacteristicString = row.get(characteristicColumn);
      if (propertyCharacteristicString != null && !propertyCharacteristicString.trim().isEmpty()) {
        throw new RowParseException(
            String.format(
                annotationPropertyCharacteristicError,
                iri.getShortForm(),
                rowNum,
                characteristicColumn,
                name));
      }
    }

    // Create the property object
    OWLAnnotationProperty property = dataFactory.getOWLAnnotationProperty(iri);

    for (int column = 0; column < templates.size(); column++) {
      String template = templates.get(column);
      String value = null;
      try {
        value = row.get(column);
      } catch (IndexOutOfBoundsException e) {
        // do nothing
      }

      if (value == null || value.trim().isEmpty()) {
        continue;
      }

      // Maybe get the split character
      String split = null;
      if (template.contains("SPLIT=")) {
        split = template.substring(template.indexOf("SPLIT=") + 6).trim();
      }

      if (template.startsWith("A") || template.startsWith("LABEL")) {
        // Handle annotations
        Set<OWLAnnotation> annotations = getAnnotations(template, value, row, column);
        for (OWLAnnotation annotation : annotations) {
          axioms.add(dataFactory.getOWLAnnotationAssertionAxiom(iri, annotation));
        }
      } else if (template.startsWith("SP")
          || template.startsWith("P") && !template.startsWith("PROPERTY_TYPE")) {
        // Handle property logic
        Set<OWLAnnotationProperty> parents =
            TemplateHelper.getAnnotationProperties(checker, value, split);
        addSubAnnotationPropertyAxioms(property, parents, row, column);
      } else if (template.startsWith("DOMAIN")) {
        // Handle domains
        Set<IRI> iris = TemplateHelper.getValueIRIs(checker, value, split);
        addAnnotationPropertyDomains(property, iris, row, column);
      } else if (template.startsWith("RANGE")) {
        // Handle ranges
        Set<IRI> iris = TemplateHelper.getValueIRIs(checker, value, split);
        addAnnotationPropertyRanges(property, iris, row, column);
      }
    }
  }

  /**
   * Given an OWLAnnotationProperty, a set of OWLAnnotationProperties, and the row containing the
   * property details, generate subPropertyOf axioms for the property where the parents are the
   * other properties. Maybe annotate the axioms.
   *
   * @param property OWLObjectProperty to create subPropertyOf axioms for
   * @param parents set of parent OWLAnnotationProperties
   * @param row list of template values for given property
   * @param column column number of logical template string
   * @throws Exception on issue getting axiom annotations
   */
  private void addSubAnnotationPropertyAxioms(
      OWLAnnotationProperty property,
      Set<OWLAnnotationProperty> parents,
      List<String> row,
      int column)
      throws Exception {
    // Maybe get an annotation on the subproperty axiom
    Set<OWLAnnotation> axiomAnnotations = maybeGetAxiomAnnotations(row, column);

    // Generate axioms
    for (OWLAnnotationProperty parent : parents) {
      axioms.add(
          dataFactory.getOWLSubAnnotationPropertyOfAxiom(property, parent, axiomAnnotations));
    }
  }

  /**
   * Given an OWLAnnotationProperty, a set of IRIs, the row containing the property details, and a
   * column location, generate domain axioms where the domains are the IRIs. Maybe annotation the
   * axioms.
   *
   * @param property OWLObjectProperty to create domain axioms for
   * @param iris set of domain IRIs
   * @param row list of template values for given property
   * @param column column number of logical template string
   * @throws Exception on issue getting axiom annotations
   */
  private void addAnnotationPropertyDomains(
      OWLAnnotationProperty property, Set<IRI> iris, List<String> row, int column)
      throws Exception {
    // Maybe get an annotation on the expression
    Set<OWLAnnotation> axiomAnnotations = maybeGetAxiomAnnotations(row, column);

    // Generate axioms
    for (IRI iri : iris) {
      axioms.add(dataFactory.getOWLAnnotationPropertyDomainAxiom(property, iri, axiomAnnotations));
    }
  }

  /**
   * Given an OWLAnnotationProperty, a set of IRIs, the row containing the property details, and a
   * column location, generate range axioms where the ranges are the IRIs. Maybe annotation the
   * axioms.
   *
   * @param property OWLAnnotationProperty to create range axioms for
   * @param iris set of range IRIs
   * @param row list of template values for given property
   * @param column column number of logical template string
   * @throws Exception on issue getting axiom annotations
   */
  private void addAnnotationPropertyRanges(
      OWLAnnotationProperty property, Set<IRI> iris, List<String> row, int column)
      throws Exception {
    // Maybe get an annotation on the expression
    Set<OWLAnnotation> axiomAnnotations = maybeGetAxiomAnnotations(row, column);

    // Generate axioms
    for (IRI iri : iris) {
      axioms.add(dataFactory.getOWLAnnotationPropertyRangeAxiom(property, iri, axiomAnnotations));
    }
  }

  /* DATATYPE AXIOMS */

  /**
   * Given a datatype IRI and the row containing the datatype details, generate datatype axioms.
   *
   * @param iri datatype IRI
   * @param row list of template values for given datatype
   * @throws Exception on issue creating datatype annotations
   */
  private void addDatatypeAxioms(IRI iri, List<String> row) throws Exception {
    // Add the declaration
    axioms.add(
        dataFactory.getOWLDeclarationAxiom(dataFactory.getOWLEntity(EntityType.DATATYPE, iri)));

    for (int column = 0; column < templates.size(); column++) {
      String template = templates.get(column);
      String value = null;
      try {
        value = row.get(column);
      } catch (IndexOutOfBoundsException e) {
        // do nothing
      }

      if (value == null || value.trim().isEmpty()) {
        continue;
      }

      // Handle annotations
      if (template.startsWith("A")) {
        // Add the annotations to the datatype
        Set<OWLAnnotation> annotations = getAnnotations(template, value, row, column);
        for (OWLAnnotation annotation : annotations) {
          axioms.add(dataFactory.getOWLAnnotationAssertionAxiom(iri, annotation));
        }
      }
      // TODO - future support for data definitions with DT
    }
  }

  /* INDIVIDUAL AXIOMS */

  /**
   * Given an individual IRI and the row containing the individual details, generate individual
   * axioms.
   *
   * @param iri individual IRI
   * @param row list of template values for given individual
   * @throws Exception on issue creating individual axioms from template
   */
  private void addIndividualAxioms(IRI iri, List<String> row) throws Exception {
    // Should not return null, as empty defaults to a class
    String typeCol = row.get(typeColumn).trim();
    // Use the 'type' to get the class assertion for the individual
    // If it is owl:Individual or owl:NamedIndividual, it will not have a class assertion
    // There may be more than one class assertion - right now only named classes are supported
    List<String> types = new ArrayList<>();
    if (typeSplit != null) {
      for (String t : typeCol.split(Pattern.quote(typeSplit))) {
        if (!t.trim().equals("")) {
          types.add(t.trim());
        }
      }
    } else {
      types.add(typeCol.trim());
    }

    // The individualType is used to determine what kind of axioms are associated
    // e.g. different individuals, same individuals...
    // The default is just "named" individual (no special axioms)
    String individualType = "named";
    if (individualTypeColumn != -1) {
      try {
        individualType = row.get(individualTypeColumn);
      } catch (IndexOutOfBoundsException e) {
        // do nothing
      }
    }

    OWLNamedIndividual individual = dataFactory.getOWLNamedIndividual(iri);
    // Add declaration
    axioms.add(dataFactory.getOWLDeclarationAxiom(dataFactory.getOWLNamedIndividual(iri)));

    for (String type : types) {
      // Trim for safety
      type = type.trim();

      // Try to resolve a CURIE
      IRI typeIRI = ioHelper.createIRI(type, true);

      // Set to IRI string or to type string
      String typeOrIRI = type;
      if (typeIRI != null) {
        typeOrIRI = typeIRI.toString();
      }

      // Add a type if the type is not owl:Individual or owl:NamedIndividual
      if (!typeOrIRI.equalsIgnoreCase("individual")
          && !typeOrIRI.equalsIgnoreCase("named individual")
          && !typeOrIRI.equalsIgnoreCase("http://www.w3.org/2002/07/owl#NamedIndividual")
          && !typeOrIRI.equalsIgnoreCase("http://www.w3.org/2002/07/owl#Individual")) {
        OWLClass typeCls = checker.getOWLClass(type);
        if (typeCls != null) {
          axioms.add(dataFactory.getOWLClassAssertionAxiom(typeCls, individual));
        } else {
          // If the class is null, assume it is a class expression
          OWLClassExpression typeExpr =
              TemplateHelper.tryParse(name, parser, type, rowNum, typeColumn);
          axioms.add(dataFactory.getOWLClassAssertionAxiom(typeExpr, individual));
        }
      }
    }

    for (int column = 0; column < templates.size(); column++) {
      String template = templates.get(column);
      String split = null;
      if (template.contains("SPLIT=")) {
        split = template.substring(template.indexOf("SPLIT=") + 6).trim();
        template = template.substring(0, template.indexOf("SPLIT=")).trim();
      }

      String value = null;
      try {
        value = row.get(column);
      } catch (IndexOutOfBoundsException e) {
        // do nothing
      }

      if (value == null || value.trim().isEmpty()) {
        continue;
      }

      // Handle annotations
      if (template.startsWith("A") || template.startsWith("LABEL")) {
        // Add the annotations to the individual
        Set<OWLAnnotation> annotations = getAnnotations(template, value, row, column);
        for (OWLAnnotation annotation : annotations) {
          axioms.add(dataFactory.getOWLAnnotationAssertionAxiom(iri, annotation));
        }
      } else if (template.startsWith("SI")) {
        // Same individuals axioms
        Set<OWLIndividual> sameIndividuals = TemplateHelper.getIndividuals(checker, value, split);
        if (!sameIndividuals.isEmpty()) {
          addSameIndividualsAxioms(individual, sameIndividuals, row, column);
        }
      } else if (template.startsWith("DI")) {
        // Different individuals axioms
        Set<OWLIndividual> differentIndividuals =
            TemplateHelper.getIndividuals(checker, value, split);
        if (!differentIndividuals.isEmpty()) {
          addDifferentIndividualsAxioms(individual, differentIndividuals, row, column);
        }
      } else if (template.startsWith("TI ")) {
        if (split != null) {
          // Add the split back on for getClassExpressions
          template = template + " SPLIT=" + split;
        }
        Set<OWLClassExpression> typeExpressions =
            TemplateHelper.getClassExpressions(name, parser, template, value, rowNum, column + 1);
        for (OWLClassExpression ce : typeExpressions) {
          axioms.add(dataFactory.getOWLClassAssertionAxiom(ce, individual));
        }
      } else if (template.startsWith("I") && !template.startsWith("INDIVIDUAL_TYPE")) {
        // Use individual type to determine how to handle expressions
        switch (individualType) {
          case "named":
            if (template.startsWith("I ")) {
              String propStr = template.substring(2).replace("'", "");
              OWLObjectProperty objectProperty = checker.getOWLObjectProperty(propStr);
              if (objectProperty != null) {
                Set<OWLIndividual> otherIndividuals =
                    TemplateHelper.getIndividuals(checker, value, split);
                addObjectPropertyAssertionAxioms(
                    individual, otherIndividuals, objectProperty, row, column);
                break;
              }
              OWLDataProperty dataProperty = checker.getOWLDataProperty(propStr);
              if (dataProperty != null) {
                Set<OWLLiteral> literals =
                    TemplateHelper.getLiterals(name, checker, value, split, rowNum, column);
                addDataPropertyAssertionAxioms(individual, literals, dataProperty, row, column);
                break;
              }
            }
            break;
          case "same":
            Set<OWLIndividual> sameIndividuals =
                TemplateHelper.getIndividuals(checker, value, split);
            if (!sameIndividuals.isEmpty()) {
              addSameIndividualsAxioms(individual, sameIndividuals, row, column);
            }
            break;
          case "different":
            Set<OWLIndividual> differentIndividuals =
                TemplateHelper.getIndividuals(checker, value, split);
            if (!differentIndividuals.isEmpty()) {
              addDifferentIndividualsAxioms(individual, differentIndividuals, row, column);
            }
            break;
          default:
            throw new RowParseException(
                String.format(
                    individualTypeError,
                    iri.getShortForm(),
                    individualType,
                    rowNum,
                    column + 1,
                    name));
        }
      }
    }
  }

  /**
   * Given an OWLIndividual, a set of OWLIndividuals, an object property expression, the row as list
   * of strings, and the column number, add each individual as the object of the object property
   * expression for that individual.
   *
   * @param individual OWLIndividual to add object property assertion axioms to
   * @param otherIndividuals set of other OWLIndividuals representing the objects of the axioms
   * @param expression OWLObjectPropertyExpression to use as property of the axioms
   * @param row list of strings
   * @param column column number
   * @throws Exception on problem handling axiom annotations
   */
  private void addObjectPropertyAssertionAxioms(
      OWLNamedIndividual individual,
      Set<OWLIndividual> otherIndividuals,
      OWLObjectPropertyExpression expression,
      List<String> row,
      int column)
      throws Exception {
    // Maybe get an annotation on the subproperty axiom
    Set<OWLAnnotation> axiomAnnotations = maybeGetAxiomAnnotations(row, column);

    for (OWLIndividual other : otherIndividuals) {
      axioms.add(
          dataFactory.getOWLObjectPropertyAssertionAxiom(
              expression, individual, other, axiomAnnotations));
    }
  }

  /**
   * Given an OWLIndividual, a set of OWLLiterals, a data property expression, the row as list of
   * strings, and the column number, add each literal as the object of the data property expression
   * for that individual.
   *
   * @param individual OWLIndividual to add data property assertion axioms to
   * @param literals set of OWLLiterals representing the objects of the axioms
   * @param expression OWLDataPropertyExpression to use as property of the axioms
   * @param row list of strings
   * @param column column number
   * @throws Exception on problem handling axiom annotations
   */
  private void addDataPropertyAssertionAxioms(
      OWLNamedIndividual individual,
      Set<OWLLiteral> literals,
      OWLDataPropertyExpression expression,
      List<String> row,
      int column)
      throws Exception {
    // Maybe get an annotation on the subproperty axiom
    Set<OWLAnnotation> axiomAnnotations = maybeGetAxiomAnnotations(row, column);

    for (OWLLiteral lit : literals) {
      axioms.add(
          dataFactory.getOWLDataPropertyAssertionAxiom(
              expression, individual, lit, axiomAnnotations));
    }
  }

  /**
   * Given an OWLIndividual, a set of same individuals, a row as list of strings, and a column
   * number, add the same individual axioms.
   *
   * @param individual OWLIndiviudal to add axioms to
   * @param sameIndividuals set of same individuals
   * @param row list of strings
   * @param column column number
   * @throws Exception on problem handling axiom annotations
   */
  private void addSameIndividualsAxioms(
      OWLNamedIndividual individual,
      Set<OWLIndividual> sameIndividuals,
      List<String> row,
      int column)
      throws Exception {
    // Maybe get an annotation on the subproperty axiom
    Set<OWLAnnotation> axiomAnnotations = maybeGetAxiomAnnotations(row, column);

    // Generate axioms
    sameIndividuals.add(individual);
    axioms.add(dataFactory.getOWLSameIndividualAxiom(sameIndividuals, axiomAnnotations));
  }

  /**
   * Given an OWLIndividual, a set of different individuals, a row as list of strings, and a column
   * number, add the different individual axioms.
   *
   * @param individual OWLIndiviudal to add axioms to
   * @param differentIndividuals set of different individuals
   * @param row list of strings
   * @param column column number
   * @throws Exception on problem handling axiom annotations
   */
  private void addDifferentIndividualsAxioms(
      OWLNamedIndividual individual,
      Set<OWLIndividual> differentIndividuals,
      List<String> row,
      int column)
      throws Exception {
    // Maybe get an annotation on the subproperty axiom
    Set<OWLAnnotation> axiomAnnotations = maybeGetAxiomAnnotations(row, column);

    // Generate axioms
    differentIndividuals.add(individual);
    axioms.add(dataFactory.getOWLDifferentIndividualsAxiom(differentIndividuals, axiomAnnotations));
  }

  /* ANNOTATION HELPERS */

  /**
   * Given a template string, a value string, a row as a list of strings, and the column number,
   * return a set of one or more OWLAnnotations.
   *
   * @param template template string
   * @param value value of annotation(s)
   * @param row list of strings
   * @param column column number
   * @return Set of OWLAnnotations
   * @throws Exception on issue getting OWLAnnotations
   */
  private Set<OWLAnnotation> getAnnotations(
      String template, String value, List<String> row, int column) throws Exception {
    if (value == null || value.trim().equals("")) {
      return new HashSet<>();
    }

    Set<OWLAnnotation> annotations =
        TemplateHelper.getAnnotations(name, checker, template, value, rowNum, column);

    // Maybe get an annotation on the annotation
    Set<OWLAnnotation> axiomAnnotations = maybeGetAxiomAnnotations(row, column);
    if (axiomAnnotations.isEmpty()) {
      // No annotations to add
      return annotations;
    }

    // Add annotations and return fixed set
    Set<OWLAnnotation> fixedAnnotations = new HashSet<>();
    for (OWLAnnotation annotation : annotations) {
      fixedAnnotations.add(annotation.getAnnotatedAnnotation(axiomAnnotations));
    }
    return fixedAnnotations;
  }

  /**
   * Given a row as a list of strings and a column number, determine if the next column contains a
   * one or more axiom annotations. If so, return the axiom annotation or annotations as a set of
   * OWLAnnotations.
   *
   * @param row list of strings
   * @param column column number
   * @return set of OWLAnnotations, maybe empty
   * @throws Exception on issue getting the OWLAnnotations
   */
  private Set<OWLAnnotation> maybeGetAxiomAnnotations(List<String> row, int column)
      throws Exception {
    Map<Integer, List<Integer>> annotationsPlusAnnotations = new HashMap<>();
    int lastAnnotation = -1;
    while (column < row.size() - 1) {
      column++;
      // Row might be longer than column
      // That's OK, just skip it
      String template;
      try {
        template = templates.get(column);
      } catch (IndexOutOfBoundsException e) {
        break;
      }
      Matcher m = Pattern.compile("^>.*").matcher(template);
      if (m.matches()) {

        m = Pattern.compile("^>[^>].*").matcher(template);
        if (m.matches()) {
          // This is an annotation on the original axiom
          lastAnnotation = column;
          annotationsPlusAnnotations.put(column, new ArrayList<>());
        }

        m = Pattern.compile("^>>.*").matcher(template);
        if (m.matches()) {
          // This is an annotation on the last axiom annotation
          // Update the map
          List<Integer> cols = annotationsPlusAnnotations.get(lastAnnotation);
          cols.add(column);
          annotationsPlusAnnotations.put(lastAnnotation, cols);
        }
      } else {
        // We are done getting the annotations
        break;
      }
    }

    Set<OWLAnnotation> annotations = new HashSet<>();
    for (Map.Entry<Integer, List<Integer>> annPlusAnns : annotationsPlusAnnotations.entrySet()) {
      int annColumn = annPlusAnns.getKey();
      List<Integer> moreAnns = annPlusAnns.getValue();

      String template = templates.get(annColumn);
      String value = row.get(annColumn).trim();
      if (value.isEmpty()) {
        continue;
      }
      Set<OWLAnnotation> firstAnnotations =
          TemplateHelper.getAnnotations(name, checker, template, value, rowNum, annColumn);

      if (moreAnns.isEmpty()) {
        annotations.addAll(firstAnnotations);
        continue;
      }

      Set<OWLAnnotation> moreAnnotations = new HashSet<>();
      for (int m : moreAnns) {
        template = templates.get(m);
        while (template.startsWith(">")) {
          template = template.substring(1);
        }
        value = row.get(m).trim();
        if (value.isEmpty()) {
          continue;
        }
        moreAnnotations.addAll(
            TemplateHelper.getAnnotations(name, checker, template, value, rowNum, annColumn));
      }

      for (OWLAnnotation f : firstAnnotations) {
        annotations.add(f.getAnnotatedAnnotation(moreAnnotations));
      }
    }
    return annotations;
  }

  /**
   * Given a property string (label or CURIE) and the column of that template string, determine if
   * this is RDFS label and if so, set the label column.
   *
   * @param property property string
   * @param column int column number
   */
  private void maybeSetLabelColumn(String property, int column) {
    OWLAnnotationProperty ap = checker.getOWLAnnotationProperty(property, true);
    if (ap != null) {
      if (ap.getIRI().toString().equals(dataFactory.getRDFSLabel().getIRI().toString())) {
        labelColumn = column;
      }
    }
  }

  /* OTHER HELPERS */

  /**
   * Given a string ID and a string label, with at least one of those being non-null, return an IRI
   * for the entity.
   *
   * @param id String ID of entity, maybe null
   * @param label String label of entity, maybe null
   * @return IRI of entity
   * @throws Exception if both id and label are null
   */
  private IRI getIRI(String id, String label) throws Exception {
    if (id == null && label == null) {
      // This cannot be hit by CLI users
      throw new Exception("You must specify either an ID or a label");
    }
    if (id != null) {
      return ioHelper.createIRI(id, true);
    }
    return checker.getIRI(label, true);
  }

  /**
   * Given a row, get the property type if it exists. If not, return default of "subproperty".
   *
   * @param row list of strings
   * @return property type
   */
  private String getPropertyType(List<String> row) {
    String propertyType = null;
    if (propertyTypeColumn != -1) {
      try {
        propertyType = row.get(propertyTypeColumn);
      } catch (IndexOutOfBoundsException e) {
        // do nothing
      }
    }
    if (propertyType == null || propertyType.trim().isEmpty()) {
      return "subproperty";
    } else {
      return propertyType.trim().toLowerCase();
    }
  }

  /**
   * Given a row, get the list of characteristics if they exist. If not, return an empty list.
   *
   * @param row list of strings
   * @return characteristics
   */
  private List<String> getCharacteristics(List<String> row) {
    if (characteristicColumn != -1) {
      String characteristicString = row.get(characteristicColumn);
      if (characteristicSplit != null && characteristicString.contains(characteristicSplit)) {
        return Arrays.asList(characteristicString.split(Pattern.quote(characteristicSplit)));
      } else {
        return Collections.singletonList(characteristicString.trim());
      }
    }
    return new ArrayList<>();
  }
}