/**
* 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.studio.io.gui.internal.steps.configuration;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Font;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.LinkedList;
import java.util.List;
import java.util.logging.Level;
import javax.swing.BorderFactory;
import javax.swing.ImageIcon;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JLayeredPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.OverlayLayout;
import javax.swing.SwingConstants;
import javax.swing.border.Border;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.JTableHeader;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumn;
import javax.swing.table.TableColumnModel;
import com.rapidminer.core.io.data.ColumnMetaData;
import com.rapidminer.core.io.data.DataSetException;
import com.rapidminer.core.io.data.DataSetMetaData;
import com.rapidminer.core.io.data.source.DataSource;
import com.rapidminer.core.io.gui.InvalidConfigurationException;
import com.rapidminer.gui.look.Colors;
import com.rapidminer.gui.tools.AttributeGuiTools;
import com.rapidminer.gui.tools.ColoredTableCellRenderer;
import com.rapidminer.gui.tools.ExtendedJScrollPane;
import com.rapidminer.gui.tools.ExtendedJTable;
import com.rapidminer.gui.tools.ProgressThread;
import com.rapidminer.gui.tools.ResourceLabel;
import com.rapidminer.gui.tools.RowNumberTable;
import com.rapidminer.gui.tools.SwingTools;
import com.rapidminer.parameter.ParameterTypeDateFormat;
import com.rapidminer.studio.io.gui.internal.DataImportWizardUtils;
import com.rapidminer.studio.io.gui.internal.DataWizardEventType;
import com.rapidminer.tools.I18N;
import com.rapidminer.tools.LogService;
/**
* The view shown during the data configuration step. It contains a table showing the loaded data
* preview and allows the user to configure the data via the table header.
*
* @author Nils Woehler, Gisa Schaefer
* @since 7.0.0
*/
final class ConfigureDataView extends JPanel {
private static final long serialVersionUID = 1L;
private static final String PROGRESS_THREAD_ID = "io.dataimport.step.data_column_configuration.prepare_data_preview";
public static final Color BACKGROUND_COLUMN_DISABLED = new Color(232, 232, 232);
public static final Color FOREGROUND_COLUMN_DISABLED = new Color(189, 189, 189);
private static final String ERROR_TOOLTIP_CONTENT = "<p style=\"padding-bottom:4px\">"
+ I18N.getGUILabel("io.dataimport.step.data_column_configuration.replace_errors_checkbox.tip") + "</p>";
/** cell renderer that displays icons */
private static final DefaultTableCellRenderer ICON_CELL_RENDERER = new DefaultTableCellRenderer() {
private static final long serialVersionUID = 1L;
@Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus,
int row, int column) {
JLabel label = (JLabel) super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
label.setText(null);
label.setIcon((ImageIcon) value);
return label;
}
};
/**
* Colored border for error cells. The empty border part together with the colored border gives
* a border of the same size as {@link ColoredTableCellRenderer#CELL_BORDER}
*/
private static final Border ERROR_BORDER = BorderFactory
.createCompoundBorder(BorderFactory.createLineBorder(Color.RED, 2), BorderFactory.createEmptyBorder(0, 8, 0, 3));
private static final Border WARNING_BORDER = BorderFactory.createCompoundBorder(
BorderFactory.createLineBorder(Color.ORANGE, 2), BorderFactory.createEmptyBorder(0, 8, 0, 3));
/** Cell renderer that marks error cells with a colored border */
private final ColoredTableCellRenderer ERROR_MARKING_CELL_RENDERER = new ColoredTableCellRenderer() {
@Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus,
int row, int column) {
JLabel label = (JLabel) super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
if (tableModel.hasError(row, column)) {
if (errorHandlingCheckBox.isSelected()) {
label.setBorder(WARNING_BORDER);
} else {
label.setBorder(ERROR_BORDER);
}
}
return label;
}
};
private List<ChangeListener> changeListeners = new LinkedList<>();
private DataSetMetaData dataSetMetaData;
private ConfigureDataTableModel tableModel;
private JPanel centerPanel;
private JPanel upperPanel;
private final JCheckBox errorHandlingCheckBox;
private final ConfigureDataValidator validator;
private final ErrorWarningTableModel errorTableModel;
private final CollapsibleErrorTable collapsibleErrorTable;
private final JComboBox<String> dateFormatField;
private Window owner;
private boolean fatalError;
/**
* The constructor that creates a new {@link ConfigureDataView} instance.
*/
public ConfigureDataView(JDialog owner) {
this.owner = owner;
validator = new ConfigureDataValidator();
errorTableModel = new ErrorWarningTableModel(validator);
errorTableModel.addTableModelListener(new TableModelListener() {
@Override
public void tableChanged(TableModelEvent e) {
fireStateChanged();
}
});
collapsibleErrorTable = new CollapsibleErrorTable(errorTableModel);
setLayout(new BorderLayout());
upperPanel = new JPanel(new GridBagLayout());
upperPanel.setBorder(BorderFactory.createEmptyBorder(0, 0, 20, 0));
upperPanel.setVisible(false);
JPanel errorHandlingPanel = new JPanel(new BorderLayout());
errorHandlingCheckBox = new JCheckBox(
I18N.getGUILabel("io.dataimport.step.data_column_configuration.replace_errors_checkbox.label"));
errorHandlingCheckBox.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
DataImportWizardUtils.logStats(DataWizardEventType.ERROR_HANDLING_CHANGED,
Boolean.toString(errorHandlingCheckBox.isSelected()));
errorTableModel.setFaultTolerant(errorHandlingCheckBox.isSelected());
tableModel.fireTableDataChanged();
}
});
errorHandlingPanel.add(errorHandlingCheckBox, BorderLayout.CENTER);
SwingTools.addTooltipHelpIconToLabel(ERROR_TOOLTIP_CONTENT, errorHandlingPanel, owner);
dateFormatField = new JComboBox<String>(ParameterTypeDateFormat.PREDEFINED_DATE_FORMATS);
dateFormatField.setEditable(true);
// do not fire action event when using keyboard to move up and down
dateFormatField.putClientProperty("JComboBox.isTableCellEditor", Boolean.TRUE);
dateFormatField.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
// prevent two updates on enter key
if (!"comboBoxChanged".equals(e.getActionCommand())) {
return;
}
String datePattern = (String) dateFormatField.getSelectedItem();
if (datePattern != null && !datePattern.isEmpty()) {
updateDateFormat(new SimpleDateFormat(datePattern));
}
}
});
JLabel datelabel = new ResourceLabel("date_format");
datelabel.setLabelFor(dateFormatField);
GridBagConstraints gbc = new GridBagConstraints();
gbc.gridx = GridBagConstraints.RELATIVE;
gbc.fill = GridBagConstraints.NONE;
gbc.insets = new Insets(0, 30, 0, 5);
gbc.weightx = 0.0;
gbc.anchor = GridBagConstraints.WEST;
upperPanel.add(datelabel, gbc);
gbc.insets = new Insets(0, 0, 0, 70);
upperPanel.add(dateFormatField, gbc);
gbc.insets = new Insets(0, 0, 0, 0);
upperPanel.add(errorHandlingPanel, gbc);
gbc.fill = GridBagConstraints.BOTH;
gbc.weightx = 1.0;
upperPanel.add(new JLabel(), gbc);
add(upperPanel, BorderLayout.NORTH);
centerPanel = new JPanel(new GridBagLayout());
add(centerPanel, BorderLayout.CENTER);
add(collapsibleErrorTable, BorderLayout.SOUTH);
collapsibleErrorTable.setVisible(false);
}
/**
* Takes the current data source, copies the meta data and configures the view according to the
* data and meta data within a progress thread.
*
* @param dataSource
* the data source to retrieve the meta data and preview data from
* @throws InvalidConfigurationException
* in case the meta data could not be retrieved
*/
void updatePreviewContent(final DataSource dataSource) throws InvalidConfigurationException {
fatalError = false;
// copy meta data to work on copy instead of real instance
try {
dataSetMetaData = dataSource.getMetadata().copy();
} catch (DataSetException e) {
SwingTools.showSimpleErrorMessage(owner,
"io.dataimport.step.data_column_configuration.error_configuring_metadata", e.getMessage());
throw new InvalidConfigurationException();
}
errorHandlingCheckBox.setSelected(dataSetMetaData.isFaultTolerant());
DateFormat dateFormat = dataSetMetaData.getDateFormat();
if (dateFormat instanceof SimpleDateFormat) {
// remove action listeners before setting the date format pattern to prevent
// unneccessary update
ActionListener[] listeners = dateFormatField.getActionListeners();
dateFormatField.removeActionListener(listeners[0]);
dateFormatField.setSelectedItem(((SimpleDateFormat) dateFormat).toPattern());
dateFormatField.addActionListener(listeners[0]);
}
errorTableModel.setColumnMetaData(dataSetMetaData.getColumnMetaData());
validator.init(dataSetMetaData.getColumnMetaData());
ProgressThread loadDataPG = new ProgressThread(PROGRESS_THREAD_ID) {
@Override
public void run() {
getProgressListener().setTotal(100);
SwingTools.invokeLater(new Runnable() {
@Override
public void run() {
showNotificationLabel("io.dataimport.step.data_column_configuration.loading_preview");
}
});
// load table model
try {
tableModel = new ConfigureDataTableModel(dataSource, dataSetMetaData, getProgressListener());
validator.setParsingErrors(tableModel.getParsingErrors());
// adapt view after table has been loaded
SwingTools.invokeLater(new Runnable() {
@Override
public void run() {
// show error message in case preview is empty
if (tableModel.getRowCount() == 0) {
showErrorNotification("io.dataimport.step.data_column_configuration.no_data_available");
return;
}
// remove all components
centerPanel.removeAll();
// add preview table
ExtendedJTable previewTable = new ExtendedJTable(tableModel, false, false, false) {
private static final long serialVersionUID = 1L;
@Override
public Component prepareRenderer(TableCellRenderer renderer, int row, int column) {
Component c = super.prepareRenderer(renderer, row, column);
ColumnMetaData metaData = dataSetMetaData.getColumnMetaData().get(column);
if (metaData.isRemoved()) {
c.setBackground(BACKGROUND_COLUMN_DISABLED);
c.setForeground(FOREGROUND_COLUMN_DISABLED);
} else {
String role = metaData.getRole();
if (role != null) {
c.setBackground(AttributeGuiTools.getColorForAttributeRole(role));
} else {
c.setBackground(Color.WHITE);
}
c.setForeground(Color.BLACK);
}
return c;
};
};
previewTable.setColumnSelectionAllowed(false);
previewTable.setCellSelectionEnabled(false);
previewTable.setRowSelectionAllowed(false);
previewTable.setColoredTableCellRenderer(ERROR_MARKING_CELL_RENDERER);
previewTable.setShowPopupMenu(false);
// ensure same background as JPanels in case of only few rows
previewTable.setBackground(Colors.PANEL_BACKGROUND);
TableColumnModel columnModel = previewTable.getColumnModel();
// set cell renderer for column headers
previewTable.setTableHeader(new JTableHeader(columnModel));
for (int columnIndex = 0; columnIndex < columnModel.getColumnCount(); columnIndex++) {
TableColumn column = columnModel.getColumn(columnIndex);
ConfigureDataTableHeader headerRenderer = new ConfigureDataTableHeader(previewTable,
columnIndex, dataSetMetaData, validator, ConfigureDataView.this);
column.setHeaderRenderer(headerRenderer);
column.setMinWidth(120);
}
previewTable.getTableHeader().setReorderingAllowed(false);
// Create a layered pane to display both, the data table and a
// "preview" overlay
JLayeredPane layeredPane = new JLayeredPane();
layeredPane.setLayout(new OverlayLayout(layeredPane));
/*
* Hack to enlarge table columns in case of few columns. Add table to a
* full size JPanel and add the table header to the scroll pane.
*/
JPanel tablePanel = new JPanel(new BorderLayout());
tablePanel.add(previewTable, BorderLayout.CENTER);
JScrollPane scrollPane = new ExtendedJScrollPane(tablePanel);
scrollPane.setColumnHeaderView(previewTable.getTableHeader());
scrollPane.setBorder(null);
// show row numbers
scrollPane.setRowHeaderView(new RowNumberTable(previewTable));
layeredPane.add(scrollPane, JLayeredPane.DEFAULT_LAYER);
// Add "Preview" overlay
JPanel previewPanel = new JPanel(new BorderLayout());
previewPanel.setOpaque(false);
JLabel previewLabel = new JLabel(I18N.getGUILabel("csv_format_specification.preview_background"),
SwingConstants.CENTER);
previewLabel.setFont(previewLabel.getFont().deriveFont(Font.BOLD, 180));
previewLabel.setForeground(DataImportWizardUtils.getPreviewFontColor());
previewPanel.add(previewLabel, BorderLayout.CENTER);
layeredPane.add(previewPanel, JLayeredPane.PALETTE_LAYER);
GridBagConstraints constraint = new GridBagConstraints();
constraint.fill = GridBagConstraints.BOTH;
constraint.weightx = 1.0;
constraint.weighty = 1.0;
centerPanel.add(layeredPane, constraint);
centerPanel.revalidate();
centerPanel.repaint();
upperPanel.setVisible(true);
setupErrorTable();
fireStateChanged();
}
});
} catch (final DataSetException e) {
SwingTools.invokeLater(new Runnable() {
@Override
public void run() {
showErrorNotification("io.dataimport.step.data_column_configuration.error_loading_data",
e.getMessage());
}
});
} finally {
getProgressListener().complete();
}
}
};
loadDataPG.addDependency(PROGRESS_THREAD_ID);
loadDataPG.start();
}
/**
* Sets up the error table.
*/
private void setupErrorTable() {
// increase row height
collapsibleErrorTable.getTable().setRowHeight(20);
// make first column smaller and add special cell renderer
collapsibleErrorTable.getTable().getColumnModel().getColumn(0).setPreferredWidth(22);
collapsibleErrorTable.getTable().getColumnModel().getColumn(0).setMaxWidth(22);
collapsibleErrorTable.getTable().getColumnModel().getColumn(0).setCellRenderer(ICON_CELL_RENDERER);
// make second column smaller
collapsibleErrorTable.getTable().getColumnModel().getColumn(1).setPreferredWidth(50);
collapsibleErrorTable.getTable().getColumnModel().getColumn(1).setMaxWidth(100);
// make third column small
collapsibleErrorTable.getTable().getColumnModel().getColumn(2).setPreferredWidth(100);
collapsibleErrorTable.getTable().getColumnModel().getColumn(2).setMaxWidth(800);
// make last column bigger
collapsibleErrorTable.getTable().getColumnModel().getColumn(5).setMaxWidth(800);
collapsibleErrorTable.getTable().getColumnModel().getColumn(5).setPreferredWidth(400);
collapsibleErrorTable.update();
collapsibleErrorTable.setVisible(true);
}
void showErrorNotification(String i18nKey, Object... arguments) {
showNotificationLabel(i18nKey, arguments);
fatalError = true;
fireStateChanged();
}
/**
* Shows a central label displaying a notification to the user (e.g. for errors or during
* loading).
*
* @param i18nKey
* the notification I18N key to lookup the label text and icon
* @param arguments
* the I18N arguments
*/
private void showNotificationLabel(String i18nKey, Object... arguments) {
upperPanel.setVisible(false);
collapsibleErrorTable.setVisible(false);
GridBagConstraints constraint = new GridBagConstraints();
constraint.fill = GridBagConstraints.BOTH;
constraint.weightx = 1.0;
constraint.weighty = 1.0;
centerPanel.removeAll();
centerPanel.add(new JPanel(), constraint);
constraint.weightx = 0.0;
constraint.weighty = 0.0;
constraint.fill = GridBagConstraints.NONE;
constraint.anchor = GridBagConstraints.CENTER;
centerPanel.add(new ResourceLabel(i18nKey, arguments), constraint);
constraint.weightx = 1.0;
constraint.weighty = 1.0;
constraint.fill = GridBagConstraints.BOTH;
centerPanel.add(new JPanel(), constraint);
centerPanel.revalidate();
centerPanel.repaint();
}
/**
* @return the meta data for the current view.
*/
DataSetMetaData getMetaData() {
dataSetMetaData.setFaultTolerant(errorHandlingCheckBox.isSelected());
return dataSetMetaData;
}
/**
* Checks whether the current view configuration is valid.
*
* @throws InvalidConfigurationException
* in case the configuration is invalid
*
*/
public void validateConfiguration() throws InvalidConfigurationException {
if (fatalError || tableModel != null && (tableModel.getRowCount() == 0 || errorTableModel.getErrorCount() > 0)) {
throw new InvalidConfigurationException();
}
}
/**
* Registers a new change listener.
*
* @param changeListener
* the listener to register
*/
void addChangeListener(ChangeListener changeListener) {
this.changeListeners.add(changeListener);
}
/**
* Fires a {@link ChangeEvent} that informs the listeners of a changed state.
*/
private void fireStateChanged() {
ChangeEvent event = new ChangeEvent(this);
for (ChangeListener listener : changeListeners) {
try {
listener.stateChanged(event);
} catch (RuntimeException rte) {
LogService.getRoot().log(Level.WARNING,
"com.rapidminer.gui.io.dataimport.AbstractWizardStep.changelistener_failed", rte);
}
}
}
/**
* Updates date format for the date and reloads it.
*
* @param format
* the new date format
*/
private void updateDateFormat(SimpleDateFormat format) {
DataImportWizardUtils.logStats(DataWizardEventType.DATE_FORMAT_CHANGED, format.toPattern());
dataSetMetaData.setDateFormat(format);
ProgressThread rereadThread = new ProgressThread("io.dataimport.step.data_column_configuration.update_date_format") {
@Override
public void run() {
try {
tableModel.reread(getProgressListener());
} catch (final DataSetException e) {
SwingTools.invokeLater(new Runnable() {
@Override
public void run() {
showErrorNotification("io.dataimport.step.data_column_configuration.error_loading_data",
e.getMessage());
}
});
return;
}
SwingTools.invokeLater(new Runnable() {
@Override
public void run() {
validator.setParsingErrors(tableModel.getParsingErrors());
tableModel.fireTableDataChanged();
}
});
}
};
rereadThread.start();
}
}