SiLeBAT/FSK-Lab

View on GitHub
de.bund.bfr.knime.fsklab.nodes/src/de/bund/bfr/knime/fsklab/v2_0/reader/ReaderNodeUtil.java

Summary

Maintainability
F
1 wk
Test Coverage
package de.bund.bfr.knime.fsklab.v2_0.reader;


import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import de.bund.bfr.fskml.FSKML;
import de.bund.bfr.fskml.FskMetaDataObject;
import de.bund.bfr.fskml.FskMetaDataObject.ResourceType;
import de.bund.bfr.fskml.RScript;
import de.bund.bfr.knime.fsklab.FskPlugin;
import de.bund.bfr.knime.fsklab.nodes.NodeUtils;
import de.bund.bfr.knime.fsklab.nodes.environment.ArchivedEnvironmentManager;
import de.bund.bfr.knime.fsklab.nodes.environment.EnvironmentManager;
import de.bund.bfr.knime.fsklab.rakip.RakipUtil;
import de.bund.bfr.knime.fsklab.v2_0.CombinedFskPortObject;
import de.bund.bfr.knime.fsklab.v2_0.FskPortObject;
import de.bund.bfr.knime.fsklab.v2_0.FskSimulation;
import de.bund.bfr.knime.fsklab.v2_0.JoinRelation;
import de.bund.bfr.knime.fsklab.v2_0.joiner.JoinerNodeModel;
import de.bund.bfr.metadata.swagger.GenericModel;
import de.bund.bfr.metadata.swagger.GenericModelDataBackground;
import de.bund.bfr.metadata.swagger.GenericModelGeneralInformation;
import de.bund.bfr.metadata.swagger.GenericModelModelMath;
import de.bund.bfr.metadata.swagger.GenericModelScope;
import de.bund.bfr.metadata.swagger.Model;
import de.bund.bfr.metadata.swagger.Parameter;
import de.bund.bfr.metadata.swagger.Parameter.ClassificationEnum;
import de.unirostock.sems.cbarchive.ArchiveEntry;
import de.unirostock.sems.cbarchive.CombineArchive;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
import metadata.SwaggerUtil;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.jdom.Text;
import org.jlibsedml.ChangeAttribute;
import org.jlibsedml.Libsedml;
import org.jlibsedml.SEDMLTags;
import org.jlibsedml.SedML;
import org.jlibsedml.XMLException;
import org.knime.core.util.FileUtil;
import org.sbml.jsbml.SBMLDocument;
import org.sbml.jsbml.SBMLReader;
import org.sbml.jsbml.ext.comp.CompModelPlugin;
import org.sbml.jsbml.ext.comp.CompSBasePlugin;
import org.sbml.jsbml.ext.comp.ReplacedBy;
import org.sbml.jsbml.xml.XMLNode;

/**
 * Utility class that updates combined models from FSK-Lab v.1.7.2 to be compatible with 1.8+. The
 * difference between the versions is entirely in the parameter suffixes, meaning this class will
 * contain methods to update the suffixes of parameters in the metadata, the simulations and
 * join-relations. Before updating, any model will be checked if their parameters adhere to the
 * standards of 1.8+.
 * 
 * @author SchueleT
 */
public class ReaderNodeUtil {
  static final Set<String> FSKFilesRegistry = new HashSet();
  private static ReentrantLock lock = new ReentrantLock();
  private ReaderNodeUtil() {
  }

  /**
   * Updates a combined model from 1.7.2 to be compatible with 1.8+. The suffixes from all
   * parameters are updated in metadata, simulations an join relations. If the model is from 1.7.2
   * (or combined at all) will be checked by looking at the current parameter suffixes. The update
   * is recursive for the entire model tree.
   * 
   * @param fskObj The combined model to be updated
   */
  public static void updateSuffixes(FskPortObject fskObj) {

    if (fskObj instanceof CombinedFskPortObject) {

      CombinedFskPortObject obj = (CombinedFskPortObject) fskObj;
      
      // check if model needs an update at all:
      if (modelNeedsUpdate(fskObj)) {
        // if children are combined models, update them first:
        if (obj.getFirstFskPortObject() instanceof CombinedFskPortObject) {
          updateSuffixes(obj.getFirstFskPortObject());
        }
        if (obj.getSecondFskPortObject() instanceof CombinedFskPortObject) {
          updateSuffixes(obj.getSecondFskPortObject());
        }
        // Get the parameter names of from the first child model to help assigning the combined
        // model parameters.
        List<String> firstModelParameters =
            SwaggerUtil.getParameter(obj.getFirstFskPortObject().modelMetadata).stream()
                .map(Parameter::getId).collect(Collectors.toList());

        // update metadata
        addSuffixesToOldModel(firstModelParameters, SwaggerUtil.getParameter(obj.modelMetadata));

        // update simulations
        addSuffixesToOldSimulations(firstModelParameters, obj.simulations);

        // update join relations
        obj.setJoinerRelation(updateJoinRelations(firstModelParameters, obj.getJoinerRelation()));
      }
    }
  }

