/**
* Copyright (C) 2001-2017 by RapidMiner and the contributors
*
* Complete list of developers available at our web site:
*
* http://rapidminer.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;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Observable;
import java.util.Observer;
import java.util.Set;
import javax.swing.Action;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.ScrollPaneConstants;
import javax.swing.SwingUtilities;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.event.TableModelEvent;
import javax.swing.table.TableColumnModel;
import javax.swing.table.TableModel;
import com.rapidminer.datatable.DataTableExampleSetAdapter;
import com.rapidminer.example.ExampleSet;
import com.rapidminer.gui.tools.CellColorProviderAlternating;
import com.rapidminer.gui.tools.ExtendedJTable;
import com.rapidminer.gui.tools.ProgressThread;
import com.rapidminer.gui.tools.ResourceAction;
import com.rapidminer.gui.tools.ResourceLabel;
import com.rapidminer.gui.tools.SwingTools;
import com.rapidminer.gui.tools.dialogs.wizards.AbstractWizard.WizardStepDirection;
import com.rapidminer.gui.tools.dialogs.wizards.WizardStep;
import com.rapidminer.gui.tools.table.EditableTableHeader;
import com.rapidminer.gui.tools.table.EditableTableHeaderColumn;
import com.rapidminer.gui.viewer.DataTableViewerTableModel;
import com.rapidminer.operator.OperatorException;
import com.rapidminer.operator.nio.model.ColumnMetaData;
import com.rapidminer.operator.nio.model.DataResultSet;
import com.rapidminer.operator.nio.model.ParsingError;
import com.rapidminer.operator.nio.model.WizardState;
import com.rapidminer.parameter.ParameterTypeDateFormat;
import com.rapidminer.tools.I18N;
/**
* This Wizard Step might be used to defined the meta data of each attribute.
*
* @author Sebastian Land, Simon Fischer
*/
public class MetaDataDeclarationWizardStep extends WizardStep {
/** Publicly exposes the method {@link #configurePropertiesFromAction(Action)} public. */
private static class ReconfigurableButton extends JButton {
private static final long serialVersionUID = 1L;
private ReconfigurableButton(Action action) {
super(action);
}
@Override
protected void configurePropertiesFromAction(Action a) {
super.configurePropertiesFromAction(a);
}
}
private Action reloadAction = new ResourceAction("wizard.validate_value_types") {
private static final long serialVersionUID = 1L;
@Override
public void actionPerformed(ActionEvent e) {
toggleReload();
}
};
private Action cancelReloadAction = new ResourceAction("wizard.abort_validate_value_types") {
private static final long serialVersionUID = 1L;
@Override
public void actionPerformed(ActionEvent e) {
toggleReload();
}
};
private ReconfigurableButton reloadButton = new ReconfigurableButton(reloadAction);
private Action guessValueTypes = new ResourceAction("wizard.guess_value_types") {
private static final long serialVersionUID = 1L;
@Override
public void actionPerformed(ActionEvent e) {
toggleGuessValueTypes();
}
};
private Action cancelGuessValueTypes = new ResourceAction("wizard.abort_guess_value_types") {
private static final long serialVersionUID = 1L;
@Override
public void actionPerformed(ActionEvent e) {
toggleGuessValueTypes();
}
};
private ReconfigurableButton guessButton = new ReconfigurableButton(guessValueTypes);
private JCheckBox errorsAsMissingBox = new JCheckBox(new ResourceAction("wizard.error_tolerant") {
private static final long serialVersionUID = 1L;
@Override
public void actionPerformed(ActionEvent e) {
state.getTranslationConfiguration().setFaultTolerant(errorsAsMissingBox.isSelected());
}
});
private JCheckBox filterErrorsBox = new JCheckBox(new ResourceAction("wizard.show_error_rows") {
private static final long serialVersionUID = 1L;
@Override
public void actionPerformed(ActionEvent e) {
if (filteredModel != null) {
filteredModel.setFilterEnabled(filterErrorsBox.isSelected());
}
}
});
private JComboBox<String> dateFormatField = new JComboBox<>(ParameterTypeDateFormat.PREDEFINED_DATE_FORMATS);
private JCheckBox limitedPreviewBox = new JCheckBox(I18N.getMessage(I18N.getGUIBundle(),
"gui.action.importwizard.limited_preview.label", ImportWizardUtils.getPreviewLength()));
private WizardState state;
private JPanel panel = new JPanel(new BorderLayout());
private JScrollPane tableScrollPane;
private ErrorTableModel errorTableModel = new ErrorTableModel();
private RowFilteringTableModel filteredModel;
private JLabel errorLabel = new JLabel();
private boolean canProceed = true;
private MetaDataValidator headerValidator;
private final LoadingContentPane loadingContentPane;
public MetaDataDeclarationWizardStep(WizardState state) {
super("importwizard.metadata");
limitedPreviewBox.setSelected(true);
this.state = state;
dateFormatField.setEditable(true);
dateFormatField.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
MetaDataDeclarationWizardStep.this.state.getTranslationConfiguration().setDatePattern(
(String) dateFormatField.getSelectedItem());
}
});
JPanel buttonPanel = new JPanel(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
gbc.gridx = 0;
gbc.gridy = 0;
gbc.insets = new Insets(0, 0, 5, 10);
gbc.anchor = GridBagConstraints.WEST;
buttonPanel.add(reloadButton, gbc);
gbc.gridx += 1;
buttonPanel.add(guessButton, gbc);
JLabel label = new ResourceLabel("date_format");
label.setLabelFor(dateFormatField);
gbc.gridx += 1;
buttonPanel.add(label, gbc);
gbc.gridx += 1;
buttonPanel.add(dateFormatField, gbc);
gbc.gridx += 1;
gbc.weightx = 1.0;
gbc.fill = GridBagConstraints.HORIZONTAL;
buttonPanel.add(new JLabel(), gbc);
gbc.gridx = 0;
gbc.gridy = 1;
gbc.gridwidth = 2;
gbc.weightx = 0.0;
gbc.fill = GridBagConstraints.HORIZONTAL;
buttonPanel.add(limitedPreviewBox, gbc);
panel.add(buttonPanel, BorderLayout.NORTH);
JPanel errorPanel = new JPanel(new GridBagLayout());
GridBagConstraints c = new GridBagConstraints();
c.fill = GridBagConstraints.BOTH;
c.anchor = GridBagConstraints.FIRST_LINE_START;
c.ipadx = c.ipady = 4;
c.weighty = 0;
c.weightx = 1;
c.gridwidth = 1;
c.weightx = 1;
errorPanel.add(errorLabel, c);
c.weightx = 0;
c.gridwidth = GridBagConstraints.RELATIVE;
errorPanel.add(errorsAsMissingBox, c);
c.weightx = 0;
c.gridwidth = GridBagConstraints.REMAINDER;
errorPanel.add(filterErrorsBox, c);
final JTable errorTable = new JTable(errorTableModel);
errorTable.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
@Override
public void valueChanged(ListSelectionEvent e) {
if (!e.getValueIsAdjusting()) {
final int selected = errorTable.getSelectedRow();
if (selected >= 0) {
ParsingError error = errorTableModel.getErrorInRow(selected);
int row = error.getExampleIndex();
row = filteredModel.inverseTranslateRow(row);
if (row == -1) {
return;
}
int col = error.getColumn();
previewTable.setRowSelectionInterval(row, row);
previewTable.setColumnSelectionInterval(col, col);
}
}
}
});
final JScrollPane errorScrollPane = new JScrollPane(errorTable);
errorScrollPane.setPreferredSize(new Dimension(500, 80));
c.weighty = 1;
c.gridwidth = GridBagConstraints.REMAINDER;
errorPanel.add(errorScrollPane, c);
panel.add(errorPanel, BorderLayout.SOUTH);
final JLabel dummy = new JLabel("-");
dummy.setPreferredSize(new Dimension(500, 500));
dummy.setMinimumSize(new Dimension(500, 500));
tableScrollPane = new JScrollPane(dummy, ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS,
ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS);
loadingContentPane = new LoadingContentPane("loading_data", tableScrollPane);
panel.add(loadingContentPane, BorderLayout.CENTER);
}
@Override
protected boolean performEnteringAction(WizardStepDirection direction) {
loadingContentPane.init();
dateFormatField.setSelectedItem(state.getTranslationConfiguration().getDatePattern());
errorsAsMissingBox.setSelected(state.getTranslationConfiguration().isFaultTolerant());
new ProgressThread("loading_data") {
@Override
public void run() {
try (DataResultSet previewResultSet = state.getDataResultSetFactory().makeDataResultSet(state.getOperator())) {
state.getTranslationConfiguration().reconfigure(previewResultSet);
} catch (OperatorException e1) {
ImportWizardUtils.showErrorMessage(state.getDataResultSetFactory().getResourceName(), e1.toString(), e1);
return;
}
try {
TableModel dataPreview = state.getDataResultSetFactory().makePreviewTableModel(getProgressListener());
// Copy name annotations to name
int nameIndex = state.getTranslationConfiguration().getNameRow();
if (nameIndex != -1 && dataPreview != null) {
for (int i = 0; i < dataPreview.getColumnCount(); i++) {
ColumnMetaData columnMetaData = state.getTranslationConfiguration().getColumnMetaData(i);
final String foundName = String.valueOf(dataPreview.getValueAt(nameIndex, i));
if (foundName != null && !foundName.isEmpty()) {
columnMetaData.setUserDefinedAttributeName(foundName);
}
}
}
} catch (Exception e) {
ImportWizardUtils.showErrorMessage(state.getDataResultSetFactory().getResourceName(), e.toString(), e);
return;
}
guessValueTypes();
}
}.start();
return true;
}
public void updateErrors(final Set<?> updateColumns) {
updateErrors();
((UpdatableHeaderRowFilteringTableModel) previewTable.getModel()).updateHeader(updateColumns);
}
public void updateErrors() {
final List<ParsingError> errorList = new ArrayList<ParsingError>();
canProceed = true;
if (headerValidator.getErrors().size() > 0) {
List<ParsingError> headerErrors = headerValidator.getErrors();
errorList.addAll(headerErrors);
canProceed = false;
}
errorList.addAll(state.getTranslator().getErrors());
final int size = errorList.size();
errorLabel.setText(size + " errors.");
if (size == 0) {
errorLabel.setIcon(SwingTools.createIcon("16/ok.png"));
} else {
errorLabel.setIcon(SwingTools.createIcon("16/error.png"));
}
errorTableModel.setErrors(errorList);
fireStateChanged();
}
private class UpdatableHeaderRowFilteringTableModel extends RowFilteringTableModel {
private static final long serialVersionUID = 1L;
public UpdatableHeaderRowFilteringTableModel(TableModel wrappedModel, int[] rowMap, boolean enabled) {
super(wrappedModel, rowMap, enabled);
}
public void updateHeader(Set<?> columns) {
fireTableCellUpdated(TableModelEvent.HEADER_ROW, TableModelEvent.ALL_COLUMNS);
}
}
private void updateTableModel(ExampleSet exampleSet) {
if (previewTable == null) {
previewTable = new ExtendedJTable(false, false, false) {
private static final long serialVersionUID = 1L;
@Override
public void createDefaultColumnsFromModel() {
TableModel m = getModel();
if (m != null) {
// Remove any current columns
TableColumnModel cm = getColumnModel();
while (cm.getColumnCount() > 0) {
cm.removeColumn(cm.getColumn(0));
}
// Create new columns from the data model info
for (int i = 0; i < m.getColumnCount(); i++) {
EditableTableHeaderColumn col = new EditableTableHeaderColumn(i);
col.setHeaderValue(state.getTranslationConfiguration().getColumnMetaData()[i]);
col.setHeaderRenderer(headerRenderer);
col.setHeaderEditor(headerEditor);
addColumn(col);
}
}
}
};
}
previewTable.setAutoCreateColumnsFromModel(true);
// data model
DataTableViewerTableModel model = new DataTableViewerTableModel(new DataTableExampleSetAdapter(exampleSet, null));
List<Integer> rowsList = new LinkedList<Integer>();
int lastHit = -1;
for (ParsingError error : state.getTranslator().getErrors()) {
if (error.getExampleIndex() != lastHit) {
rowsList.add(error.getExampleIndex());
lastHit = error.getExampleIndex();
}
}
int[] rowMap = new int[rowsList.size()];
int j = 0;
for (Integer row : rowsList) {
rowMap[j++] = row;
}
filteredModel = new UpdatableHeaderRowFilteringTableModel(model, rowMap, filterErrorsBox.isSelected());
previewTable.setModel(filteredModel);
// Header validator
this.headerValidator = new MetaDataValidator();
headerValidator.addObserver(new Observer() {
@Override
public void update(Observable o, Object arg) {
if (arg instanceof Set<?>) {
updateErrors((Set<?>) arg);
}
}
});
// Header model
TableColumnModel columnModel = previewTable.getColumnModel();
previewTable.setTableHeader(new EditableTableHeader(columnModel));
headerRenderer = new MetaDataTableHeaderCellEditor(headerValidator);
headerEditor = new MetaDataTableHeaderCellEditor(headerValidator);
for (int i = 0; i < previewTable.getColumnCount(); i++) {
EditableTableHeaderColumn col = (EditableTableHeaderColumn) previewTable.getColumnModel().getColumn(i);
ColumnMetaData cmd = state.getTranslationConfiguration().getColumnMetaData()[i];
headerValidator.addColumnMetaData(cmd, i);
col.setHeaderValue(cmd);
col.setHeaderRenderer(headerRenderer);
col.setHeaderEditor(headerEditor);
}
headerValidator.checkForDuplicates();
previewTable.getTableHeader().setReorderingAllowed(false);
previewTable.setCellColorProvider(new CellColorProviderAlternating() {
@Override
public Color getCellColor(int row, int column) {
row = filteredModel.translateRow(row);
ParsingError error = state.getTranslator().getErrorByExampleIndexAndColumn(row, column);
if (error != null) {
return SwingTools.DARK_YELLOW;
} else {
return super.getCellColor(row, column);
}
}
});
tableScrollPane.setViewportView(previewTable);
}
@Override
protected boolean performLeavingAction(WizardStepDirection direction) {
if (direction == WizardStepDirection.FINISH) {
try {
if (state.getTranslator() != null) {
state.getTranslator().close();
}
} catch (OperatorException e) {
ImportWizardUtils.showErrorMessage(state.getDataResultSetFactory().getResourceName(), e.toString(), e);
}
// use settings edited by user even if he never pressed Enter or otherwise confirmed his
// changes
for (int i = 0; i < previewTable.getColumnCount(); i++) {
EditableTableHeaderColumn col = (EditableTableHeaderColumn) previewTable.getColumnModel().getColumn(i);
if (col.getHeaderEditor() instanceof MetaDataTableHeaderCellEditor) {
MetaDataTableHeaderCellEditor editor = (MetaDataTableHeaderCellEditor) col.getHeaderEditor();
editor.updateColumnMetaData();
}
}
}
return true;
}
@Override
protected boolean canGoBack() {
return true;
}
@Override
protected boolean canProceed() {
return canProceed;
}
@Override
protected JComponent getComponent() {
return panel;
}
private void reload() {
reloadButton.configurePropertiesFromAction(cancelReloadAction);
new ProgressThread("loading_data") {
@Override
public void run() {
try (DataResultSet resultSet = state.getDataResultSetFactory().makeDataResultSet(null)) {
if (state.getTranslator() != null) {
state.getTranslator().close();
}
state.getTranslator().clearErrors();
final ExampleSet exampleSet = state.readNow(resultSet, limitedPreviewBox.isSelected(),
getProgressListener());
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
updateTableModel(exampleSet);
updateErrors();
}
});
} catch (OperatorException e) {
ImportWizardUtils.showErrorMessage(state.getDataResultSetFactory().getResourceName(), e.toString(), e);
} finally {
getProgressListener().complete();
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
reloadButton.configurePropertiesFromAction(reloadAction);
reloadButton.setEnabled(true);
isReloading = false;
}
});
}
}
}.start();
}
private void cancelReload() {
state.getTranslator().cancelLoading();
reloadButton.setEnabled(false);
}
private void guessValueTypes() {
loadingContentPane.init();
guessButton.configurePropertiesFromAction(cancelGuessValueTypes);
isGuessing = true;
new ProgressThread("guessing_value_types") {
@Override
public void run() {
Thread.yield();
getProgressListener().setTotal(100);
getProgressListener().setCompleted(1);
try (DataResultSet resultSet = state.getDataResultSetFactory().makeDataResultSet(null)) {
if (state.getTranslator() != null) {
state.getTranslator().close();
}
state.getTranslator().clearErrors();
state.getTranslationConfiguration().resetValueTypes();
state.getTranslator().guessValueTypes(state.getTranslationConfiguration(), resultSet,
state.getNumberOfPreviewRows(), getProgressListener());
if (!state.getTranslator().isGuessingCancelled()) {
setDisplayLabel("generate_preview");
final ExampleSet exampleSet = state.readNow(resultSet, limitedPreviewBox.isSelected(),
getProgressListener());
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
updateTableModel(exampleSet);
updateErrors();
loadingContentPane.loadingFinished();
}
});
} else {
loadingContentPane.loadingFinished();
}
} catch (OperatorException e) {
ImportWizardUtils.showErrorMessage(state.getDataResultSetFactory().getResourceName(), e.toString(), e);
} finally {
getProgressListener().complete();
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
guessButton.configurePropertiesFromAction(guessValueTypes);
guessButton.setEnabled(true);
isGuessing = false;
}
});
}
}
}.start();
}
private boolean isGuessing = false;
private boolean isReloading = false;
private ExtendedJTable previewTable;
private MetaDataTableHeaderCellEditor headerRenderer;
private MetaDataTableHeaderCellEditor headerEditor;
private void cancelGuessing() {
state.getTranslator().cancelGuessing();
state.getTranslator().cancelLoading();
guessButton.setEnabled(false);
}
private void toggleGuessValueTypes() {
isGuessing = !isGuessing;
if (isGuessing) {
guessValueTypes();
} else {
cancelGuessing();
}
}
private void toggleReload() {
isReloading = !isReloading;
if (isReloading) {
reload();
} else {
cancelReload();
}
}
}