<commit_before>/**
 *
 */
package edu.ucsd.sbrg.cobra;

import static edu.ucsd.sbrg.bigg.ModelPolisher.mpMessageBundle;
import static java.text.MessageFormat.format;
import static org.sbml.jsbml.util.Pair.pairOf;

import java.io.File;
import java.io.IOException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.logging.Logger;

import javax.xml.stream.XMLStreamException;

import org.sbml.jsbml.CVTerm;
import org.sbml.jsbml.Model;
import org.sbml.jsbml.Reaction;
import org.sbml.jsbml.SBMLDocument;
import org.sbml.jsbml.SBase;
import org.sbml.jsbml.Species;
import org.sbml.jsbml.Unit;
import org.sbml.jsbml.UnitDefinition;
import org.sbml.jsbml.ext.fbc.FBCConstants;
import org.sbml.jsbml.ext.fbc.FBCModelPlugin;
import org.sbml.jsbml.ext.fbc.FBCReactionPlugin;
import org.sbml.jsbml.ext.fbc.FBCSpeciesPlugin;
import org.sbml.jsbml.ext.fbc.FluxObjective;
import org.sbml.jsbml.ext.fbc.GeneProduct;
import org.sbml.jsbml.ext.fbc.Objective;
import org.sbml.jsbml.ext.groups.Group;
import org.sbml.jsbml.ext.groups.GroupsConstants;
import org.sbml.jsbml.ext.groups.GroupsModelPlugin;
import org.sbml.jsbml.util.ModelBuilder;

import de.zbit.sbml.util.SBMLtools;
import de.zbit.util.Utils;
import edu.ucsd.sbrg.bigg.BiGGId;
import edu.ucsd.sbrg.miriam.Registry;
import edu.ucsd.sbrg.util.SBMLUtils;
import edu.ucsd.sbrg.util.UpdateListener;

import us.hebi.matlab.mat.format.Mat5;
import us.hebi.matlab.mat.format.Mat5File;
import us.hebi.matlab.mat.types.*;
import us.hebi.matlab.mat.types.MatFile.Entry;

/**
 * @author <NAME>uml;ger
 */
public class COBRAparser {

  private static final String DELIM = " ,;\t\n\r\f";
  // BiGGID prefixes
  private static final String GENE_PRODUCT_PREFIX = "G";
  private static final String REACTION_PREFIX = "R";
  private static final String METABOLITE_PREFIX = "M";
  /**
   *
   */
  private MatlabFields mlField;
  /**
   * A {@link Logger} for this class.
   */
  private static final transient Logger logger = Logger.getLogger(COBRAparser.class.getName());

  /**
   *
   */

  private class MatlabFields {

    Array description;
    Cell author;
    Cell citations;
    Cell comments;
    Cell confidenceScores;
    Cell ecNumbers;
    Cell genedate;
    Cell geneindex;
    Cell genesource;
    Cell genes;
    Cell grRules;
    Cell metCHEBIID;
    Cell metFormulas;
    Cell metHMDB;
    Cell metInchiString;
    Cell metKeggID;
    Cell metNames;
    Cell metPubChemID;
    Cell metSmile;
    Cell mets;
    Cell name;
    Cell notes;
    Cell organism;
    Cell rxnKeggID;
    Cell rxns;
    Cell rxnNames;
    Cell subSystems;
    Char csense;
    Matrix lb; //Double
    Matrix metCharge; //Double
    Matrix ub; //Double
    Matrix b; //NumericArray<?>
    Matrix coefficients; //NumericArray<?>
    Matrix rev; //NumericArray<?>
    Sparse S;
    Struct struct;


    /**
     * @param struct
     */
    MatlabFields(Struct struct) {
      this.struct = struct;
      initializeFields();
    }


    /**
     *
     */
    void initializeFields() {
      b = toNumericArrayMatrix(getStructField(ModelField.b));
      citations = toCell(getStructField(ModelField.citations),ModelField.citations.name());
      coefficients = toNumericArrayMatrix(getStructField(ModelField.c));
      comments = toCell(getStructField(ModelField.comments),ModelField.comments.name());
      confidenceScores = toCell(getStructField(ModelField.confidenceScores),ModelField.confidenceScores.name());
      description = toCell(getStructField(ModelField.description),ModelField.description.name());
      csense = toChar(getStructField(ModelField.csense),ModelField.csense.name());
      ecNumbers = toCell(getStructField(ModelField.ecNumbers),ModelField.ecNumbers.name());
      genes = toCell(getStructField(ModelField.mets),ModelField.mets.name());
      grRules = toCell(getStructField(ModelField.grRules),ModelField.grRules.name());
      lb = toDouble(getStructField(ModelField.lb),ModelField.lb.name());
      mets = toCell(getStructField(ModelField.mets),ModelField.mets.name());
      metCharge = toDouble(getStructField(ModelField.metCharge),ModelField.metCharge.name());
      metCHEBIID = toCell(getStructField(ModelField.metCHEBIID),ModelField.metCHEBIID.name());
      metFormulas = toCell(getStructField(ModelField.metFormulas),ModelField.metFormulas.name());
      metHMDB = toCell(getStructField(ModelField.metHMDB),ModelField.metHMDB.name());
      metInchiString = toCell(getStructField(ModelField.metInchiString),ModelField.metInchiString.name());
      metKeggID = toCell(getStructField(ModelField.metKEGGID),ModelField.metKEGGID.name());
      metNames = toCell(getStructField(ModelField.metNames),ModelField.metNames.name());
      metPubChemID = toCell(getStructField(ModelField.metPubChemID),ModelField.metPubChemID.name());
      metSmile = toCell(getStructField(ModelField.metSmile),ModelField.metSmile.name());
      rev = toNumericArrayMatrix(getStructField(ModelField.rev));
      rxns = toCell(getStructField(ModelField.rxns),ModelField.rxns.name());
      rxnKeggID = toCell(getStructField(ModelField.rxnKeggID),ModelField.rxnKeggID.name());
      rxnNames = toCell(getStructField(ModelField.rxnNames),ModelField.rxnNames.name());
      S = toSparse(getStructField(ModelField.S),ModelField.S.name());
      subSystems = toCell(getStructField(ModelField.subSystems),ModelField.subSystems.name());
      ub = toDouble(getStructField(ModelField.ub),ModelField.ub.name());
    }