  /**
   * This method adds the correct suffix to each parameter from the metadata.
   * 
   * @param firstModelParameters Reference parameter id's from the first sub-model to compare with
   *        parameters of the combined model.
   * @param originalParameters The parameters which are updated by adding a legal suffix to them.
   */
  static void addSuffixesToOldModel(List<String> firstModelParameters,
      List<Parameter> originalParameters) {

    for (Parameter p : originalParameters) {
      
      p.setId(addSuffix(firstModelParameters, p.getId()));
    }
  }

  /**
   * Method to update the parameter id's contained in the join relations of a combined model to make
   * it compatible to FSK-Lab version 1.8+. Source parameter, target parameter and the parameter
   * mentioned in the join command are updated to have the appropriate suffix.
   * 
   * @param firstModelParameters Reference parameter id's from the first sub-model to compare with
   *        parameters of the combined model.
   * @param relations The join relations of the combined model to be updated.
   * @return An array of JoinRelation objects containing updated parameter id's.
   */
  static JoinRelation[] updateJoinRelations(List<String> firstModelParameters,
      JoinRelation[] relations) {


    List<JoinRelation> newRelations = new ArrayList<>();

    for (JoinRelation relation : relations) {

      String command =
          updateJoinCommand(firstModelParameters, relation.getCommand(), relation.getSourceParam());
      String source = addSuffix(firstModelParameters, relation.getSourceParam());
      String target = addSuffix(firstModelParameters, relation.getTargetParam());
      String language = relation.getLanguage_written_in();

      newRelations.add(new JoinRelation(source, target, command, language));

    }

    return newRelations.toArray(new JoinRelation[0]);
  }

  /**
   * Method to update parameters in the simulations of a combined model, so that they comply to
   * version 1.8+.
   * 
   * @param firstModelParameters Reference parameter id's from the first sub-model to compare with
   *        parameters of the combined model.
   * @param simulations Simulations containing the parameters of the combined models.
   */
  static void addSuffixesToOldSimulations(List<String> firstModelParameters,
      List<FskSimulation> simulations) {


    for (FskSimulation oldSim : simulations) {

      Iterator<String> iterator = oldSim.getParameters().keySet().iterator();
      LinkedHashMap<String, String> newSim = new LinkedHashMap<>();
      // Iterate over the keyset of the each simulations linkedHashMap. From each parameter take
      // the name and value and create a new simulation with the updated parameter id.
      while (iterator.hasNext()) {

        String oldName = iterator.next();
        String newName = addSuffix(firstModelParameters, oldName);

        newSim.put(newName, oldSim.getParameters().get(oldName));
        iterator.remove(); // old parameter (i.e. key) is removed from simulation

      }

      oldSim.getParameters().putAll(newSim);
    }
  }

  /**
   * Support method for adding brackets and the correct suffix to the parameter within a join
   * command.
   * 
   * @param firstModelParameters Reference parameter id's from the first sub-model to compare with
   *        parameters of the combined model.
   * @param cmd The Join command to be applied to the targetParameter.
   * @param sourceParam The source parameter to be updated.
   * @return Join Command with updated source parameter and brackets: "[source]"
   */
  private static String updateJoinCommand(List<String> firstModelParameters, String cmd,
      String sourceParam) {

    return cmd.replace(sourceParam, "[" + addSuffix(firstModelParameters, sourceParam) + "]");
  }

  /**
   * Method to add an official suffix to a parameter id. If the parameter has the suffix "_dup", it
   * will be replaced by the official suffix for the first model (only parameters from the first
   * model get the "_dup" suffix.
   * 
   * @param firstModelParameters Parameter id's from the first child model of a combined model
   * @param param A parameter id from any source that needs the suffix
   * @return New parameter id with correct suffix
   */
  static String addSuffix(List<String> firstModelParameters, String param) {

    if (param.endsWith("_dup")) {
      return param.substring(0, param.length() - 4) + JoinerNodeModel.SUFFIX_FIRST;
    }

    if (firstModelParameters.contains(param)) {
      return param + JoinerNodeModel.SUFFIX_FIRST;
    }
    return param + JoinerNodeModel.SUFFIX_SECOND;
  }

