/**
* Copyright 2014 VU University Medical Center.
* Licensed under the Apache License version 2.0 (see http://www.apache.org/licenses/LICENSE-2.0.html).
*/
package nl.vumc.biomedbridges.examples.gui;
import com.google.inject.Guice;
import com.google.inject.Injector;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSeparator;
import javax.swing.JTextField;
import javax.swing.JTextPane;
import javax.swing.SpringLayout;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.WindowConstants;
import javax.swing.border.TitledBorder;
import javax.swing.text.BadLocationException;
import javax.swing.text.DefaultStyledDocument;
import javax.swing.text.StyledDocument;
import nl.vumc.biomedbridges.core.Constants;
import nl.vumc.biomedbridges.core.DefaultGuiceModule;
import nl.vumc.biomedbridges.core.WorkflowType;
import nl.vumc.biomedbridges.examples.RandomLinesExample;
import nl.vumc.biomedbridges.galaxy.metadata.GalaxyStepInput;
import nl.vumc.biomedbridges.galaxy.metadata.GalaxyStepInputConnection;
import nl.vumc.biomedbridges.galaxy.metadata.GalaxyToolConditional;
import nl.vumc.biomedbridges.galaxy.metadata.GalaxyToolOption;
import nl.vumc.biomedbridges.galaxy.metadata.GalaxyToolParameterMetadata;
import nl.vumc.biomedbridges.galaxy.metadata.GalaxyToolWhen;
import nl.vumc.biomedbridges.galaxy.metadata.GalaxyWorkflowEngineMetadata;
import nl.vumc.biomedbridges.galaxy.metadata.GalaxyWorkflowMetadata;
import nl.vumc.biomedbridges.galaxy.metadata.GalaxyWorkflowStep;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This class contains all the base code for using the metadata for a Galaxy workflow to build a simple read-only Swing
* GUI that looks a bit like the web interface of this workflow in Galaxy.
*
* @author <a href="mailto:f.debruijn@vumc.nl">Freek de Bruijn</a>
*/
public class BaseGuiExample {
/**
* The logger for this class.
*/
private static final Logger logger = LoggerFactory.getLogger(RandomLinesExample.class);
/**
* The frame width.
*/
private static final int FRAME_WIDTH = 800;
/**
* The frame height.
*/
private static final int FRAME_HEIGHT = 660;
/**
* The font name for the GUI components.
*/
private static final String GUI_FONT_NAME = "SansSerif";
/**
* The title font.
*/
private static final Font TITLE_FONT = new Font(GUI_FONT_NAME, Font.BOLD, 24);
/**
* The header level 2 font.
*/
private static final Font HEADER_2_FONT = new Font(GUI_FONT_NAME, Font.BOLD, 14);
/**
* The default GUI component font.
*/
private static final Font DEFAULT_GUI_FONT = new Font(GUI_FONT_NAME, Font.PLAIN, 14);
/**
* The font name for the workflow results.
*/
private static final String RESULTS_FONT_NAME = "Monospaced";
/**
* The results font.
*/
private static final Font RESULTS_FONT = new Font(RESULTS_FONT_NAME, Font.PLAIN, 14);
/**
* Small distance between components.
*/
private static final int SMALL_PAD = 5;
/**
* Two unit distance between components.
*/
private static final int DOUBLE_PAD = 2 * SMALL_PAD;
/**
* Four unit distance between components.
*/
private static final int QUAD_PAD = 4 * SMALL_PAD;
/**
* The text shown for null or empty values.
*/
private static final String EMPTY_VALUE = "------";
/**
* The frame of the Swing GUI.
*/
private JFrame frame;
/**
* The main panel for the GUI.
*/
private JPanel guiPanel;
/**
* The layout manager for the GUI panel.
*/
private SpringLayout guiLayout;
/**
* The button to run the workflow.
*/
private JButton runWorkflowButton;
/**
* The list of the step panels.
*/
private List<JPanel> stepPanels;
/**
* The map of parameter names to text fields.
*/
private Map<String, JTextField> parameterTextFieldsMap;
/**
* Schedule a job for the event-dispatching thread: creating and showing this application's GUI.
*
* @param workflowName the name of the workflow to show.
* @return the new frame.
*/
protected JFrame createGuiExample(final String workflowName) {
return createGuiExample(workflowName, true);
}
/**
* Schedule a job for the event-dispatching thread: creating and (possibly) showing this application's GUI.
*
* @param workflowName the name of the workflow to show.
* @param makeVisible whether the frame should be visible or invisible.
* @return the new frame.
*/
protected JFrame createGuiExample(final String workflowName, final boolean makeVisible) {
try {
SwingUtilities.invokeAndWait(new Runnable() {
@Override
public void run() {
constructGuiExample(workflowName, makeVisible);
}
});
} catch (final InvocationTargetException | InterruptedException e) {
e.printStackTrace();
}
return frame;
}
/**
* Run the GUI example with a visible or invisible frame.
*
* @param workflowName the name of the workflow to show.
* @param makeVisible whether the frame should be visible or invisible.
* @return the new frame.
*/
private JFrame constructGuiExample(final String workflowName, final boolean makeVisible) {
// Create the GUI.
frame = new JFrame(workflowName + " gui example");
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
guiLayout = new SpringLayout();
guiPanel = new JPanel(guiLayout);
frame.getContentPane().setLayout(new BorderLayout());
frame.getContentPane().add(guiPanel, BorderLayout.CENTER);
final GalaxyWorkflowMetadata workflowMetadata = new GalaxyWorkflowEngineMetadata().getWorkflow(workflowName);
final Component previousComponent = addTitleAndAnnotation(workflowName, workflowMetadata);
addStepPanelsAndButton(workflowName, workflowMetadata, previousComponent);
// Center the frame and show it.
frame.setSize(FRAME_WIDTH, FRAME_HEIGHT);
frame.setLocationRelativeTo(null);
frame.setVisible(makeVisible);
adjustStepPanelSizes(true);
return frame;
}
/**
* Add the step panels and possibly the run workflow button.
*
* @param workflowName the name of the workflow to show.
* @param workflowMetadata the workflow metadata.
* @param initialPreviousComponent the previous component that was added to the GUI panel (used for layout).
*/
private void addStepPanelsAndButton(final String workflowName, final GalaxyWorkflowMetadata workflowMetadata,
final Component initialPreviousComponent) {
Component previousComponent = initialPreviousComponent;
stepPanels = new ArrayList<>();
parameterTextFieldsMap = new HashMap<>();
for (int stepIndex = 0; stepIndex < workflowMetadata.getSteps().size(); stepIndex++)
previousComponent = addStepPanel(workflowMetadata, stepIndex, guiPanel, guiLayout, previousComponent);
if (workflowName.equals(Constants.WORKFLOW_RANDOM_LINES_TWICE)) {
runWorkflowButton = new JButton("Run workflow");
runWorkflowButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(final ActionEvent e) {
runWorkflow(false);
}
});
guiPanel.add(runWorkflowButton);
guiLayout.putConstraint(SpringLayout.NORTH, runWorkflowButton, QUAD_PAD, SpringLayout.SOUTH, previousComponent);
guiLayout.putConstraint(SpringLayout.WEST, runWorkflowButton, SMALL_PAD, SpringLayout.WEST, guiPanel);
guiLayout.putConstraint(SpringLayout.EAST, runWorkflowButton, -SMALL_PAD, SpringLayout.EAST, guiPanel);
}
}
/**
* Add the title, the annotation (if available), and the separator line.
*
* @param workflowName the name of the workflow to show.
* @param workflowMetadata the workflow metadata.
* @return the last component added to the GUI panel (the separator line).
*/
private Component addTitleAndAnnotation(final String workflowName, final GalaxyWorkflowMetadata workflowMetadata) {
final String titleText = String.format("Running workflow \"%s\"", workflowName);
final JLabel titleLabel = addLabel(guiPanel, guiLayout, titleText, TITLE_FONT, null);
final String annotation = workflowMetadata.getAnnotation();
final JLabel annotationLabel = annotation != null
? addLabel(guiPanel, guiLayout, annotation, DEFAULT_GUI_FONT, titleLabel, 1)
: null;
final JLabel previousLabel = annotationLabel != null ? annotationLabel : titleLabel;
final JSeparator separatorLine = new JSeparator(SwingConstants.HORIZONTAL);
guiPanel.add(separatorLine);
guiLayout.putConstraint(SpringLayout.NORTH, separatorLine, QUAD_PAD, SpringLayout.SOUTH, previousLabel);
guiLayout.putConstraint(SpringLayout.WEST, separatorLine, SMALL_PAD, SpringLayout.WEST, guiPanel);
guiLayout.putConstraint(SpringLayout.EAST, separatorLine, -SMALL_PAD, SpringLayout.EAST, guiPanel);
return separatorLine;
}
/**
* Add a label to a panel.
*
* @param panel the panel to add the new label to.
* @param springLayout the spring layout to add GUI constraints to.
* @param text the text for the label.
* @param font the font for the label.
* @param previousComponent the previous component that was added to the GUI panel (used for layout).
* @return the new label.
*/
private JLabel addLabel(final JPanel panel, final SpringLayout springLayout, final String text, final Font font,
final Component previousComponent) {
return addLabel(panel, springLayout, text, font, previousComponent, 0);
}
/**
* Add a label to a panel.
*
* @param panel the panel to add the new label to.
* @param springLayout the spring layout to add GUI constraints to.
* @param text the text for the label.
* @param font the font for the label.
* @param previousComponent the previous component that was added to the GUI panel (used for layout).
* @param extraPadWest optional extra padding for the west constraint.
* @return the new label.
*/
private JLabel addLabel(final JPanel panel, final SpringLayout springLayout, final String text, final Font font,
final Component previousComponent, final int extraPadWest) {
final JLabel label = new JLabel(text);
label.setFont(font);
panel.add(label);
final Component anchorComponent = previousComponent == null ? panel : previousComponent;
final String anchorEdge = previousComponent == null ? SpringLayout.NORTH : SpringLayout.SOUTH;
springLayout.putConstraint(SpringLayout.NORTH, label, SMALL_PAD, anchorEdge, anchorComponent);
springLayout.putConstraint(SpringLayout.WEST, label, SMALL_PAD + extraPadWest, SpringLayout.WEST, panel);
return label;
}
/**
* Add a new panel for a workflow step.
*
* @param workflowMetadata the workflow metadata.
* @param stepIndex the step index.
* @param guiPanel the GUI panel to add the new panel to.
* @param guiLayout the GUI layout to add GUI constrains to.
* @param previousComponent the previous component that was added to the GUI panel (used for layout).
* @return the new panel.
*/
private JPanel addStepPanel(final GalaxyWorkflowMetadata workflowMetadata, final int stepIndex,
final JPanel guiPanel, final SpringLayout guiLayout, final Component previousComponent) {
final JPanel stepPanel = new JPanel();
stepPanels.add(stepPanel);
final GalaxyWorkflowStep step = workflowMetadata.getSteps().get(stepIndex);
final String toolVersion = step.getToolVersion();
final String stepText = "Step " + (stepIndex + 1) + ": " + step.getName()
+ (toolVersion != null ? " (version " + toolVersion + ")" : "");
final SpringLayout stepLayout = new SpringLayout();
stepPanel.setLayout(stepLayout);
stepPanel.setBorder(new TitledBorder(stepText));
Component previousStepComponent = null;
for (final GalaxyStepInput stepInput : step.getInputs())
previousStepComponent = addStepRow(stepPanel, stepInput.getName(), stepInput.getDescription(), step, null,
stepLayout, previousStepComponent);
if (step.getToolMetadata() != null) {
for (final GalaxyToolParameterMetadata parameter : step.getToolMetadata().getParameters())
previousStepComponent = addStepRow(stepPanel, parameter.getLabel(), parameter.getValue(), step,
parameter, stepLayout, previousStepComponent);
addConditionalParameters(stepPanel, step, stepLayout, previousStepComponent);
}
guiPanel.add(stepPanel);
guiLayout.putConstraint(SpringLayout.NORTH, stepPanel, SMALL_PAD, SpringLayout.SOUTH, previousComponent);
guiLayout.putConstraint(SpringLayout.WEST, stepPanel, SMALL_PAD, SpringLayout.WEST, guiPanel);
guiLayout.putConstraint(SpringLayout.EAST, stepPanel, -SMALL_PAD, SpringLayout.EAST, guiPanel);
return stepPanel;
}
/**
* Add a new row with a title and text to a Galaxy step panel for an input or a parameter.
*
* @param stepPanel the step panel to add the new row to.
* @param title the title of the input or the parameter.
* @param text the text of the input or the parameter.
* @param step the Galaxy step.
* @param parameter the parameter metadata.
* @param stepLayout the step layout to add GUI constrains to.
* @param previousStepComponent the previous step component that was added to the GUI panel (used for layout).
* @return the last component that was added to the step panel.
*/
private Component addStepRow(final JPanel stepPanel, final String title, final String text,
final GalaxyWorkflowStep step, final GalaxyToolParameterMetadata parameter,
final SpringLayout stepLayout, final Component previousStepComponent) {
final JLabel titleLabel = new JLabel(title);
titleLabel.setFont(HEADER_2_FONT);
stepPanel.add(titleLabel);
final Component anchorComponent = previousStepComponent == null ? stepPanel : previousStepComponent;
final String anchorEdge = previousStepComponent == null ? SpringLayout.NORTH : SpringLayout.SOUTH;
stepLayout.putConstraint(SpringLayout.NORTH, titleLabel, DOUBLE_PAD, anchorEdge, anchorComponent);
stepLayout.putConstraint(SpringLayout.WEST, titleLabel, SMALL_PAD, SpringLayout.WEST, stepPanel);
stepLayout.putConstraint(SpringLayout.EAST, titleLabel, -SMALL_PAD, SpringLayout.EAST, stepPanel);
final Component component;
// todo: quick test to attempt editing some parameters (of the random lines twice workflow).
if ("Randomly select".equals(title)) {
final String parameterKey = step.getId() + "-" + parameter.getName();
final JTextField textField = new JTextField(getFinalText(text, step, parameter));
parameterTextFieldsMap.put(parameterKey, textField);
component = textField;
} else
component = new JLabel(getFinalText(text, step, parameter));
component.setFont(DEFAULT_GUI_FONT);
stepPanel.add(component);
stepLayout.putConstraint(SpringLayout.NORTH, component, SMALL_PAD, SpringLayout.SOUTH, titleLabel);
stepLayout.putConstraint(SpringLayout.WEST, component, SMALL_PAD, SpringLayout.WEST, stepPanel);
stepLayout.putConstraint(SpringLayout.EAST, component, -SMALL_PAD, SpringLayout.EAST, stepPanel);
return component;
}
/**
* Determine the text to show for an input or parameter.
*
* @param text the text of the input or the parameter.
* @param step the Galaxy step.
* @param parameter the parameter metadata.
* @return the text to show for an input or parameter.
*/
private String getFinalText(final String text, final GalaxyWorkflowStep step,
final GalaxyToolParameterMetadata parameter) {
String finalText = text != null && !"".equals(text) ? text : EMPTY_VALUE;
if (parameter != null) {
final Map<String, Object> toolState = step.getToolState();
final String parameterName = parameter.getName();
final GalaxyStepInputConnection inputConnection = inputConnectionForParameter(parameterName, step);
if (inputConnection != null)
finalText = String.format("Output dataset '%s' from step %d", inputConnection.getOutputName(),
inputConnection.getId() + 1);
else if (toolState.containsKey(parameterName) && toolState.get(parameterName) != null)
finalText = getFinalTextFromParameter(toolState, parameterName);
}
return finalText;
}
/**
* Determine the text to show for a parameter.
*
* @param toolState the tool state from a Galaxy step.
* @param parameterName the name of the parameter.
* @return the text to show for a parameter.
*/
private String getFinalTextFromParameter(final Map<String, Object> toolState, final String parameterName) {
final Object value = toolState.get(parameterName);
String finalText = value != null && !"".equals(value) ? value.toString() : EMPTY_VALUE;
final List<String> booleanStrings = Arrays.asList("true", "false");
if (value instanceof Map) {
final Map valueMap = (Map) value;
final String valueKey = "value";
if (valueMap.containsKey(valueKey)) {
final boolean unvalidated = "UnvalidatedValue".equals(valueMap.get("__class__"));
finalText = valueMap.get(valueKey) + (unvalidated ? " (value not yet validated)" : "");
}
} else
finalText = booleanStrings.contains(finalText.toLowerCase())
? Character.toUpperCase(finalText.charAt(0)) + finalText.substring(1)
: finalText;
return finalText;
}
/**
* Determine whether a parameter is connected to an input connection. The end of the input connection name is
* matched with the parameter name, since these input connection names can have a "queries_[0-9]+\|" prefix.
*
* @param parameterName the parameter name.
* @param step the Galaxy step.
* @return the related input connection or null.
*/
private GalaxyStepInputConnection inputConnectionForParameter(final String parameterName,
final GalaxyWorkflowStep step) {
GalaxyStepInputConnection inputConnection = null;
for (final Map.Entry<String, GalaxyStepInputConnection> inputConnectionEntry : step.getInputConnections().entrySet())
if (inputConnectionEntry.getKey().endsWith(parameterName)) {
inputConnection = inputConnectionEntry.getValue();
break;
}
return inputConnection;
}
/**
* Add conditional parameters (if there are any).
*
* @param stepPanel the step panel to add the new row to.
* @param step the Galaxy step.
* @param stepLayout the step layout to add GUI constrains to.
* @param previousStepComponent the previous step component that was added to the GUI panel (used for layout).
*/
private void addConditionalParameters(final JPanel stepPanel, final GalaxyWorkflowStep step,
final SpringLayout stepLayout, final Component previousStepComponent) {
Component previousComponent = previousStepComponent;
for (final GalaxyToolConditional conditional : step.getToolMetadata().getConditionals()) {
final GalaxyToolParameterMetadata parameter = conditional.getSelectorParameter();
String text = EMPTY_VALUE;
String selectedOptionValue = null;
for (final GalaxyToolOption option : conditional.getOptions())
if (option.isSelected()) {
text = option.getText();
selectedOptionValue = option.getValue();
break;
}
previousComponent = addStepRow(stepPanel, parameter.getLabel(), text, step, parameter, stepLayout,
previousComponent);
for (final GalaxyToolWhen when : conditional.getWhens())
if (when.getValue().equals(selectedOptionValue))
for (final GalaxyToolParameterMetadata whenParameter : when.getParameters())
previousComponent = addStepRow(stepPanel, whenParameter.getLabel(),
whenParameter.getValue(), step, whenParameter,
stepLayout, previousComponent);
}
}
/**
* Adjust the preferred sizes of the step panels.
*
* @param visible whether the step panels should be visible.
*/
private void adjustStepPanelSizes(final boolean visible) {
final int minimumBottomY = 30;
final int widthDecrement = 40;
final int bottomIncrement = 10;
for (final JPanel stepPanel : stepPanels) {
stepPanel.setVisible(visible);
int bottomY = minimumBottomY;
if (visible)
for (final Component stepComponent : stepPanel.getComponents())
bottomY = Math.max(bottomY, stepComponent.getY() + stepComponent.getHeight());
final int height = visible ? bottomY + bottomIncrement : 0;
stepPanel.setPreferredSize(new Dimension(FRAME_WIDTH - widthDecrement, height));
stepPanel.revalidate();
}
guiPanel.revalidate();
}
/**
* Run the workflow.
*
* @param waitForWorkflowToFinish whether the method should wait for the workflow to finish.
*/
// todo: currently only works for the RandomLinesTwice workflow.
protected void runWorkflow(final boolean waitForWorkflowToFinish) {
// Create the thread to run the workflow.
final Thread runWorkflowThread = new Thread(new Runnable() {
@Override
public void run() {
runRandomLinesWorkflow(Guice.createInjector(new DefaultGuiceModule()), adaptGuiForRunningWorkflow());
}
});
// Run the thread.
runWorkflowThread.start();
// If requested, wait for the thread to finish.
if (waitForWorkflowToFinish)
try {
runWorkflowThread.join();
} catch (final InterruptedException e) {
logger.error("Exception while running a workflow", e);
}
}
/**
* Run the "random lines twice" workflow.
*
* @param injector the Guice injector to build the RandomLinesExample object.
* @param resultsDocument the results document connected to the results text pane.
*/
private void runRandomLinesWorkflow(final Injector injector, final StyledDocument resultsDocument) {
final int initialLineCount = Integer.parseInt(parameterTextFieldsMap.get("1-num_lines").getText());
final int definitiveLineCount = Integer.parseInt(parameterTextFieldsMap.get("2-num_lines").getText());
final RandomLinesExample randomLinesExample = injector.getInstance(RandomLinesExample.class);
randomLinesExample.setWorkflowType(WorkflowType.DEMONSTRATION);
randomLinesExample.setLineCounts(initialLineCount, definitiveLineCount);
final boolean result = randomLinesExample.runExample(Constants.CENTRAL_GALAXY_URL);
final List<String> outputLines = result ? randomLinesExample.getOutputLines() : null;
if (outputLines != null) {
final List<String> resultLines = new ArrayList<>();
final String message = "The workflow ran successfully in %1.1f seconds and produced the following output:";
resultLines.add(String.format(message, randomLinesExample.getDurationSeconds()));
resultLines.add("");
final String outputSeparator = "======";
resultLines.add(outputSeparator);
resultLines.addAll(outputLines);
resultLines.add(outputSeparator);
addLinesToResults(resultsDocument, resultLines.toArray(new String[resultLines.size()]));
} else {
final String message = "The workflow failed after running for %1.1f seconds.";
addLinesToResults(resultsDocument, String.format(message, randomLinesExample.getDurationSeconds()));
}
}
/**
* Adapt the GUI for the running workflow by hiding the step panels and showing the results text pane.
*
* @return the results document connected to the results text pane.
*/
private StyledDocument adaptGuiForRunningWorkflow() {
// Add results text pane.
final JTextPane resultsTextPane = new JTextPane();
resultsTextPane.setEditable(false);
resultsTextPane.setFont(RESULTS_FONT);
final StyledDocument resultsDocument = new DefaultStyledDocument();
final String initialText = String.format("Running workflow \"%s\"...", Constants.WORKFLOW_RANDOM_LINES_TWICE);
addLinesToResults(resultsDocument, initialText, "", "");
resultsTextPane.setDocument(resultsDocument);
final JScrollPane resultsScrollPane = new JScrollPane(resultsTextPane);
resultsScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
resultsScrollPane.setBorder(new TitledBorder("Results"));
guiPanel.add(resultsScrollPane);
// Adjust step panel sizes.
guiLayout.putConstraint(SpringLayout.NORTH, resultsScrollPane, QUAD_PAD, SpringLayout.SOUTH, runWorkflowButton);
guiLayout.putConstraint(SpringLayout.WEST, resultsScrollPane, SMALL_PAD, SpringLayout.WEST, guiPanel);
guiLayout.putConstraint(SpringLayout.EAST, resultsScrollPane, -SMALL_PAD, SpringLayout.EAST, guiPanel);
guiLayout.putConstraint(SpringLayout.SOUTH, resultsScrollPane, -SMALL_PAD, SpringLayout.SOUTH, guiPanel);
adjustStepPanelSizes(false);
return resultsDocument;
}
/**
* Add the lines to the results text pane.
*
* @param resultsDocument the results document connected to the results text pane.
* @param lines the lines to add.
*/
private void addLinesToResults(final StyledDocument resultsDocument, final String... lines) {
try {
for (final String line : lines)
resultsDocument.insertString(resultsDocument.getLength(), line + "\n", null);
} catch (final BadLocationException e) {
logger.error("Exception while adding text to a PlainDocument object.", e);
}
}
}