SBFC for Developers

From System Biology Format Converter framework (SBFC)
Jump to: navigation, search

Developer Guide

Introduction

This chapter illustrates how to develop new converters or add new features to SBFC. SBFC is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or any later version.


The SBFC user and developer manuals can also be downloaded as pdf documents from the following web links: user manual and developer manual.


If you wish to contribute with new converters or join the team, please let us know using the following e-mail address:

sbfc-devel [at] googlegroups.com.

We welcome new contributors, and your contributions can be included in the official SBFC and become publicly available.


The next sections show a description for the steps required for developing SBFC using Eclipse and getting the SBFC source code. A guide for creating new converters is also provided.

To visit the User Guide, please follow this link.

Installation and Configuration

Currently, SBFC is available in two branches: a) main branch called core and b) the OSGi branch called osgi. This chapter will show how to install and develop with both these branches. The following links provide instructions to set up SBFC with Eclipse:


How to add a new converter

To facilitate the implementation of additional converters, SBFC was designed with an high degree of modularity. At the software core are:

  • the interface GeneralModel in the package org.sbfc.converter.models; and
  • the abstract class GeneralConverter in the package org.sbfc.converter.

NOTE: For the OSGi branch of SBFC both these two classes are located in the package org.sbfc.api.

The interface GeneralModel is used for data exchange and describes the services that every input or output computational model object must implement to interact with SBFC.

UML class diagram for the GeneralModel class hierarchy
/**
 * Interface defining the specifications that each Model must implement.
 */
public interface GeneralModel {
  /**
   * Set the Model from a file in the file system.
   * @param fileName path to the file containing the model
   * @throws ReadModelException
   */
  public void setModelFromFile(String fileName) throws ReadModelException;
  
  /**
   * Set the model from a String.
   * @param modelString Model
   * @throws ReadModelException
   */
  public void setModelFromString(String modelString) throws ReadModelException;
  
  /**
   * Write the Model into a new file.
   * @param fileName path at which the new file will be created
   * @throws WriteModelException
   */
  public void modelToFile(String fileName) throws WriteModelException;
  
  /**
   * Return the Model as a String.
   * @return Model
   * @throws WriteModelException
   */
  public String modelToString() throws WriteModelException;
  
  /**
   * Return an array of model file type extension (e.g. [.xml, .sbml] for SBML, [.owl] for BIOPAX)
   * The first is the preferred extension.
   *
   * @return file type extensions
   */
  public String[] getExtensions();
  
  /**
   * This method is used to distinguish between converters with the same file extension.
   * For example, a file ending with .xml could be either an SBML model, or some other XML file type. 
   * <p>
   * Implementers should perform a quick heuristic, not a full validation. For example
   * XML file types may examine the root element to determine if it has the correct name or namespace.
   * <p>
   * Implementers should <b>only</b> return false if they are <b>certain</b> that the file
   * type is wrong. If the correctness could not be determined for sure, the method should 
   * always return true. 
   * @return false if the file is not the correct type to be used with this GeneralModel
   */
  public boolean isCorrectType(File f);

  /**
   * Return a URI for the model
   * e.g. MIME types: image/png, application/matlab, text/xpp
   * e.g. COMBINE spec ids: http://identifiers.org/combine.specifications/sbml
   * 
   * @return the model URI
   */
  public String getURI();
}


The abstract class GeneralConverter represents the generic algorithm for converting one model into another.

UML class diagram for the GeneralConverter class hierarchy
/** 
 * Abstract class defining the specifications that each Converter must implement. 
 */
public abstract class GeneralConverter {
  /**
   * The input model to be converted.
   */
  protected GeneralModel inputModel = null;  

  /**
   * The options for the converter. Each option is defined as a pair (name, value). 
   * For instance, for the converter SBML2SBML, one option is ("sbml.target.level", "3").
   */
  protected Map<String, String> options;
  
  /**
   * Method to convert a GeneralModel into another.
   * @param model
   * @return GeneralModel
   */
  public abstract GeneralModel convert(GeneralModel model) throws ConversionException, ReadModelException;
  
  /**
   * Return the extension of the Result file.
   * @return String
   */
  public abstract String getResultExtension();
  
  /**
   * Set the converter options.
   * @param options
   */
  public void setOptions(Map<String, String> options) {
    this.options = options;
  }

  /** 
   * Return the input model.
   * @return the input model
   */
  public GeneralModel getInputModel() {
    return inputModel;
  }
  
  /**
   * Return the converter name as it should be displayed.
   * @return the name
   */
  public abstract String getName();
  
  /**
   * Return the converter description.
   * @return the description
   */
  public abstract String getDescription();

  /**
   * Return the converter description in HTML format.
   * @return the HTML description
   */
  public abstract String getHtmlDescription();
} 