  /**
   * Method checks if a model needs to update its parameters to be compliant to FSK-Lab version
   * 1.8+. It compares the number of parameter in the metadata suffixes with the maximum depth of
   * the model. If the depth is greater than the number of legal suffixes, it means the model needs
   * an update to be executable in FSK-Lab.
   * 
   * @param fskObj The model that is to be checked.
   * @return true if the model is outdated and needs an update.
   */
  static boolean modelNeedsUpdate(FskPortObject fskObj) {

    if (fskObj instanceof CombinedFskPortObject) {

      int depthFirst = getNumberOfSubmodels(fskObj);
      int numberSuffixes = getNumberOfSuffixes(SwaggerUtil.getParameter(fskObj.modelMetadata));

      if (depthFirst > numberSuffixes) {
        return true;
      }
    }
    return false;
  }

  /**
   * Support method to get the highest number of suffixes that any parameter has in a model.
   * 
   * @param parameter List of parameters from the metadata of the combined model.
   * @return number of legal suffixes that a parameter can have. We only consider output parameters
   *         since they can't be overwritten by join commands.
   */
  private static int getNumberOfSuffixes(List<Parameter> params) {

    int maxNumber = 0;
    for (Parameter param : params) {

      if (param.getClassification().equals(ClassificationEnum.OUTPUT)) {

        int number = getNumberOfSuffixes(param.getId());
        if (maxNumber < number) {
          maxNumber = number;
        }
      }
    }

    return maxNumber;
  }

  /**
   * Method to count the number of legal suffixes of a parameter id.
   * 
   * @param param Parameter id.
   * @return number of legal suffixes.
   */
  private static int getNumberOfSuffixes(String param) {

    int number = 0;

    while (param.endsWith(JoinerNodeModel.SUFFIX_FIRST)
        || param.endsWith(JoinerNodeModel.SUFFIX_SECOND)) {

      number++;
      param = param.substring(0, param.length() - JoinerNodeModel.SUFFIX_FIRST.length());
    }

    return number;
  }

  /**
   * Method to determine the depth of a combined model, i.e. how many submodels a model has.
   * 
   * @param fskObj The combined model.
   * @return the maximum depth of a combined model.
   */
  private static int getNumberOfSubmodels(FskPortObject fskObj) {

    if (!(fskObj instanceof CombinedFskPortObject)) {
      return 0;
    }

    CombinedFskPortObject combObj = (CombinedFskPortObject) fskObj;

    int depthFirst = 1 + getNumberOfSubmodels(combObj.getFirstFskPortObject());
    int depthSecond = 1 + getNumberOfSubmodels(combObj.getSecondFskPortObject());

    return (depthFirst > depthSecond) ? depthFirst : depthSecond;
  }
  
