/*
* Eoulsan development code
*
* This code may be freely distributed and modified under the
* terms of the GNU Lesser General Public License version 2.1 or
* later and CeCILL-C. This should be distributed with the code.
* If you do not have a copy, see:
*
* http://www.gnu.org/licenses/lgpl-2.1.txt
* http://www.cecill.info/licences/Licence_CeCILL-C_V1-en.txt
*
* Copyright for this code is held jointly by the Genomic platform
* of the Institut de Biologie de l'École normale supérieure and
* the individual authors. These should be listed in @author doc
* comments.
*
* For more information on the Eoulsan project and its aims,
* or to join the Eoulsan Google group, visit the home page
* at:
*
* http://outils.genomique.biologie.ens.fr/eoulsan
*
*/
package fr.ens.biologie.genomique.eoulsan.core.workflow;
import static com.google.common.base.Preconditions.checkNotNull;
import static fr.ens.biologie.genomique.eoulsan.util.StringUtils.xmlEscape;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.HashSet;
import java.util.Set;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import fr.ens.biologie.genomique.eoulsan.Globals;
import fr.ens.biologie.genomique.eoulsan.core.Parameter;
import fr.ens.biologie.genomique.eoulsan.core.Step;
import fr.ens.biologie.genomique.eoulsan.data.DataFile;
import fr.ens.biologie.genomique.eoulsan.data.DataFormat;
/**
* Convert a Workflow to Graphviz
* @author Laurent Jourdren
* @since 2.0
*/
public class Workflow2Graphviz {
private final AbstractWorkflow workflow;
private void addRow(final StringBuilder sb, final String s) {
sb.append(
"<tr><td bgcolor=\"white\" align=\"center\" colspan=\"2\"><font color=\"black\">");
sb.append(s);
sb.append("</font></td></tr>");
}
/**
* Convert the workflow to Graphviz format
* @return a string with the workflow converted to Graphviz format
*/
private String convert() {
final StringBuilder sb = new StringBuilder();
sb.append("## Generated by " + Globals.APP_NAME + " ");
sb.append(Globals.APP_VERSION_STRING);
sb.append('\n');
sb.append(
"## Command to get the layout: \"dot -Gsize=40 -Tpng workflow.gv > workflow.png\"\n");
sb.append("digraph g {\n graph [fontsize=30 labelloc=\"t\" label=\"\" "
+ "splines=true overlap=false rankdir = \"LR\"]\n"
+ " ratio = auto;\n");
// Create nodes
for (Step step : this.workflow.getSteps()) {
if (step == this.workflow.getFirstStep()
|| step == this.workflow.getCheckerStep()) {
continue;
}
sb.append(" \"step");
sb.append(step.getNumber());
sb.append(
"\" [ style = \"filled\" penwidth = 1 fillcolor = \"white\" fontname = \"Courier New\" shape = \"Mrecord\" label =");
sb.append(
"<<table border=\"0\" cellborder=\"0\" cellpadding=\"3\" bgcolor=\"white\">");
sb.append(
"<tr><td bgcolor=\"black\" align=\"center\" colspan=\"2\"><font color=\"white\">");
sb.append(step.getId());
sb.append("</font></td></tr>");
addRow(sb, step.getModuleName() + " " + step.getStepVersion());
for (Parameter p : step.getParameters()) {
addRow(sb, xmlEscape(p.getName()) + " = " + xmlEscape(p.getValue()));
}
sb.append("</table>> ] ;\n");
}
sb.append('\n');
final Multimap<AbstractStep, AbstractStep> linkedSteps =
HashMultimap.create();
// Create links from output ports
for (Step step : this.workflow.getSteps()) {
// Do not handle first and check step
if (step == this.workflow.getFirstStep()
|| step == this.workflow.getCheckerStep()) {
continue;
}
final int stepNumber = step.getNumber();
AbstractStep abstractStep = (AbstractStep) step;
// For each port
for (StepOutputPort outputPort : abstractStep.getWorkflowOutputPorts()) {
// For each port link
for (StepInputPort link : outputPort.getLinks()) {
final AbstractStep linkedStep = link.getStep();
// Do not handle first and check step
if (linkedStep == this.workflow.getFirstStep()
|| linkedStep == this.workflow.getCheckerStep()) {
continue;
}
linkedSteps.put(linkedStep, abstractStep);
sb.append(" step");
sb.append(stepNumber);
sb.append(" -> step");
sb.append(linkedStep.getNumber());
sb.append(
" [ penwidth = 5 fontsize = 28 fontcolor = \"black\" label = \"");
final DataFormat format = outputPort.getFormat();
String formatName =
format.getAlias() == null || "".equals(format.getAlias())
? format.getName() : format.getAlias();
sb.append(formatName);
sb.append("\" ];\n");
}
}
}
// Create other links between steps
for (Step step : this.workflow.getSteps()) {
// Do not handle first and check step
if (step == this.workflow.getFirstStep()
|| step == this.workflow.getCheckerStep()) {
continue;
}
AbstractStep abstractStep = (AbstractStep) step;
StepStateObserver observer = ((AbstractStep) step).getStepStateObserver();
Set<AbstractStep> requiredSteps =
new HashSet<>(observer.getRequiredSteps());
requiredSteps.removeAll(linkedSteps.get(abstractStep));
// Do not handle first and check step
requiredSteps.remove(this.workflow.getFirstStep());
requiredSteps.remove(this.workflow.getCheckerStep());
for (AbstractStep requiredStep : requiredSteps) {
sb.append(" step");
sb.append(requiredStep.getNumber());
sb.append(" -> step");
sb.append(step.getNumber());
sb.append(" [ penwidth = 5 fontsize = 28 fontcolor = \"green\"");
sb.append(" ];\n");
}
}
sb.append("}\n");
return sb.toString();
}
/**
* Convert and save the workflow as a Graphviz file.
* @param outputFile output file
* @throws IOException if an error occurs while creating the output file
*/
public void save(final DataFile outputFile) throws IOException {
checkNotNull(outputFile, "outputFile parameter cannot be null");
final Writer writer = new OutputStreamWriter(outputFile.create());
writer.write(convert());
writer.close();
}
//
// Constructor
//
/**
* Public constructor.
* @param workflow the workflow
*/
public Workflow2Graphviz(final AbstractWorkflow workflow) {
checkNotNull(workflow, "workflow parameter cannot be null");
this.workflow = workflow;
}
}