    /**
     *
     */
    void setDescriptionFields() {
      Struct descrStruct = (Struct) description;
      name = toCell(getStructField(descrStruct, ModelField.name),ModelField.name.name());
      organism = toCell(getStructField(descrStruct, ModelField.organism),ModelField.organism.name());
      author = toCell(getStructField(descrStruct, ModelField.author),ModelField.author.name());
      geneindex = toCell(getStructField(descrStruct, ModelField.geneindex),ModelField.geneindex.name());
      genedate = toCell(getStructField(descrStruct, ModelField.genedate),ModelField.genedate.name());
      genesource = toCell(getStructField(descrStruct, ModelField.genesource),ModelField.genesource.name());
      notes = toCell(getStructField(descrStruct, ModelField.notes),ModelField.notes.name());
    }


    /**
     * @param field
     * @return
     */
    Array getStructField(ModelField field) {
      return struct.get(field.name());
    }


    /**
     * @param struct
     * @param field
     * @return
     */
    Array getStructField(Struct struct, ModelField field) {
      return struct.get(field.name());
    }
  }


  /**
   * @param matFile
   * @param omitGenericTerms
   * @return
   * @throws IOException
   */
  public static List<SBMLDocument> read(File matFile, boolean omitGenericTerms) throws IOException {
    COBRAparser parser = new COBRAparser();
    parser.setOmitGenericTerms(omitGenericTerms);
    return parser.parse(matFile);
  }


  /**
   * @param mat5File
   * @return
   */
  //returns Map of arrayName, Array
  private HashMap<String, Array> getModels(Mat5File mat5File) {
    Iterable<Entry> entries = mat5File.getEntries();
    HashMap<String, Array> content = new HashMap<>();
    //add entries to content
    //analogous to lookup in the library
    for(Entry entry : entries){
      content.put(entry.getName(), entry.getValue());
    }

    Iterator<String> keyIter = content.keySet().iterator();
    HashMap<String, Array> models = new HashMap<String, Array>();
    while (keyIter.hasNext()) {
      String name = keyIter.next();
      Array array = content.get(name);
      if ((array.getNumElements() == 1) && array.getType() == MatlabType.Structure) {
        models.put(name, array);
      }
    }
    if (content.keySet().size() > 1) {
      logger.warning(
              format(mpMessageBundle.getString("MORE_MODELS_COBRA_FILE"), content.keySet().size(), models.size()));
    }
    return models;
  }


  /**
   * @param models
   * @return
   */
  //input = HashMap<arrayName,Array>
  private List<SBMLDocument> parseModels(HashMap<String, Array> models) {
    Iterator<String> nameIter = models.keySet().iterator();
    List<SBMLDocument> docs = new ArrayList<>();

    while (nameIter.hasNext()) {
      String name = nameIter.next();
      Array model = models.get(name);
      ModelBuilder builder = new ModelBuilder(3, 1);
      builder.buildModel(SBMLtools.toSId(name), null);
      SBMLDocument doc = builder.getSBMLDocument();
      doc.addTreeNodeChangeListener(new UpdateListener());
      //pass builder, arrayName, array for parsing
      parseModel(builder, name, model);
      docs.add(doc);
    }
    return docs;
  }


  /**
   *
   */
  private COBRAparser() {
    super();
    setOmitGenericTerms(false);
  }

  /**
   *
   */
  private boolean omitGenericTerms;


  /**
   * @return the omitGenericTerms
   */
  public boolean getOmitGenericTerms() {
    return omitGenericTerms;
  }


  /**
   * @param omitGenericTerms
   *        the omitGenericTerms to set
   */
  public void setOmitGenericTerms(boolean omitGenericTerms) {
    this.omitGenericTerms = omitGenericTerms;
  }


  /**
   * @param matFile
   * @return
   * @throws IOException
   */
  //returns List<SBMLDocument> after parsing
  private List<SBMLDocument> parse(File matFile) throws IOException {
    Mat5File mat5File = Mat5.readFromFile(matFile);
    return parseModels(getModels(mat5File));
  }


  /**
   * @param builder
   */

  private void parseGenes(ModelBuilder builder) {
    if (mlField.genes == null) {
      logger.info(mpMessageBundle.getString("GENES_MISSING"));
      return;
    }
    Model model = builder.getModel();
    FBCModelPlugin modelPlug = (FBCModelPlugin) model.getPlugin(FBCConstants.shortLabel);
    for (int i = 0; i < mlField.genes.getNumElements(); i++) {
      parseGene(modelPlug, i);
    }
  }


  /**
   * @param modelPlug
   * @param i
   */

  private void parseGene(FBCModelPlugin modelPlug, int i) {
    String id = toString(mlField.genes.get(i), ModelField.genes.name(), i + 1);
    if (id.length() > 0) {
      BiGGId biggId;
      try {
        biggId = new BiGGId(correctId(id));
      } catch (IllegalArgumentException exc) {
        logException(exc);
        return;
      }
      if (!biggId.isSetPrefix()) {
        biggId.setPrefix(GENE_PRODUCT_PREFIX);
      }
      GeneProduct gp = modelPlug.createGeneProduct(biggId.toBiGGId());
      gp.setLabel(id);
      gp.setName(id);
    }
  }


  /**
   * @param builder
   */

  private void parseMetabolites(ModelBuilder builder) {
    Model model = builder.getModel();
    for (int i = 0; (mlField.mets != null) && (i < mlField.mets.getNumElements()); i++) {
      parseMetabolite(model, i);
    }
  }