To add a new converter, a developer needs to extend the class GeneralConverter and implement the method GeneralModel convert(GeneralModel model), where the parameter model is the model to convert. The model type is an implementation of the interface GeneralModel. UML class diagrams for the GeneralModel and GeneralConverter class hierarchies are shown in the two figures, illustrating the model types and the converters currently implemented in SBFC.


SBFC also supports the creation of workflows enabling the use of multiple converters in a pipeline. For each intermediate conversion, the SBFC framework checks that the output format from the previous step is the same as the input format for the next step. If this is the case, SBFC exports the output model format as a string using the method modelToString() and then imports this string as input model format using the method setModelFromString(String). The general command line to launch a conversion job from the SBFC package is provided by the class Converter. The syntax is minimal:

java Converter [Class Input] [Converter Class] [input file]. 


Examples: how to create new converters

Example: how to create a new converter from existing converters

This section presents a Java example illustrating the implementation from scratch of new converters between trivial model formats and how to create a converter from existing converters. It is assumed that all these new models classes are located in the SBFC Java package: org.sbfc.converter.models, whereas the converter classes are located in the package: org.sbfc.converter.example. A graphic diagram for this example is provided in the adjacent Figure.

Creation of complex converters. (A) In this scenario, three existing SBFC models (A, B, and C) and two converters (A2B and B2C) are considered. Each of these models represents a model format and implements the interface GeneralModel. The current converters extend the abstract class GeneralConverter and translate from Model A to Model B, and from Model B to Model C. (B) A new converter A2C to translate from Model A to Model C, can be added without effort by invoking the method convert() implemented in the converters A2B and B2C. (C) Java source code illustrating the implementation of the method convert() for the converter class A2C.

First of all, we create three new classes, each representing a new model format. Let's call these classes: AModel, BModel, and CModel. For simplicity, each of these classes extends the abstract class StringModel located in the package org.sbfc.converter.models. This abstract class implements the methods for reading and writing a file or string model, as declared in the interface GeneralModel.

package org.sbfc.converter.models;

import java.io.File;
import org.sbfc.converter.models.StringModel;

public class AModel extends StringModel {

  @Override
  public String[] getExtensions() { return new String[] { ".a" }; }

  @Override
  public boolean isCorrectType(File f) { return true; }

  @Override
  public String getURI() { return "text/a"; }
}
package org.sbfc.converter.models;

import java.io.File;
import org.sbfc.converter.models.StringModel;

public class BModel extends StringModel {

  @Override
  public String[] getExtensions() { return new String[] { ".b" }; }

  @Override
  public boolean isCorrectType(File f) { return true; }

  @Override
  public String getURI() { return "text/b"; }
}
package org.sbfc.converter.models;

import java.io.File;
import org.sbfc.converter.models.StringModel;

public class CModel extends StringModel {

  @Override
  public String[] getExtensions() { return new String[] { ".c" }; }

  @Override
  public boolean isCorrectType(File f) { return true; }

  @Override
  public String getURI() { return "text/c"; }
}

Now, we would like to create two new converters: one from AModel to BModel, called A2B, and another from BModel to CModel, called B2C. As expected, both these converters extends the abstract class GeneralConverter. The structure is the same and the two converters would differ for the way data is extracted from the input model and translated into the output model.

package org.sbfc.converter.example;

import org.sbfc.converter.exceptions.ConversionException;
import org.sbfc.converter.exceptions.ReadModelException;
import org.sbfc.converter.GeneralConverter;
import org.sbfc.converter.models.GeneralModel;
import org.sbfc.converter.models.AModel;
import org.sbfc.converter.models.BModel;

/** Convert model format A to model format B. */
public class A2B extends GeneralConverter {

  /** Constructor A2B. */
  public A2B() { super(); }

  /** Convert model format A to model format B. */
  public BModel bExport(AModel aModel) 
  throws ReadModelException, ConversionException {
    String bStringModel = "I am a B model!";
    // extract species, reactions, parameters, compartment, rules, 
    // events, and functionDefinitions from aModel
    // and store the converted values in bStringModel 
      
    // if aModel cannot be parsed, throw ReadModelException
    // if aModel cannot be converted to bStringModel, throw ConversionException
    BModel bModel = new BModel();
    bModel.setModelFromString(bStringModel);
    return bModel;
  }

  @Override
  public GeneralModel convert(GeneralModel model) 
  throws ConversionException, ReadModelException {
    // assume model is of type AModel
    try { return bExport((AModel)model); }
    catch (ReadModelException e) { throw e; }
    catch (ConversionException e) { throw e; }
  }
  
  @Override
  public String getResultExtension() { return ".b"; }
  
