/*
* RapidMiner
*
* Copyright (C) 2001-2011 by Rapid-I and the contributors
*
* Complete list of developers available at our web site:
*
* http://rapid-i.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*/
package com.rapidminer.operator.nio.xml;
import java.awt.Color;
import java.awt.Component;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.GridLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.ByteArrayOutputStream;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JTextArea;
import javax.swing.ListCellRenderer;
import javax.swing.border.EmptyBorder;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.table.AbstractTableModel;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import com.rapidminer.gui.tools.ExtendedJScrollPane;
import com.rapidminer.gui.tools.ExtendedJTable;
import com.rapidminer.gui.tools.ExtendedJTextField;
import com.rapidminer.gui.tools.ExtendedJTextField.TextChangeListener;
import com.rapidminer.gui.tools.ResourceAction;
import com.rapidminer.gui.tools.UpdateQueue;
import com.rapidminer.gui.tools.dialogs.wizards.AbstractWizard;
import com.rapidminer.gui.tools.dialogs.wizards.AbstractWizard.WizardStepDirection;
import com.rapidminer.gui.tools.dialogs.wizards.WizardStep;
import com.rapidminer.io.process.XMLTools;
import com.rapidminer.operator.OperatorException;
import com.rapidminer.tools.I18N;
import com.rapidminer.tools.XMLException;
import com.rapidminer.tools.container.Pair;
import com.rapidminer.tools.io.Encoding;
/**
* This step allows to enter an XPath expression whose
* matches will be used as examples.
*
* @author Sebastian Land, Marius Helf
*/
public class XMLExampleExpressionWizardStep extends WizardStep {
private static Properties XML_PROPERTIES = new Properties();
{
XML_PROPERTIES.setProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
}
/**
* A model which contains rows of element names, attribute names and attribute values.
*
* @author Sebastian Land, Marius Helf
*
*/
private class AttributeTableModel extends AbstractTableModel {
private static final long serialVersionUID = 1L;
private static final int ELEMENT_COLUMN = 0;
private static final int ATTRIBUTE_COLUMN = 1;
private static final int VALUE_COLUMN = 2;
private static final int COLUMN_COUNT = 3;
private List<XMLDomHelper.AttributeNamespaceValue> attributes = new LinkedList<XMLDomHelper.AttributeNamespaceValue>();
@Override
public int getRowCount() {
if (attributes != null)
return attributes.size();
return 0;
}
@Override
public int getColumnCount() {
return COLUMN_COUNT;
}
@Override
public String getColumnName(int column) {
switch (column) {
case ELEMENT_COLUMN:
return I18N.getGUILabel("importwizard.xml.example_expression.attribute_table.element_column_header");
case ATTRIBUTE_COLUMN:
return I18N.getGUILabel("importwizard.xml.example_expression.attribute_table.attribute_column_header");
case VALUE_COLUMN:
return I18N.getGUILabel("importwizard.xml.example_expression.attribute_table.value_column_header");
}
return "";
}
@Override
public Object getValueAt(int rowIndex, int columnIndex) {
switch (columnIndex) {
case ATTRIBUTE_COLUMN:
String namespace = attributes.get(rowIndex).getNamespace();
String name = attributes.get(rowIndex).getName();
name = (namespace!=null)?configuration.getNamespaceId(namespace)+":"+name:name;
return name;
case VALUE_COLUMN:
return attributes.get(rowIndex).getValue();
case ELEMENT_COLUMN:
return attributes.get(rowIndex).getElement();
}
return null;
}
public void setAttributes(Set<XMLDomHelper.AttributeNamespaceValue> allAttributes) {
attributes.clear();
attributes.addAll(allAttributes);
fireTableDataChanged();
}
}
private XMLResultSetConfiguration configuration;
private JPanel component = new JPanel(new GridBagLayout());
private XMLTreeModel xmlTreeModel;
private XMLTreeView xmlTreeView = new XMLTreeView(new HashMap<String,String>());
private AttributeTableModel attributeTableModel = new AttributeTableModel();
private ExtendedJTable attributeTable = new ExtendedJTable();
private JList matchesList = new JList();
/**
* A label which displays the status of the current XPath entered/selected by the user.
*/
JLabel errorLabel = new JLabel();
/**
* A model which provides the XML code of all elements which match the current selected XPath.
*/
private XPathMatchesListModel matchesListModel;
/**
* User editable field for displaying/entering the current XPath.
*/
private ExtendedJTextField expressionField = new ExtendedJTextField();
private JButton applyButton;
/**
* There must be a configuration given, but might be empty.
*
* @throws OperatorException
*/
public XMLExampleExpressionWizardStep(AbstractWizard parent, final XMLResultSetConfiguration configuration) throws OperatorException {
super("importwizard.xml.example_expression");
this.configuration = configuration;
attributeTable.setModel(attributeTableModel);
// only select entire rows
attributeTable.setCellSelectionEnabled(false);
attributeTable.setRowSelectionAllowed(true);
// adding components
JPanel leftBarPanel = new JPanel(new GridBagLayout());
{
GridBagConstraints leftBarConstraints = new GridBagConstraints();
leftBarConstraints.insets = new Insets(0, 5, 5, 5);
leftBarConstraints.fill = GridBagConstraints.BOTH;
leftBarConstraints.weightx = 1;
leftBarConstraints.weighty = 0.7;
leftBarConstraints.gridwidth = GridBagConstraints.REMAINDER;
leftBarPanel.add(new ExtendedJScrollPane(xmlTreeView), leftBarConstraints);
leftBarConstraints.weighty = 0.3;
leftBarConstraints.insets = new Insets(5, 5, 5, 5);
leftBarPanel.add(new ExtendedJScrollPane(attributeTable), leftBarConstraints);
leftBarConstraints.weighty = 0;
leftBarConstraints.insets = new Insets(5, 5, 0, 5);
applyButton = new JButton();
applyButton.setAction(new ResourceAction("importwizard.xml.example_expression.apply_selection") {
private static final long serialVersionUID = 1L;
@Override
public void actionPerformed(ActionEvent e) {
String xpath = getXPathFromSelection();
expressionField.setText(xpath);
}
});
applyButton.setEnabled(false);
leftBarPanel.add(applyButton, leftBarConstraints);
}
JPanel rightBarPanel = new JPanel(new GridBagLayout());
{
GridBagConstraints rightBarConstraints = new GridBagConstraints();
rightBarConstraints.insets = new Insets(0, 5, 0, 5);
rightBarConstraints.weightx = 1;
rightBarConstraints.weighty = 0;
rightBarConstraints.gridwidth = GridBagConstraints.REMAINDER;
rightBarConstraints.fill = GridBagConstraints.BOTH;
JLabel matchesLabel = new JLabel(I18N.getGUILabel("importwizard.xml.example_expression.matches_label", "100"));
rightBarPanel.add(matchesLabel, rightBarConstraints);
rightBarConstraints.weighty = 1;
rightBarConstraints.insets = new Insets(5, 5, 5, 5);
rightBarPanel.add(new ExtendedJScrollPane(matchesList), rightBarConstraints);
}
GridBagConstraints c = new GridBagConstraints();
c.insets = new Insets(5, 5, 5, 5);
c.fill = GridBagConstraints.BOTH;
c.weighty = 1d;
c.weightx = 0.3d;
component.add(leftBarPanel, c);
c.gridwidth = GridBagConstraints.REMAINDER;
c.weightx = .7;
component.add(rightBarPanel, c);
c.weightx = 1d;
c.weighty = 0d;
errorLabel.setForeground(Color.RED);
c.weighty = 0;
component.add(errorLabel, c);
component.add(expressionField, c);
// listeners
final UpdateQueue xpathUpdateQueue = new UpdateQueue("xpath_updater");
parent.addWindowListener(new WindowAdapter() {
@Override
public void windowClosed(WindowEvent e) {
xpathUpdateQueue.shutdown();
}
});
xpathUpdateQueue.start();
expressionField.getModel().addTextChangeListener(new TextChangeListener() {
@Override
public void informTextChanged(final String newValue) {
errorLabel.setForeground(Color.GRAY);
errorLabel.setText(I18N.getGUILabel("xml_reader.wizard.evaluating"));
if (matchesListModel != null) {
xpathUpdateQueue.execute(new Runnable() {
public void run() {
matchesListModel.setXPathExpression(newValue);
fireStateChanged();
}
});
}
}
});
xmlTreeView.getSelectionModel().addTreeSelectionListener(new TreeSelectionListener() {
/**
* Whenever the selection changes, the attributeTableModel is updated to contain only
* those attributes which have the same names and values in all selected Elements.
*
* @see javax.swing.event.TreeSelectionListener#valueChanged(javax.swing.event.TreeSelectionEvent)
*/
@Override
public void valueChanged(TreeSelectionEvent e) {
Set<Element> elements = xmlTreeView.getElementsFromSelection();
List<Pair<String,String>> commonAncestors = XMLDomHelper.getCommonAncestorNames(elements);
int ancestorCount = commonAncestors.size();
Set<Element> ancestorsAtCurrentLevel = elements;
Set<XMLDomHelper.AttributeNamespaceValue> allAttributes = new HashSet<XMLDomHelper.AttributeNamespaceValue>();
for (int i = ancestorCount-1; i >= 0; --i) {
Set<XMLDomHelper.AttributeNamespaceValue> currentLevelAttributes = XMLDomHelper.getCommonAttributes(ancestorsAtCurrentLevel);
for(XMLDomHelper.AttributeNamespaceValue attribute : currentLevelAttributes) {
String elementName = commonAncestors.get(i).getSecond();
elementName = getXPathFromElementList(commonAncestors.subList(0, i+1));
attribute.setElement(elementName);
}
allAttributes.addAll(currentLevelAttributes);
ancestorsAtCurrentLevel = XMLDomHelper.getDirectAncestors(ancestorsAtCurrentLevel);
}
attributeTableModel.setAttributes(allAttributes);
applyButton.setEnabled(!elements.isEmpty());
}
});
// configure renderer
matchesList.setCellRenderer(new ListCellRenderer() {
private JTextArea area = new JTextArea();
private JPanel wraperPanel = new JPanel(new GridLayout(1, 1));
private JPanel emptyPanel = new JPanel();
{
wraperPanel.add(area);
wraperPanel.setBorder(new EmptyBorder(new Insets(0, 0, 1, 0)));
wraperPanel.setBackground(Color.BLACK);
wraperPanel.setOpaque(true);
}
@Override
public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
ByteArrayOutputStream os = new ByteArrayOutputStream();
try {
Charset encoding = Encoding.getEncoding("UTF-8");
XMLTools.stream(new DOMSource((Node) value), new StreamResult(new OutputStreamWriter(os, encoding)), encoding, XML_PROPERTIES);
area.setText(os.toString("UTF-8"));
return wraperPanel;
} catch (XMLException e) {
return emptyPanel;
} catch (UnsupportedEncodingException e) {
return emptyPanel;
}
}
});
}
/**
* Returns an XPath which resembles the user's selection in both the xml tree and the
* attribute table.
*/
private String getXPathFromSelection() {
List<Pair<String,String>> commonAncestors = XMLDomHelper.getCommonAncestorNames(xmlTreeView.getElementsFromSelection());
int[] selectedAttributeRows = attributeTable.getSelectedRows();
// map element names to attribute value xpaths
Map<String,String> elementToAttributeValues = new HashMap<String, String>();
for (int rowIndex : selectedAttributeRows) {
int modelIndex = attributeTable.getModelIndex(rowIndex);
String elementName = (String)attributeTableModel.getValueAt(modelIndex, AttributeTableModel.ELEMENT_COLUMN);
String attributeName = (String)attributeTableModel.getValueAt(modelIndex, AttributeTableModel.ATTRIBUTE_COLUMN);
String attributeValue = (String)attributeTableModel.getValueAt(modelIndex, AttributeTableModel.VALUE_COLUMN);
StringBuilder builder = new StringBuilder();
String currentXPath = elementToAttributeValues.get(elementName);
if (currentXPath != null) {
builder.append(currentXPath);
}
builder.append("[@");
builder.append(attributeName);
builder.append("=\"");
builder.append(attributeValue);
builder.append("\"]");
elementToAttributeValues.put(elementName, builder.toString());
}
int i = 0;
StringBuilder builder = new StringBuilder();
if (!commonAncestors.isEmpty()) {
builder.append("/");
for (Pair<String,String > ancestor : commonAncestors) {
++i;
String xPathWoAttributes = getXPathFromElementList(commonAncestors.subList(0, i));
String xPathForAttributes = elementToAttributeValues.get(xPathWoAttributes);
builder.append("/");
if (ancestor.getFirst() != null) {
builder.append(configuration.getNamespaceId(ancestor.getFirst()));
builder.append(":");
}
builder.append(ancestor.getSecond());
if (xPathForAttributes != null) {
builder.append(xPathForAttributes);
}
}
}
return builder.toString();
}
/**
* Creates an XPath expression from the given element names.
* @param elementNames A list of {@link Pair}s with first==elementNamespace and second==elementName.
*/
private String getXPathFromElementList(List<Pair<String, String>> elementNames) {
StringBuilder sb = new StringBuilder();
if (!elementNames.isEmpty()) {
sb.append("/");
}
for (Pair<String,String> element : elementNames) {
sb.append("/");
if (element.getFirst() != null) {
sb.append(configuration.getNamespaceId(element.getFirst()));
sb.append(":");
}
sb.append(element.getSecond());
}
return sb.toString();
}
@Override
protected boolean performEnteringAction(WizardStepDirection direction) {
try {
xmlTreeModel = new XMLTreeModel(configuration.getDocumentObjectModel().getDocumentElement(), false);
xmlTreeView.setNamespacesMap(configuration.getNamespacesMap());
xmlTreeView.setModel(xmlTreeModel);
matchesListModel = new XPathMatchesListModel(configuration.getDocumentObjectModel(), configuration.getNamespacesMap(), configuration.getDefaultNamespaceURI(), 100);
matchesListModel.addListener(new XPathMatchesListModel.XPathMatchesResultListener() {
@Override
public void informStateChange(String message, boolean error) {
errorLabel.setText(message);
if (error) {
errorLabel.setForeground(Color.RED);
} else {
errorLabel.setForeground(Color.BLACK);
}
}
});
matchesList.setModel(matchesListModel);
if (configuration.getExampleXPath() != null)
expressionField.setText(configuration.getExampleXPath());
else
expressionField.setText("");
} catch (OperatorException e) {
errorLabel.setForeground(Color.RED);
errorLabel.setText(I18N.getGUILabel("xml_reader.wizard.cannot_load_dom", e));
}
return true;
}
@Override
protected boolean performLeavingAction(WizardStepDirection direction) {
configuration.setExampleXPath(expressionField.getText());
return true;
}
@Override
protected JComponent getComponent() {
return component;
}
@Override
protected boolean canProceed() {
return matchesListModel != null && matchesListModel.getSize() > 0;
}
@Override
protected boolean canGoBack() {
return true;
}
}