/*******************************************************************************
* Copyright 2006 - 2014 Vienna University of Technology,
* Department of Software Technology and Interactive Systems, IFS
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and limitations under the License.
******************************************************************************/
package eu.scape_project.planning.services.taverna.generator;
import java.io.IOException;
import java.io.Writer;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.github.mustachejava.DefaultMustacheFactory;
import com.github.mustachejava.Mustache;
import com.github.mustachejava.MustacheFactory;
import eu.scape_project.planning.services.myexperiment.domain.ComponentConstants;
import eu.scape_project.planning.services.myexperiment.domain.Port;
import eu.scape_project.planning.services.myexperiment.domain.WorkflowDescription;
import eu.scape_project.planning.services.taverna.generator.model.Dataflow;
import eu.scape_project.planning.services.taverna.generator.model.Datalink;
import eu.scape_project.planning.services.taverna.generator.model.InputPort;
import eu.scape_project.planning.services.taverna.generator.model.OutputPort;
import eu.scape_project.planning.services.taverna.generator.model.Workflow;
import eu.scape_project.planning.services.taverna.generator.model.processor.NestedWorkflow;
import eu.scape_project.planning.services.taverna.generator.model.processor.Processor;
import eu.scape_project.planning.services.taverna.generator.model.processor.TextConstant;
/**
* Generator for executable plans in Taverna t2flow format.
*/
public class T2FlowExecutablePlanGenerator {
/**
* The input source of a component.
*/
public enum InputSource {
/**
* The source object of the workflow.
*/
SOURCE_OBJECT,
/**
* The target object of the workflow.
*/
TARGET_OBJECT
}
/**
* The related object of a port.
*/
public enum RelatedObject {
/**
* The left object of the workflow.
*/
LEFT_OBJECT {
/**
* Returns a string representation of the enum.
*
* @return the enum as string
*/
public String toString() {
return ComponentConstants.VALUE_LEFT_OBJECT;
}
},
/**
* The right object of the workflow.
*/
RIGHT_OBJECT {
/**
* Returns a string representation of the enum.
*
* @return the enum as string
*/
public String toString() {
return ComponentConstants.VALUE_RIGHT_OBJECT;
}
}
}
private static final String SOURCE_PORT_NAME = "source";
private static final String TARGET_PORT_NAME = "target";
private static final Pattern PURL_DP_MEASURE_PATTERN = Pattern
.compile("http:\\/\\/purl\\.org\\/DP\\/quality\\/(measures)#(\\d+)");
private static final Pattern GENERIC_MEASURE_PATTERN = Pattern.compile("http.?:\\/\\/(.+)");
private static final Pattern PURL_DP_PORTNAME_PATTERN = Pattern.compile("(measures)_(\\d+)");
private final String sourceMimetype;
private final String targetMimetype;
private Workflow workflow;
private NestedWorkflow migration;
private String migrationTargetPortName;
/**
* Creates a new Executable Plan generator as t2flow workflow.
*
* @param name
* the name of the plan
* @param author
* the author of the plan
*/
public T2FlowExecutablePlanGenerator(String name, String author) {
this(name, author, null, null);
}
/**
* Creates a new Executable Plan generator as t2flow workflow.
*
* @param name
* the name of the plan
* @param author
* the author of the plan
* @param sourceMimetype
* the source mimetype of the plan
* @param targetMimetype
* the target mimetype of the plan
*/
public T2FlowExecutablePlanGenerator(String name, String author, String sourceMimetype, String targetMimetype) {
String semanticAnnotations = "<> <http://purl.org/DP/components#fits> <http://purl.org/DP/components#ExecutablePlan> .\n";
if (sourceMimetype != null && targetMimetype != null) {
semanticAnnotations += "<> <http://purl.org/DP/components#migrates>"
+ "[ a <http://purl.org/DP/components#MigrationPath> ;"
+ "<http://purl.org/DP/components#sourceMimetype> \"" + sourceMimetype + "\" ;"
+ "<http://purl.org/DP/components#targetMimetype> \"" + targetMimetype + "\" ] .";
}
this.sourceMimetype = sourceMimetype;
this.targetMimetype = targetMimetype;
workflow = new Workflow(name, author, semanticAnnotations);
}
/**
* Adds a source port with 0 as port depth.
*
* @see {@link #addSourcePort(int)}
*/
public void addSourcePort() {
addSourcePort(0);
}
/**
* Adds a source port with the provided depth.
*
* @param depth
* the port depth
*/
public void addSourcePort(int depth) {
InputPort inputPort = new InputPort(SOURCE_PORT_NAME, depth,
"<> <http://purl.org/DP/components#accepts>\n"
+ " <http://purl.org/DP/components#SourceObject> .");
workflow.addInputPort(inputPort);
}
/**
* Adds a target port.
*/
public void addTargetPort() {
OutputPort outputPort = new OutputPort(TARGET_PORT_NAME,
"<> <http://purl.org/DP/components#provides> <http://purl.org/DP/components#TargetObject> .");
workflow.addOutputPort(outputPort);
}
/**
* Adds a measure port for the provided measure.
*
* @param measure
* the measure
*/
public void addMeasurePort(String measure) {
String portName = createMeasurePortName(measure);
if (portName == null) {
throw new IllegalArgumentException("The provided measure " + measure + " is not valid");
}
OutputPort outputPort = new OutputPort(portName, "<> <http://purl.org/DP/components#provides> <"
+ measure + "> .");
workflow.addOutputPort(outputPort);
}
/**
* Sets the migration component.
*
* @param workflowDescription
* workflow description of the migration component
* @param workflowContent
* the actual workflow content of the component
* @param parameters
* map with parameters of the component
*/
public void setMigrationComponent(WorkflowDescription workflowDescription, String workflowContent,
Map<String, String> parameters) {
// Dataflow
migration = new NestedWorkflow("Migration", workflowDescription.getDataflowId());
workflow.addProcessor(migration);
workflowContent = convertWorkflowToNested(workflowContent);
workflow.addDataflow(new Dataflow(workflowDescription.getDataflowId(), workflowContent));
// Input ports
List<Port> inputPorts = workflowDescription.getInputPorts();
for (Port p : inputPorts) {
if (ComponentConstants.VALUE_SOURCE_OBJECT.equals(p.getValue())) {
migration.addInputPort(new InputPort(p.getName(), 0));
workflow.addDatalink(new Datalink(workflow, SOURCE_PORT_NAME, migration, p.getName()));
} else if (ComponentConstants.VALUE_PARAMETER.equals(p.getValue())) {
migration.addInputPort(new InputPort(p.getName(), 0));
TextConstant c = new TextConstant(p.getName(), parameters.get(p.getName()));
workflow.addProcessor(c);
workflow.addDatalink(new Datalink(c, "value", migration, p.getName()));
}
}
// Output ports
List<Port> outputPorts = workflowDescription.getOutputPorts();
for (Port p : outputPorts) {
if (ComponentConstants.VALUE_TARGET_OBJECT.equals(p.getValue())) {
migrationTargetPortName = p.getName();
migration.addOutputPort(new OutputPort(migrationTargetPortName));
workflow.addDatalink(new Datalink(migration, migrationTargetPortName, workflow, TARGET_PORT_NAME));
}
}
}
/**
* Adds a QA component.
*
* Adds measure ports for the provided {@code measures} to the workflow if
* they are provided by the QA component and are not already present.
*
* If a port specifies the related object, only
* {@link RelatedObject#RIGHT_OBJECT} is considered.
*
* @param workflowDescription
* workflow description of the migration component
* @param workflowContent
* the actual workflow content of the component
* @param parameters
* map with parameters of the component
* @param measures
* measures of output ports to connect the component to
*
* @see {@link #addQaComponent(WorkflowDescription, String, Map, List, RelatedObject)}
*/
public void addQaComponent(final WorkflowDescription workflowDescription, final String workflowContent,
final Map<String, String> parameters, final List<String> measures) {
addQaComponent(workflowDescription, workflowContent, parameters, measures, RelatedObject.RIGHT_OBJECT);
}
/**
* Adds a QA component.
*
* Adds measure ports for the provided {@code measures} to the workflow if
* they are provided by the QA component and are not already present.
*
* If an output port specifies a related object, the port will only be
* connected if the {@code relatedObject} matches.
*
* The sources for the left and right inputs are set according to supported
* mimetypes of the workflow.
*
* @param workflowDescription
* workflow description of the migration component
* @param workflowContent
* the actual workflow content of the component
* @param parameters
* map with parameters of the component
* @param measures
* measures of output ports to connect the component to
* @param relatedObject
* the related object of measures to use if present
* @see {@link #addQaComponent(WorkflowDescription, String, InputSource, InputSource, Map, List, RelatedObject)}
*/
public void addQaComponent(final WorkflowDescription workflowDescription, final String workflowContent,
final Map<String, String> parameters, final List<String> measures, RelatedObject relatedObject) {
InputSource leftSource = null;
InputSource rightSource = null;
if (hasMigration() && workflowDescription.acceptsMimetypes(sourceMimetype, targetMimetype)) {
leftSource = InputSource.SOURCE_OBJECT;
rightSource = InputSource.TARGET_OBJECT;
} else if (hasMigration() && workflowDescription.acceptsMimetypes(targetMimetype, sourceMimetype)) {
leftSource = InputSource.TARGET_OBJECT;
rightSource = InputSource.SOURCE_OBJECT;
} else if (workflowDescription.acceptsLeftMimetype(sourceMimetype)) {
leftSource = InputSource.SOURCE_OBJECT;
} else if (workflowDescription.acceptsRightMimetype(sourceMimetype)) {
rightSource = InputSource.SOURCE_OBJECT;
} else if (hasMigration() && workflowDescription.acceptsLeftMimetype(targetMimetype)) {
leftSource = InputSource.TARGET_OBJECT;
} else if (hasMigration() && workflowDescription.acceptsRightMimetype(targetMimetype)) {
rightSource = InputSource.TARGET_OBJECT;
}
addQaComponent(workflowDescription, workflowContent, leftSource, rightSource, parameters, measures,
relatedObject);
}
/**
* Adds a QA component.
*
* The source for the left input is set to {@code leftSource}, the source
* for the right input is set to {@code rightSource}. If these parameters
* are null, the input are not connected.
*
* Adds measure ports for the provided {@code measures} to the workflow if
* they are provided by the QA component and are not already present.
*
* @param workflowDescription
* workflow description of the migration component
* @param workflowContent
* the actual workflow content of the component
* @param leftSource
* the source of the left input or null
* @param rightSource
* the source of the right input or null
* @param parameters
* map with parameters of the component
* @param measures
* measures of output ports to connect the component to
* @param relatedObject
* the related object of measures to use if present
*/
public void addQaComponent(final WorkflowDescription workflowDescription, final String workflowContent,
final InputSource leftSource, final InputSource rightSource, final Map<String, String> parameters,
final List<String> measures, RelatedObject relatedObject) {
// Dataflow
NestedWorkflow qa = new NestedWorkflow(createProcessorName(workflowDescription.getName()),
workflowDescription.getDataflowId());
workflow.addProcessor(qa);
String dataflowContent = convertWorkflowToNested(workflowContent);
workflow.addDataflow(new Dataflow(workflowDescription.getDataflowId(), dataflowContent));
// Input ports
List<Port> inputPorts = workflowDescription.getInputPorts();
for (Port p : inputPorts) {
if (ComponentConstants.VALUE_LEFT_OBJECT.equals(p.getValue())) {
if (leftSource == InputSource.SOURCE_OBJECT) {
qa.addInputPort(new InputPort(p.getName(), 0));
workflow.addDatalink(new Datalink(workflow, SOURCE_PORT_NAME, qa, p.getName()));
} else if (leftSource == InputSource.TARGET_OBJECT) {
qa.addInputPort(new InputPort(p.getName(), 0));
workflow.addDatalink(new Datalink(migration, migrationTargetPortName, qa, p.getName()));
}
} else if (ComponentConstants.VALUE_RIGHT_OBJECT.equals(p.getValue())) {
if (rightSource == InputSource.SOURCE_OBJECT) {
qa.addInputPort(new InputPort(p.getName(), 0));
workflow.addDatalink(new Datalink(workflow, SOURCE_PORT_NAME, qa, p.getName()));
} else if (rightSource == InputSource.TARGET_OBJECT) {
qa.addInputPort(new InputPort(p.getName(), 0));
workflow.addDatalink(new Datalink(migration, migrationTargetPortName, qa, p.getName()));
}
} else if (ComponentConstants.VALUE_PARAMETER.equals(p.getValue())) {
qa.addInputPort(new InputPort(p.getName(), 0));
TextConstant c = new TextConstant(p.getName(), parameters.get(p.getName()));
workflow.addProcessor(c);
workflow.addDatalink(new Datalink(c, "value", qa, p.getName()));
}
}
// Output ports
List<Port> outputPorts = workflowDescription.getOutputPorts();
for (Port p : outputPorts) {
if (measures.contains(p.getValue())) {
if (p.getRelatedObject() == null || p.getRelatedObject().equals(relatedObject.toString())) {
String measurePortName = createMeasurePortName(p.getValue());
if (!workflow.hasSink(measurePortName)) {
addMeasurePort(p.getValue());
}
qa.addOutputPort(new OutputPort(p.getName()));
workflow.addDatalink(new Datalink(qa, p.getName(), workflow, measurePortName));
}
}
}
}
/**
* Adds a CC component connected to the workflow's target object.
*
* Adds measure ports for the provided {@code measures} to the workflow if
* they are provided by the CC component and are not already present.
*
* @param workflowDescription
* workflow description of the migration component
* @param workflowContent
* the actual workflow content of the component
* @param parameters
* map with parameters of the component
* @param measures
* measures of output ports to connect the component to
*
* @see {@link #addCcComponent(WorkflowDescription, String, Map, List, InputSource)}
*/
public void addCcComponent(final WorkflowDescription workflowDescription, final String workflowContent,
final Map<String, String> parameters, final List<String> measures) {
addCcComponent(workflowDescription, workflowContent, parameters, measures, InputSource.TARGET_OBJECT);
}
/**
* Adds a CC component connected to the provided {@code inputSource}.
*
* Adds measure ports for the provided {@code measures} to the workflow if
* they are provided by the CC component and are not already present.
*
* @param workflowDescription
* workflow description of the migration component
* @param workflowContent
* the actual workflow content of the component
* @param parameters
* map with parameters of the component
* @param measures
* measures of output ports to connect the component to
* @param inputSource
* the input source of the CC component
*/
public void addCcComponent(final WorkflowDescription workflowDescription, final String workflowContent,
final Map<String, String> parameters, final List<String> measures, final InputSource inputSource) {
// Dataflow
NestedWorkflow cc = new NestedWorkflow(createProcessorName(workflowDescription.getName()),
workflowDescription.getDataflowId());
workflow.addProcessor(cc);
String dataflowContent = convertWorkflowToNested(workflowContent);
workflow.addDataflow(new Dataflow(workflowDescription.getDataflowId(), dataflowContent));
// Input ports
List<Port> inputPorts = workflowDescription.getInputPorts();
for (Port p : inputPorts) {
if (ComponentConstants.VALUE_SOURCE_OBJECT.equals(p.getValue())) {
if (inputSource == InputSource.SOURCE_OBJECT && workflowDescription.handlesMimetype(sourceMimetype)) {
cc.addInputPort(new InputPort(p.getName(), 0));
workflow.addDatalink(new Datalink(workflow, SOURCE_PORT_NAME, cc, p.getName()));
} else if (inputSource == InputSource.TARGET_OBJECT
&& workflowDescription.handlesMimetype(targetMimetype)) {
cc.addInputPort(new InputPort(p.getName(), 0));
workflow.addDatalink(new Datalink(migration, migrationTargetPortName, cc, p.getName()));
}
} else if (ComponentConstants.VALUE_PARAMETER.equals(p.getValue())) {
cc.addInputPort(new InputPort(p.getName(), 0));
TextConstant c = new TextConstant(p.getName(), parameters.get(p.getName()));
workflow.addProcessor(c);
workflow.addDatalink(new Datalink(c, "value", cc, p.getName()));
}
}
// Output ports
List<Port> outputPorts = workflowDescription.getOutputPorts();
for (Port p : outputPorts) {
if (measures.contains(p.getValue())) {
String measurePortName = createMeasurePortName(p.getValue());
if (!workflow.hasSink(measurePortName)) {
addMeasurePort(p.getValue());
}
cc.addOutputPort(new OutputPort(p.getName()));
workflow.addDatalink(new Datalink(cc, p.getName(), workflow, measurePortName));
}
}
}
/**
* Generates the executable plan from this description and writes it to the
* provided writer.
*
* @param writer
* the writer where the plan is written to
* @throws IOException
* if an error occurred during write
*/
public void generate(Writer writer) throws IOException {
MustacheFactory mf = new DefaultMustacheFactory();
Mustache mustache = mf.compile("data/t2flow/workflow.mustache");
mustache.execute(writer, workflow).flush();
}
/**
* Creates a valid port name from the provided measure URI.
*
* @param measure
* the measure to use
* @return the port name or null if the measure format is not recognised
*/
public static String createMeasurePortName(final String measure) {
Matcher purlMatcher = PURL_DP_MEASURE_PATTERN.matcher(measure);
if (purlMatcher.matches()) {
return purlMatcher.group(1) + "_" + purlMatcher.group(2);
}
Matcher genericMatcher = GENERIC_MEASURE_PATTERN.matcher(measure);
if (genericMatcher.matches()) {
return genericMatcher.group(1).replaceAll("\\s", "_").replaceAll("\\W", "_");
}
return null;
}
/**
* Creates a measure URL from the provided port name.
*
* @param portName
* the port name
* @return the port name or null if the measure format is not recognised
*/
public static String guessMeasureUrl(final String portName) {
Matcher purlMatcher = PURL_DP_PORTNAME_PATTERN.matcher(portName);
if (purlMatcher.matches()) {
return "http://purl.org/DP/quality/" + purlMatcher.group(1) + "#" + purlMatcher.group(2);
}
return null;
}
/**
* Creates a valid processor name from the provided name.
*
* @param name
* the name to use
* @return a valid processor name
*/
private String createProcessorName(final String name) {
String baseName = name.replaceAll("\\s", "_").replaceAll("\\W", "");
String processorName = baseName;
int postfix = 1;
while (containsProcessorName(processorName)) {
postfix++;
processorName = baseName + "_" + postfix;
}
return processorName;
}
/**
* Checks if the workflow contains a processor with the provided
* {@code name}.
*
* @param name
* the processor name
* @return true if a processor with the name exists, false otherwise
*/
private boolean containsProcessorName(final String name) {
for (Processor p : workflow.getProcessors()) {
if (p.getName().equals(name)) {
return true;
}
}
return false;
}
/**
* Converts the workflow content string to a nested dataflow string.
*
* Note that the returned string is not a valid workflow by itself anymore
* but can be added as a dataflow element to an enclosing workflow.
*
* @param workflowContent
* the workflow to convert
* @return the converted dataflow
*/
private String convertWorkflowToNested(final String workflowContent) {
return workflowContent.replaceAll("<\\?xml.*>", "").replaceAll("<workflow.+?>", "")
.replaceAll("</workflow>", "").replaceAll("role=\"top\"", "role=\"nested\"");
}
/**
* Checks if this generator has a migration.
*
* @return true if a migration was added, false otherwise
*/
private boolean hasMigration() {
return migration != null && migrationTargetPortName != null && !migrationTargetPortName.isEmpty();
}
}