  @Override
  public String getName() { return "A2B"; }
  
  @Override
  public String getDescription() { 
    return "It converts a model format from A to B"; 
  }

  @Override
  public String getHtmlDescription() { 
    return "It converts a model format from A to B"; 
  }
}


package org.sbfc.converter.example;

import org.sbfc.converter.exceptions.ConversionException;
import org.sbfc.converter.exceptions.ReadModelException;
import org.sbfc.converter.GeneralConverter;
import org.sbfc.converter.models.GeneralModel;
import org.sbfc.converter.models.BModel;
import org.sbfc.converter.models.CModel;

/** Convert model format B to model format C. */
public class B2C extends GeneralConverter {

  /** Constructor B2C. */
  public B2C() { super(); }

  /** Convert model format B to model format C. */
  public CModel cExport(BModel bModel) 
  throws ReadModelException, ConversionException {
    String cStringModel = "I am a C model!";
    // extract species, reactions, parameters, compartment, rules, 
    // events, and functionDefinitions from aModel
    // and store the converted values in cStringModel 
      
    // if aModel cannot be parsed, throw ReadModelException
    // if aModel cannot be converted to cStringModel, throw ConversionException
    CModel cModel = new CModel();
    cModel.setModelFromString(cStringModel);
    return cModel;
  }

  @Override
  public GeneralModel convert(GeneralModel model) 
  throws ConversionException, ReadModelException {
    // assume model is of type BModel
    try { return cExport((BModel)model); }
    catch (ReadModelException e) { throw e; }
    catch (ConversionException e) { throw e; }
  }
  
  @Override
  public String getResultExtension() { return ".c"; }
  
  @Override
  public String getName() { return "B2C"; }
  
  @Override
  public String getDescription() { 
    return "It converts a model format from B to C"; 
  }

  @Override
  public String getHtmlDescription() { 
    return "It converts a model format from B to C"; 
  }
}

At this point we could quickly implement a converter translating an AModel to a CModel, called A2C, by simply reusing the previously implemented converters. In order to do this, we simply invoke the converters A2B and B2C as a simple pipeline. The implementation of this new converter created from existing converters is therefore straightforward and illustrated as below.

package org.sbfc.converter.example;

import org.sbfc.converter.exceptions.ConversionException;
import org.sbfc.converter.exceptions.ReadModelException;
import org.sbfc.converter.GeneralConverter;
import org.sbfc.converter.models.GeneralModel;
import org.sbfc.converter.models.AModel;
import org.sbfc.converter.models.CModel;
import org.sbfc.converter.example.A2B;
import org.sbfc.converter.example.B2C;

/** Convert model format A to model format C re-using the converter 
 *  A2B and B2C. 
 */
public class A2C extends GeneralConverter {

  /** Constructor A2C. */
  public A2C() { super(); }

  /** Convert model format A to model format C. */
  public CModel cExport(AModel aModel) 
  throws ReadModelException, ConversionException {
    A2B a2b = new A2B();
    B2C b2c = new B2C();
    // concatenate the conversion as a transitive relationship
    return (CModel) b2c.convert(a2b.convert(aModel));
  }

  @Override
  public GeneralModel convert(GeneralModel model) 
  throws ConversionException, ReadModelException {
    // assume model is of type AModel
    try { return cExport((AModel)model); }
    catch (ReadModelException e) { throw e; }
    catch (ConversionException e) { throw e; }
  }
  
  @Override
  public String getResultExtension() { return ".c"; }
  
  @Override
  public String getName() { return "A2C"; }
  
  @Override
  public String getDescription() { 
    return "It converts a model format from A to C"; 
  }

  @Override
  public String getHtmlDescription() { 
    return "It converts a model format from A to C"; 
  }
}

Another way to do this is to simply use the static method String Convert.convertFromString(String inputModelType, String converterType, String modelString) from the class Convert in the package org.sbfc.converter to convert models as string. This is particularly useful if one does not need to implement a new converter but simply wants to create a quick pipeline from existing converters.

String aModel;
// ... build the string aModel with data using the format for a 
// model of type AModel.
String bModel = Convert.convertFromString(AModel, A2B, aModel);
String cModel = Convert.convertFromString(BModel, B2C, bModel);
// we now have a conversion from model a model of type AModel to 
// a model of type CModel.

The same can be achieved with the method String Convert.convertFromFile(String inputModelType, String converterType, String inputFileName).

A third and final way to achieve the same result with no Java programming, is to use the script sbfConverter.sh (or sbfConverter.bat on Windows machines), and run:

./sbfConverter.sh AModel A2B [file.a | folder containing files with extension .a]
./sbfConverter.sh BModel B2C [file.b | folder containing files with extension .b]

This will generate a file.c (or a set of files with extension .c) of type CModel.



