http://git-wip-us.apache.org/repos/asf/incubator-taverna-workbench-common-activities/blob/163747de/taverna-xpath-activity-ui/src/main/java/org/apache/taverna/activities/xpath/ui/config/XPathActivityConfigurationPanel.java ---------------------------------------------------------------------- diff --git a/taverna-xpath-activity-ui/src/main/java/org/apache/taverna/activities/xpath/ui/config/XPathActivityConfigurationPanel.java b/taverna-xpath-activity-ui/src/main/java/org/apache/taverna/activities/xpath/ui/config/XPathActivityConfigurationPanel.java new file mode 100644 index 0000000..efe7fbc --- /dev/null +++ b/taverna-xpath-activity-ui/src/main/java/org/apache/taverna/activities/xpath/ui/config/XPathActivityConfigurationPanel.java @@ -0,0 +1,1202 @@ +package org.apache.taverna.activities.xpath.ui.config; + +import java.awt.AWTEvent; +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Component; +import java.awt.Cursor; +import java.awt.Dimension; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.GridLayout; +import java.awt.Insets; +import java.awt.Point; +import java.awt.Toolkit; +import java.awt.event.AWTEventListener; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.FocusEvent; +import java.awt.event.FocusListener; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.geom.Area; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.prefs.Preferences; + +import javax.swing.AbstractAction; +import javax.swing.BorderFactory; +import javax.swing.BoxLayout; +import javax.swing.JButton; +import javax.swing.JCheckBoxMenuItem; +import javax.swing.JFileChooser; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JSeparator; +import javax.swing.JTabbedPane; +import javax.swing.JTable; +import javax.swing.JTextArea; +import javax.swing.JTextField; +import javax.swing.JToggleButton; +import javax.swing.ListSelectionModel; +import javax.swing.Popup; +import javax.swing.PopupFactory; +import javax.swing.SwingUtilities; +import javax.swing.Timer; +import javax.swing.event.CaretEvent; +import javax.swing.event.CaretListener; +import javax.swing.filechooser.FileFilter; +import javax.swing.table.DefaultTableModel; + +import org.apache.taverna.activities.xpath.XPathActivityConfigurationBean; +import org.apache.taverna.activities.xpath.ui.config.xmltree.TableCellListener; +import org.apache.taverna.activities.xpath.ui.config.xmltree.XPathActivityXMLTree; +import org.apache.taverna.activities.xpath.ui.servicedescription.XPathActivityIcon; +import org.apache.taverna.workbench.icons.WorkbenchIcons; + +import org.apache.log4j.Logger; +import org.dom4j.Document; +import org.dom4j.DocumentException; +import org.dom4j.DocumentHelper; +import org.dom4j.InvalidXPathException; +import org.dom4j.Node; +import org.dom4j.XPath; +import org.dom4j.XPathException; + +/** + * @author Sergejs Aleksejevs + */ +@SuppressWarnings("serial") +public class XPathActivityConfigurationPanel extends JPanel { + + private Logger logger = Logger.getLogger(XPathActivityConfigurationPanel.class); + + // --- CONSTANTS --- + public static final int MAX_NUMBER_OF_MATCHING_NODES_TO_HIGHLIGHT_IN_THE_TREE = 100; + + private static final Color INACTIVE_PANEL_BACKGROUND_COLOR = new Color(215, + 215, 215); + + private static final String EXAMPLE_XML_PROMPT = "Paste example XML here..."; + + private static final String XPATH_XML_DOCUMENT_DIR_PROPERTY="XPathXMLDocumentDir"; + + private XPathActivityConfigurationPanel thisPanel; + + // --- COMPONENTS FOR ACTIVITY CONFIGURATION PANEL --- + private JPanel jpActivityConfiguration; + + private JPanel jpLeft; + private JPanel jpRight; + + private JToggleButton bShowXMLTreeSettings; + private Popup xmlTreeSettingsMenu; + private long xmlTreeSettingsMenuLastShownAt; + private JButton bGenerateXPathExpression; + private JPanel jpXMLTreeSettingsMenuContents; + private JCheckBoxMenuItem miIncludeAttributes; + private JCheckBoxMenuItem miIncludeValues; + private JCheckBoxMenuItem miIncludeNamespaces; + + private JTextArea taSourceXML; + private JButton bLoadXMLDocument; + private JButton bParseXML; + private XPathActivityXMLTree xmlTree; + private JScrollPane spXMLTreePlaceholder; + + // --- COMPONENTS FOR XPATH EDITING PANEL --- + private JLabel jlXPathExpressionStatus; + private JLabel jlXPathExpression; + private JTextField tfXPathExpression; + private Map<String, String> xpathNamespaceMap; + private JButton bRunXPath; + + private JLabel jlShowHideNamespaceMappings; + private JTable jtXPathNamespaceMappings; + private JButton bAddMapping; + private JButton bRemoveMapping; + private JPanel jpNamespaceMappingsWithButton; + + // --- COMPONENTS FOR XPATH TESTING PANEL --- + private JPanel jpXPathTesting; + + private JTextField tfExecutedXPathExpression; + private JTextField tfMatchingElementCount; + + private JTabbedPane tpExecutedXPathExpressionResults; + private JTextArea taExecutedXPathExpressionResultsAsText; + private JScrollPane spExecutedXPathExpressionResultsAsText; + private JTextArea taExecutedXPathExpressionResultsAsXML; + private JScrollPane spExecutedXPathExpressionResultsAsXML; + + public XPathActivityConfigurationPanel() { + this.thisPanel = this; + + this.setLayout(new GridBagLayout()); + GridBagConstraints c = new GridBagConstraints(); + + c.gridx = 0; + c.gridy = 0; + c.fill = GridBagConstraints.BOTH; + c.weightx = 1.0; + c.weighty = 0.50; + c.insets = new Insets(0, 10, 10, 10); + this.jpActivityConfiguration = createActivityConfigurationPanel(); + this.add(this.jpActivityConfiguration, c); + + c.gridy++; + c.fill = GridBagConstraints.HORIZONTAL; + c.weighty = 0; + c.insets = new Insets(0, 10, 0, 10); + this.add(new JSeparator(), c); + + // XPath expression editing panel + c.gridy++; + c.fill = GridBagConstraints.BOTH; + c.weighty = 0.05; + c.insets = new Insets(5, 10, 5, 10); + this.add(createXPathExpressionEditingPanel(), c); + + c.gridy++; + ; + c.fill = GridBagConstraints.HORIZONTAL; + c.weighty = 0; + c.insets = new Insets(0, 10, 0, 10); + this.add(new JSeparator(), c); + + // XPath expression testing panel + c.gridy++; + c.fill = GridBagConstraints.BOTH; + c.weighty = 0.35; + c.insets = new Insets(5, 10, 0, 10); + this.jpXPathTesting = createXPathExpressionTestingPanel(); + this.add(this.jpXPathTesting, c); + } + + private JPanel createActivityConfigurationPanel() { + JPanel jpConfig = new JPanel(new GridBagLayout()); + GridBagConstraints c = new GridBagConstraints(); + + // text area for example XML document + c.gridx = 0; + c.gridy = 0; + c.fill = GridBagConstraints.BOTH; + c.weightx = 0.5; + c.weighty = 1.0; + c.insets = new Insets(5, 0, 0, 5); + taSourceXML = new JTextArea(10, 30); + taSourceXML + .setToolTipText("<html>Use this text area to paste or load an example XML document.<br>" + + "This document can then be parsed by clicking the button<br>" + + "with a green arrow in order to see its tree structure.</html>"); + taSourceXML.setText(EXAMPLE_XML_PROMPT); + taSourceXML.addFocusListener(new FocusListener() { + public void focusGained(FocusEvent e) { + taSourceXML.selectAll(); + } + + public void focusLost(FocusEvent e) { /* do nothing */ + } + }); + taSourceXML.addCaretListener(new CaretListener() { + public void caretUpdate(CaretEvent e) { + // make sure that it is only allowed to "parse example XML" + // when something is actually present in the text area + bParseXML.setEnabled(taSourceXML.getText().trim().length() > 0 + && !taSourceXML.getText().trim().equals( + EXAMPLE_XML_PROMPT)); + } + }); + jpLeft = new JPanel(new GridLayout(1, 1)); + jpLeft.add(new JScrollPane(taSourceXML)); + jpConfig.add(jpLeft, c); + + // button to parse example XML document + + c.gridx++; + c.fill = GridBagConstraints.NONE; + c.weightx = 0; + c.weighty = 0; + c.insets = new Insets(0, 0, 0, 0); + bParseXML = new JButton( + XPathActivityIcon + .getIconById(XPathActivityIcon.XPATH_ACTIVITY_CONFIGURATION_PARSE_XML_ICON)); + bParseXML + .setToolTipText("Parse example XML document and generate its tree structure"); + bParseXML.setEnabled(false); + bParseXML.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + parseXML(); + } + }); + jpConfig.add(bParseXML, c); + + // placeholder for XML tree (will be replaced by a real tree when the + // parsing is done) + + c.gridx++; + c.fill = GridBagConstraints.BOTH; + c.weightx = 0.5; + c.weighty = 1.0; + c.insets = new Insets(5, 5, 0, 0); + JTextArea taXMLTreePlaceholder = new JTextArea(10, 30); + taXMLTreePlaceholder + .setToolTipText("<html>This area will show tree structure of the example XML after you<br>" + + "paste it into the space on the left-hand side and press 'Parse'<br>" + + "button with the green arrow.</html>"); + taXMLTreePlaceholder.setEditable(false); + taXMLTreePlaceholder.setBackground(INACTIVE_PANEL_BACKGROUND_COLOR); + spXMLTreePlaceholder = new JScrollPane(taXMLTreePlaceholder); + jpRight = new JPanel(new GridLayout(1, 1)); + jpRight.add(spXMLTreePlaceholder); + jpConfig.add(jpRight, c); + + // Button to load XML document from a file + + bLoadXMLDocument = new JButton("Load XML from file", WorkbenchIcons.openIcon); + bLoadXMLDocument.addActionListener(new ActionListener() { + + @Override + public void actionPerformed(ActionEvent e) { + JFileChooser fileChooser = new JFileChooser(); + Preferences prefs = Preferences.userNodeForPackage(getClass()); + String curDir = prefs.get(XPATH_XML_DOCUMENT_DIR_PROPERTY, System.getProperty("user.home")); + fileChooser.setDialogTitle("Select file to load XML from"); + fileChooser.setFileFilter(new FileFilter() { + public boolean accept(File f) { + return f.isDirectory() || f.getName().toLowerCase().endsWith(".xml"); + } + + public String getDescription() { + return ".xml files"; + } + }); + fileChooser.setCurrentDirectory(new File(curDir)); + int returnVal = fileChooser.showOpenDialog(((JButton) e + .getSource()).getParent()); + if (returnVal == JFileChooser.APPROVE_OPTION) { + prefs.put(XPATH_XML_DOCUMENT_DIR_PROPERTY, fileChooser + .getCurrentDirectory().toString()); + File file = fileChooser.getSelectedFile(); + // Read the contents of a file into a string + // and set the value of the XML document text area to it + FileInputStream fis = null; + try{ + byte[] fileBytes = new byte[(int)file.length()]; + fis = new FileInputStream(file); + fis.read(fileBytes); + String xmlDocument = new String(fileBytes, "UTF-8"); + setSourceXML(xmlDocument); + } + catch(Exception ex){ + logger.error("An error occured while trying to read the XML document from file " + file.getAbsolutePath(), ex); + JOptionPane.showMessageDialog( + ((JButton) e.getSource()).getParent(), + "There was an error while trying to read the file", + "XPath Activity", + JOptionPane.ERROR_MESSAGE); + } + finally{ + try { + fis.close(); + } catch (IOException e1) { + // Ignore + } + } + + } + } + }); + c.gridx = 0; + c.gridy++; + c.gridwidth = 1; + c.fill = GridBagConstraints.NONE; + c.weightx = 0; + c.weighty = 0; + c.insets = new Insets(5, 0, 0, 5); + c.anchor = GridBagConstraints.EAST; + jpConfig.add(bLoadXMLDocument, c); + + // settings for the view of XML tree from example XML document + + miIncludeAttributes = new JCheckBoxMenuItem("Show XML node attributes"); + miIncludeAttributes.setSelected(true); + miIncludeAttributes.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + refreshXMLTreeUI(); + } + }); + + miIncludeValues = new JCheckBoxMenuItem( + "Show values of XML elements and attributes"); + miIncludeValues.setSelected(true); + miIncludeValues.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + refreshXMLTreeUI(); + } + }); + + miIncludeNamespaces = new JCheckBoxMenuItem( + "Show namespaces of XML elements"); + miIncludeNamespaces.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + refreshXMLTreeUI(); + } + }); + + jpXMLTreeSettingsMenuContents = new JPanel(); + jpXMLTreeSettingsMenuContents.setBorder(BorderFactory + .createRaisedBevelBorder()); + jpXMLTreeSettingsMenuContents.setLayout(new BoxLayout( + jpXMLTreeSettingsMenuContents, BoxLayout.Y_AXIS)); + jpXMLTreeSettingsMenuContents.add(miIncludeAttributes); + jpXMLTreeSettingsMenuContents.add(miIncludeValues); + jpXMLTreeSettingsMenuContents.add(miIncludeNamespaces); + + bShowXMLTreeSettings = new JToggleButton("Show XML tree settings...", + XPathActivityIcon.getIconById(XPathActivityIcon.UNFOLD_ICON)); + bShowXMLTreeSettings.setSelectedIcon(XPathActivityIcon + .getIconById(XPathActivityIcon.FOLD_ICON)); + bShowXMLTreeSettings.setEnabled(false); + bShowXMLTreeSettings.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + if (xmlTreeSettingsMenu == null) { + xmlTreeSettingsMenuLastShownAt = System.currentTimeMillis(); + + Point parentPosition = bShowXMLTreeSettings + .getLocationOnScreen(); + xmlTreeSettingsMenu = PopupFactory.getSharedInstance() + .getPopup( + bShowXMLTreeSettings, + jpXMLTreeSettingsMenuContents, + parentPosition.x, + parentPosition.y + + bShowXMLTreeSettings.getHeight()); + xmlTreeSettingsMenu.show(); + } else { + bShowXMLTreeSettings.setSelected(false); + } + } + }); + + bGenerateXPathExpression = new JButton("Generate XPath expression", + XPathActivityIcon + .getIconById(XPathActivityIcon.XML_TREE_NODE_ICON)); + bGenerateXPathExpression.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + updateXPathEditingPanelValues(); + } + }); + + JPanel xmlTreeButtonPanel = new JPanel(); + xmlTreeButtonPanel.add(bGenerateXPathExpression); + xmlTreeButtonPanel.add(bShowXMLTreeSettings); + + c.gridx = 2; + c.gridwidth = 1; + c.fill = GridBagConstraints.NONE; + c.weightx = 0; + c.weighty = 0; + c.insets = new Insets(5, 0, 0, 0); + c.anchor = GridBagConstraints.EAST; + jpConfig.add(xmlTreeButtonPanel, c); + + // register a new listener for all AWT mouse events - this will be used + // to identify clicks outside of the XML tree popup menu and the toggle + // button used to show/hide it + Toolkit.getDefaultToolkit().addAWTEventListener(new AWTEventListener() { + public void eventDispatched(AWTEvent event) { + if (event instanceof MouseEvent && xmlTreeSettingsMenu != null) { + MouseEvent e = (MouseEvent) event; + if (e.getClickCount() > 0 + && (e.getWhen() - xmlTreeSettingsMenuLastShownAt) > 100) { + // convert a point where mouse click was made from + // relative coordinates of the source component + // to the coordinates of the panel that represents the + // contents of the popup menu + Point clickRelativeToOverlay = SwingUtilities + .convertPoint((Component) e.getSource(), e + .getPoint(), + jpXMLTreeSettingsMenuContents); + + Area areaOfPopupPanelAndToggleButton = new Area( + jpXMLTreeSettingsMenuContents.getBounds()); + + // only hide the popup menu if a click was made outside + // of the calculated area -- + // plus not on one of the associated toggle buttons + if (!areaOfPopupPanelAndToggleButton + .contains(clickRelativeToOverlay)) { + xmlTreeSettingsMenu.hide(); + bShowXMLTreeSettings.setSelected(false); + + // if the popup menu was dismissed by a click on the + // toggle button that + // has made it visible, this timer makes sure that + // this click doesn't + // re-show the popup menu + new Timer(100, new ActionListener() { + public void actionPerformed(ActionEvent e) { + ((Timer) e.getSource()).stop(); + xmlTreeSettingsMenu = null; + } + }).start(); + + } + } + } + } + }, AWTEvent.MOUSE_EVENT_MASK); + + return (jpConfig); + } + + private JPanel createXPathExpressionEditingPanel() { + this.jlXPathExpressionStatus = new JLabel(); + + this.jlXPathExpression = new JLabel("XPath expression"); + + this.bRunXPath = new JButton("Run XPath"); + this.bRunXPath.setEnabled(false); + this.bRunXPath.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + runXPath(true); + } + }); + + this.tfXPathExpression = new JTextField(30); + this.tfXPathExpression.setPreferredSize(new Dimension(0, this.bRunXPath + .getPreferredSize().height)); + this.tfXPathExpression.setMinimumSize(new Dimension(0, this.bRunXPath + .getPreferredSize().height)); + this.tfXPathExpression.addCaretListener(new CaretListener() { + public void caretUpdate(CaretEvent e) { + validateXPathAndUpdateUI(); + } + }); + this.tfXPathExpression.addKeyListener(new KeyListener() { + public void keyPressed(KeyEvent e) { + if (e.getKeyCode() == KeyEvent.VK_ENTER) { + if (bRunXPath.isEnabled()) { + // it is safe to check that ENTER key may execute the + // XPath expression if the + // "Run XPath" button is enabled, as expression + // validation is responsible for + // enabling / disabling the button as the expression + // changes + runXPath(true); + } + } + } + + public void keyReleased(KeyEvent e) { /* not in use */ + } + + public void keyTyped(KeyEvent e) { /* not in use */ + } + }); + + JPanel jpXPath = new JPanel(new GridBagLayout()); + GridBagConstraints c = new GridBagConstraints(); + c.fill = GridBagConstraints.HORIZONTAL; + c.weighty = 0; + + c.gridx = 0; + c.gridy = 0; + c.weightx = 0; + jpXPath.add(jlXPathExpressionStatus); + + c.gridx++; + c.weightx = 0.0; + c.insets = new Insets(0, 10, 0, 0); + jpXPath.add(jlXPathExpression, c); + + c.gridx++; + c.weightx = 1.0; + c.insets = new Insets(0, 10, 0, 10); + jpXPath.add(tfXPathExpression, c); + + c.gridx++; + c.weightx = 0; + c.insets = new Insets(0, 0, 0, 0); + jpXPath.add(bRunXPath, c); + + c.gridx = 2; + c.gridy++; + c.weightx = 1.0; + c.weighty = 0; + c.gridwidth = 2; + c.fill = GridBagConstraints.NONE; + c.anchor = GridBagConstraints.WEST; + c.insets = new Insets(0, 10, 0, 10); + jlShowHideNamespaceMappings = new JLabel("Show namespace mappings..."); + jlShowHideNamespaceMappings.setForeground(Color.BLUE); + jlShowHideNamespaceMappings.setCursor(new Cursor(Cursor.HAND_CURSOR)); + jlShowHideNamespaceMappings.addMouseListener(new MouseAdapter() { + public void mouseClicked(MouseEvent e) { + jpNamespaceMappingsWithButton + .setVisible(!jpNamespaceMappingsWithButton.isVisible()); + jlShowHideNamespaceMappings + .setText((jpNamespaceMappingsWithButton.isVisible() ? "Hide" + : "Show") + + " namespace mappings..."); + thisPanel.validate(); + } + }); + jpXPath.add(jlShowHideNamespaceMappings, c); + + // namespace mapping table + DefaultTableModel tableModel = new DefaultTableModel(); + tableModel.addColumn("Namespace Prefix"); + tableModel.addColumn("Namespace URI"); + + jtXPathNamespaceMappings = new JTable(); + jtXPathNamespaceMappings.setModel(tableModel); + // ((DefaultCellEditor)jtXPathNamespaceMappings.getDefaultEditor(String.class)).setClickCountToStart(1); + // // TODO - enable if one-click-to-start-editing behaviour is required + // TODO - next line is to be enabled when Taverna is migrated to Java + // 1.6; for now it's fine to run without this + // jtXPathNamespaceMappings.setFillsViewportHeight(true); // makes sure + // that when the dedicated area is larger than the table, the latter is + // stretched vertically to fill the empty space + jtXPathNamespaceMappings + .setSelectionMode(ListSelectionModel.SINGLE_SELECTION); // only one row can be selected at a time + jtXPathNamespaceMappings + .setPreferredScrollableViewportSize(new Dimension(200, 50)); // NB! this prevents the table from occupying most of the space in the panel when screen is maximized + jtXPathNamespaceMappings.addKeyListener(new KeyAdapter() { + public void keyReleased(KeyEvent e) { + if (e.getKeyCode() == KeyEvent.VK_DELETE) { + removeNamespaceMapping(); + } + } + }); + + TableCellListener cellListener = new TableCellListener( + jtXPathNamespaceMappings, new AbstractAction() { + public void actionPerformed(ActionEvent e) { + TableCellListener tcl = (TableCellListener) e + .getSource(); + + if (tcl.getColumn() == 0) { + // prefix was modified + String newPrefix = (String) tcl.getNewValue(); + if (xpathNamespaceMap.containsKey(newPrefix)) { + // such prefix already exists - change won't be + // saved + JOptionPane + .showMessageDialog( + thisPanel, + "Cannot update namespace prefix: " + + "updated value already exists", + "XPath Activity", + JOptionPane.WARNING_MESSAGE); + } else { + // update the map with the new prefix for the + // same URI value + String oldPrefix = (String) tcl.getOldValue(); + xpathNamespaceMap.put(newPrefix, + xpathNamespaceMap.remove(oldPrefix)); + } + } else { + // simple case - just the URI value has changed: + // just overwrite the value in the namespace map + String prefixOfUpdatedURI = (String) jtXPathNamespaceMappings + .getModel().getValueAt(tcl.getRow(), 0); + xpathNamespaceMap.put(prefixOfUpdatedURI, + (String) tcl.getNewValue()); + } + + // either way - reload from the local map (map could be + // not updated if the validation didn't succeed) + reloadNamespaceMappingTableFromLocalMap(); + } + }); + + jtXPathNamespaceMappings.getColumnModel().getColumn(0) + .setPreferredWidth(20); // set relative sizes of columns + jtXPathNamespaceMappings.getColumnModel().getColumn(1) + .setPreferredWidth(300); + + JScrollPane spXPathNamespaceMappings = new JScrollPane( + jtXPathNamespaceMappings); + spXPathNamespaceMappings.setAlignmentY(TOP_ALIGNMENT); + spXPathNamespaceMappings.setMinimumSize(new Dimension(200, 50)); // makes the table to have at least two rows visible in all cases - no matter how small the parent panel is + + bAddMapping = new JButton("Add Mapping"); + bAddMapping.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + addNamespaceMapping(); + } + }); + + bRemoveMapping = new JButton("Remove Mapping"); + bRemoveMapping.setEnabled(false); + bRemoveMapping.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + removeNamespaceMapping(); + } + }); + + bAddMapping.setMinimumSize(bRemoveMapping.getPreferredSize()); // make sure that the 'Add Mapping' button is of the same size as 'Remove Mapping' + + bAddMapping.setPreferredSize(bRemoveMapping.getPreferredSize()); // -- both are required to achieve desired behaviour when window is resized / namespace mapping table is enabled/disabled + + bRunXPath.setMinimumSize(bRemoveMapping.getPreferredSize()); // do the same for 'Run XPath' button + + bRunXPath.setPreferredSize(bRemoveMapping.getPreferredSize()); + + JPanel jpAddRemoveButtons = new JPanel(); + jpAddRemoveButtons.setLayout(new GridBagLayout()); + GridBagConstraints cAddRemove = new GridBagConstraints(); + cAddRemove.gridx = 0; + cAddRemove.gridy = 0; + cAddRemove.weightx = 1.0; + cAddRemove.anchor = GridBagConstraints.NORTH; + cAddRemove.fill = GridBagConstraints.HORIZONTAL; + jpAddRemoveButtons.add(bAddMapping, cAddRemove); + cAddRemove.gridy++; + cAddRemove.weighty = 1.0; + cAddRemove.insets = new Insets(2, 0, 0, 0); + jpAddRemoveButtons.add(bRemoveMapping, cAddRemove); + + jpNamespaceMappingsWithButton = new JPanel(); + jpNamespaceMappingsWithButton.setVisible(false); + jpNamespaceMappingsWithButton.setLayout(new BorderLayout(10, 0)); + jpNamespaceMappingsWithButton.add(spXPathNamespaceMappings, + BorderLayout.CENTER); + jpNamespaceMappingsWithButton + .add(jpAddRemoveButtons, BorderLayout.EAST); + + c.gridx = 0; + c.gridy++; + c.gridwidth = 4; + c.fill = GridBagConstraints.BOTH; + c.weightx = 1.0; + c.weighty = 1.0; + c.insets = new Insets(5, 0, 0, 0); + jpXPath.add(jpNamespaceMappingsWithButton, c); + + // initialise some values / tooltips + resetXPathEditingPanel(); + + return (jpXPath); + } + + protected void addNamespaceMapping() { + TwoFieldQueryPanel queryPanel = new TwoFieldQueryPanel( + "Namespace prefix:", "Namespace URI:"); + int result = JOptionPane.showConfirmDialog(this, queryPanel, + "XPath Activity - Create new namespace mapping", + JOptionPane.OK_CANCEL_OPTION); + + if (result == JOptionPane.OK_OPTION) { + boolean bInvalidMapping = true; + do { + bInvalidMapping = queryPanel.getFirstValue().length() == 0 + || queryPanel.getSecondValue().length() == 0 + || xpathNamespaceMap.containsKey(queryPanel + .getFirstValue()); + + if (bInvalidMapping) { + queryPanel = new TwoFieldQueryPanel( + "<html><center><font color=\"red\">ERROR: you must " + + "enter values for both namespace prefix and URI. Prefix must be<br>" + + "unique in the mapping table - duplicates are not allowed!</font></center></html>", + "Namespace prefix:", queryPanel.getFirstValue(), + "Namespace URI:", queryPanel.getSecondValue()); + result = JOptionPane.showConfirmDialog(this, queryPanel, + "XPath Activity - Create new namespace mapping", + JOptionPane.OK_CANCEL_OPTION); + } + } while (bInvalidMapping && result == JOptionPane.OK_OPTION); + + if (result == JOptionPane.OK_OPTION && !bInvalidMapping) { + // the value appears to be valid and OK was pressed - create new + // mapping + this.xpathNamespaceMap.put(queryPanel.getFirstValue(), + queryPanel.getSecondValue()); + reloadNamespaceMappingTableFromLocalMap(); + } + } + } + + protected void removeNamespaceMapping() { + int selectedRow = jtXPathNamespaceMappings.getSelectedRow(); + if (selectedRow != -1) { + // some row is selected - need to delete it and refresh table's UI + // (but first stop editing to avoid + // problems with cell editor trying to store an edited value after + // edited row has been deleted) + if (jtXPathNamespaceMappings.getCellEditor() != null) { + jtXPathNamespaceMappings.getCellEditor().stopCellEditing(); + } + xpathNamespaceMap.remove(jtXPathNamespaceMappings.getValueAt( + selectedRow, 0)); + reloadNamespaceMappingTableFromLocalMap(); + + // select another row in the table + int rowCount = jtXPathNamespaceMappings.getRowCount(); + if (rowCount > 0) { + if (selectedRow < jtXPathNamespaceMappings.getRowCount()) { + // select the row that followed the one that was deleted + jtXPathNamespaceMappings.getSelectionModel() + .setSelectionInterval(selectedRow, selectedRow); + } else { + // last row in the table was deleted - select the one that + // is the new last row + jtXPathNamespaceMappings.getSelectionModel() + .setSelectionInterval(rowCount - 1, rowCount - 1); + } + } + } else { + JOptionPane.showMessageDialog(thisPanel, + "Please select a mapping to delete in the table first!", + "XPath Activity", JOptionPane.WARNING_MESSAGE); + } + } + + private JPanel createXPathExpressionTestingPanel() { + JPanel jpTesting = new JPanel(new GridBagLayout()); + GridBagConstraints c = new GridBagConstraints(); + + c.gridx = 0; + c.gridy = 0; + c.gridwidth = 1; + c.anchor = GridBagConstraints.WEST; + c.fill = GridBagConstraints.NONE; + c.weightx = 0; + c.weighty = 0; + c.insets = new Insets(0, 0, 10, 10); + jpTesting.add(new JLabel("Executed XPath expression:"), c); + + c.gridx++; + c.fill = GridBagConstraints.HORIZONTAL; + c.weightx = 1.0; + c.weighty = 0; + c.insets = new Insets(0, 0, 10, 10); + tfExecutedXPathExpression = new JTextField(); + tfExecutedXPathExpression.setEditable(false); + tfExecutedXPathExpression.setBorder(null); + jpTesting.add(tfExecutedXPathExpression, c); + + c.gridx = 0; + c.gridy++; + c.fill = GridBagConstraints.NONE; + c.weightx = 0; + c.weighty = 0; + c.insets = new Insets(0, 0, 5, 10); + jpTesting.add(new JLabel("Number of matching nodes:"), c); + + c.gridx++; + c.fill = GridBagConstraints.HORIZONTAL; + c.weightx = 1.0; + c.weighty = 0; + c.insets = new Insets(0, 0, 5, 10); + tfMatchingElementCount = new JTextField(); + tfMatchingElementCount.setEditable(false); + tfMatchingElementCount.setBorder(null); + jpTesting.add(tfMatchingElementCount, c); + + c.gridx = 0; + c.gridy++; + c.gridwidth = 2; + c.fill = GridBagConstraints.BOTH; + c.weightx = 1.0; + c.weighty = 1.0; + tpExecutedXPathExpressionResults = new JTabbedPane(); + jpTesting.add(tpExecutedXPathExpressionResults, c); + + taExecutedXPathExpressionResultsAsText = new JTextArea(); + taExecutedXPathExpressionResultsAsText.setEditable(false); + spExecutedXPathExpressionResultsAsText = new JScrollPane( + taExecutedXPathExpressionResultsAsText); + spExecutedXPathExpressionResultsAsText.setPreferredSize(new Dimension( + 200, 60)); + spExecutedXPathExpressionResultsAsText.setBorder(BorderFactory + .createLineBorder(INACTIVE_PANEL_BACKGROUND_COLOR, 3)); + tpExecutedXPathExpressionResults.add("Results as text", + spExecutedXPathExpressionResultsAsText); + + taExecutedXPathExpressionResultsAsXML = new JTextArea(); + taExecutedXPathExpressionResultsAsXML.setEditable(false); + spExecutedXPathExpressionResultsAsXML = new JScrollPane( + taExecutedXPathExpressionResultsAsXML); + spExecutedXPathExpressionResultsAsXML.setPreferredSize(new Dimension( + 200, 60)); + spExecutedXPathExpressionResultsAsXML.setBorder(BorderFactory + .createLineBorder(INACTIVE_PANEL_BACKGROUND_COLOR, 3)); + tpExecutedXPathExpressionResults.add("Results as XML", + spExecutedXPathExpressionResultsAsXML); + + // initialise some values / tooltips + resetXPathTestingPanel(); + + return (jpTesting); + } + + protected void parseXML() { + String xmlData = taSourceXML.getText(); + + try { + xmlTree = XPathActivityXMLTree.createFromXMLData(xmlData, + miIncludeAttributes.isSelected(), miIncludeValues + .isSelected(), miIncludeNamespaces.isSelected(), + this); + xmlTree + .setToolTipText("<html>This is a tree structure of the XML document that you have pasted.<br><br>" + + "Clicking on the nodes in this tree will automatically generate a<br>" + + "corresponding XPath expression. Multiple <b>identical</b> nodes can<br>" + + "be selected at once - in this case <b>wildcards</b> will be used in the<br>" + + "generated XPath expression to if selected nodes have different<br>" + + "ancestors. Other nodes that match the generated XPath expression<br>" + + "will also be selected in the tree.<br><br>" + + "Contextual menu provides convenience methods for expanding or<br>" + + "collapsing the tree." + "</html>"); + xmlTree.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); + JScrollPane spXMLTree = new JScrollPane(xmlTree); + spXMLTree.setPreferredSize(spXMLTreePlaceholder.getPreferredSize()); + jpRight.removeAll(); + jpRight.add(spXMLTree); + + // all successful - enable options to modify the tree + this.bShowXMLTreeSettings.setEnabled(true); + + // data structures inside the XML tree were reset (as the tree was + // re-created) - + // now reset the UI to the initial state as well + resetXPathEditingPanel(); + resetXPathTestingPanel(); + + // XML tree has pre-populated the namespace map with the namespaces + // specified in the + // root element of the tree - load these values + updateXPathEditingPanelValues(); + + this.validate(); + this.repaint(); + } catch (DocumentException e) { + JOptionPane.showMessageDialog(this, e.getMessage(), + "XPath Activity", JOptionPane.ERROR_MESSAGE); + this.taSourceXML.requestFocusInWindow(); + return; + } + } + + /** + * Makes the {@link XPathActivityXMLTree} to refresh its UI from the + * original XML document that was used to create it in first place. + * + * The reason for using this method is to apply new options to the way the + * tree is rendered - e.g. attributes shown/hidden in the tree, values and + * namespaces shown/hidden, etc. + */ + protected void refreshXMLTreeUI() { + this.xmlTree.refreshFromExistingDocument(this.miIncludeAttributes + .isSelected(), this.miIncludeValues.isSelected(), + this.miIncludeNamespaces.isSelected()); + } + + /** + * Initialises XPath Editing panel: -- resets XPath expression that is being + * shown; -- resets local copy of namespace map; -- resets UI of namespace + * mapping table; + */ + private void resetXPathEditingPanel() { + tfXPathExpression.setText(""); + validateXPathAndUpdateUI(); + + // clear the local copy of namespace map + xpathNamespaceMap = new HashMap<String, String>(); + + // clear the namespace mapping table and reload the data from the map + DefaultTableModel tableModel = (DefaultTableModel) jtXPathNamespaceMappings + .getModel(); + tableModel.getDataVector().removeAllElements(); + } + + /** + * Initialises XPath testing panel which shows results of executing current + * XPath expression against the example XML - the panel is returned to the + * way it looks when it is first loaded. + */ + private void resetXPathTestingPanel() { + this.tfExecutedXPathExpression.setText("--"); + this.tfMatchingElementCount.setText("--"); + + this.taExecutedXPathExpressionResultsAsText.setText(""); + this.taExecutedXPathExpressionResultsAsText + .setBackground(INACTIVE_PANEL_BACKGROUND_COLOR); + + this.taExecutedXPathExpressionResultsAsXML.setText(""); + this.taExecutedXPathExpressionResultsAsXML + .setBackground(INACTIVE_PANEL_BACKGROUND_COLOR); + } + + public void updateXPathEditingPanelValues() { + if (xmlTree.getCurrentXPathExpression() != null) { + tfXPathExpression.setText(xmlTree.getCurrentXPathExpression() + .getText()); + } + + // clear the local copy of namespace map and update it with all values + // from + // the map in XML tree instance (which was apparently just re-generated + // on user request) + xpathNamespaceMap.clear(); + xpathNamespaceMap.putAll(xmlTree.getCurrentXPathNamespaces()); + + // clear the namespace mapping table and reload the data from the map + reloadNamespaceMappingTableFromLocalMap(); + } + + protected void reloadNamespaceMappingTableFromLocalMap() { + // clear the namespace mapping table and reload the data from the map + DefaultTableModel tableModel = (DefaultTableModel) jtXPathNamespaceMappings + .getModel(); + tableModel.getDataVector().removeAllElements(); + for (Map.Entry<String, String> mapping : this.xpathNamespaceMap + .entrySet()) { + tableModel.addRow(new Object[] { mapping.getKey(), + mapping.getValue() }); + } + + bRemoveMapping.setEnabled(this.xpathNamespaceMap.entrySet().size() > 0); + + repaint(); + } + + private String getXPathValidationErrorMessage() { + try { + // try to parse the XPath expression... + DocumentHelper.createXPath(tfXPathExpression.getText().trim()); + // ...success + return (""); + } catch (InvalidXPathException e) { + // ...failed to parse the XPath expression: notify of the error + return (e.getMessage()); + } + } + + /** + * Validates the current XPath expression and updates UI accordingly: -- + * XPath status icon is updated; -- tooltip for the icon explains the + * status; -- 'Run XPath' button is enabled/disabled depending on validity + * of XPath expression and existence of example data in the XML tree + */ + protected void validateXPathAndUpdateUI() { + String candidatePath = tfXPathExpression.getText(); + int xpathStatus = XPathActivityConfigurationBean + .validateXPath(candidatePath); + + switch (xpathStatus) { + case XPathActivityConfigurationBean.XPATH_VALID: + // success: expression is correct + jlXPathExpressionStatus.setIcon(XPathActivityIcon + .getIconById(XPathActivityIcon.XPATH_STATUS_OK_ICON)); + jlXPathExpressionStatus + .setToolTipText("Current XPath expression is well-formed and valid"); + + // could allow to execute against example XML, with only condition: + // XML tree must be populated + // (that is, there should be something to run the expression + // against) + if (xmlTree != null) { + this.bRunXPath.setEnabled(true); + this.bRunXPath + .setToolTipText("<html>Evaluate current XPath expression against the XML document<br>" + + "whose structure is shown in the tree view above.</html>"); + } else { + this.bRunXPath.setEnabled(false); + this.bRunXPath + .setToolTipText("<html>No XML document to evaluate the current XPath expression against.<br><br>" + + "Paste some example XML into the area in the top-left section of the<br>" + + "window, then parse it by clicking on the button with the green arrow<br>" + + "in order to test your XPath expression.</html>"); + } + break; + + case XPathActivityConfigurationBean.XPATH_EMPTY: + // no XPath expression - can't tell if it is correct + nothing to + // execute + jlXPathExpressionStatus.setIcon(XPathActivityIcon + .getIconById(XPathActivityIcon.XPATH_STATUS_UNKNOWN_ICON)); + jlXPathExpressionStatus + .setToolTipText("<html>There is no XPath expression to validate.<br><br>" + + "<b>Hint:</b> select something in the tree view showing the structure<br>" + + "of the XML document that you have pasted (or type the XPath<br>" + + "expression manually).</html>"); + this.bRunXPath.setEnabled(false); + this.bRunXPath.setToolTipText("No XPath expression to execute"); + break; + + case XPathActivityConfigurationBean.XPATH_INVALID: + // failed to parse the XPath expression: notify of the error + jlXPathExpressionStatus.setIcon(XPathActivityIcon + .getIconById(XPathActivityIcon.XPATH_STATUS_ERROR_ICON)); + jlXPathExpressionStatus + .setToolTipText(getXPathValidationErrorMessage()); + + this.bRunXPath.setEnabled(false); + this.bRunXPath + .setToolTipText("Cannot execute invalid XPath expression"); + break; + } + + } + + /** + * Executes the current XPath expression against the current XML tree. + * + * @param displayResults + * <code>true</code> to execute and display results in the XPath + * activity configuration panel (this happens when the 'Run + * XPath' button is clicked);<br/> + * <false> to run the expression quietly and simply return the + * number of matching nodes. + * @return Number of nodes in the XML tree that match the current XPath + * expression. (Or <code>-1</code> if an error has occurred during + * the execution -- error messages will only be shown if + * <code>displayResults == true</code>). + */ + public int runXPath(boolean displayResults) { + // ----- RUNNING THE XPath EXPRESSION ----- + XPath expr = null; + try { + expr = DocumentHelper.createXPath(this.tfXPathExpression.getText()); + expr.setNamespaceURIs(this.xpathNamespaceMap); + } catch (InvalidXPathException e) { + if (displayResults) { + JOptionPane + .showMessageDialog( + thisPanel, + "Incorrect XPath Expression\n\n" + + "Please check the expression if you have manually modified it;\n" + + "Alternatively, try to select another node from the XML tree.\n\n" + + "------------------------------------------------------------------------------------\n\n" + + "XPath processing library reported the following error:\n" + + e.getMessage(), "XPath Activity", + JOptionPane.ERROR_MESSAGE); + } + return (-1); + } + + Document doc = xmlTree.getDocumentUsedToPopulateTree(); + List<Node> matchingNodes = null; + int matchingNodeCount = -1; + try { + matchingNodes = expr.selectNodes(doc); + matchingNodeCount = matchingNodes.size(); + } catch (XPathException e) { + if (displayResults) { + JOptionPane + .showMessageDialog( + thisPanel, + "Unexpected error has occurred while executing the XPath expression.\n\n" + + "If you have manually modified the XPath expression and/or namespace mappings,\n" + + "please check you changes. Alternatively, make your selection in the XML tree and\n" + + "a correct XPath expression with corresponding namespace mapping will be generated.\n\n" + + "-------------------------------------------------------------------------------------------------------------\n\n" + + "XPath processing library reported the following error:\n" + + e.getMessage(), "XPath Activity", + JOptionPane.ERROR_MESSAGE); + } + return (-1); + } + + // ----- DISPLAYING THE RESULTS ----- + if (displayResults) { + tfExecutedXPathExpression.setText(expr.getText()); + tfMatchingElementCount.setText("" + matchingNodeCount); + + StringBuffer outNodesText = new StringBuffer(); + StringBuffer outNodesXML = new StringBuffer(); + for (Node n : matchingNodes) { + if (n.getStringValue() != null + && n.getStringValue().length() > 0) { + outNodesText.append(n.getStringValue() + "\n"); + } + outNodesXML.append(n.asXML() + "\n"); + } + + // tpExecutedXPathExpressionResults.setSelectedIndex(0); // open the + // first tab (should be the one with textual results) // TODO - + // enable if needed + + taExecutedXPathExpressionResultsAsText.setText(outNodesText + .toString()); + taExecutedXPathExpressionResultsAsText.setBackground(Color.WHITE); + taExecutedXPathExpressionResultsAsText.setCaretPosition(0); + spExecutedXPathExpressionResultsAsText.setBorder(BorderFactory + .createLineBorder(Color.WHITE, 3)); + + taExecutedXPathExpressionResultsAsXML.setText(outNodesXML + .toString()); + taExecutedXPathExpressionResultsAsXML.setBackground(Color.WHITE); + taExecutedXPathExpressionResultsAsXML.setCaretPosition(0); + spExecutedXPathExpressionResultsAsXML.setBorder(BorderFactory + .createLineBorder(Color.WHITE, 3)); + } + + return (matchingNodeCount); + } + + protected void setSourceXML(String xmlData) { + this.taSourceXML.setText(xmlData); + } + + protected String getCurrentXPathExpression() { + return (this.tfXPathExpression.getText().trim()); + } + + protected void setCurrentXPathExpression(String xpathExpression) { + this.tfXPathExpression.setText(xpathExpression); + } + + protected Map<String, String> getCurrentXPathNamespaceMap() { + return (this.xpathNamespaceMap); + } + + /** + * This method doesn't simply set a reference to the passed map, but rather + * performs a shallow copy of values. + * + * This is because the method is used during configuration panel's + * initialisation from the values that are held in the configuration bean. + * In case of simple reference assignment, any changes made to map in the + * configuration panel are also taking effect on the same map - referenced + * from the configuration bean, which leads to undesired behaviour. + */ + protected void setCurrentXPathNamespaceMapValues( + Map<String, String> xpathNamespaceMap) { + this.xpathNamespaceMap.clear(); + this.xpathNamespaceMap.putAll(xpathNamespaceMap); + } + + protected XPathActivityXMLTree getCurrentXMLTree() { + return (this.xmlTree); + } + + /** + * For testing + */ + public static void main(String[] args) { + JFrame frame = new JFrame(); + frame.getContentPane().add(new XPathActivityConfigurationPanel()); + frame.pack(); + frame.setSize(new Dimension(900, 600)); + frame.setLocationRelativeTo(null); + frame.setVisible(true); + } + +}
http://git-wip-us.apache.org/repos/asf/incubator-taverna-workbench-common-activities/blob/163747de/taverna-xpath-activity-ui/src/main/java/org/apache/taverna/activities/xpath/ui/config/XPathActivityConfigurationPanelProvider.java ---------------------------------------------------------------------- diff --git a/taverna-xpath-activity-ui/src/main/java/org/apache/taverna/activities/xpath/ui/config/XPathActivityConfigurationPanelProvider.java b/taverna-xpath-activity-ui/src/main/java/org/apache/taverna/activities/xpath/ui/config/XPathActivityConfigurationPanelProvider.java new file mode 100644 index 0000000..c652d98 --- /dev/null +++ b/taverna-xpath-activity-ui/src/main/java/org/apache/taverna/activities/xpath/ui/config/XPathActivityConfigurationPanelProvider.java @@ -0,0 +1,158 @@ +package org.apache.taverna.activities.xpath.ui.config; + +import java.awt.BorderLayout; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import javax.swing.BorderFactory; +import javax.swing.JOptionPane; + +import org.apache.taverna.activities.xpath.XPathActivityConfigurationBean; +import org.apache.taverna.workbench.ui.views.contextualviews.activity.ActivityConfigurationPanel; +import org.apache.taverna.commons.services.ServiceRegistry; +import org.apache.taverna.scufl2.api.activity.Activity; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; + +/** + * + * @author Sergejs Aleksejevs + * @author David Withers + */ +@SuppressWarnings("serial") +public class XPathActivityConfigurationPanelProvider extends ActivityConfigurationPanel { + + private XPathActivityConfigurationPanel configPanel; + private final ServiceRegistry serviceRegistry; + + public XPathActivityConfigurationPanelProvider(Activity activity, ServiceRegistry serviceRegistry) { + super(activity); + this.serviceRegistry = serviceRegistry; + initialise(); + } + + @Override + protected void initialise() { + super.initialise(); + removeAll(); + setLayout(new BorderLayout()); + + // create actual contents of the config panel + this.configPanel = new XPathActivityConfigurationPanel(); + add(configPanel, BorderLayout.CENTER); + + // place the whole configuration panel into a raised area, so that + // automatically added 'Apply' / 'Close' buttons visually apply to + // the whole of the panel, not just part of it + this.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createEmptyBorder(12, 12, 2, 12), + BorderFactory.createRaisedBevelBorder())); + + // Populate fields from activity configuration bean + refreshConfiguration(); + } + + /** + * Prepare a new configuration bean from the UI, to be returned with + * getConfiguration() + */ + @Override + public void noteConfiguration() { + if (configPanel.getCurrentXMLTree() != null) { + setProperty("exampleXmlDocument", configPanel.getCurrentXMLTree() + .getDocumentUsedToPopulateTree().asXML()); + } + setProperty("xpathExpression", configPanel.getCurrentXPathExpression()); + + Map<String, String> xPathNamespaceMap = configPanel.getCurrentXPathNamespaceMap(); + if (xPathNamespaceMap.isEmpty()) { + json.remove("xpathNamespaceMap"); + } else { + ArrayNode namespaceMapNode = json.arrayNode(); + for (Entry<String, String> namespaceMapping : xPathNamespaceMap.entrySet()) { + namespaceMapNode.addObject().put("prefix", namespaceMapping.getKey()).put("uri", namespaceMapping.getValue()); + } + json.set("xpathNamespaceMap", namespaceMapNode); + } + + configureInputPorts(serviceRegistry); + configureOutputPorts(serviceRegistry); +} + + /** + * Check that user values in the UI are valid. + */ + @Override + public boolean checkValues() { + // the only validity condition is the correctness of the XPath + // expression -- so checking that + int xpathExpressionStatus = XPathActivityConfigurationBean.validateXPath(this.configPanel + .getCurrentXPathExpression()); + + // show an explicit warning message to explain the problem + if (xpathExpressionStatus == XPathActivityConfigurationBean.XPATH_EMPTY) { + JOptionPane.showMessageDialog(this, "XPath expression should not be empty", + "XPath Activity", JOptionPane.WARNING_MESSAGE); + } else if (xpathExpressionStatus == XPathActivityConfigurationBean.XPATH_INVALID) { + JOptionPane.showMessageDialog(this, + "<html><center>XPath expression is invalid - hover the mouse over the XPath status<br>" + + "icon to get more information</center></html>", "XPath Activity", + JOptionPane.WARNING_MESSAGE); + } + + return (xpathExpressionStatus == XPathActivityConfigurationBean.XPATH_VALID); + } + + /** + * Update GUI from a changed configuration bean (perhaps by undo / redo). + */ + @Override + public void refreshConfiguration() { + if (json.has("exampleXmlDocument")) { + configPanel.setSourceXML(getProperty("exampleXmlDocument")); + configPanel.parseXML(); + } + + configPanel.setCurrentXPathExpression(getProperty("xpathExpression")); + + Map<String, String> xpathNamespaceMap = new HashMap<>(); + if (json.has("xpathNamespaceMap")) { + for (JsonNode namespaceMapping : json.get("xpathNamespaceMap")) { + xpathNamespaceMap.put(namespaceMapping.get("prefix").asText(), namespaceMapping.get("uri").asText()); + } + } + configPanel.setCurrentXPathNamespaceMapValues(xpathNamespaceMap); + configPanel.reloadNamespaceMappingTableFromLocalMap(); + + // if the XML tree was populated, (re-)run the XPath expression + // and restore selection of nodes in the tree, if possible + if (configPanel.getCurrentXMLTree() != null) { + configPanel.runXPath(true); + + // convert the XPath expression into the required list form; + // discard the first 'leg', as it's a side effect of + // "String.split()" - + // non-existent string to the left of the first "/" + String[] xpathLegs = getProperty("xpathExpression").split("/"); + List<String> xpathLegList = new ArrayList<String>(); + for (int i = 1; i < xpathLegs.length; i++) { + xpathLegList.add("/" + xpathLegs[i]); + } + + // if nothing was obtained, we should be looking at the root node - + // but add the actual expression as it was, just in case + if (xpathLegList.size() == 0) { + xpathLegList.add(configPanel.getCurrentXPathExpression()); + } + + // invoke selection handler of the XML tree to do the job + configPanel.getCurrentXMLTree().getXMLTreeSelectionHandler() + .selectAllNodesThatMatchTheCurrentXPath(xpathLegList, null); + } + + } +} http://git-wip-us.apache.org/repos/asf/incubator-taverna-workbench-common-activities/blob/163747de/taverna-xpath-activity-ui/src/main/java/org/apache/taverna/activities/xpath/ui/config/XPathActivityConfigureAction.java ---------------------------------------------------------------------- diff --git a/taverna-xpath-activity-ui/src/main/java/org/apache/taverna/activities/xpath/ui/config/XPathActivityConfigureAction.java b/taverna-xpath-activity-ui/src/main/java/org/apache/taverna/activities/xpath/ui/config/XPathActivityConfigureAction.java new file mode 100644 index 0000000..b57d9f7 --- /dev/null +++ b/taverna-xpath-activity-ui/src/main/java/org/apache/taverna/activities/xpath/ui/config/XPathActivityConfigureAction.java @@ -0,0 +1,52 @@ +package org.apache.taverna.activities.xpath.ui.config; + +import java.awt.Frame; +import java.awt.event.ActionEvent; + +import org.apache.taverna.servicedescriptions.ServiceDescriptionRegistry; +import org.apache.taverna.workbench.activityicons.ActivityIconManager; +import org.apache.taverna.workbench.edits.EditManager; +import org.apache.taverna.workbench.file.FileManager; +import org.apache.taverna.workbench.ui.actions.activity.ActivityConfigurationAction; +import org.apache.taverna.workbench.ui.views.contextualviews.activity.ActivityConfigurationDialog; +import org.apache.taverna.commons.services.ServiceRegistry; +import org.apache.taverna.scufl2.api.activity.Activity; + +/** + * @author Sergejs Aleksejevs + * @author David Withers + */ +@SuppressWarnings("serial") +public class XPathActivityConfigureAction extends ActivityConfigurationAction { + + private final EditManager editManager; + private final FileManager fileManager; + private final ServiceRegistry serviceRegistry; + + public XPathActivityConfigureAction(Activity activity, Frame owner, + EditManager editManager, FileManager fileManager, + ActivityIconManager activityIconManager, + ServiceDescriptionRegistry serviceDescriptionRegistry, ServiceRegistry serviceRegistry) { + super(activity, activityIconManager, serviceDescriptionRegistry); + this.editManager = editManager; + this.fileManager = fileManager; + this.serviceRegistry = serviceRegistry; + } + + public void actionPerformed(ActionEvent e) { + ActivityConfigurationDialog currentDialog = ActivityConfigurationAction.getDialog(getActivity()); + + if (currentDialog != null) { + currentDialog.toFront(); + return; + } + + XPathActivityConfigurationPanelProvider panel = new XPathActivityConfigurationPanelProvider( + getActivity(), serviceRegistry); + ActivityConfigurationDialog dialog = new ActivityConfigurationDialog( + getActivity(), panel, editManager); + + ActivityConfigurationAction.setDialog(getActivity(), dialog, fileManager); + } + +} http://git-wip-us.apache.org/repos/asf/incubator-taverna-workbench-common-activities/blob/163747de/taverna-xpath-activity-ui/src/main/java/org/apache/taverna/activities/xpath/ui/config/xmltree/TableCellListener.java ---------------------------------------------------------------------- diff --git a/taverna-xpath-activity-ui/src/main/java/org/apache/taverna/activities/xpath/ui/config/xmltree/TableCellListener.java b/taverna-xpath-activity-ui/src/main/java/org/apache/taverna/activities/xpath/ui/config/xmltree/TableCellListener.java new file mode 100644 index 0000000..08c5f42 --- /dev/null +++ b/taverna-xpath-activity-ui/src/main/java/org/apache/taverna/activities/xpath/ui/config/xmltree/TableCellListener.java @@ -0,0 +1,186 @@ +package org.apache.taverna.activities.xpath.ui.config.xmltree; + +import java.awt.event.*; +import javax.swing.*; +import java.beans.*; + +/** + * This class listens for changes made to the data in the table via the + * TableCellEditor. When editing is started, the value of the cell is saved + * When editing is stopped the new value is saved. When the oold and new + * values are different, then the provided Action is invoked. + * + * The source of the Action is a TableCellListener instance. + * + * TODO: update to work with Java 1.6, when Taverna is migrated to that version + * (see the next TODO tag - this is where the change needs to be made) + * + * @author Robert Camick + * + * @see <a href="http://tips4java.wordpress.com/2009/06/07/table-cell-listener/">http://tips4java.wordpress.com/2009/06/07/table-cell-listener/</a> + * @see <a href="http://www.camick.com/java/source/TableCellListener.java">http://www.camick.com/java/source/TableCellListener.java</a> + */ +public class TableCellListener implements PropertyChangeListener, Runnable +{ + private JTable table; + private Action action; + + private int row; + private int column; + private Object oldValue; + private Object newValue; + + /** + * Create a TableCellListener. + * + * @param table the table to be monitored for data changes + * @param action the Action to invoke when cell data is changed + */ + public TableCellListener(JTable table, Action action) + { + this.table = table; + this.action = action; + this.table.addPropertyChangeListener( this ); + } + + /** + * Create a TableCellListener with a copy of all the data relevant to + * the change of data for a given cell. + * + * @param row the row of the changed cell + * @param column the column of the changed cell + * @param oldValue the old data of the changed cell + * @param newValue the new data of the changed cell + */ + private TableCellListener(JTable table, int row, int column, Object oldValue, Object newValue) + { + this.table = table; + this.row = row; + this.column = column; + this.oldValue = oldValue; + this.newValue = newValue; + } + + /** + * Get the column that was last edited - value as in table model, not in the UI + * + * @return the column that was edited + */ + public int getColumn() + { + return column; + } + + /** + * Get the new value in the cell + * + * @return the new value in the cell + */ + public Object getNewValue() + { + return newValue; + } + + /** + * Get the old value of the cell + * + * @return the old value of the cell + */ + public Object getOldValue() + { + return oldValue; + } + + /** + * Get the row that was last edited - value as in table model, not in the UI + * + * @return the row that was edited + */ + public int getRow() + { + return row; + } + + /** + * Get the table of the cell that was changed + * + * @return the table of the cell that was changed + */ + public JTable getTable() + { + return table; + } + + // + // Implement the PropertyChangeListener interface + // + public void propertyChange(PropertyChangeEvent e) + { + // A cell has started/stopped editing + + if ("tableCellEditor".equals(e.getPropertyName())) + { + if (table.isEditing()) + processEditingStarted(); + else + processEditingStopped(); + } + } + + /* + * Save information of the cell about to be edited + */ + private void processEditingStarted() + { + // The invokeLater is necessary because the editing row and editing + // column of the table have not been set when the "tableCellEditor" + // PropertyChangeEvent is fired. + // This results in the "run" method being invoked + + SwingUtilities.invokeLater( this ); + } + + /* + * See above. + */ + public void run() + { +// TODO - the next line is a correct implementation for Java 1.6 +// row = table.convertRowIndexToModel( table.getEditingRow() ); + +// in Java 1.5 which is currently used, tables are not easily sortable +// (and that is the case for the XPath Activity, where this class is used) -- +// hence, row numbers in the view and in the model will always be identical +// --> HACK: just use the row number from the view... + row = table.getEditingRow(); + + column = table.convertColumnIndexToModel( table.getEditingColumn() ); + oldValue = table.getModel().getValueAt(row, column); + newValue = null; + } + + /* + * Update the Cell history when necessary + */ + private void processEditingStopped() + { + newValue = table.getModel().getValueAt(row, column); + + // The data has changed, invoke the supplied Action + + if (! newValue.equals(oldValue)) + { + // Make a copy of the data in case another cell starts editing + // while processing this change + + TableCellListener tcl = new TableCellListener( + getTable(), getRow(), getColumn(), getOldValue(), getNewValue()); + + ActionEvent event = new ActionEvent( + tcl, + ActionEvent.ACTION_PERFORMED, + ""); + action.actionPerformed(event); + } + } +} http://git-wip-us.apache.org/repos/asf/incubator-taverna-workbench-common-activities/blob/163747de/taverna-xpath-activity-ui/src/main/java/org/apache/taverna/activities/xpath/ui/config/xmltree/XPathActivityXMLTree.java ---------------------------------------------------------------------- diff --git a/taverna-xpath-activity-ui/src/main/java/org/apache/taverna/activities/xpath/ui/config/xmltree/XPathActivityXMLTree.java b/taverna-xpath-activity-ui/src/main/java/org/apache/taverna/activities/xpath/ui/config/xmltree/XPathActivityXMLTree.java new file mode 100644 index 0000000..e6b2e9f --- /dev/null +++ b/taverna-xpath-activity-ui/src/main/java/org/apache/taverna/activities/xpath/ui/config/xmltree/XPathActivityXMLTree.java @@ -0,0 +1,572 @@ +package org.apache.taverna.activities.xpath.ui.config.xmltree; + +import java.awt.Component; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import javax.swing.JLabel; +import javax.swing.JMenuItem; +import javax.swing.JPopupMenu; +import javax.swing.JTree; +import javax.swing.event.TreeSelectionListener; +import javax.swing.text.Position; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.DefaultTreeCellRenderer; +import javax.swing.tree.DefaultTreeModel; +import javax.swing.tree.TreePath; + +import org.apache.taverna.activities.xpath.ui.config.XPathActivityConfigurationPanel; +import org.apache.taverna.activities.xpath.ui.servicedescription.XPathActivityIcon; + +import org.dom4j.Attribute; +import org.dom4j.Document; +import org.dom4j.DocumentException; +import org.dom4j.DocumentHelper; +import org.dom4j.Element; +import org.dom4j.Namespace; +import org.dom4j.QName; +import org.dom4j.XPath; + + +/** + * + * @author Sergejs Aleksejevs + */ +public class XPathActivityXMLTree extends JTree +{ + private XPathActivityXMLTree instanceOfSelf; + private XPathActivityXMLTreeRenderer treeRenderer; + + private JPopupMenu contextualMenu; + + private TreeSelectionListener[] allSelectionListeners; + private XPathActivityXMLTreeSelectionHandler xmlTreeSelectionHandler; + + /** + * + */ + private XPathActivityConfigurationPanel parentConfigPanel; + + private Document documentUsedToPopulateTree; + + /** + * holds value of the current XPath expression obtained from + * the combination of nodes selected in the XML tree + */ + private XPath currentXPathExpression; + + private Map<String,String> currentXPathNamespaces; + + + + private XPathActivityXMLTree(XPathActivityXMLTreeNode root, Document documentUsedToPopulateTree, + boolean bIncludeElementValues, boolean bIncludeElementNamespaces, XPathActivityConfigurationPanel parentConfigPanel) + { + super(root); + + this.instanceOfSelf = this; + this.allSelectionListeners = new TreeSelectionListener[0]; + + this.parentConfigPanel = parentConfigPanel; + + this.documentUsedToPopulateTree = documentUsedToPopulateTree; + this.currentXPathExpression = null; + this.currentXPathNamespaces = new HashMap<String,String>(); + this.prepopulateNamespaceMap(); + + + // custom renderer of the nodes in the XML tree + this.treeRenderer = new XPathActivityXMLTreeRenderer(bIncludeElementValues, bIncludeElementNamespaces); + this.setCellRenderer(treeRenderer); + + + // add listener to handle various selections of nodes in the tree + this.xmlTreeSelectionHandler = new XPathActivityXMLTreeSelectionHandler(parentConfigPanel, this); + this.addTreeSelectionListener(xmlTreeSelectionHandler); + + + // --- CONTEXTUAL MENU FOR EXPANDING / COLLAPSING THE TREE --- + + // create popup menu for expanding / collapsing all nodes in the tree + JMenuItem miExpandAll = new JMenuItem("Expand all", XPathActivityIcon.getIconById(XPathActivityIcon.XML_TREE_EXPAND_ALL_ICON)); + miExpandAll.setToolTipText("Expand all nodes in the filtering tree"); + miExpandAll.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + for (int i = 0; i < getRowCount(); i++) { + instanceOfSelf.expandRow(i); + } + } + }); + JMenuItem miCollapseAll = new JMenuItem("Collapse all", XPathActivityIcon.getIconById(XPathActivityIcon.XML_TREE_COLLAPSE_ALL_ICON)); + miCollapseAll.setToolTipText("Collapse all expanded nodes in the filtering tree"); + miCollapseAll.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + for (int i = getRowCount() - 1; i >= 0; i--) { + instanceOfSelf.collapseRow(i); + } + } + }); + + // populate the popup menu with created menu items + contextualMenu = new JPopupMenu(); + contextualMenu.add(miExpandAll); + contextualMenu.add(miCollapseAll); + + // mouse events may cause the contextual menu to be shown - adding a listener + this.addMouseListener(new MouseAdapter() + { + public void mousePressed(MouseEvent e) { + if (e.isPopupTrigger()) { + contextualMenu.show(instanceOfSelf, e.getX(), e.getY()); + } + } + public void mouseReleased(MouseEvent e) { + if (e.isPopupTrigger()) { + // another way a popup menu may be called on different systems + contextualMenu.show(instanceOfSelf, e.getX(), e.getY()); + } + } + }); + + } + + + /** + * Pre-populates namespace map with the namespaced declared in the root + * node of the XML document, which was used to populate the tree. + */ + private void prepopulateNamespaceMap() + { + Document doc = this.getDocumentUsedToPopulateTree(); + Element root = doc.getRootElement(); + + // get opening tag of the root node + String rootAsXML = root.asXML().substring(0, root.asXML().indexOf(">")); + + // split the opening tag into tokens (all attributes are separated by a space) + String[] rootTokens = rootAsXML.split(" "); + + // for each attribute check if that's a namespace declaration + for (String token : rootTokens) { + if (token.startsWith("xmlns")) + { + String[] namespacePrefixAndURI = token.split("="); + + // a prefix is either given explicitly, or an empty one will be used + String prefix = namespacePrefixAndURI[0].indexOf(":") == -1 ? + "" : + namespacePrefixAndURI[0].split(":")[1]; + + // URI is the value of the XML attribute, so need to strip out surrounding quotes + String URI = namespacePrefixAndURI[1].replaceAll("\"", ""); + + // now add the details of the current namespace to the map + this.addNamespaceToXPathMap(new Namespace(prefix, URI)); + } + } + } + + + protected XPathActivityConfigurationPanel getParentConfigPanel() { + return parentConfigPanel; + } + + public XPathActivityXMLTreeSelectionHandler getXMLTreeSelectionHandler() { + return xmlTreeSelectionHandler; + } + + public Document getDocumentUsedToPopulateTree() { + return documentUsedToPopulateTree; + } + + public XPath getCurrentXPathExpression() { + return currentXPathExpression; + } + protected void setCurrentXPathExpression(XPath xpathExpression) { + this.currentXPathExpression = xpathExpression; + } + + + public Map<String,String> getCurrentXPathNamespaces() { + return currentXPathNamespaces; + } + + + + protected void removeAllSelectionListeners() + { + this.allSelectionListeners = this.getTreeSelectionListeners(); + for (TreeSelectionListener listener : this.allSelectionListeners) { + this.removeTreeSelectionListener(listener); + } + } + + protected void restoreAllSelectionListeners() + { + for (TreeSelectionListener listener : this.allSelectionListeners) { + this.addTreeSelectionListener(listener); + } + } + + + + /** + * Creates an instance of the XML tree from provided XML data. + * + * @param xmlData XML document in the form of a <code>String</code> to + * derive the tree from. + * @param bIncludeAttributesIntoTree + * @param bIncludeValuesIntoTree + * @param bIncludeElementNamespacesIntoTree + * @param parentConfigPanel + * @return + * @throws DocumentException if <code>xmlData</code> does not + * contain a valid XML document. + * + */ + public static XPathActivityXMLTree createFromXMLData(String xmlData, boolean bIncludeAttributesIntoTree, + boolean bIncludeValuesIntoTree, boolean bIncludeElementNamespacesIntoTree, + XPathActivityConfigurationPanel parentConfigPanel) throws DocumentException + { + // ----- XML DOCUMENT PARSING ----- + // try to parse the XML document - the next line will throw an exception if + // the document is not well-formed; proceed otherwise + Document doc = DocumentHelper.parseText(xmlData); + Element rootElement = doc.getRootElement(); + + + // ----- POPULATE XML TREE ----- + XPathActivityXMLTreeElementNode rootNode = new XPathActivityXMLTreeElementNode(rootElement); + populate(rootNode, rootElement, bIncludeAttributesIntoTree); + + return (new XPathActivityXMLTree(rootNode, doc, bIncludeValuesIntoTree, bIncludeElementNamespacesIntoTree, parentConfigPanel)); + } + + + /** + * Worker method for populating the tree recursively from a list of Elements. + * + * @param node + * @param element + */ + private static void populate(DefaultMutableTreeNode node, Element element, + boolean bIncludeAttributesIntoTree) + { + Iterator<Element> elementIterator = element.elements().iterator(); + while (elementIterator.hasNext()) { + Element childElement = elementIterator.next(); + XPathActivityXMLTreeElementNode childNode = new XPathActivityXMLTreeElementNode(childElement); + node.add(childNode); + + // recursively repeat for all children of the current child element + populate(childNode, childElement, bIncludeAttributesIntoTree); + } + + + // add attributes of the element as its children, if necessary + if (bIncludeAttributesIntoTree) { + List<Attribute> attributes = element.attributes(); + for (Attribute attribute : attributes) { + node.add(new XPathActivityXMLTreeAttributeNode(attribute)); + } + } + } + + + // ---------------- RESPONDING TO REQUESTS TO CHANGE APPEARANCE OF EXISTING TREE ----------------- + + /** + * NB! May be inefficient, as this solution re-generates the whole tree from + * stored XML document and replaces the root node of itself with a newly + * generated root node (that will be populated with updated children, + * according to the new values of options). + * + * However, this is a simple solution that will work for now. + * + * @param bIncludeAttributes + * @param bIncludeValues + * @param bIncludeNamespaces + */ + public void refreshFromExistingDocument(boolean bIncludeAttributes, boolean bIncludeValues, boolean bIncludeNamespaces) + { + this.setEnabled(false); + removeAllSelectionListeners(); + + // store expansion and selection state of the XML tree + // see documentation for restoreExpandedPaths() for more details + // + // stored paths to expanded nodes are quite reliable, as paths are recorded; + // stored selected rows are less reliable, as only indices are kept -- however, + // the tree is re-created from the same document, so ordering/number of nodes + // cannot change (apart from attributes that may be added / removed - the attributes + // appear after other child nodes of some node in the tree, therefore only their + // selection could be affected) + HashMap<String,ArrayList<String>> toExpand = new HashMap<String,ArrayList<String>>(); + ArrayList<Integer> toSelect = new ArrayList<Integer>(); + for( int i = 1; i < this.getRowCount(); i++) { + if( this.isExpanded(i) ) { + TreePath path = this.getPathForRow(i); + String parentPath = path.getParentPath().toString(); + ArrayList<String> values = toExpand.get(parentPath); + if(values == null) { + values = new ArrayList<String>(); + } + values.add(path.getLastPathComponent().toString()); + toExpand.put(parentPath, values); + } + if (this.isRowSelected(i)) { + toSelect.add(i); + } + } + + + // update presentation options + this.treeRenderer.setIncludeElementValues(bIncludeValues); + this.treeRenderer.setIncludeElementNamespaces(bIncludeNamespaces); + + // re-create the root node of the tree and replace the old one with it + Element rootElement = this.documentUsedToPopulateTree.getRootElement(); + XPathActivityXMLTreeNode newRootNode = new XPathActivityXMLTreeElementNode(rootElement); + populate(newRootNode, rootElement, bIncludeAttributes); + ((DefaultTreeModel)this.getModel()).setRoot(newRootNode); + + + // restore previous state of the tree from saved values + restoreExpandedPaths(toExpand, this.getPathForRow(0)); + restoreSelectedPaths(toSelect); + + this.restoreAllSelectionListeners(); + this.setEnabled(true); + } + + + /** + * This method can only reliably work when the tree is re-generated from the same + * XML document, so that number / order of nodes would not change. + * + * @param toSelect List of indices of rows to re-select after tree was re-generated. + */ + private void restoreSelectedPaths(ArrayList<Integer> toSelect) + { + if (toSelect == null || toSelect.isEmpty()) return; + + // something definitely needs to be selected, so include root element into selection + this.addSelectionRow(0); + + // select all stored rows + for (Integer value : toSelect) { + this.addSelectionRow(value); + } + } + + + + /** + * Taken from: <a href="http://java.itags.org/java-core-gui-apis/58504/">http://java.itags.org/java-core-gui-apis/58504/</a> + * + * This method recursively expands all previously stored paths. + * Works under assumption that the name of the root node did not change. + * Otherwise, it can handle changed structure of the tree. + * + * To achieve its goal, it cannot simply use stored TreePath from your the original tree, + * since the paths are invalid after the tree is refreshed. Instead, a HashMap which links + * a String representation of the parent tree path to all expanded child node names is used. + * + * @param toExpand Map which links a String representation of the parent tree path to all + * expanded child node names is used. + * @param rootPath Path to root node. + */ + void restoreExpandedPaths(HashMap<String,ArrayList<String>> toExpand, TreePath rootPath) + { + ArrayList<String> values = toExpand.remove(rootPath.toString()); + if (values == null) return; + + int row = this.getRowForPath(rootPath); + for (String value : values) + { + TreePath nextMatch = this.getNextMatch(value, row, Position.Bias.Forward); + this.expandPath(nextMatch); + if (toExpand.containsKey(nextMatch.toString())) { + restoreExpandedPaths(toExpand, nextMatch); + } + } + } + + + + // ---------------- TREE SELECTION MODEL + XPath GENERATION ----------------- + + + protected String generateXPathFromTreePath(TreePath path) + { + StringBuilder xpath = new StringBuilder(); + + for (String leg : generateXPathFromTreePathAsLegList(path)) { + xpath.append(leg); + } + + return (xpath.toString()); + } + + + protected List<String> generateXPathFromTreePathAsLegList(TreePath path) + { + List<String> pathLegs = new LinkedList<String>(); + + TreePath parentPath = path; + for (int i = 0; i < path.getPathCount(); i++) + { + XPathActivityXMLTreeNode lastXMLTreeNodeInThisPath = (XPathActivityXMLTreeNode)parentPath.getLastPathComponent(); + pathLegs.add(0, this.getXMLTreeNodeEffectiveQualifiedNameAsXPathLeg(lastXMLTreeNodeInThisPath)); + + parentPath = parentPath.getParentPath(); + } + + return (pathLegs); + } + + + protected String getXMLTreeNodeEffectiveQualifiedNameAsXPathLeg(XPathActivityXMLTreeNode node) + { + QName qname = node.getNodeQName(); + String effectiveNamespacePrefix = addNamespaceToXPathMap(qname.getNamespace()); + + return("/" + + (node.isAttribute() ? "@" : "") + + (effectiveNamespacePrefix.length() > 0 ? (effectiveNamespacePrefix + ":") : "") + + qname.getName()); + } + + + + private String addNamespaceToXPathMap(Namespace namespace) + { + // EMTPY PREFIX + if (namespace.getPrefix().length() == 0) { + if (namespace.getURI().length() == 0) { + // DEFAULT NAMESPACE with no URI - nothing to worry about + return ""; + } + else { + // DEFAULT NAMESPACE WITH NO PREFIX, BUT URI IS KNOWN + return (addNamespaceToXPathMap(new Namespace("default", namespace.getURI()))); + } + } + + // NEW NON-EMPTY PREFIX + if (!this.currentXPathNamespaces.containsKey(namespace.getPrefix())) { + this.currentXPathNamespaces.put(namespace.getPrefix(), namespace.getURI()); + return (namespace.getPrefix()); + } + + // EXISTING NON-EMPTY PREFIX AND THE SAME URI - NO NEED TO ADD AGAIN + else if (this.currentXPathNamespaces.get(namespace.getPrefix()).equals(namespace.getURI())) { + return (namespace.getPrefix()); + } + + // EXISTING NON-EMPTY PREFIX, BUT DIFFERENT URI + else { + String repeatedPrefix = namespace.getPrefix(); + + int i = 0; + while (this.currentXPathNamespaces.containsKey(repeatedPrefix + i)) { + // check if current alternative prefix wasn't yet applied to current URI + if (this.currentXPathNamespaces.get(repeatedPrefix + i).equals(namespace.getURI())) { + return (repeatedPrefix + i); + } + else { + // still another URI for the same prefix, keep trying to increase the ID in the prefix + i++; + } + } + + String modifiedPrefix = repeatedPrefix + i; + this.currentXPathNamespaces.put(modifiedPrefix, namespace.getURI()); + return (modifiedPrefix); + } + } + + + // ----------------------- Tree Cell Renderer -------------------------- + + /** + * + * @author Sergejs Aleksejevs + */ + private class XPathActivityXMLTreeRenderer extends DefaultTreeCellRenderer + { + private boolean bIncludeElementValues; + private boolean bIncludeElementNamespaces; + + public XPathActivityXMLTreeRenderer(boolean bIncludeElementValues, boolean bIncludeElementNamespaces) { + super(); + this.bIncludeElementValues = bIncludeElementValues; + this.bIncludeElementNamespaces = bIncludeElementNamespaces; + } + + + public boolean getIncludeElementValues() { + return bIncludeElementValues; + } + public void setIncludeElementValues(boolean bIncludeElementValues) { + this.bIncludeElementValues = bIncludeElementValues; + } + + public boolean getIncludeElementNamespaces() { + return bIncludeElementNamespaces; + } + public void setIncludeElementNamespaces(boolean bIncludeElementNamespaces) { + this.bIncludeElementNamespaces = bIncludeElementNamespaces; + } + + + public Component getTreeCellRendererComponent(JTree tree, Object value, + boolean selected, boolean expanded, boolean leaf, int row, + boolean hasFocus) + { + // obtain the default rendering, we'll then customize it + Component defaultRendering = super.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus); + + // it is most likely that the default rendering will be a JLabel, check just to be safe + if (defaultRendering instanceof JLabel) + { + JLabel defaultRenderedLabel = ((JLabel)defaultRendering); + + // ---------- CHOOSE APPROPRIATE ICON FOR THE NODE ------------ + if (row == 0) { + // set the icon for the XML tree root node + defaultRenderedLabel.setIcon(XPathActivityIcon.getIconById(XPathActivityIcon.XML_TREE_ROOT_ICON)); + } + else { + // set the icon for the XML tree node + if (value instanceof XPathActivityXMLTreeNode && + ((XPathActivityXMLTreeNode)value).isAttribute()) + { + defaultRenderedLabel.setIcon(XPathActivityIcon.getIconById(XPathActivityIcon.XML_TREE_ATTRIBUTE_ICON)); + } + else { + defaultRenderedLabel.setIcon(XPathActivityIcon.getIconById(XPathActivityIcon.XML_TREE_NODE_ICON)); + } + } + + + // ----------- CHOOSE THE DISPLAY TITLE FOR THE NODE ------------ + if (value instanceof XPathActivityXMLTreeNode) { + defaultRenderedLabel.setText(((XPathActivityXMLTreeNode)value).getTreeNodeDisplayLabel( + this.bIncludeElementValues, this.bIncludeElementNamespaces, true)); + } + } + + return (defaultRendering); + } + + } +} http://git-wip-us.apache.org/repos/asf/incubator-taverna-workbench-common-activities/blob/163747de/taverna-xpath-activity-ui/src/main/java/org/apache/taverna/activities/xpath/ui/config/xmltree/XPathActivityXMLTreeAttributeNode.java ---------------------------------------------------------------------- diff --git a/taverna-xpath-activity-ui/src/main/java/org/apache/taverna/activities/xpath/ui/config/xmltree/XPathActivityXMLTreeAttributeNode.java b/taverna-xpath-activity-ui/src/main/java/org/apache/taverna/activities/xpath/ui/config/xmltree/XPathActivityXMLTreeAttributeNode.java new file mode 100644 index 0000000..4a37839 --- /dev/null +++ b/taverna-xpath-activity-ui/src/main/java/org/apache/taverna/activities/xpath/ui/config/xmltree/XPathActivityXMLTreeAttributeNode.java @@ -0,0 +1,50 @@ +package org.apache.taverna.activities.xpath.ui.config.xmltree; + +import org.dom4j.Attribute; + +/** + * + * @author Sergejs Aleksejevs + */ +public class XPathActivityXMLTreeAttributeNode extends XPathActivityXMLTreeNode +{ + private Attribute associatedAttribute; + + public XPathActivityXMLTreeAttributeNode(Attribute associatedAttribute) { + super(associatedAttribute, true); + this.associatedAttribute = associatedAttribute; + } + + public Attribute getAssociatedAttribute() { + return associatedAttribute; + } + + public String getTreeNodeDisplayLabel(boolean bIncludeValue, boolean bUseStyling) + { + StringBuilder label = new StringBuilder(); + + // add qualified attribute name (possibly) with styling + label.append((bUseStyling ? "<font color=\"purple\">" : "") + + this.associatedAttribute.getQualifiedName() + + (bUseStyling ? "</font>" : "")); + + // add attribute value + if (bIncludeValue) + { + String attributeTextValue = this.associatedAttribute.getText(); + + if (attributeTextValue != null && attributeTextValue.length() > 0) { + label.append((bUseStyling ? "<font color=\"gray\"> - </font><font color=\"green\">" : "") + + truncateElementTextValue(stripAllHTML(attributeTextValue)) + + (bUseStyling ? "</font>" : "")); + } + } + + if (bUseStyling) { + label.insert(0, "<html>"); + label.append("</html>"); + } + + return (label.toString()); + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/incubator-taverna-workbench-common-activities/blob/163747de/taverna-xpath-activity-ui/src/main/java/org/apache/taverna/activities/xpath/ui/config/xmltree/XPathActivityXMLTreeElementNode.java ---------------------------------------------------------------------- diff --git a/taverna-xpath-activity-ui/src/main/java/org/apache/taverna/activities/xpath/ui/config/xmltree/XPathActivityXMLTreeElementNode.java b/taverna-xpath-activity-ui/src/main/java/org/apache/taverna/activities/xpath/ui/config/xmltree/XPathActivityXMLTreeElementNode.java new file mode 100644 index 0000000..cedcc95 --- /dev/null +++ b/taverna-xpath-activity-ui/src/main/java/org/apache/taverna/activities/xpath/ui/config/xmltree/XPathActivityXMLTreeElementNode.java @@ -0,0 +1,62 @@ +package org.apache.taverna.activities.xpath.ui.config.xmltree; + +import org.dom4j.Element; +import org.dom4j.Namespace; + + +/** + * + * @author Sergejs Aleksejevs + */ +public class XPathActivityXMLTreeElementNode extends XPathActivityXMLTreeNode +{ + private Element associatedElement; + + public XPathActivityXMLTreeElementNode(Element associatedElement) { + super(associatedElement, false); + this.associatedElement = associatedElement; + } + + public Element getAssociatedElement() { + return associatedElement; + } + + public String getTreeNodeDisplayLabel(boolean bIncludeValue, boolean bIncludeNamespace, boolean bUseStyling) + { + StringBuilder label = new StringBuilder(); + + // add qualified element name + label.append(this.associatedElement.getQualifiedName()); + + // add element namespace + if (bIncludeNamespace) + { + Namespace ns = this.associatedElement.getNamespace(); + + label.append((bUseStyling ? "<font color=\"gray\">" : "") + + " - xmlns" + (ns.getPrefix().length() > 0 ? (":" + ns.getPrefix()) : "") + "=\"" + + this.associatedElement.getNamespaceURI() + + (bUseStyling ? "\"</font>" : "")); + } + + // add element value + if (bIncludeValue) + { + String elementTextValue = this.associatedElement.getTextTrim(); + + if (elementTextValue != null && elementTextValue.length() > 0) { + label.append((bUseStyling ? "<font color=\"gray\"> - </font><font color=\"blue\">" : "") + + truncateElementTextValue(stripAllHTML(elementTextValue)) + + (bUseStyling ? "</font>" : "")); + } + } + + if (bUseStyling) { + label.insert(0, "<html>"); + label.append("</html>"); + } + + return (label.toString()); + } + +} \ No newline at end of file
