| /******************************************************************************* |
| * 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 org.apache.taverna.databundle.DataBundles; |
| import org.apache.taverna.databundle.ErrorDocument; |
| import org.apache.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; |
| } |
| } |
| } |