  /**
   * @param model
   * @param i
   */

  private void parseMetabolite(Model model, int i) {
    String id = toString(mlField.mets.get(i), ModelField.mets.name(), i + 1);
    if (id.length() < 1) {
      return;
    }
    BiGGId biggId;
    try {
      biggId = new BiGGId(correctId(id));
    } catch (IllegalArgumentException exc) {
      logException(exc);
      return;
    }
    if (!biggId.isSetPrefix()) {
      biggId.setPrefix(METABOLITE_PREFIX);
    }
    Species species = model.createSpecies(biggId.toBiGGId());
    parseSpeciesFields(species, i);
    parseAnnotation(species, i);
  }


  /**
   * @param species
   * @param i
   */

  private void parseAnnotation(Species species, int i) {
    CVTerm term = new CVTerm();
    term.setQualifierType(CVTerm.Type.BIOLOGICAL_QUALIFIER);
    term.setBiologicalQualifierType(CVTerm.Qualifier.BQB_IS);
    addResource(mlField.metCHEBIID, i, term, "ChEBI");
    addResource(mlField.metHMDB, i, term, "HMDB");
    addResource(mlField.metInchiString, i, term, "InChI");
    addKEGGResources(term, i);
    addPubChemResources(term, i);
    if (term.getResourceCount() > 0) {
      species.addCVTerm(term);
    }
  }


  /**
   * @param term
   * @param i
   * @return
   */
  private boolean addKEGGResources(CVTerm term, int i) {
    // use short circuit evaluation to only run addResource until one of them returns true
    // return type is needed for this to work
    return addResource(mlField.metKeggID, i, term, "KEGG Compound")
            || addResource(mlField.metKeggID, i, term, "KEGG Drug") || addResource(mlField.metKeggID, i, term, "KEGG Genes")
            || addResource(mlField.metKeggID, i, term, "KEGG Glycan")
            || addResource(mlField.metKeggID, i, term, "KEGG Pathway");
  }


  /**
   * @param i
   * @param term
   * @return
   */
  private boolean addPubChemResources(CVTerm term, int i) {
    return addResource(mlField.metPubChemID, i, term, "PubChem-compound")
            || addResource(mlField.metPubChemID, i, term, "PubChem-substance");
  }


  /**
   * @param species
   * @param i
   */

  private void parseSpeciesFields(Species species, int i) {
    if (mlField.metNames != null) {
      species.setName(toString(mlField.metNames.get(i), ModelField.metNames.name(), i + 1));
    }
    if ((mlField.metFormulas != null) || (mlField.metCharge != null)) {
      FBCSpeciesPlugin specPlug = (FBCSpeciesPlugin) species.getPlugin(FBCConstants.shortLabel);
      if (exists(mlField.metFormulas, i)) {
        specPlug.setChemicalFormula(toString(mlField.metFormulas.get(i), ModelField.metFormulas.name(), i + 1));
      }
      if ((mlField.metCharge != null) && (mlField.metCharge.getNumElements() > i)) {
        double charge = mlField.metCharge.getDouble(i);
        specPlug.setCharge((int) charge);
        if (charge - ((int) charge) != 0d) {
          logger.warning(format(mpMessageBundle.getString("CHARGE_TO_INT_COBRA"), charge, specPlug.getCharge()));
        }
      }
    }
    if ((mlField.metSmile != null) && (mlField.metSmile.get(i) != null)) {
      // For some reason, the mat files appear to store the creation date in the field smile, rather than a smiles
      // string.
      String smile = toString(mlField.metSmile.get(i), ModelField.metSmile.name(), i + 1);
      Date date = parseDate(smile);
      if (date != null) {
        species.createHistory().setCreatedDate(date);
      } else if (!isEmptyString(smile)) {
        try {
          species.appendNotes("<html:p>SMILES: " + smile + "</html:p>");
        } catch (XMLStreamException exc) {
          logException(exc);
        }
      }
    }
    if (species.isSetAnnotation()) {
      species.setMetaId(species.getId());
    }
  }


  /**
   * Attempts to parse the given String into a {@link Date} object. To this end,
   * it applies some date format patterns that have been found in some mat
   * files.
   *
   * @param dateString
   *        the {@link String} to be parsed, i.e., what is assumed to
   *        be a representation of some date.
   * @return a {@link Date} object or {@code null} if parsing fails.
   */
  private Date parseDate(String dateString) {
    try {
      return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(dateString);
    } catch (ParseException exc) {
      try {
        return new SimpleDateFormat("yyyy/MM/dd").parse(dateString);
      } catch (ParseException exc1) {
        logException(exc1);
      }
    }
    return null;
  }


  /**
   * Adds resources to provided CVTerm if catalog and id from MLCell
   * provide a valid URI. Logs ids not matching collection patterns
   * and invalid collections. In both cases no resource is added.
   *
   * @param cell
   * @param i
   *        the index within the cell.
   * @param term
   * @param catalog
   */

  private boolean addResource(Cell cell, int i, CVTerm term, String catalog) {
    boolean success = false;
    if (exists(cell, i)) {
      String id = toMIRIAMid((Array) cell.get(i));
      if ((id != null) && !id.isEmpty()) {
        id = checkId(id);
        if (validId(catalog, id)) {
          String resource = Registry.getURI(catalog, id);
          if (!resource.isEmpty()) {
            term.addResource(resource);
            success = true;
            logger.finest(format(mpMessageBundle.getString("ADDED_URI_COBRA"), resource));
          } else {
            logger.severe(format(mpMessageBundle.getString("ADD_URI_FAILED_COBRA"), catalog, id));
          }
        }
      }
    }
    return success;
  }


  /**
   * Necessary to check for a special whitespace (code 160) at beginning of id
   * (iCHOv1.mat, possibly other models) and to remove trailing ';'
   *
   * @param id
   * @return: trimmed id without ';' at the end
   */
  private String checkId(String id) {
    if (id.startsWith(Character.toString((char) 160)) || id.startsWith("/")) {
      id = id.substring(1);
    }
    if (id.endsWith(";")) {
      id = id.substring(0, id.length() - 1);
    } else if (id.contains(";")) {
      logger.warning(mpMessageBundle.getString("TRUNCATED_ID") + id);
      id = id.substring(0, id.indexOf(";"));
    }
    return id;
  }


  /**
   * Checks if id belongs to a given collection by matching it with the
   * respective regexp
   *
   * @param catalog:
   *        Miriam collection
   * @param id:
   *        id to test for membership
   * @return {@code true}, if it matches, else {@code false}
   */
  private boolean validId(String catalog, String id) {
    if (id.isEmpty()) {
      return false;
    }
    String pattern = Registry.getPattern(catalog);
    boolean validId = false;
    if (!pattern.equals("")) {
      validId = Registry.checkPattern(id, catalog);
      if (!validId) {
        logger.warning(format(mpMessageBundle.getString("PATTERN_MISMATCH"), id, pattern));
      }
    } else {
      logger.severe(format(mpMessageBundle.getString("COLLECTION_UNKNOWN"), catalog));
    }
    return validId;
  }


  /**
   * @param array:
   *        MLArray to be stringified
   * @return String representation of the given array
   */
  private String toMIRIAMid(Array array) {
    return toMIRIAMid(toString(array));
  }


  /**
   * @param idCandidate:
   *        id part for the uri
   * @return finalized id, without [, ], ' at the string boundaries
   */
  private String toMIRIAMid(String idCandidate) {
    if ((idCandidate == null) || idCandidate.isEmpty()) {
      return null;
    }
    int start = 0;
    int end = idCandidate.length() - 1;
    if ((idCandidate.charAt(start) == '[') || (idCandidate.charAt(start) == '\'')) {
      start++;
    }
    if ((idCandidate.charAt(end) == ']') || (idCandidate.charAt(end) == '\'')) {
      end--;
    }
    // substring method already decrements second index
    return (start < end) ? idCandidate.substring(start, end + 1) : null;
  }


  /**
   * @param cell
   * @param i
   * @return
   */

  private boolean exists(Array cell, int i) {
    if (cell != null) {
      if (cell instanceof Cell) {
        return (((Cell) cell).get(i) != null);
      }
      return true;
    }
    return false;
  }


  /**
   * @param id
   * @return
   */
  private String correctId(String id) {
    StringBuilder newId = new StringBuilder(id.length() + 4);
    char c = id.charAt(0);
    // Must start with letter or '_'.
    if (!(((c >= 97) && (c <= 122)) || ((c >= 65) && (c <= 90)) || c == '_')) {
      newId.append("_");
    }
    // May contain letters, digits or '_'
    for (int i = 0; i < id.length(); i++) {
      c = id.charAt(i);
      if (((c == ' ') || (c < 48) || ((57 < c) && (c < 65)) || ((90 < c) && (c < 97)))) {
        if (i < id.length() - 1) {
          newId.append('_'); // Replace spaces and special characters with "_"
        }
      } else {
        newId.append(c);
      }
    }
    if (!newId.toString().equals(id)) {
      logger.info(format(mpMessageBundle.getString("CHANGED_METABOLITE_ID"), id, newId));
    }
    return newId.toString();
  }


  // TODO: Refactor
  /**
   * @param builder
   * @param array
   * @return
   */
  //input = builder, arrayName, array
  private void parseModel(ModelBuilder builder,String name, Array array) {
    Struct struct = (Struct) array;
    // Check that the given data structure only contains allowable entries
    Struct correctedStruct = Mat5.newStruct(struct.getDimensions());
    List<String> field_names = struct.getFieldNames();
    for (String field_name : field_names) {
      Array field = struct.get(field_name);
      checkModelField(correctedStruct, name, field, field_name);
    }
    Model model = parseModel(correctedStruct, builder);
    parseGPRsAndSubsystems(model);
    FBCModelPlugin fbc = (FBCModelPlugin) model.getPlugin(FBCConstants.shortLabel);
    Objective obj = fbc.createObjective("obj");
    obj.setType(Objective.Type.MAXIMIZE);
    fbc.getListOfObjectives().setActiveObjective(obj.getId());
    parseCSense(model);
    buildReactantsProducts(model, obj);
    parseBValue(model);
  }


  /**
   * @param model
   */

  private void parseCSense(Model model) {
    for (int i = 0; (mlField.csense != null) && (i < mlField.csense.getNumElements()); i++) {
      char c = mlField.csense.getChar(i, 0);
      // TODO: only 'E' (equality) is supported for now!
      if (c != 'E' && model.getListOfSpecies().size() > i) {
        logger.severe(format(mpMessageBundle.getString("NEQ_RELATION_UNSUPPORTED"), model.getSpecies(i).getId()));
      }
    }
  }


  /**
   * @param model
   * @param obj
   */

  private void buildReactantsProducts(Model model, Objective obj) {
    for (int i = 0; (mlField.coefficients != null) && (i < mlField.coefficients.getNumElements()); i++) {
      double coefficient = mlField.coefficients.getDouble(i);
      if (coefficient != 0d) {
        Reaction r = model.getReaction(i);
        if (r == null) {
          break;
        }
        FluxObjective fo = obj.createFluxObjective("fo_" + r.getId());
        fo.setCoefficient(coefficient);
        fo.setReaction(r);
      }
    }
  }<commit_msg>catch exception for struct field not present, csense.getChar<commit_after>/**
 *
 */
package edu.ucsd.sbrg.cobra;

import static edu.ucsd.sbrg.bigg.ModelPolisher.mpMessageBundle;
import static java.text.MessageFormat.format;
import static org.sbml.jsbml.util.Pair.pairOf;

import java.io.File;
import java.io.IOException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.logging.Logger;

import javax.xml.stream.XMLStreamException;

import org.sbml.jsbml.CVTerm;
import org.sbml.jsbml.Model;
import org.sbml.jsbml.Reaction;
import org.sbml.jsbml.SBMLDocument;
import org.sbml.jsbml.SBase;
import org.sbml.jsbml.Species;
import org.sbml.jsbml.Unit;
import org.sbml.jsbml.UnitDefinition;
import org.sbml.jsbml.ext.fbc.FBCConstants;
import org.sbml.jsbml.ext.fbc.FBCModelPlugin;
import org.sbml.jsbml.ext.fbc.FBCReactionPlugin;
import org.sbml.jsbml.ext.fbc.FBCSpeciesPlugin;
import org.sbml.jsbml.ext.fbc.FluxObjective;
import org.sbml.jsbml.ext.fbc.GeneProduct;
import org.sbml.jsbml.ext.fbc.Objective;
import org.sbml.jsbml.ext.groups.Group;
import org.sbml.jsbml.ext.groups.GroupsConstants;
import org.sbml.jsbml.ext.groups.GroupsModelPlugin;
import org.sbml.jsbml.util.ModelBuilder;

import de.zbit.sbml.util.SBMLtools;
import de.zbit.util.Utils;
import edu.ucsd.sbrg.bigg.BiGGId;
import edu.ucsd.sbrg.miriam.Registry;
import edu.ucsd.sbrg.util.SBMLUtils;
import edu.ucsd.sbrg.util.UpdateListener;

import us.hebi.matlab.mat.format.Mat5;
import us.hebi.matlab.mat.format.Mat5File;
import us.hebi.matlab.mat.types.*;
import us.hebi.matlab.mat.types.MatFile.Entry;

/**
 * @author <NAME>
 */
public class COBRAparser {

  private static final String DELIM = " ,;\t\n\r\f";
  // BiGGID prefixes
  private static final String GENE_PRODUCT_PREFIX = "G";
  private static final String REACTION_PREFIX = "R";
  private static final String METABOLITE_PREFIX = "M";
  /**
   *
   */
  private MatlabFields mlField;
  /**
   * A {@link Logger} for this class.
   */
  private static final transient Logger logger = Logger.getLogger(COBRAparser.class.getName());

  /**
   *
   */

  private class MatlabFields {

    Array description;
    Cell author;
    Cell citations;
    Cell comments;
    Cell confidenceScores;
    Cell ecNumbers;
    Cell genedate;
    Cell geneindex;
    Cell genesource;
    Cell genes;
    Cell grRules;
    Cell metCHEBIID;
    Cell metFormulas;
    Cell metHMDB;
    Cell metInchiString;
    Cell metKeggID;
    Cell metNames;
    Cell metPubChemID;
    Cell metSmile;
    Cell mets;
    Cell name;
    Cell notes;
    Cell organism;
    Cell rxnKeggID;
    Cell rxns;
    Cell rxnNames;
    Cell subSystems;
    Char csense;
    Matrix lb; //Double
    Matrix metCharge; //Double
    Matrix ub; //Double
    Matrix b; //NumericArray<?>
    Matrix coefficients; //NumericArray<?>
    Matrix rev; //NumericArray<?>
    Sparse S;
    Struct struct;


    /**
     * @param struct
     */
    MatlabFields(Struct struct) {
      this.struct = struct;
      initializeFields();
    }


    /**
     *
     */
    void initializeFields() {
      b = toNumericArrayMatrix(getStructField(ModelField.b));
      citations = toCell(getStructField(ModelField.citations),ModelField.citations.name());
      coefficients = toNumericArrayMatrix(getStructField(ModelField.c));
      comments = toCell(getStructField(ModelField.comments),ModelField.comments.name());
      confidenceScores = toCell(getStructField(ModelField.confidenceScores),ModelField.confidenceScores.name());
      description = toCell(getStructField(ModelField.description),ModelField.description.name());
      csense = toChar(getStructField(ModelField.csense),ModelField.csense.name());
      ecNumbers = toCell(getStructField(ModelField.ecNumbers),ModelField.ecNumbers.name());
      genes = toCell(getStructField(ModelField.mets),ModelField.mets.name());
      grRules = toCell(getStructField(ModelField.grRules),ModelField.grRules.name());
      lb = toDouble(getStructField(ModelField.lb),ModelField.lb.name());
      mets = toCell(getStructField(ModelField.mets),ModelField.mets.name());
      metCharge = toDouble(getStructField(ModelField.metCharge),ModelField.metCharge.name());
      metCHEBIID = toCell(getStructField(ModelField.metCHEBIID),ModelField.metCHEBIID.name());
      metFormulas = toCell(getStructField(ModelField.metFormulas),ModelField.metFormulas.name());
      metHMDB = toCell(getStructField(ModelField.metHMDB),ModelField.metHMDB.name());
      metInchiString = toCell(getStructField(ModelField.metInchiString),ModelField.metInchiString.name());
      metKeggID = toCell(getStructField(ModelField.metKEGGID),ModelField.metKEGGID.name());
      metNames = toCell(getStructField(ModelField.metNames),ModelField.metNames.name());
      metPubChemID = toCell(getStructField(ModelField.metPubChemID),ModelField.metPubChemID.name());
      metSmile = toCell(getStructField(ModelField.metSmile),ModelField.metSmile.name());
      rev = toNumericArrayMatrix(getStructField(ModelField.rev));
      rxns = toCell(getStructField(ModelField.rxns),ModelField.rxns.name());
      rxnKeggID = toCell(getStructField(ModelField.rxnKeggID),ModelField.rxnKeggID.name());
      rxnNames = toCell(getStructField(ModelField.rxnNames),ModelField.rxnNames.name());
      S = toSparse(getStructField(ModelField.S),ModelField.S.name());
      subSystems = toCell(getStructField(ModelField.subSystems),ModelField.subSystems.name());
      ub = toDouble(getStructField(ModelField.ub),ModelField.ub.name());
    }


    /**
     *
     */
    void setDescriptionFields() {
      Struct descrStruct = (Struct) description;
      name = toCell(getStructField(descrStruct, ModelField.name),ModelField.name.name());
      organism = toCell(getStructField(descrStruct, ModelField.organism),ModelField.organism.name());
      author = toCell(getStructField(descrStruct, ModelField.author),ModelField.author.name());
      geneindex = toCell(getStructField(descrStruct, ModelField.geneindex),ModelField.geneindex.name());
      genedate = toCell(getStructField(descrStruct, ModelField.genedate),ModelField.genedate.name());
      genesource = toCell(getStructField(descrStruct, ModelField.genesource),ModelField.genesource.name());
      notes = toCell(getStructField(descrStruct, ModelField.notes),ModelField.notes.name());
    }


    /**
     * @param field
     * @return
     */
    Array getStructField(ModelField field) {
      try{
        return struct.get(field.name());
      }catch (Exception e){
        logger.info(e.toString());
        return null;
      }
    }


    /**
     * @param struct
     * @param field
     * @return
     */
    Array getStructField(Struct struct, ModelField field) {
      try{
        return struct.get(field.name());
      }catch (Exception e){
        logger.info(e.toString());
        return null;
      }
    }
  }


  /**
   * @param matFile
   * @param omitGenericTerms
   * @return
   * @throws IOException
   */
  public static List<SBMLDocument> read(File matFile, boolean omitGenericTerms) throws IOException {
    COBRAparser parser = new COBRAparser();
    parser.setOmitGenericTerms(omitGenericTerms);
    return parser.parse(matFile);
  }


  /**
   * @param mat5File
   * @return
   */
  //returns Map of arrayName, Array
  private HashMap<String, Array> getModels(Mat5File mat5File) {
    Iterable<Entry> entries = mat5File.getEntries();
    HashMap<String, Array> content = new HashMap<>();
    //add entries to content
    //analogous to lookup in the library
    for(Entry entry : entries){
      content.put(entry.getName(), entry.getValue());
    }

    Iterator<String> keyIter = content.keySet().iterator();
    HashMap<String, Array> models = new HashMap<String, Array>();
    while (keyIter.hasNext()) {
      String name = keyIter.next();
      Array array = content.get(name);
      if ((array.getNumElements() == 1) && array.getType() == MatlabType.Structure) {
        models.put(name, array);
      }
    }
    if (content.keySet().size() > 1) {
      logger.warning(
              format(mpMessageBundle.getString("MORE_MODELS_COBRA_FILE"), content.keySet().size(), models.size()));
    }
    return models;
  }


  /**
   * @param models
   * @return
   */
  //input = HashMap<arrayName,Array>
  private List<SBMLDocument> parseModels(HashMap<String, Array> models) {
    Iterator<String> nameIter = models.keySet().iterator();
    List<SBMLDocument> docs = new ArrayList<>();

    while (nameIter.hasNext()) {
      String name = nameIter.next();
      Array model = models.get(name);
      ModelBuilder builder = new ModelBuilder(3, 1);
      builder.buildModel(SBMLtools.toSId(name), null);
      SBMLDocument doc = builder.getSBMLDocument();
      doc.addTreeNodeChangeListener(new UpdateListener());
      //pass builder, arrayName, array for parsing
      parseModel(builder, name, model);
      docs.add(doc);
    }
    return docs;
  }


  /**
   *
   */
  private COBRAparser() {
    super();
    setOmitGenericTerms(false);
  }

  /**
   *
   */
  private boolean omitGenericTerms;


  /**
   * @return the omitGenericTerms
   */
  public boolean getOmitGenericTerms() {
    return omitGenericTerms;
  }


  /**
   * @param omitGenericTerms
   *        the omitGenericTerms to set
   */
  public void setOmitGenericTerms(boolean omitGenericTerms) {
    this.omitGenericTerms = omitGenericTerms;
  }


  /**
   * @param matFile
   * @return
   * @throws IOException
   */
  //returns List<SBMLDocument> after parsing
  private List<SBMLDocument> parse(File matFile) throws IOException {
    Mat5File mat5File = Mat5.readFromFile(matFile);
    return parseModels(getModels(mat5File));
  }


  /**
   * @param builder
   */

  private void parseGenes(ModelBuilder builder) {
    if (mlField.genes == null) {
      logger.info(mpMessageBundle.getString("GENES_MISSING"));
      return;
    }
    Model model = builder.getModel();
    FBCModelPlugin modelPlug = (FBCModelPlugin) model.getPlugin(FBCConstants.shortLabel);
    for (int i = 0; i < mlField.genes.getNumElements(); i++) {
      parseGene(modelPlug, i);
    }
  }


  /**
   * @param modelPlug
   * @param i
   */

  private void parseGene(FBCModelPlugin modelPlug, int i) {
    String id = toString(mlField.genes.get(i), ModelField.genes.name(), i + 1);
    if (id.length() > 0) {
      BiGGId biggId;
      try {
        biggId = new BiGGId(correctId(id));
      } catch (IllegalArgumentException exc) {
        logException(exc);
        return;
      }
      if (!biggId.isSetPrefix()) {
        biggId.setPrefix(GENE_PRODUCT_PREFIX);
      }
      GeneProduct gp = modelPlug.createGeneProduct(biggId.toBiGGId());
      gp.setLabel(id);
      gp.setName(id);
    }
  }


  /**
   * @param builder
   */

  private void parseMetabolites(ModelBuilder builder) {
    Model model = builder.getModel();
    for (int i = 0; (mlField.mets != null) && (i < mlField.mets.getNumElements()); i++) {
      parseMetabolite(model, i);
    }
  }


  /**
   * @param model
   * @param i
   */

  private void parseMetabolite(Model model, int i) {
    String id = toString(mlField.mets.get(i), ModelField.mets.name(), i + 1);
    if (id.length() < 1) {
      return;
    }
    BiGGId biggId;
    try {
      biggId = new BiGGId(correctId(id));
    } catch (IllegalArgumentException exc) {
      logException(exc);
      return;
    }
    if (!biggId.isSetPrefix()) {
      biggId.setPrefix(METABOLITE_PREFIX);
    }
    Species species = model.createSpecies(biggId.toBiGGId());
    parseSpeciesFields(species, i);
    parseAnnotation(species, i);
  }


  /**
   * @param species
   * @param i
   */

  private void parseAnnotation(Species species, int i) {
    CVTerm term = new CVTerm();
    term.setQualifierType(CVTerm.Type.BIOLOGICAL_QUALIFIER);
    term.setBiologicalQualifierType(CVTerm.Qualifier.BQB_IS);
    addResource(mlField.metCHEBIID, i, term, "ChEBI");
    addResource(mlField.metHMDB, i, term, "HMDB");
    addResource(mlField.metInchiString, i, term, "InChI");
    addKEGGResources(term, i);
    addPubChemResources(term, i);
    if (term.getResourceCount() > 0) {
      species.addCVTerm(term);
    }
  }


  /**
   * @param term
   * @param i
   * @return
   */
  private boolean addKEGGResources(CVTerm term, int i) {
    // use short circuit evaluation to only run addResource until one of them returns true
    // return type is needed for this to work
    return addResource(mlField.metKeggID, i, term, "KEGG Compound")
            || addResource(mlField.metKeggID, i, term, "KEGG Drug") || addResource(mlField.metKeggID, i, term, "KEGG Genes")
            || addResource(mlField.metKeggID, i, term, "KEGG Glycan")
            || addResource(mlField.metKeggID, i, term, "KEGG Pathway");
  }


  /**
   * @param i
   * @param term
   * @return
   */
  private boolean addPubChemResources(CVTerm term, int i) {
    return addResource(mlField.metPubChemID, i, term, "PubChem-compound")
            || addResource(mlField.metPubChemID, i, term, "PubChem-substance");
  }


  /**
   * @param species
   * @param i
   */

  private void parseSpeciesFields(Species species, int i) {
    if (mlField.metNames != null) {
      species.setName(toString(mlField.metNames.get(i), ModelField.metNames.name(), i + 1));
    }
    if ((mlField.metFormulas != null) || (mlField.metCharge != null)) {
      FBCSpeciesPlugin specPlug = (FBCSpeciesPlugin) species.getPlugin(FBCConstants.shortLabel);
      if (exists(mlField.metFormulas, i)) {
        specPlug.setChemicalFormula(toString(mlField.metFormulas.get(i), ModelField.metFormulas.name(), i + 1));
      }
      if ((mlField.metCharge != null) && (mlField.metCharge.getNumElements() > i)) {
        double charge = mlField.metCharge.getDouble(i);
        specPlug.setCharge((int) charge);
        if (charge - ((int) charge) != 0d) {
          logger.warning(format(mpMessageBundle.getString("CHARGE_TO_INT_COBRA"), charge, specPlug.getCharge()));
        }
      }
    }
    if ((mlField.metSmile != null) && (mlField.metSmile.get(i) != null)) {
      // For some reason, the mat files appear to store the creation date in the field smile, rather than a smiles
      // string.
      String smile = toString(mlField.metSmile.get(i), ModelField.metSmile.name(), i + 1);
      Date date = parseDate(smile);
      if (date != null) {
        species.createHistory().setCreatedDate(date);
      } else if (!isEmptyString(smile)) {
        try {
          species.appendNotes("<html:p>SMILES: " + smile + "</html:p>");
        } catch (XMLStreamException exc) {
          logException(exc);
        }
      }
    }
    if (species.isSetAnnotation()) {
      species.setMetaId(species.getId());
    }
  }


  /**
   * Attempts to parse the given String into a {@link Date} object. To this end,
   * it applies some date format patterns that have been found in some mat
   * files.
   *
   * @param dateString
   *        the {@link String} to be parsed, i.e., what is assumed to
   *        be a representation of some date.
   * @return a {@link Date} object or {@code null} if parsing fails.
   */
  private Date parseDate(String dateString) {
    try {
      return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(dateString);
    } catch (ParseException exc) {
      try {
        return new SimpleDateFormat("yyyy/MM/dd").parse(dateString);
      } catch (ParseException exc1) {
        logException(exc1);
      }
    }
    return null;
  }


  /**
   * Adds resources to provided CVTerm if catalog and id from MLCell
   * provide a valid URI. Logs ids not matching collection patterns
   * and invalid collections. In both cases no resource is added.
   *
   * @param cell
   * @param i
   *        the index within the cell.
   * @param term
   * @param catalog
   */

  private boolean addResource(Cell cell, int i, CVTerm term, String catalog) {
    boolean success = false;
    if (exists(cell, i)) {
      String id = toMIRIAMid((Array) cell.get(i));
      if ((id != null) && !id.isEmpty()) {
        id = checkId(id);
        if (validId(catalog, id)) {
          String resource = Registry.getURI(catalog, id);
          if (!resource.isEmpty()) {
            term.addResource(resource);
            success = true;
            logger.finest(format(mpMessageBundle.getString("ADDED_URI_COBRA"), resource));
          } else {
            logger.severe(format(mpMessageBundle.getString("ADD_URI_FAILED_COBRA"), catalog, id));
          }
        }
      }
    }
    return success;
  }


  /**
   * Necessary to check for a special whitespace (code 160) at beginning of id
   * (iCHOv1.mat, possibly other models) and to remove trailing ';'
   *
   * @param id
   * @return: trimmed id without ';' at the end
   */
  private String checkId(String id) {
    if (id.startsWith(Character.toString((char) 160)) || id.startsWith("/")) {
      id = id.substring(1);
    }
    if (id.endsWith(";")) {
      id = id.substring(0, id.length() - 1);
    } else if (id.contains(";")) {
      logger.warning(mpMessageBundle.getString("TRUNCATED_ID") + id);
      id = id.substring(0, id.indexOf(";"));
    }
    return id;
  }