  private static FskPortObject readFskPortObject(CombineArchive archive, List<String> ListOfPaths,
      int readLevel) throws Exception {
    Map<String, URI> URIS = FSKML.getURIS(1, 0, 12);

    Model model = new Model();

    // more one than one element means this model is joined one
    if (ListOfPaths != null && ListOfPaths.size() > 1) {
      String firstelement = ListOfPaths.get(ListOfPaths.size() % 2);
      // classify the pathes into two groups, each belongs to sub model
      List<String> firstGroup = ListOfPaths.stream().filter(line -> line.startsWith(firstelement))
          .collect(Collectors.toList());
      List<String> secondGroup = ListOfPaths.stream().filter(line -> !firstGroup.contains(line))
          .collect(Collectors.toList());
      if (secondGroup.size() == 2) {
        secondGroup.remove(0);
      }

      // invoke this mothod recursively to get the sub model using the corresponding path group
      FskPortObject firstFskPortObject = readFskPortObject(archive, firstGroup, ++readLevel);
      FskPortObject secondFskPortObject = readFskPortObject(archive, secondGroup, ++readLevel);
      String tempString = firstelement.substring(0, firstelement.length() - 2);
      String parentPath = tempString.substring(0, tempString.lastIndexOf('/'));

      // Gets metadata
      {
        // Find metadata entry
        Optional<ArchiveEntry> metadataEntry = archive.getEntriesWithFormat(URIS.get("json"))
            .stream().filter(entry -> entry.getEntityPath().startsWith(parentPath))
            .filter(entry -> StringUtils.countMatches(entry.getEntityPath(),
                "/") == StringUtils.countMatches(parentPath, "/") + 1)
            .filter(entry -> entry.getEntityPath().endsWith("metaData.json")).findAny();

        if (metadataEntry.isPresent()) {
          model = readMetadata(metadataEntry.get());
        }
      }

      // Get workspace
      URI rdataUri = URIS.get("rdata");
      File workspace = null; // null if missing

      Optional<ArchiveEntry> workspaceEntry = archive.getEntriesWithFormat(rdataUri).stream()
          .filter(entry -> entry.getEntityPath().startsWith(parentPath))
          .filter(entry -> StringUtils.countMatches(entry.getEntityPath(),"/")
              == StringUtils.countMatches(parentPath, "/") + 1)
          .findAny();
      if (workspaceEntry.isPresent()) {
        ArchiveEntry entry = workspaceEntry.get();
        FskMetaDataObject fmdo = new FskMetaDataObject(entry.getDescriptions().get(0));
        if (fmdo.getResourceType() == ResourceType.workspace) {
            workspace = FileUtil.createTempFile("workspace", ".RData");
            entry.extractFile(workspace);
        }
      }

      List<JoinRelation> connectionList = new ArrayList<>();

      // Find SBML entry in archive
      Optional<ArchiveEntry> sbmlEntry =
          archive.getEntriesWithFormat(URIS.get("sbml")).stream().filter(entry -> {
            final String path = entry.getEntityPath();
            return path.startsWith(parentPath) && StringUtils.countMatches(path,
                "/") == StringUtils.countMatches(parentPath, "/") + 1;
          }).findAny();

      String firstModelId = "";
      if (sbmlEntry.isPresent()) {

        // Extract entry to temporary file
        File temporaryFile = File.createTempFile("model", ".sbml");
        sbmlEntry.get().extractFile(temporaryFile);
        SBMLDocument sbmlDocument = SBMLReader.read(temporaryFile);
        temporaryFile.delete();

        // Get first model's id
        CompModelPlugin compModelPlugin =
            (CompModelPlugin) sbmlDocument.getModel().getExtension("comp");
        firstModelId =
            compModelPlugin.getNumSubmodels() > 0 ? compModelPlugin.getSubmodel(0).getModelRef()
                : "";

            for (org.sbml.jsbml.Parameter parameter : sbmlDocument.getModel().getListOfParameters()) {

              //********************** CUT THE SHENANIGANS *************************
              // Find metadata of target parameter. Connected parameter in 2nd model
              //          final String parameterId =
              //              parameter.getId().replaceAll(JoinerNodeModel.SUFFIX_SECOND, "");
              //          Optional<Parameter> targetParameter = SwaggerUtil
              //              .getParameter(secondFskPortObject.modelMetadata).stream().filter(currentParameter -> {
              //                final String currentParameterId =
              //                    currentParameter.getId().replaceAll(JoinerNodeModel.SUFFIX_SECOND, "");
              //                if (parameterId.equals(currentParameterId)) {
              //                  currentParameter.setId(parameter.getId());
              //                  return true;
              //                } else {
              //                  return false;
              //                }
              //              }).findAny();
              //
              //          // If the metadata of targetParamter is not found, skip to next parameter
              //          if (!targetParameter.isPresent()) {
              //            continue;
              //          }

              //********************** CUT THE SHENANIGANS *************************



              //          // Find metadata of source parameter (connected parameter of 1st model)
              final ReplacedBy replacedBy =
                  ((CompSBasePlugin) parameter.getExtension("comp")).getReplacedBy();

              //********************** CUT THE SHENANIGANS *************************         
              //          final String replacement =
              //              replacedBy.getIdRef().replaceAll(JoinerNodeModel.SUFFIX_FIRST, "");
              //          Optional<Parameter> sourceParameter = SwaggerUtil
              //              .getParameter(firstFskPortObject.modelMetadata).stream().filter(currentParameter -> {
              //                final String currentParameterId =
              //                    currentParameter.getId().replaceAll(JoinerNodeModel.SUFFIX_FIRST, "");
              //                if (replacement.equals(currentParameterId)) {
              //                  currentParameter.setId(replacedBy.getIdRef());
              //                  return true;
              //                } else {
              //                  return false;
              //                }
              //              }).findAny();
              //
              //          // If the metadata of sourceParmeter is not found, skip to next parameter
              //          if (!sourceParameter.isPresent()) {
              //            continue;
              //          }
              //********************** CUT THE SHENANIGANS *************************


              String command = null;
              if (parameter.getAnnotation() != null
                  && parameter.getAnnotation().getNonRDFannotation() != null) {
                XMLNode nonRDFannotation = parameter.getAnnotation().getNonRDFannotation();
                XMLNode commandNode = nonRDFannotation.getChildElement("command", "");
                if (commandNode != null
                    && commandNode.hasAttr(NodeUtils.METADATA_COMMAND_VALUE)) {
                  command = commandNode.getAttrValue(NodeUtils.METADATA_COMMAND_VALUE);
                }
              }

              connectionList.add(new JoinRelation(replacedBy.getIdRef(),
                  parameter.getId(), command, null));
              //          connectionList.add(new JoinRelation(sourceParameter.get().getId(),
              //              targetParameter.get().getId(), command, null));
            }
      }

      CombinedFskPortObject topfskObj;
      String currentModelID =
          SwaggerUtil.getModelName(firstFskPortObject.modelMetadata).replaceAll("\\W", "");
      if (currentModelID.equals(firstModelId)) {
        topfskObj = new CombinedFskPortObject("", "", model, Optional.empty(), new ArrayList<>(),
            firstFskPortObject, secondFskPortObject);
      } else {
        topfskObj = new CombinedFskPortObject("", "", model, Optional.empty(), new ArrayList<>(),
            secondFskPortObject, firstFskPortObject);
      }

      topfskObj.setViz(getEmbedSecondFSKObject(topfskObj).getViz());

      // Get select simulation index and simulations for joined model
      Optional<ArchiveEntry> simulationsEntry = archive.getEntriesWithFormat(URIS.get("sedml"))
          .stream().filter(entry -> entry.getEntityPath().startsWith(parentPath))
          .filter(entry -> StringUtils.countMatches(entry.getEntityPath(),
              "/") == StringUtils.countMatches(parentPath, "/") + 1)
          .findAny();
      if (simulationsEntry.isPresent()) {
        SimulationSettings simulationSettings = readSimulationSettings(simulationsEntry.get());
        topfskObj.selectedSimulationIndex = simulationSettings.selectedSimulationIndex;
        topfskObj.simulations.addAll(simulationSettings.simulations);
      }
      
      if (workspace != null) {
        topfskObj.setWorkspace(workspace.toPath());
      }

      topfskObj.setJoinerRelation(connectionList.toArray(new JoinRelation[connectionList.size()]));

      return topfskObj;
    } else {
      String modelScript = "";
      String visualizationScript = "";
      File workspace = null; // null if missing
      String pathToResource = ListOfPaths.get(0);
      String readme = "";

      // Get entries of the current model (but ignore files in /simulations/ )
      List<ArchiveEntry> entries = archive.getEntries().stream()
          .filter(entry -> entry.getEntityPath().indexOf(pathToResource) == 0 
          && !entry.getEntityPath().startsWith(pathToResource + "simulations" + pathToResource))
          .collect(Collectors.toList());

      URI textUri = URI.create("http://purl.org/NET/mediatypes/text-xplain");
      URI csvUri = URIS.get("csv");
      URI xlsxUri = URIS.get("xlsx");
      URI rdataUri = URIS.get("rdata");
      URI jsonUri = URIS.get("json");
      URI sedmlUri = URIS.get("sedml");
      //URI htmllUri = URI.create("https://www.iana.org/assignments/media-types/text/html");
      URI rmdUri = URI.create("https://www.iana.org/assignments/media-types/text/markdown");
      List<URI> fileUris = new ArrayList<URI>();
      fileUris.add(textUri);
      fileUris.add(csvUri);
      fileUris.add(xlsxUri);
      fileUris.add(rdataUri);
      fileUris.add(rmdUri);
      

      // Gets metadata from metadata entry (metaData.json)
      Optional<ArchiveEntry> metadataEntry =
          entries.stream().filter(entry -> entry.getFormat().equals(jsonUri))
          .filter(entry -> entry.getEntityPath().endsWith("metaData.json")).findAny();
      if (metadataEntry.isPresent()) {
        model = readMetadata(metadataEntry.get());
      }

      // The URI of the model script (varies on the language of the model)
      // TODO: MAKE IT MORE GENERIC
      String languageWrittenIn = StringUtils.defaultString(SwaggerUtil.getLanguageWrittenIn(model), "r");
      languageWrittenIn = languageWrittenIn.toLowerCase().startsWith("r") ? "r" : "py" ;
      URI scriptUri = URIS.get(languageWrittenIn);

      // Load scripts
      for (ArchiveEntry entry : entries) {
        // workaround to make python models from version 1.7.2 compatible with 1.8.x
        // those models had scripts that ended in ".r" instead of ".py"
        if (( entry.getFormat().equals(scriptUri) || entry.getFormat().equals(URIS.get("r")) )
            && !entry.getDescriptions().isEmpty()) {
          FskMetaDataObject fmdo = new FskMetaDataObject(entry.getDescriptions().get(0));
          ResourceType resourceType = fmdo.getResourceType();

          if (resourceType == ResourceType.modelScript) {
            modelScript = loadTextEntry(entry);
          } else if (resourceType == ResourceType.visualizationScript) {
            visualizationScript = loadTextEntry(entry);
          } else if (resourceType == ResourceType.workspace) {
            // Legacy check. Look for R workspace with R URI (from old files)
            workspace = FileUtil.createTempFile("workspace", ".RData");
            entry.extractFile(workspace);
          }
        }
      }

      // Read readme
      Optional<ArchiveEntry> readmeEntry = getArchiveEntry(entries, textUri);


      if (readmeEntry.isPresent()) {
        readme = loadTextEntry(readmeEntry.get());
      }

      // Extract workspace
      Optional<ArchiveEntry> workspaceEntry = getArchiveEntry(entries, rdataUri);
      if (workspaceEntry.isPresent()) {
        FskMetaDataObject fmdo =
            new FskMetaDataObject(workspaceEntry.get().getDescriptions().get(0));
        if (fmdo.getResourceType() == ResourceType.workspace) {
          workspace = FileUtil.createTempFile("workspace", ".RData");
          workspaceEntry.get().extractFile(workspace);
        }
      }

      // Retrieves resources
      List<ArchiveEntry> resourceEntries = new ArrayList<>();
      // Add r scripts without descriptions (no model or visualization script)
      entries.stream().filter(entry -> entry.getFormat().equals(scriptUri) && entry.getDescriptions().isEmpty())
      .forEach(resourceEntries::add);
      // ... add other entries (not R scripts)
      entries.stream().filter(entry ->  fileUris.contains(entry.getFormat())).forEach(resourceEntries::add);

      String[] resourcePaths = resourceEntries.stream().map(ArchiveEntry::getEntityPath).toArray(String[]::new);

      EnvironmentManager environmentManager =
          new ArchivedEnvironmentManager(archive.getZipLocation().getAbsolutePath(), resourcePaths);


      // Retrieve missing libraries from CRAN
      HashSet<String> packagesSet = new HashSet<>();
      if (!modelScript.isEmpty()) {
        packagesSet.addAll(new RScript(modelScript).getLibraries());
      }

      if (!visualizationScript.isEmpty()) {
        packagesSet.addAll(new RScript(visualizationScript).getLibraries());
      }
      List<String> packagesList = new ArrayList<>(packagesSet);

      Path workspacePath = workspace == null ? null : workspace.toPath();

      // The reader node is not using currently the plot, if present. Therefore an
      // empty string is used.
      String plotPath = "";

      FskPortObject fskObj = new FskPortObject(modelScript, visualizationScript, model,
          workspacePath, packagesList, Optional.of(environmentManager), plotPath, readme);

      // Read selected simulation index and simulations
      Optional<ArchiveEntry> simulationEntry =
          entries.stream().filter(entry -> entry.getFormat().equals(sedmlUri)).findAny();
      if (simulationEntry.isPresent()) {
        SimulationSettings simulationSettings = readSimulationSettings(simulationEntry.get());
        fskObj.selectedSimulationIndex = simulationSettings.selectedSimulationIndex;
        fskObj.simulations.addAll(simulationSettings.simulations);
      }

      return fskObj;
    }
  }
  public static void lock(File in) {
    try {
      lock.lock();
      
      if(!FSKFilesRegistry.contains(in.getPath())) {
        FSKFilesRegistry.add(in.getPath());
      }else {
        while (FSKFilesRegistry.contains(in.getPath())) {
          TimeUnit.MILLISECONDS.sleep(10);
        }
        FSKFilesRegistry.add(in.getPath());
      }
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    finally {
      lock.unlock();
    }
  }
  public static void release(File in) {
    FSKFilesRegistry.remove(in.getPath());
  }
  
  /**
   * 
   * @param in a file to be read and converted into FSK Object
   * @return FskPortObject
   * @throws Exception
   */
  public static FskPortObject readArchive(File in) throws Exception {
    FskPortObject fskObj = null;
    lock(in);
    try (final CombineArchive archive = new CombineArchive(in)) {

      // 1. Get SBML URI
      URI sbmlURI = FSKML.getURIS(1, 0, 12).get("sbml");

      // 2. Get number of SBML documents
      final int numberOfSBML = archive.getNumEntriesWithFormat(sbmlURI);

      // 3. Get the paths or model folders. A model folder contains a model and is the root
      // folder in case of a single model or can be nested folders in case of joined models.
      // If there is only one SBML document the archive contains a single model and several
      // SBML documents means a joined model.
      List<String> modelFolders;

      if (numberOfSBML > 1) {
        // Get directories inside the archive without duplication. The directories are
        // sorted to have the related directories after each other.
        // E.g.:
        // /SimpleModel2/SimpleModel2 <- Joined model
        // /SimpleModel2/SimpleModel2/SimpleModel1 <- Child model 1
        // /SimpleModel2/SimpleModel1/SimpleModel2 <- Child model 2
        TreeSet<String> entries = archive.getEntries().parallelStream()
            .map(ArchiveEntry::getFilePath)
            .map(fullPath -> fullPath.substring(0, fullPath.lastIndexOf("/") + 1))
            .filter(
                path -> StringUtils.countMatches(path, "/") > 2 && !path.endsWith("simulations/"))
            .collect(Collectors.toCollection(TreeSet::new));

        modelFolders = new ArrayList<>(entries);
      } else {
        modelFolders = Arrays.asList("/");
      }

      fskObj = readFskPortObject(archive, modelFolders, 0);
    }
    finally {
      release(in);
    }
    

    // ADD PARAMETER SUFFIXES to 1.7.2 version models (combined)
    ReaderNodeUtil.updateSuffixes(fskObj);
    
    return fskObj;
  }

  private static FskPortObject getEmbedSecondFSKObject(CombinedFskPortObject comFskObj) {
    FskPortObject embedFSKObject = comFskObj.getSecondFskPortObject();
    if (embedFSKObject instanceof CombinedFskPortObject) {
      embedFSKObject = getEmbedSecondFSKObject((CombinedFskPortObject) embedFSKObject);
    }
    return embedFSKObject;
  }

  /** @return entry of specific URI out of an {@link ArchiveEntry}. */
  private static Optional<ArchiveEntry> getArchiveEntry(List<ArchiveEntry> entries, URI uri ){

    Optional<ArchiveEntry> archive_entry =
        entries.stream().filter(entry -> entry.getFormat().equals(uri))
        .filter(entry -> !entry.getDescriptions().isEmpty()).findAny();

    return archive_entry;
  }

  /** @return text content out of an {@link ArchiveEntry}. */
  private static String loadTextEntry(final ArchiveEntry entry) throws IOException {

    // Create temporary file with script
    File temp = File.createTempFile("temp", null);
    String contents;

    try {
      // extractFile throws IOException if the file does not exist (was deleted manually) or is
      // not writable.
      entry.extractFile(temp);

      // readFileToString throws IOException if the file was deleted manually
      contents = FileUtils.readFileToString(temp, "UTF-8");
    } catch (IOException exception) {
      throw exception;
    } finally {
      temp.delete();
    }

    return contents;
  }

  private static Model readMetadata(ArchiveEntry metadataEntry)
      throws JsonProcessingException, IOException {

    // Create temporary file with metadata
    File temp = File.createTempFile("metadata", ".json");
    metadataEntry.extractFile(temp);

    // Load metadata from temporary file
    final ObjectMapper mapper = FskPlugin.getDefault().MAPPER104;
    JsonNode jsonNode = mapper.readTree(temp);

    temp.delete(); // Delete temporary file

    Model model;

    // New swagger models have the modelType property (1.0.4)
    if (jsonNode.has("modelType")) {
      String modelType = jsonNode.get("modelType").asText();
      Class<? extends Model> modelClass = FskPortObject.Serializer.modelClasses.get(modelType);
      model = mapper.treeToValue(jsonNode, modelClass);
    } else if (jsonNode.has("version")) {
      // 1.0.3 (with EMF)
      GenericModel gm = new GenericModel();
      gm.setModelType("genericModel");
      gm.setGeneralInformation(mapper.treeToValue(jsonNode.get("generalInformation"),
          GenericModelGeneralInformation.class));
      gm.setScope(mapper.treeToValue(jsonNode.get("scope"), GenericModelScope.class));
      gm.setDataBackground(
          mapper.treeToValue(jsonNode.get("dataBackground"), GenericModelDataBackground.class));
      gm.setModelMath(mapper.treeToValue(jsonNode.get("modelMath"), GenericModelModelMath.class));

      model = gm;
    } else {
      // Pre-RAKIP
      de.bund.bfr.knime.fsklab.rakip.GenericModel rakipModel =
          mapper.treeToValue(jsonNode, de.bund.bfr.knime.fsklab.rakip.GenericModel.class);

      GenericModel gm = new GenericModel();
      gm.setModelType("genericModel");
      gm.setGeneralInformation(RakipUtil.convert(rakipModel.generalInformation));
      gm.setScope(RakipUtil.convert(rakipModel.scope));
      gm.dataBackground(RakipUtil.convert(rakipModel.dataBackground));
      gm.modelMath(RakipUtil.convert(rakipModel.modelMath));
      model = gm;
    }

    return model;
  }

  private static class SimulationSettings {
    final int selectedSimulationIndex;
    final List<FskSimulation> simulations;

    SimulationSettings(final int selectedSimulationIndex, final List<FskSimulation> simulations) {
      this.selectedSimulationIndex = selectedSimulationIndex;
      this.simulations = simulations;
    }
  }

  /**
   * @return SimulationSettings with selected simulation index and list of simulations.
   * 
   *         <p>
   *         In SedML every simulation is encoded as a {@link org.jlibsedml.Model} with the
   *         parameter values defined as a {@link org.jlibsedml.ChangeAttribute}.
   *         </p>
   * 
   *         <pre>
   * {@code
   * <model id="simulation1">
   *   <listOfChanges>
   *     <changeAttribute newValue="1" target="a" />
   *     <changeAttribute newValue="2" target="b" />
   *   </listOfChanges>
   * </model>
   * <model id="simulation2">
   *   <listOfChanges>
   *     <changeAttribute newValue="3" target="c" />
   *     <changeAttribute newValue="4" target="d" />
   *   </listOfChanges>
   * </model> 
   * }
   * </pre>
   */
  private static SimulationSettings readSimulationSettings(ArchiveEntry simulationsEntry)
      throws IOException, XMLException {

    // Create temporary file for extracting SEDML and read it.
    File tempFile = File.createTempFile("simulations", ".sedml");
    simulationsEntry.extractFile(tempFile);

    // Read SEDML and delete temporary file
    SedML sedml = Libsedml.readDocument(tempFile).getSedMLModel();
    tempFile.delete();

    // Read selected simulation
    int selectedSimulationIndex = 0;
    final List<org.jlibsedml.Annotation> annotations = sedml.getAnnotation();
    if (annotations != null && annotations.size() > 0) {
      org.jlibsedml.Annotation indexAnnotation = annotations.get(0);
      Text indexAnnotationText = (Text) indexAnnotation.getAnnotationElement().getContent().get(0);
      selectedSimulationIndex = Integer.parseInt(indexAnnotationText.getText());
    }

    // Read simulations
    List<FskSimulation> simulations = new ArrayList<>(sedml.getModels().size());
    for (org.jlibsedml.Model model : sedml.getModels()) {

   // linked hash map needed to preserve order of parameters
      LinkedHashMap<String, String> params = model.getListOfChanges()
          .stream()
          .filter(change -> change.getChangeKind().equals(SEDMLTags.CHANGE_ATTRIBUTE_KIND))
          .map(change -> (ChangeAttribute) change)
          .collect(
            LinkedHashMap::new,
            (map, item) -> map.put(item.getTargetXPath().toString(), item.getNewValue()),
            Map::putAll); 

      FskSimulation sim = new FskSimulation(model.getId());
      sim.getParameters().putAll(params);

      simulations.add(sim);
    }

    return new SimulationSettings(selectedSimulationIndex, simulations);
  }
}