blob: f7469e2ec3ebeaaea1ece5eba55102a7f254fb0f [file] [log] [blame]
/*******************************************************************************
* Copyright (C) 2007 The University of Manchester
*
* Modifications to the initial code base are copyright of their
* respective authors, or their employers as appropriate.
*
* This program 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 (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
******************************************************************************/
package net.sf.taverna.t2.reference.ui;
import static com.hp.hpl.jena.sparql.graph.GraphFactory.makeDefaultModel;
import static java.awt.BorderLayout.CENTER;
import static java.awt.BorderLayout.EAST;
import static java.awt.BorderLayout.SOUTH;
import static java.awt.BorderLayout.WEST;
import static javax.swing.BoxLayout.X_AXIS;
import static javax.swing.JOptionPane.ERROR_MESSAGE;
import static javax.swing.JOptionPane.showMessageDialog;
import static net.sf.taverna.t2.workbench.icons.WorkbenchIcons.closeIcon;
import static org.apache.jena.riot.Lang.TURTLE;
import static org.apache.jena.riot.RDFLanguages.contentTypeToLang;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.BoxLayout;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTabbedPane;
import javax.swing.JToolBar;
import javax.swing.border.EmptyBorder;
import javax.swing.border.TitledBorder;
import net.sf.taverna.t2.lang.observer.Observable;
import net.sf.taverna.t2.lang.observer.Observer;
import net.sf.taverna.t2.lang.ui.DialogTextArea;
import net.sf.taverna.t2.reference.ui.referenceactions.ReferenceActionSPI;
import net.sf.taverna.t2.workbench.edits.EditManager;
import net.sf.taverna.t2.workbench.edits.EditManager.AbstractDataflowEditEvent;
import net.sf.taverna.t2.workbench.edits.EditManager.EditManagerEvent;
import net.sf.taverna.t2.workbench.file.FileManager;
import net.sf.taverna.t2.workbench.file.events.ClosedDataflowEvent;
import net.sf.taverna.t2.workbench.file.events.FileManagerEvent;
import net.sf.taverna.t2.workbench.report.ReportManager;
import net.sf.taverna.t2.workbench.ui.Workbench;
import org.apache.batik.swing.JSVGCanvas;
import org.apache.jena.riot.Lang;
import org.apache.jena.riot.RDFDataMgr;
import org.apache.jena.riot.RIOT;
import org.apache.log4j.Logger;
import org.purl.wf4ever.robundle.Bundle;
import uk.org.taverna.configuration.database.DatabaseConfiguration;
import uk.org.taverna.databundle.DataBundles;
import uk.org.taverna.scufl2.api.annotation.Annotation;
import uk.org.taverna.scufl2.api.common.Scufl2Tools;
import uk.org.taverna.scufl2.api.common.URITools;
import uk.org.taverna.scufl2.api.common.WorkflowBean;
import uk.org.taverna.scufl2.api.container.WorkflowBundle;
import uk.org.taverna.scufl2.api.core.Workflow;
import uk.org.taverna.scufl2.api.port.InputWorkflowPort;
import com.hp.hpl.jena.rdf.model.Model;
import com.hp.hpl.jena.rdf.model.Property;
import com.hp.hpl.jena.rdf.model.Resource;
/**
* A simple workflow launch window, uses a tabbed layout to display a set of named RegistrationPanel
* instances, and a 'run workflow' button. Also shows a pane contining a picture of the workflow,
* the author and the description.
* We use one WorkflowLaunchWindow per workflow, multiple runs of the same workflow get the same
* window.
*
* @author Tom Oinn
* @author David Withers
* @author Stian Soiland-Reyes
* @author Alan R Williams
* @author Alex Nenadic
*/
@SuppressWarnings("serial")
public abstract class WorkflowLaunchWindow extends JFrame {
private static final Logger logger = Logger.getLogger(WorkflowLaunchWindow.class);
private static final String LAUNCH_WORKFLOW = "Run workflow";
private static final ImageIcon launchIcon = new ImageIcon(
WorkflowLaunchWindow.class.getResource("/icons/start_task.gif"));
private static final ImageIcon addTextIcon = new ImageIcon(
WorkflowLaunchWindow.class.getResource("/icons/addtext_co.gif"));
private static final String NO_WORKFLOW_DESCRIPTION = "No description";
private static final String NO_WORKFLOW_AUTHOR = "No author";
/**
* An action enabled when all inputs are enabled and used to trigger the
* {@link #handleLaunch(Bundle)} method
*/
private Action launchAction;
/**
* A map of input port names to input registration panels (from the previous
* run of the same workflow, if any)
*/
private Map<String, RegistrationPanel> inputPanelMap = new HashMap<>();
/** A pane holding various tabs for workflow input ports */
private JTabbedPane tabsPane;
private Workflow workflow;
private DialogTextArea workflowDescriptionArea;
private DialogTextArea workflowAuthorArea;
private JSVGCanvas createWorkflowGraphic;
/**
* Whether the original workflow has been modified in the design perspective
* so we know to refresh this dialog
*/
private boolean workflowModified = false;
private FileManager fileManager;
/** Observer of workflow closing events so we can dispose off the window */
private FileManagerObserver fileManagerObserver = new FileManagerObserver();
private EditManager editManager;
private EditManagerObserver editManagerObserver = new EditManagerObserver();
private JPanel overallPanel;
private JPanel workflowPart;
private JPanel portsPart;
@SuppressWarnings("unused")
private final Workbench workbench;
@SuppressWarnings("unused")
private final ReportManager reportManager;
private final List<ReferenceActionSPI> referenceActionSPIs;
private final DatabaseConfiguration databaseConfiguration;
private final Scufl2Tools scufl2Tools = new Scufl2Tools();
private final URITools uriTools = new URITools();
public WorkflowLaunchWindow(Workflow workflow, EditManager editManager,
FileManager fileManager, ReportManager reportManager,
Workbench workbench, List<ReferenceActionSPI> referenceActionSPIs,
DatabaseConfiguration databaseConfiguration) {
super();
// Initialize RIOT reader
RIOT.register();
this.workflow = workflow;
this.editManager = editManager;
this.fileManager = fileManager;
this.reportManager = reportManager;
this.workbench = workbench;
this.referenceActionSPIs = referenceActionSPIs;
this.databaseConfiguration = databaseConfiguration;
initComponents();
// Handle refreshing the frame when it receives focus
addWindowFocusListener(new WindowAdapter() {
@Override
public void windowGainedFocus(WindowEvent e) {
if (workflowModified) {
// Clear all previous components
getContentPane().removeAll();
// Redraw the window
initComponents();
overallPanel.revalidate();
overallPanel.repaint();
workflowModified = false;
}
}
});
// Handle window closing
addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent winEvt) {
handleCancel(); // do not dispose the window, just hide it
}
});
// Start observing workflow closing events on File Manager
fileManager.addObserver(fileManagerObserver);
// Start observing edit workflow events on Edit Manager
editManager.addObserver(editManagerObserver);
}
/**
* Set the title of the window to contain the workflow name and its file/url
* location so that users can easily identify which workflow is being run.
*
* @param title
*/
private void setWindowTitle(String title) {
String windowTitle = "Input values for ";
if (title != null && !title.isEmpty())
windowTitle += "'" + title + "' ";
else
// Fall back to its name
windowTitle += "'" + workflow.getName() + "' ";
Object workflowLocation = fileManager.getDataflowSource(workflow.getParent());
windowTitle += (workflowLocation == null) ? "" : "from " + workflowLocation.toString();
setTitle(windowTitle);
}
/**
* Draw the components of the frame.
*/
public void initComponents() {
workflowPart = new JPanel(new GridLayout(3, 1));
portsPart = new JPanel(new BorderLayout());
createWorkflowGraphic = createWorkflowGraphic(workflow);
createWorkflowGraphic.setBorder(new TitledBorder("Diagram"));
workflowPart.add(new JScrollPane(createWorkflowGraphic));
workflowDescriptionArea = new DialogTextArea(NO_WORKFLOW_DESCRIPTION, 5, 40);
workflowDescriptionArea.setBorder(new TitledBorder("Workflow description"));
workflowDescriptionArea.setEditable(false);
workflowDescriptionArea.setLineWrap(true);
workflowDescriptionArea.setWrapStyleWord(true);
workflowPart.add(new JScrollPane(workflowDescriptionArea));
workflowAuthorArea = new DialogTextArea(NO_WORKFLOW_AUTHOR, 1, 40);
workflowAuthorArea.setBorder(new TitledBorder("Workflow author"));
workflowAuthorArea.setEditable(false);
workflowAuthorArea.setLineWrap(true);
workflowAuthorArea.setWrapStyleWord(true);
workflowPart.add(new JScrollPane(workflowAuthorArea));
launchAction = new AbstractAction(LAUNCH_WORKFLOW, launchIcon) {
@Override
public void actionPerformed(ActionEvent ae) {
// First of all - is the workflow valid?
// TODO convert to Scufl2 validation
// if (!CheckWorkflowStatus.checkWorkflow(dataflowOriginal, workbench, editManager,
// fileManager, reportManager)) {
// setVisible(false);
// return;
// }
/*
* Check if user had entered input values for all input ports -
* otherwise there is no point in attempting to run the workflow
*/
for (InputWorkflowPort input : workflow.getInputPorts()) {
RegistrationPanel registrationPanel = inputPanelMap.get(input.getName());
Object userInput = registrationPanel.getUserInput();
if (userInput == null) {
showMessageDialog(
WorkflowLaunchWindow.this,
"You have not provided input values for all workflow inputs",
"Workflow input value error", ERROR_MESSAGE);
// exit
return;
}
}
setState(ICONIFIED);
try {
Bundle inputDataBundle = createInputDataBundle();
handleLaunch(inputDataBundle);
} catch (IOException e) {
showMessageDialog(WorkflowLaunchWindow.this,
"An error occurred while creating input values\n"
+ e.getMessage(), "Error creating inputs",
ERROR_MESSAGE);
return;
}
}
};
WorkflowBundle workflowBundle = workflow.getParent();
Model annotations = annotationsForBean(workflowBundle, workflow);
Resource bean = annotations.getResource(uriTools.uriForBean(workflow)
.toASCIIString());
String title = null;
Property titleProp = annotations
.createProperty("http://purl.org/dc/terms/title");
if (bean.hasProperty(titleProp))
title = bean.getProperty(titleProp).getString();
setWindowTitle(title);
Property descProp = annotations
.createProperty("http://purl.org/dc/terms/description");
if (bean.hasProperty(descProp))
setWorkflowDescription(bean.getProperty(descProp).getString());
Property creatorProp = annotations
.createProperty("http://purl.org/dc/elements/1.1/creator");
if (bean.hasProperty(creatorProp))
setWorkflowAuthor(bean.getProperty(creatorProp).getString());
Action useExamplesAction = new AbstractAction("Use examples", addTextIcon) {
@Override
public void actionPerformed(ActionEvent arg0) {
List<InputWorkflowPort> inputPorts = new ArrayList<>(
workflow.getInputPorts());
/*
* Create tabs for input ports (but only for the one that are
* connected!)
*/
for (InputWorkflowPort inputPort : inputPorts) {
RegistrationPanel rp = inputPanelMap.get(inputPort
.getName());
Object example = rp.getExample();
if (example != null && inputPort.getDepth() == 0
&& rp.getValue() == null)
rp.setValue(example);
}
}
};
JButton useExamplesButton = new JButton(useExamplesAction);
useExamplesButton
.setToolTipText("Use the example value (if any) for ports that you have not set a value for");
// Construct tool bar
JToolBar toolBar = new JToolBar();
toolBar.setFloatable(false);
toolBar.add(useExamplesButton);
toolBar.add(new JButton(launchAction));
toolBar.add(new JButton(new AbstractAction("Cancel", closeIcon) {
@Override
public void actionPerformed(ActionEvent e) {
handleCancel();
}
}));
JToolBar loadButtonsBar = new JToolBar();
loadButtonsBar.setFloatable(false);
for (ReferenceActionSPI spi : referenceActionSPIs) {
ReferenceActionSPI action = (ReferenceActionSPI) spi.getAction();
action.setInputPanelMap(inputPanelMap);
JButton loadButton = new JButton((AbstractAction) action);
loadButtonsBar.add(loadButton);
}
JPanel toolBarPanel = new JPanel(new BorderLayout());
toolBarPanel.add(loadButtonsBar, WEST);
toolBarPanel.add(toolBar, EAST);
toolBarPanel.setBorder(new EmptyBorder(5, 10, 5, 20));
portsPart.add(toolBarPanel, SOUTH);
/*
* Construct tab container - tabs will be populated based on the wf
* input ports
*/
tabsPane = new JTabbedPane();
List<InputWorkflowPort> inputPorts = new ArrayList<>(workflow.getInputPorts());
Set<String> inputNames = new HashSet<>();
/*
* Create tabs for input ports (but only for the one that are
* connected!)
*/
for (InputWorkflowPort inputPort : inputPorts) {
// Is this input port connected to anything?
if (scufl2Tools.datalinksFrom(inputPort).isEmpty())
continue;
String portDescription = "";
String portExample = "";
annotations = annotationsForBean(workflowBundle, inputPort);
bean = annotations.getResource(uriTools.uriForBean(inputPort)
.toASCIIString());
if (bean.hasProperty(descProp))
portDescription = bean.getProperty(descProp).getString();
Property exDataProp = annotations
.createProperty("http://biocatalogue.org/attribute/exampleData");
if (bean.hasProperty(exDataProp))
portExample = bean.getProperty(exDataProp).getString();
// add tabs for wf input ports
String name = inputPort.getName();
inputNames.add(name);
addInput(name, inputPort.getDepth(), portDescription, portExample);
}
portsPart.add(tabsPane, CENTER);
workflowPart.setPreferredSize(new Dimension(300, 500));
portsPart.setPreferredSize(new Dimension(650, 500));
overallPanel = new JPanel();
overallPanel.setLayout(new BoxLayout(overallPanel, X_AXIS));
overallPanel.add(workflowPart);
overallPanel.add(portsPart);
setLayout(new BorderLayout());
getContentPane().add(new JScrollPane(overallPanel), CENTER);
pack();
}
private Model annotationsForBean(WorkflowBundle workflowBundle,
WorkflowBean bean) {
Model model = makeDefaultModel();
for (Annotation annotation : scufl2Tools.annotationsFor(bean,
workflowBundle)) {
// System.out.println(annotation.getBody());
URI base = uriTools.uriForBean(workflowBundle);
URI body = base.resolve(annotation.getBody());
// System.out.println(body);
URI path = uriTools.relativePath(workflowBundle.getGlobalBaseURI(),
body);
// System.out.println(path.getPath());
String mediaType = workflowBundle.getResources()
.getResourceEntry(path.getPath()).getMediaType();
Lang lang = contentTypeToLang(mediaType);
if (lang == null)
// Safe fallback
lang = TURTLE;
// System.out.println(mediaType);
try (InputStream inputStream = workflowBundle.getResources()
.getResourceAsInputStream(path.getPath())) {
RDFDataMgr.read(model, inputStream, body.toASCIIString(), lang);
// model.read(inputStream, body.toASCIIString(), mediaType);
} catch (IOException e) {
logger.warn("Can't read " + body, e);
continue;
}
}
return model;
}
/**
* User clicked the cancel button.
*/
public void cancelPressed() {
this.setVisible(true);
}
/**
* Creates an SVGCanvas loaded with the SVGDocument for the Dataflow.
*
* @param dataflow
* @return
*/
private JSVGCanvas createWorkflowGraphic(Workflow worklfow) {
JSVGCanvas svgCanvas = new JSVGCanvas();
//SVGGraphController graphController = GraphViewComponent.graphControllerMap.get(workflow);
//if (graphController != null) {
// SVGDocument svgDoc = graphController.getSVGDocument();
// svgCanvas.setDocument((SVGDocument) svgDoc.cloneNode(true));
//}
return svgCanvas;
}
public synchronized void addInput(final String inputName, final int inputDepth) {
addInput(inputName, inputDepth, null, null);
}
public void addInput(final String inputName, final int inputDepth, String inputDescription,
String inputExample) {
/*
* Don't do anything if we already have the input registration panel for
* this input port
*/
RegistrationPanel inputRegistrationPanel = inputPanelMap.get(inputName);
if ((inputRegistrationPanel == null) || (inputRegistrationPanel.getDepth() != inputDepth)) {
inputRegistrationPanel = new RegistrationPanel(inputDepth, inputName, inputDescription,
inputExample, databaseConfiguration);
inputPanelMap.put(inputName, inputRegistrationPanel);
} else {
inputRegistrationPanel.setStatus(
"Drag to re-arrange, or drag files, URLs, or text to add", null);
inputRegistrationPanel.setDescription(inputDescription);
inputRegistrationPanel.setExample(inputExample);
}
tabsPane.addTab(inputName, inputRegistrationPanel);
tabsPane.revalidate();
tabsPane.repaint();
}
public synchronized void removeInputTab(final String inputName) {
/*
* Only do something if we have a registration panel for this input port
* to begin with
*/
if (!inputPanelMap.containsKey(inputName))
return;
RegistrationPanel inputRegistrationPanelToRemove = inputPanelMap
.remove(inputName);
tabsPane.remove(inputRegistrationPanelToRemove);
}
private Bundle createInputDataBundle() throws IOException {
Bundle bundle = DataBundles.createBundle();
Path inputs = DataBundles.getInputs(bundle);
for (String input : inputPanelMap.keySet()) {
RegistrationPanel registrationPanel = inputPanelMap.get(input);
Object userInput = registrationPanel.getUserInput();
Path port = DataBundles.getPort(inputs, input);
setValue(port, userInput);
}
return bundle;
}
private void setValue(Path port, Object userInput) throws IOException {
if (userInput instanceof File)
DataBundles.setReference(port, ((File) userInput).toURI());
else if (userInput instanceof URL)
try {
DataBundles.setReference(port, ((URL) userInput).toURI());
} catch (URISyntaxException e) {
logger.warn(String.format("Error converting %1$s to URI",
userInput), e);
}
else if (userInput instanceof String)
DataBundles.setStringValue(port, (String) userInput);
else if (userInput instanceof List<?>) {
DataBundles.createList(port);
List<?> list = (List<?>) userInput;
for (Object object : list)
setValue(DataBundles.newListItem(port), object);
} else
logger.warn("Unknown input type : "
+ userInput.getClass().getName());
}
/**
* Called when the run workflow action has been performed
*
* @param workflowInputs
* a map of named inputs in the form of T2Reference instances
*/
public abstract void handleLaunch(Bundle workflowInputs);
public abstract void handleCancel();
private static void selectTopOfTextArea(DialogTextArea textArea) {
textArea.setSelectionStart(0);
textArea.setSelectionEnd(0);
}
public void setWorkflowDescription(String workflowDescription) {
if (workflowDescription != null && !workflowDescription.isEmpty()) {
workflowDescriptionArea.setText(workflowDescription);
selectTopOfTextArea(workflowDescriptionArea);
}
}
void setWorkflowAuthor(String workflowAuthor) {
if (workflowAuthor != null && !workflowAuthor.isEmpty()) {
workflowAuthorArea.setText(workflowAuthor);
selectTopOfTextArea(workflowAuthorArea);
}
}
public String getWorkflowDescription() {
return workflowDescriptionArea.getText();
}
@Override
protected void finalize() throws Throwable {
createWorkflowGraphic.stopProcessing();
super.finalize();
}
public class FileManagerObserver implements Observer<FileManagerEvent> {
@Override
public void notify(Observable<FileManagerEvent> sender, FileManagerEvent message)
throws Exception {
if (message instanceof ClosedDataflowEvent
&& ((ClosedDataflowEvent) message).getDataflow() == workflow
.getParent()) {
// Remove listeners of various events
editManager.removeObserver(editManagerObserver);
fileManager.removeObserver(fileManagerObserver);
setVisible(false);
dispose(); // dispose off this window if the original workflow has been closed
}
}
}
public class EditManagerObserver implements Observer<EditManagerEvent> {
@Override
public void notify(Observable<EditManagerEvent> sender,
final EditManagerEvent message) throws Exception {
if (message instanceof AbstractDataflowEditEvent
&& ((AbstractDataflowEditEvent) message).getDataFlow() == workflow
.getParent()) {
workflowModified = true;
}
}
}
}