  /**
   * Checks if id belongs to a given collection by matching it with the
   * respective regexp
   *
   * @param catalog:
   *        Miriam collection
   * @param id:
   *        id to test for membership
   * @return {@code true}, if it matches, else {@code false}
   */
  private boolean validId(String catalog, String id) {
    if (id.isEmpty()) {
      return false;
    }
    String pattern = Registry.getPattern(catalog);
    boolean validId = false;
    if (!pattern.equals("")) {
      validId = Registry.checkPattern(id, catalog);
      if (!validId) {
        logger.warning(format(mpMessageBundle.getString("PATTERN_MISMATCH"), id, pattern));
      }
    } else {
      logger.severe(format(mpMessageBundle.getString("COLLECTION_UNKNOWN"), catalog));
    }
    return validId;
  }


  /**
   * @param array:
   *        MLArray to be stringified
   * @return String representation of the given array
   */
  private String toMIRIAMid(Array array) {
    return toMIRIAMid(toString(array));
  }


  /**
   * @param idCandidate:
   *        id part for the uri
   * @return finalized id, without [, ], ' at the string boundaries
   */
  private String toMIRIAMid(String idCandidate) {
    if ((idCandidate == null) || idCandidate.isEmpty()) {
      return null;
    }
    int start = 0;
    int end = idCandidate.length() - 1;
    if ((idCandidate.charAt(start) == '[') || (idCandidate.charAt(start) == '\'')) {
      start++;
    }
    if ((idCandidate.charAt(end) == ']') || (idCandidate.charAt(end) == '\'')) {
      end--;
    }
    // substring method already decrements second index
    return (start < end) ? idCandidate.substring(start, end + 1) : null;
  }


  /**
   * @param cell
   * @param i
   * @return
   */

  private boolean exists(Array cell, int i) {
    if (cell != null) {
      if (cell instanceof Cell) {
        return (((Cell) cell).get(i) != null);
      }
      return true;
    }
    return false;
  }


  /**
   * @param id
   * @return
   */
  private String correctId(String id) {
    StringBuilder newId = new StringBuilder(id.length() + 4);
    char c = id.charAt(0);
    // Must start with letter or '_'.
    if (!(((c >= 97) && (c <= 122)) || ((c >= 65) && (c <= 90)) || c == '_')) {
      newId.append("_");
    }
    // May contain letters, digits or '_'
    for (int i = 0; i < id.length(); i++) {
      c = id.charAt(i);
      if (((c == ' ') || (c < 48) || ((57 < c) && (c < 65)) || ((90 < c) && (c < 97)))) {
        if (i < id.length() - 1) {
          newId.append('_'); // Replace spaces and special characters with "_"
        }
      } else {
        newId.append(c);
      }
    }
    if (!newId.toString().equals(id)) {
      logger.info(format(mpMessageBundle.getString("CHANGED_METABOLITE_ID"), id, newId));
    }
    return newId.toString();
  }


  // TODO: Refactor
  /**
   * @param builder
   * @param array
   * @return
   */
  //input = builder, arrayName, array
  private void parseModel(ModelBuilder builder,String name, Array array) {
    Struct struct = (Struct) array;
    // Check that the given data structure only contains allowable entries
    Struct correctedStruct = Mat5.newStruct(struct.getDimensions());
    List<String> field_names = struct.getFieldNames();
    for (String field_name : field_names) {
      Array field = struct.get(field_name);
      checkModelField(correctedStruct, name, field, field_name);
    }
    Model model = parseModel(correctedStruct, builder);
    parseGPRsAndSubsystems(model);
    FBCModelPlugin fbc = (FBCModelPlugin) model.getPlugin(FBCConstants.shortLabel);
    Objective obj = fbc.createObjective("obj");
    obj.setType(Objective.Type.MAXIMIZE);
    fbc.getListOfObjectives().setActiveObjective(obj.getId());
    parseCSense(model);
    buildReactantsProducts(model, obj);
    parseBValue(model);
  }


  /**
   * @param model
   */

  private void parseCSense(Model model) {
    if(mlField.csense == null){
      return;
    }

    for (int i = 0; (mlField.csense != null) && (i < mlField.csense.getNumElements()); i++) {
      try{
        char c = mlField.csense.getChar(i, 0);
        // TODO: only 'E' (equality) is supported for now!
        if (c != 'E' && model.getListOfSpecies().size() > i) {
          logger.severe(format(mpMessageBundle.getString("NEQ_RELATION_UNSUPPORTED"), model.getSpecies(i).getId()));
        }
      }catch (Exception e){
        logger.info(e.toString());
        return;
      }
    }
  }


  /**
   * @param model
   * @param obj
   */

  private void buildReactantsProducts(Model model, Objective obj) {
    for (int i = 0; (mlField.coefficients != null) && (i < mlField.coefficients.getNumElements()); i++) {
      double coefficient = mlField.coefficients.getDouble(i);
      if (coefficient != 0d) {
        Reaction r = model.getReaction(i);
        if (r == null) {
          break;
        }
        FluxObjective fo = obj.createFluxObjective("fo_" + r.getId());
        fo.setCoefficient(coefficient);
        fo.setReaction(r);
      }
    }
  }
{'fraction_non_alphanumeric': 0.1346342129829914, 'fraction_numerical': 0.003142148045265147, 'mean_word_length': 3.025295573274677, 'pattern_counts': {'":': 0, '<': 79, '<?xml version=': 0, '>': 63, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 2}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '919584', 'n_tokens_mistral': 14646, 'n_tokens_neox': 14125, 'n_words': 2956}