Example: create a new converter invoking non-Java external programs

This example shows how to create a new converter which invokes an external program. This can be particularly useful when a converter programmed in a language different from Java should be used. In such a case, we could define a converter that works as a bridge between our code and this external existing converter.

First of all, we create a class representing a CellMLModel.

package org.sbfc.converter.models;

import java.io.File;

/**
 * A {@link StringModel} representing a CellML model (http://www.cellml.org).
 */
public class CellMLModel extends StringModel {
  @Override
  public String[] getExtensions() {
    return new String[] { ".cellml", ".xml" };
  }

  @Override
  public boolean isCorrectType(File f) {
    return true;
  }

  @Override
  public String getURI() {
    return "http://identifiers.org/combine.specifications/cellml";
  }
}

Then we can proceed with the creation of a new converter called CellML2SBML.

package org.sbfc.converter.example;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;

import org.sbfc.converter.GeneralConverter;
import org.sbfc.converter.exceptions.ConversionException;
import org.sbfc.converter.exceptions.ReadModelException;
import org.sbfc.converter.exceptions.WriteModelException;
import org.sbfc.converter.models.GeneralModel;
import org.sbfc.converter.models.SBMLModel;

/** 
 * This converter will use an external program to
 * do the conversion. As an example, we will convert from CellML to SBML using
 * antimony (http://antimony.sourceforge.net/).
 * 
 * <p>By using this template, you can easily add a new converter that
 * call an external program.
 */
public class CellML2SBML extends GeneralConverter {

  /**
   * Path to the antimony sbtranslate binary.
   */
  public final static String PROGRAM = "/path/to/antimony/bin/sbtranslate";
  
  /** Creates a new instance. */
  public CellML2SBML() { super(); }

  @Override
  public GeneralModel convert(GeneralModel model) 
  throws ConversionException, ReadModelException 
  {
    try {
      //
      // 1. Dealing with the input format
      //
      // We first need to save the model into a temporary file so that the
      // external program can read it.
      // GeneralModel does not store the original file location and it can
      // also be initialize directly from String.
      
      // We could check here that the input format correspond to the CellML URI.
      
      // creating the temporary file
      File inputFile = File.createTempFile("cellml-", ".xml");

      // writing the model to the temporary file
      model.modelToFile(inputFile.getAbsolutePath()); 

      //
      // 2. Running the external program
      //
      
      // creating a second temporary file for the output
      File outputFile = File.createTempFile("sbml-", ".xml");
      
      // using the Runtime exec method to run the external program:
      Process p = Runtime.getRuntime().exec(PROGRAM + " -o sbml-comp -outfile " 
          + outputFile.getAbsolutePath() + " " + inputFile.getAbsolutePath());
      // waiting for the program to finish.
      p.waitFor();
      
      // read the output messages from the command
      BufferedReader stdInput = new BufferedReader(new
          InputStreamReader(p.getInputStream()));
      String line;

      while ((line = stdInput.readLine()) != null) {
        // You might want to read the process output to check that the conversion went fine
        // and if not, you can throw an exception with an appropriate error message.
        System.out.println("Output: " + line);
      }

      // read the error messages from the command
      BufferedReader stdError = new BufferedReader(new
          InputStreamReader(p.getErrorStream()));

      while ((line = stdError.readLine()) != null) {
        // You might want to read the process error output to check that the conversion went fine
        // and if not, you can throw an exception with an appropriate error message.
        System.out.println("Errors: " + line);
        if (line.startsWith("Segmentation fault")) {
          throw new ConversionException("Encountered a Segmentation fault while running antomony !!");
        }
      }
      
      //
      // 3. Returning the output format
      //
      
      // creating a new empty SBMLModel
      GeneralModel outputModel = new SBMLModel();
      
      // reading the output file into the SBMLModel
      outputModel.setModelFromFile(outputFile.getAbsolutePath());
      
      return outputModel;
    }
    catch (IOException e) {
      throw new ReadModelException(e);
    } catch (WriteModelException e) {
      throw new ReadModelException(e);
    } catch (InterruptedException e) {
      throw new ReadModelException(e);
    } 
  }
  
  @Override
  public String getResultExtension() { return ".xml"; }
  
  @Override
  public String getName() { return "CellML2SBML"; }
  
  @Override
  public String getDescription() { 
	    return "Convert a CellML model to SBML"; 
  }

  @Override
  public String getHtmlDescription() { 
    return "Convert a CellML model to SBML"; 
  }
}

In the code above, the converter uses the Runtime method exec to start a new process.

To use this new converter the command will be:

./sbfConverter.sh CellMLModel CellML2SBML [file.cellml | file.xml | folder containing files with extension .cellml or .xml]