![JAR search and dependency download from the Maven repository](/logo.png)
org.datacleaner.widgets.properties.MultipleInputColumnsPropertyWidget Maven / Gradle / Ivy
/**
* DataCleaner (community edition)
* Copyright (C) 2014 Free Software Foundation, Inc.
*
* This copyrighted material is made available to anyone wishing to use, modify,
* copy, or redistribute it subject to the terms and conditions of the GNU
* Lesser General Public License, as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
* for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this distribution; if not, write to:
* Free Software Foundation, Inc.
* 51 Franklin Street, Fifth Floor
* Boston, MA 02110-1301 USA
*/
package org.datacleaner.widgets.properties;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import javax.inject.Inject;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComponent;
import javax.swing.border.CompoundBorder;
import javax.swing.event.DocumentEvent;
import org.apache.commons.lang.ArrayUtils;
import org.apache.metamodel.util.CollectionUtils;
import org.apache.metamodel.util.HasNameMapper;
import org.datacleaner.actions.AddExpressionBasedColumnActionListener;
import org.datacleaner.actions.ReorderColumnsActionListener;
import org.datacleaner.api.ExpressionBasedInputColumn;
import org.datacleaner.api.InputColumn;
import org.datacleaner.data.MutableInputColumn;
import org.datacleaner.descriptors.ComponentDescriptor;
import org.datacleaner.descriptors.ConfiguredPropertyDescriptor;
import org.datacleaner.job.builder.AnalysisJobBuilder;
import org.datacleaner.job.builder.ComponentBuilder;
import org.datacleaner.job.builder.SourceColumnChangeListener;
import org.datacleaner.job.builder.TransformerChangeListener;
import org.datacleaner.job.builder.TransformerComponentBuilder;
import org.datacleaner.panels.DCPanel;
import org.datacleaner.util.DCDocumentListener;
import org.datacleaner.util.IconUtils;
import org.datacleaner.util.LabelUtils;
import org.datacleaner.util.ReflectionUtils;
import org.datacleaner.util.StringUtils;
import org.datacleaner.util.WidgetFactory;
import org.datacleaner.util.WidgetUtils;
import org.datacleaner.widgets.DCCheckBox;
import org.datacleaner.widgets.DCCheckBox.Listener;
import org.jdesktop.swingx.HorizontalLayout;
import org.jdesktop.swingx.JXTextField;
import org.jdesktop.swingx.VerticalLayout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Property widget for multiple input columns. Displays these as checkboxes.
*/
public class MultipleInputColumnsPropertyWidget extends AbstractPropertyWidget[]>
implements SourceColumnChangeListener, TransformerChangeListener, MutableInputColumn.Listener,
ReorderColumnsActionListener.ReorderColumnsCallback {
private static final Logger logger = LoggerFactory.getLogger(MultipleInputColumnsPropertyWidget.class);
private final Listener> checkBoxListener = (item, selected) -> {
if (isBatchUpdating()) {
return;
}
fireValueChanged();
};
private final Class> _dataType;
private final Map, DCCheckBox>> _checkBoxes;
private final ActionListener selectAllActionListener = e -> selectAll();
private final ActionListener selectNoneActionListener = e -> selectNone();
private final Map>, JComponent> _checkBoxDecorations;
private final DCPanel _buttonPanel;
private final JXTextField _searchDatastoreTextField;
private final DCCheckBox> _notAvailableCheckBox;
private volatile boolean _firstUpdate;
@Inject
public MultipleInputColumnsPropertyWidget(final ComponentBuilder componentBuilder,
final ConfiguredPropertyDescriptor propertyDescriptor) {
super(componentBuilder, propertyDescriptor);
_checkBoxes = new LinkedHashMap<>();
_checkBoxDecorations = new IdentityHashMap<>();
_firstUpdate = true;
_dataType = propertyDescriptor.getTypeArgument(0);
setLayout(new VerticalLayout(2));
_searchDatastoreTextField = WidgetFactory.createTextField("Search/filter columns");
_searchDatastoreTextField
.setBorder(new CompoundBorder(WidgetUtils.BORDER_CHECKBOX_LIST_INDENTATION, WidgetUtils.BORDER_THIN));
_searchDatastoreTextField.getDocument().addDocumentListener(new DCDocumentListener() {
@Override
protected void onChange(final DocumentEvent event) {
String text = _searchDatastoreTextField.getText();
if (StringUtils.isNullOrEmpty(text)) {
// when there is no search query, set all datastores
// visible
for (final JCheckBox cb : _checkBoxes.values()) {
cb.setVisible(true);
}
} else {
// do a case insensitive search
text = text.trim().toLowerCase();
for (final JCheckBox cb : _checkBoxes.values()) {
final String name = cb.getText().toLowerCase();
cb.setVisible(name.contains(text));
}
}
}
});
if (_dataType == null || _dataType == Object.class) {
_notAvailableCheckBox =
new DCCheckBox<>("- no columns available -", false);
} else {
_notAvailableCheckBox = new DCCheckBox<>(
"- no " + LabelUtils.getDataTypeLabel(_dataType)
+ " columns available -", false);
}
_notAvailableCheckBox.setEnabled(false);
_buttonPanel = new DCPanel();
_buttonPanel.setLayout(new HorizontalLayout(2));
_buttonPanel.setBorder(WidgetUtils.BORDER_CHECKBOX_LIST_INDENTATION);
final JButton selectAllButton = WidgetFactory.createDefaultButton("Select all");
selectAllButton.setFont(WidgetUtils.FONT_SMALL);
selectAllButton.addActionListener(selectAllActionListener);
_buttonPanel.add(selectAllButton);
final JButton selectNoneButton = WidgetFactory.createDefaultButton("Select none");
selectNoneButton.setFont(WidgetUtils.FONT_SMALL);
selectNoneButton.addActionListener(selectNoneActionListener);
_buttonPanel.add(selectNoneButton);
if (propertyDescriptor.isArray()) {
if (_dataType == String.class || _dataType == Object.class) {
final JButton expressionColumnButton =
WidgetFactory.createSmallButton(IconUtils.MODEL_COLUMN_EXPRESSION);
expressionColumnButton.setToolTipText("Create expression/value based column");
expressionColumnButton
.addActionListener(AddExpressionBasedColumnActionListener.forMultipleColumns(this));
_buttonPanel.add(expressionColumnButton);
}
final JButton reorderColumnsButton = WidgetFactory.createSmallButton(IconUtils.ACTION_REORDER_COLUMNS);
reorderColumnsButton.setToolTipText("Reorder columns");
reorderColumnsButton.addActionListener(new ReorderColumnsActionListener(this));
_buttonPanel.add(reorderColumnsButton);
}
add(_buttonPanel);
add(_searchDatastoreTextField);
}
@Override
public void initialize(final InputColumn>[] value) {
updateComponents(value);
_firstUpdate = false;
if (value != null && value.length > 0) {
// update selections in checkboxes
batchUpdateWidget(() -> {
reorderValue(value);
for (final DCCheckBox> cb : _checkBoxes.values()) {
if (ArrayUtils.contains(value, cb.getValue())) {
cb.setSelected(true);
} else {
cb.setSelected(false);
}
}
onValuesBatchSelected(Arrays.asList(value));
});
}
updateUI();
}
protected boolean isAllInputColumnsSelectedIfNoValueExist() {
return false;
}
private void updateComponents() {
final InputColumn>[] currentValue = getCurrentValue();
updateComponents(currentValue);
}
private void updateComponents(final InputColumn>[] value) {
// fetch available input columns
final List> availableColumns =
getAnalysisJobBuilder().getAvailableInputColumns(getComponentBuilder(), _dataType);
final Set> inputColumnsToBeRemoved = new HashSet<>();
inputColumnsToBeRemoved.addAll(_checkBoxes.keySet());
if (value != null) {
// retain selected expression based input columns
for (final InputColumn> col : value) {
if (col instanceof ExpressionBasedInputColumn) {
inputColumnsToBeRemoved.remove(col);
availableColumns.add(col);
} else {
if (!availableColumns.contains(col)) {
logger.warn(
"The value contains a column which is not found in the 'available' set of input columns: {}",
col);
}
}
}
}
for (final InputColumn> col : availableColumns) {
inputColumnsToBeRemoved.remove(col);
final DCCheckBox> checkBox = _checkBoxes.get(col);
if (checkBox == null) {
addAvailableInputColumn(col, isEnabled(col, value));
} else {
// handle updated names from transformed columns.
checkBox.setText(col.getName());
}
}
for (final InputColumn> col : inputColumnsToBeRemoved) {
removeAvailableInputColumn(col);
}
updateVisibility();
}
private void updateVisibility() {
_searchDatastoreTextField.setVisible(_checkBoxes.size() > 16);
if (_checkBoxes.isEmpty()) {
add(_notAvailableCheckBox);
} else {
remove(_notAvailableCheckBox);
}
}
/**
* Method that allows decorating a checkbox for an input column. Subclasses
* can eg. wrap the checkbox in a panel, in order to make additional widgets
* available.
*
* @param checkBox
* @return a {@link JComponent} to add to the widget's parent.
*/
protected JComponent decorateCheckBox(final DCCheckBox> checkBox) {
return checkBox;
}
private boolean isEnabled(final InputColumn> inputColumn, final InputColumn>[] currentValue) {
if (_firstUpdate) {
if (currentValue == null || currentValue.length == 0) {
// set all to true if this is the only required inputcolumn
// property
final ComponentDescriptor> componentDescriptor = getPropertyDescriptor().getComponentDescriptor();
if (componentDescriptor.getConfiguredPropertiesForInput(false).size() == 1) {
return isAllInputColumnsSelectedIfNoValueExist();
}
return false;
}
}
for (final InputColumn> col : currentValue) {
if (inputColumn == col) {
return true;
}
}
return false;
}
@Override
public boolean isSet() {
for (final JCheckBox checkBox : _checkBoxes.values()) {
if (checkBox.isSelected()) {
return true;
}
}
return false;
}
@Override
public InputColumn>[] getValue() {
final List> result = getSelectedInputColumns();
if (logger.isDebugEnabled()) {
final List names = CollectionUtils.map(result, new HasNameMapper());
logger.debug("getValue() returning: {}", names);
}
return result.toArray(new InputColumn>[result.size()]);
}
@Override
protected void setValue(final InputColumn>[] values) {
if (values == null) {
logger.debug("setValue(null) - delegating to setValue([])");
setValue(new InputColumn[0]);
return;
}
if (logger.isDebugEnabled()) {
final List names = CollectionUtils.map(values, new HasNameMapper());
logger.debug("setValue({})", names);
}
// if checkBoxes is empty it means that the value is being set before
// initializing the widget. This can occur in subclasses and automatic
// creating of checkboxes should be done.
if (_checkBoxes.isEmpty()) {
for (final InputColumn> value : values) {
addAvailableInputColumn(value, true);
}
}
// add expression based input columns if needed.
for (final InputColumn> inputColumn : values) {
if (inputColumn instanceof ExpressionBasedInputColumn && !_checkBoxes.containsKey(inputColumn)) {
addAvailableInputColumn(inputColumn, true);
}
}
// update selections in checkboxes
batchUpdateWidget(() -> {
final List> valueList = new ArrayList<>();
for (final DCCheckBox> cb : _checkBoxes.values()) {
if (ArrayUtils.contains(values, cb.getValue())) {
cb.setSelected(true, true);
valueList.add(cb.getValue());
} else {
cb.setSelected(false, true);
}
}
onValuesBatchSelected(valueList);
});
updateUI();
}
protected List> getSelectedInputColumns() {
final List> result = new ArrayList<>();
final Collection>> checkBoxes = _checkBoxes.values();
for (final DCCheckBox> cb : checkBoxes) {
if (cb.isSelected()) {
final InputColumn> value = cb.getValue();
if (value != null) {
result.add(value);
}
}
}
return result;
}
@Override
public void onAdd(final InputColumn> sourceColumn) {
if (_dataType == Object.class || ReflectionUtils.is(sourceColumn.getDataType(), _dataType)) {
addAvailableInputColumn(sourceColumn);
updateVisibility();
updateUI();
}
}
@Override
public void onRemove(final InputColumn> sourceColumn) {
removeAvailableInputColumn(sourceColumn);
updateVisibility();
updateUI();
}
@Override
public void onAdd(final TransformerComponentBuilder> transformerJobBuilder) {
}
@Override
public void onRemove(final TransformerComponentBuilder> transformerJobBuilder) {
}
@Override
protected void onPanelAdd() {
super.onPanelAdd();
final AnalysisJobBuilder analysisJobBuilder = getAnalysisJobBuilder();
analysisJobBuilder.addSourceColumnChangeListener(this);
analysisJobBuilder.addTransformerChangeListener(this);
}
@Override
public void onPanelRemove() {
super.onPanelRemove();
final AnalysisJobBuilder analysisJobBuilder = getAnalysisJobBuilder();
analysisJobBuilder.removeSourceColumnChangeListener(this);
analysisJobBuilder.removeTransformerChangeListener(this);
// remove all listeners on columns too
final Set> inputColumns = _checkBoxes.keySet();
for (final InputColumn> inputColumn : inputColumns) {
if (inputColumn instanceof MutableInputColumn) {
((MutableInputColumn>) inputColumn).removeListener(this);
}
}
}
@Override
public void onOutputChanged(final TransformerComponentBuilder> transformerJobBuilder,
final List> outputColumns) {
// Makes sure it makes sense to do this (rather destructive) update
if (transformerJobBuilder == getComponentBuilder()
|| transformerJobBuilder.getAnalysisJobBuilder() != getAnalysisJobBuilder()) {
return;
}
// we need to save the current value before we update the components
// here. Otherwise any previous selections will be lost.
final InputColumn>[] value = getValue();
getComponentBuilder().setConfiguredProperty(getPropertyDescriptor(), value);
updateComponents(value);
updateUI();
}
@Override
public void onConfigurationChanged(final TransformerComponentBuilder> transformerJobBuilder) {
if (transformerJobBuilder == getComponentBuilder()) {
return;
}
updateComponents();
updateUI();
}
@Override
public void onRequirementChanged(final TransformerComponentBuilder> transformerJobBuilder) {
}
/**
* Overrideable method for subclasses to get informed when values have been
* selected in a batch update
*
* @param values
*/
protected void onValuesBatchSelected(final List> values) {
}
protected void selectAll() {
batchUpdateWidget(() -> {
final List> valueList = new ArrayList<>();
for (final DCCheckBox> cb : _checkBoxes.values()) {
if (cb.getValue() instanceof MutableInputColumn) {
final MutableInputColumn> mutableInputColumn = (MutableInputColumn>) cb.getValue();
if (mutableInputColumn.isHidden()) {
// skip hidden columns
continue;
}
}
if (cb.isEnabled()) {
cb.setSelected(true);
valueList.add(cb.getValue());
}
}
onValuesBatchSelected(valueList);
});
}
protected void selectNone() {
batchUpdateWidget(() -> {
for (final DCCheckBox> cb : _checkBoxes.values()) {
cb.setSelected(false);
}
onValuesBatchSelected(Collections.emptyList());
});
}
private void addAvailableInputColumn(final InputColumn> col) {
addAvailableInputColumn(col, false);
}
private void addAvailableInputColumn(final InputColumn> col, final boolean selected) {
final JComponent decoration = getOrCreateCheckBoxDecoration(col, selected);
add(decoration);
}
private void removeAvailableInputColumn(final InputColumn> col) {
boolean valueChanged = false;
final DCCheckBox> checkBox = _checkBoxes.remove(col);
if (checkBox != null) {
if (checkBox.isSelected()) {
valueChanged = true;
}
final JComponent decoration = _checkBoxDecorations.remove(checkBox);
remove(decoration);
}
if (col instanceof MutableInputColumn) {
((MutableInputColumn>) col).removeListener(this);
}
if (valueChanged) {
fireValueChanged();
}
}
@Override
public void reorderColumns(final InputColumn>[] newValue) {
reorderValue(newValue);
}
@Override
public InputColumn>[] getColumns() {
return getValue();
}
/**
* Reorders the values
*
* @param sortedValue
*/
public void reorderValue(final InputColumn>[] sortedValue) {
// the offset represents the search textfield and the button panel
final int offset = 2;
// reorder the visual components
for (int i = 0; i < sortedValue.length; i++) {
final InputColumn> inputColumn = sortedValue[i];
final JComponent decoration = getOrCreateCheckBoxDecoration(inputColumn, true);
final int position = offset + i;
add(decoration, position);
}
updateUI();
// recreate the _checkBoxes map
final TreeMap, DCCheckBox>> checkBoxesCopy = new TreeMap<>(_checkBoxes);
_checkBoxes.clear();
for (final InputColumn> inputColumn : sortedValue) {
final DCCheckBox> checkBox = checkBoxesCopy.get(inputColumn);
_checkBoxes.put(inputColumn, checkBox);
}
_checkBoxes.putAll(checkBoxesCopy);
setValue(sortedValue);
}
private JComponent getOrCreateCheckBoxDecoration(final InputColumn> inputColumn, final boolean selected) {
DCCheckBox> checkBox = _checkBoxes.get(inputColumn);
if (checkBox == null) {
final String name = inputColumn.getName();
checkBox = new DCCheckBox<>(name, selected);
checkBox.addListener(checkBoxListener);
checkBox.setValue(inputColumn);
_checkBoxes.put(inputColumn, checkBox);
}
JComponent decoration = _checkBoxDecorations.get(checkBox);
if (decoration == null) {
decoration = decorateCheckBox(checkBox);
_checkBoxDecorations.put(checkBox, decoration);
if (inputColumn instanceof MutableInputColumn) {
final MutableInputColumn> mutableInputColumn = (MutableInputColumn>) inputColumn;
mutableInputColumn.addListener(this);
if (mutableInputColumn.isHidden()) {
//If the column was already selected before it was hidden, we still show it.
if (!checkBox.isSelected()) {
decoration.setVisible(false);
}
}
}
}
return decoration;
}
public Map, DCCheckBox>> getCheckBoxes() {
return Collections.unmodifiableMap(_checkBoxes);
}
public Map>, JComponent> getCheckBoxDecorations() {
return Collections.unmodifiableMap(_checkBoxDecorations);
}
public DCPanel getButtonPanel() {
return _buttonPanel;
}
@Override
public void onNameChanged(final MutableInputColumn> inputColumn, final String oldName, final String newName) {
final DCCheckBox> checkBox = getCheckBoxes().get(inputColumn);
if (checkBox == null) {
return;
}
checkBox.setText(newName);
}
@Override
public void onVisibilityChanged(final MutableInputColumn> inputColumn, final boolean hidden) {
final DCCheckBox> checkBox = getCheckBoxes().get(inputColumn);
if (checkBox == null) {
return;
}
if (checkBox.isSelected()) {
// don't hide columns that are selected
return;
}
final JComponent decoration = getCheckBoxDecorations().get(checkBox);
if (decoration == null) {
return;
}
decoration.setVisible(!hidden);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy