blob: f1a76d3145b9f0b7c0540c3392b9bf87b8115660 [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.workbench.views.results.workflow;
import static java.awt.BorderLayout.CENTER;
import static java.awt.BorderLayout.NORTH;
import static java.awt.Color.GRAY;
import static java.awt.event.ItemEvent.SELECTED;
import static javax.swing.BoxLayout.LINE_AXIS;
import static javax.swing.SwingUtilities.invokeLater;
import static net.sf.taverna.t2.renderers.RendererUtils.getInputStream;
import static net.sf.taverna.t2.results.ResultsUtils.getMimeTypes;
import static net.sf.taverna.t2.workbench.icons.WorkbenchIcons.refreshIcon;
import static org.apache.commons.lang.StringEscapeUtils.escapeHtml;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.FlowLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.swing.BoxLayout;
import javax.swing.DefaultComboBoxModel;
import javax.swing.DefaultListCellRenderer;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTree;
import javax.swing.ListCellRenderer;
import javax.swing.text.JTextComponent;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeCellRenderer;
import net.sf.taverna.t2.lang.ui.DialogTextArea;
import net.sf.taverna.t2.renderers.Renderer;
import net.sf.taverna.t2.renderers.RendererException;
import net.sf.taverna.t2.renderers.RendererRegistry;
import net.sf.taverna.t2.workbench.views.results.saveactions.SaveIndividualResultSPI;
import org.apache.log4j.Logger;
import uk.org.taverna.databundle.DataBundles;
import uk.org.taverna.databundle.ErrorDocument;
import uk.org.taverna.scufl2.api.port.OutputWorkflowPort;
import eu.medsea.mimeutil.MimeType;
/**
* Creates a component that renders an individual result from an output port.
* The component can render the result according to the renderers existing for
* the output port's MIME type or display an error document.
*
* @author Ian Dunlop
* @author Alex Nenadic
*/
@SuppressWarnings("serial")
public class RenderedResultComponent extends JPanel {
private static final Logger logger = Logger.getLogger(RenderedResultComponent.class);
private static final String WRAP_TEXT = "Wrap text";
private static final String ERROR_DOCUMENT = "Error Document";
/** Panel containing rendered result*/
private JPanel renderedResultPanel;
/** Combo box containing possible result types*/
private JComboBox<String> renderersComboBox;
/**
* Button to refresh (re-render) the result, especially needed for large
* results that are not rendered or are partially rendered and the user
* wished to re-render them
*/
private JButton refreshButton;
/**
* Preferred result type renderers (the ones recognised to be able to handle
* the result's MIME type)
*/
private List<Renderer> recognisedRenderersForMimeType;
/**
* All other result type renderers (the ones not recognised to be able to
* handle the result's MIME type) in case user wants to use them.
*/
private List<Renderer> otherRenderers;
/** Renderers' registry */
private final RendererRegistry rendererRegistry;
/**
* List of all MIME strings from all available renderers to be used for
* {@link #renderersComboBox}. Those that come from
* {@link #recognisedRenderersForMimeType} are the preferred ones. Those
* from {@link #otherRenderers} will be greyed-out in the combobox list but
* could still be used.
*/
private String[] mimeList;
/**
* List of all available renderers but ordered to match the corresponding
* MIME type strings in mimeList: first the preferred renderers from
* {@link #recognisedRenderersForMimeType} then the ones from
* {@link #otherRenderers}.
*/
private ArrayList<Renderer> rendererList;
/**
* Remember the MIME type of the last used renderer. Use "
* <tt>text/plain</tt>" by default until user changes it - then use that one
* for all result items of the port (in case result contains a list). "
* <tt>text/plain</tt>" will always be added to the {@link #mimeList}.
*/
// text renderer will always be available
private String lastUsedMIMEtype = "text/plain";
/** If result is "text/plain" - provide possibility to wrap wide text */
private JCheckBox wrapTextCheckBox;
/** Reference to the object being displayed (contained in the tree node) */
private Path path;
/**
* In case the node can be rendered as "<tt>text/plain</tt>", map the hash
* code of the node to the wrap text check box selection value for that node
* (that remembers if user wanted the text wrapped or not).
*/
private Map<Path, Boolean> nodeToWrapSelection = new HashMap<>();
/** List of all output ports - needs to be passed to 'save result' actions. */
List<OutputWorkflowPort> dataflowOutputPorts = null;
/** Panel containing all 'save results' buttons */
JPanel saveButtonsPanel = null;
/**
* Creates the component.
*/
public RenderedResultComponent(RendererRegistry rendererRegistry,
List<SaveIndividualResultSPI> saveActions) {
this.rendererRegistry = rendererRegistry;
setLayout(new BorderLayout());
// Results type combo box
renderersComboBox = new JComboBox<>();
renderersComboBox.setModel(new DefaultComboBoxModel<String>()); // initially empty
renderersComboBox.setRenderer(new ColorCellRenderer());
renderersComboBox.setEditable(false);
renderersComboBox.setEnabled(false); // initially disabled
// Set the new listener - listen for changes in the currently selected renderer
renderersComboBox.addItemListener(new ItemListener() {
@Override
public void itemStateChanged(ItemEvent e) {
if (e.getStateChange() == ItemEvent.SELECTED
&& !ERROR_DOCUMENT.equals(e.getItem()))
// render the result using the newly selected renderer
renderResult();
}
});
JPanel resultsTypePanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
resultsTypePanel.add(new JLabel("Value type"));
resultsTypePanel.add(renderersComboBox);
// Refresh (re-render) button
refreshButton = new JButton("Refresh", refreshIcon);
refreshButton.setEnabled(false);
refreshButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
renderResult();
refreshButton.getParent().requestFocusInWindow();
/*
* so that the button does not stay focused after it is clicked
* on and did its action
*/
}
});
resultsTypePanel.add(refreshButton);
// Check box for wrapping text if result is of type "text/plain"
wrapTextCheckBox = new JCheckBox(WRAP_TEXT);
wrapTextCheckBox.setVisible(false);
wrapTextCheckBox.addItemListener(new ItemListener() {
@Override
public void itemStateChanged(ItemEvent e) {
// Should have only one child component holding the rendered result
// Check for empty just as well
if (renderedResultPanel.getComponents().length == 0)
return;
Component component = renderedResultPanel.getComponent(0);
if (component instanceof DialogTextArea) {
nodeToWrapSelection.put(path,
e.getStateChange() == SELECTED);
renderResult();
}
}
});
resultsTypePanel.add(wrapTextCheckBox);
// 'Save result' buttons panel
saveButtonsPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
for (SaveIndividualResultSPI action : saveActions) {
action.setResultReference(null);
final JButton saveButton = new JButton(action.getAction());
saveButton.setEnabled(false);
saveButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
saveButton.getParent().requestFocusInWindow();
/*
* so that the button does not stay focused after it is
* clicked on and did its action
*/
}
});
saveButtonsPanel.add(saveButton);
}
// Top panel contains result type combobox and various save buttons
JPanel topPanel = new JPanel();
topPanel.setLayout(new BoxLayout(topPanel, LINE_AXIS));
topPanel.add(resultsTypePanel);
topPanel.add(saveButtonsPanel);
// Rendered results panel - initially empty
renderedResultPanel = new JPanel(new BorderLayout());
// Add all components
add(topPanel, NORTH);
add(new JScrollPane(renderedResultPanel), CENTER);
}
/**
* Sets the path this components renders the results for, and update the
* rendered results panel.
*/
public void setPath(final Path path) {
this.path = path;
invokeLater(new Runnable() {
@Override
public void run() {
if (path == null || DataBundles.isList(path))
clearResult();
else
updateResult();
}
});
}
/**
* Update the component based on the node selected from the
* ResultViewComponent tree.
*/
public void updateResult() {
if (recognisedRenderersForMimeType == null)
recognisedRenderersForMimeType = new ArrayList<>();
if (otherRenderers == null)
otherRenderers = new ArrayList<>();
// Enable the combo box
renderersComboBox.setEnabled(true);
/*
* Update the 'save result' buttons appropriately as the result node had
* changed
*/
for (int i = 0; i < saveButtonsPanel.getComponents().length; i++) {
JButton saveButton = (JButton) saveButtonsPanel.getComponent(i);
SaveIndividualResultSPI action = (SaveIndividualResultSPI) saveButton
.getAction();
// Update the action with the new result reference
action.setResultReference(path);
saveButton.setEnabled(true);
}
if (DataBundles.isValue(path) || DataBundles.isReference(path)) {
// Enable refresh button
refreshButton.setEnabled(true);
List<MimeType> mimeTypes = new ArrayList<>();
try (InputStream inputstream = getInputStream(path)) {
mimeTypes.addAll(getMimeTypes(inputstream));
} catch (IOException e) {
logger.warn("Error getting mimetype", e);
}
if (mimeTypes.isEmpty())
// If MIME types is empty - add "plain/text" MIME type
mimeTypes.add(new MimeType("text/plain"));
else if (mimeTypes.size() == 1
&& mimeTypes.get(0).toString().equals("chemical/x-fasta")) {
/*
* If MIME type is recognised as "chemical/x-fasta" only then
* this might be an error from MIME magic (i.e., sometimes it
* recognises stuff that is not "chemical/x-fasta" as
* "chemical/x-fasta" and then Seq Vista renderer is used that
* causes errors) - make sure we also add the renderers for
* "text/plain" and "text/xml" as it is most probably just
* normal xml text and push the "chemical/x-fasta" to the bottom
* of the list.
*/
mimeTypes.add(0, new MimeType("text/plain"));
mimeTypes.add(1, new MimeType("text/xml"));
}
for (MimeType mimeType : mimeTypes) {
List<Renderer> renderersList = rendererRegistry.getRenderersForMimeType(mimeType
.toString());
for (Renderer renderer : renderersList)
if (!recognisedRenderersForMimeType.contains(renderer))
recognisedRenderersForMimeType.add(renderer);
}
// if there are no renderers then force text/plain
if (recognisedRenderersForMimeType.isEmpty())
recognisedRenderersForMimeType = rendererRegistry
.getRenderersForMimeType("text/plain");
/*
* Add all other available renderers that are not recognised to be
* able to handle the MIME type of the result
*/
otherRenderers = new ArrayList<>(rendererRegistry.getRenderers());
otherRenderers.removeAll(recognisedRenderersForMimeType);
mimeList = new String[recognisedRenderersForMimeType.size()
+ otherRenderers.size()];
rendererList = new ArrayList<>();
/*
* First add the ones that can handle the MIME type of the result
* item
*/
for (int i = 0; i < recognisedRenderersForMimeType.size(); i++) {
mimeList[i] = recognisedRenderersForMimeType.get(i).getType();
rendererList.add(recognisedRenderersForMimeType.get(i));
}
// Then add the other renderers just in case
for (int i = 0; i < otherRenderers.size(); i++) {
mimeList[recognisedRenderersForMimeType.size() + i] = otherRenderers.get(i)
.getType();
rendererList.add(otherRenderers.get(i));
}
renderersComboBox.setModel(new DefaultComboBoxModel<String>(mimeList));
if (mimeList.length > 0) {
int index = 0;
// Find the index of the current MIME type for this output port.
for (int i = 0; i < mimeList.length; i++)
if (mimeList[i].equals(lastUsedMIMEtype)) {
index = i;
break;
}
int previousindex = renderersComboBox.getSelectedIndex();
renderersComboBox.setSelectedIndex(index);
/*
* force rendering as setSelectedIndex will not fire an
* itemstatechanged event if previousindex == index and we still
* need render the result as we may have switched from a
* different result item in a result list but the renderer index
* stayed the same
*/
if (previousindex == index)
renderResult(); // draw the rendered result component
}
} else if (DataBundles.isError(path)) {
// Disable refresh button
refreshButton.setEnabled(false);
// Hide wrap text check box - only works for actual data
wrapTextCheckBox.setVisible(false);
// Reset the renderers as we have an error item
recognisedRenderersForMimeType = null;
otherRenderers = null;
DefaultMutableTreeNode root = new DefaultMutableTreeNode("Error Trace");
try {
ErrorDocument errorDocument = DataBundles.getError(path);
try {
buildErrorDocumentTree(root, errorDocument);
} catch (IOException e) {
logger.warn("Error building error document tree", e);
}
} catch (IOException e) {
logger.warn("Error getting the error document", e);
}
JTree errorTree = new JTree(root);
errorTree.setCellRenderer(new DefaultTreeCellRenderer() {
@Override
public Component getTreeCellRendererComponent(JTree tree,
Object value, boolean selected, boolean expanded,
boolean leaf, int row, boolean hasFocus) {
Component renderer = null;
if (value instanceof DefaultMutableTreeNode) {
DefaultMutableTreeNode treeNode = (DefaultMutableTreeNode) value;
Object userObject = treeNode.getUserObject();
if (userObject instanceof ErrorDocument)
renderer = renderErrorDocument(tree, selected,
expanded, leaf, row, hasFocus,
(ErrorDocument) userObject);
}
if (renderer == null)
renderer = super.getTreeCellRendererComponent(tree,
value, selected, expanded, leaf, row, hasFocus);
if (renderer instanceof JLabel) {
JLabel label = (JLabel) renderer;
label.setIcon(null);
}
return renderer;
}
private Component renderErrorDocument(JTree tree,
boolean selected, boolean expanded, boolean leaf,
int row, boolean hasFocus, ErrorDocument errorDocument) {
return super.getTreeCellRendererComponent(tree, "<html>"
+ escapeHtml(errorDocument.getMessage())
+ "</html>", selected, expanded, leaf, row,
hasFocus);
}
});
renderersComboBox.setModel(new DefaultComboBoxModel<>(
new String[] { ERROR_DOCUMENT }));
renderedResultPanel.removeAll();
renderedResultPanel.add(errorTree, CENTER);
repaint();
}
}
public void buildErrorDocumentTree(DefaultMutableTreeNode node,
ErrorDocument errorDocument) throws IOException {
DefaultMutableTreeNode child = new DefaultMutableTreeNode(errorDocument);
String trace = errorDocument.getTrace();
if (trace != null && !trace.isEmpty())
for (String line : trace.split("\n"))
child.add(new DefaultMutableTreeNode(line));
node.add(child);
List<Path> causes = errorDocument.getCausedBy();
for (Path cause : causes)
if (DataBundles.isError(cause)) {
ErrorDocument causeErrorDocument = DataBundles.getError(cause);
if (causes.size() == 1)
buildErrorDocumentTree(node, causeErrorDocument);
else
buildErrorDocumentTree(child, causeErrorDocument);
} else if (DataBundles.isList(cause)) {
List<ErrorDocument> errorDocuments = getErrorDocuments(cause);
if (errorDocuments.size() == 1)
buildErrorDocumentTree(node, errorDocuments.get(0));
else
for (ErrorDocument errorDocument2 : errorDocuments)
buildErrorDocumentTree(child, errorDocument2);
}
}
public List<ErrorDocument> getErrorDocuments(Path reference)
throws IOException {
List<ErrorDocument> errorDocuments = new ArrayList<>();
if (DataBundles.isError(reference))
errorDocuments.add(DataBundles.getError(reference));
else if (DataBundles.isList(reference))
for (Path element : DataBundles.getList(reference))
errorDocuments.addAll(getErrorDocuments(element));
return errorDocuments;
}
/**
* Renders the result panel using the last used renderer.
*/
public void renderResult() {
if (ERROR_DOCUMENT.equals(renderersComboBox.getSelectedItem()))
// skip error documents - do not (re)render
return;
int selectedIndex = renderersComboBox.getSelectedIndex();
if (mimeList != null && selectedIndex >= 0) {
Renderer renderer = rendererList.get(selectedIndex);
if (renderer.getType().equals("Text")) { // if the result is "text/plain"
/*
* We use node's hash code as the key in the nodeToWrapCheckBox
* map as node's user object may be too large
*/
if (nodeToWrapSelection.get(path) == null)
// initially not selected
nodeToWrapSelection.put(path, false);
wrapTextCheckBox.setSelected(nodeToWrapSelection.get(path));
wrapTextCheckBox.setVisible(true);
} else
wrapTextCheckBox.setVisible(false);
/*
* Remember the last used renderer - use it for all result items of
* this port
*/
// currentRendererIndex = selectedIndex;
lastUsedMIMEtype = mimeList[selectedIndex];
JComponent component = null;
try {
component = renderer.getComponent(path);
if (component instanceof DialogTextArea)
if (wrapTextCheckBox.isSelected())
((JTextArea) component).setLineWrap(wrapTextCheckBox.isSelected());
if (component instanceof JTextComponent)
((JTextComponent) component).setEditable(false);
else if (component instanceof JTree)
((JTree) component).setEditable(false);
} catch (RendererException e1) {
// maybe this should be Exception
/*
* show the user that something unexpected has happened but
* continue
*/
component = new DialogTextArea(
"Could not render using renderer type "
+ renderer.getClass()
+ "\n"
+ "Please try with a different renderer if available and consult log for details of problem");
((DialogTextArea) component).setEditable(false);
logger.warn("Couln not render using " + renderer.getClass(), e1);
}
renderedResultPanel.removeAll();
renderedResultPanel.add(component, CENTER);
repaint();
revalidate();
}
}
/**
* Clears the result panel.
*/
public void clearResult() {
refreshButton.setEnabled(false);
wrapTextCheckBox.setVisible(false);
renderedResultPanel.removeAll();
// Update the 'save result' buttons appropriately
for (int i = 0; i < saveButtonsPanel.getComponents().length; i++) {
JButton saveButton = (JButton) saveButtonsPanel.getComponent(i);
SaveIndividualResultSPI action = (SaveIndividualResultSPI) saveButton
.getAction();
// Update the action
action.setResultReference(null);
saveButton.setEnabled(false);
}
renderersComboBox.setModel(new DefaultComboBoxModel<String>());
renderersComboBox.setEnabled(false);
revalidate();
repaint();
}
class ColorCellRenderer implements ListCellRenderer<String> {
protected DefaultListCellRenderer defaultRenderer = new DefaultListCellRenderer();
@Override
public Component getListCellRendererComponent(
JList<? extends String> list, String value, int index,
boolean isSelected, boolean cellHasFocus) {
JComponent renderer = (JComponent) defaultRenderer
.getListCellRendererComponent(list, value, index,
isSelected, cellHasFocus);
if (recognisedRenderersForMimeType == null) // error occurred
return renderer;
if (value != null && index >= recognisedRenderersForMimeType.size())
// one of the non-preferred renderers - show it in grey
renderer.setForeground(GRAY);
return renderer;
}
}
}