/**
* 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.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import com.rapidminer.core.io.data.ColumnMetaData;
import com.rapidminer.tools.AbstractObservable;
import com.rapidminer.tools.I18N;
/**
* Validates the settings of the {@link ConfigureDataStep}. Notifies observers if the parsing errors
* or the column errors have changed. Notifies observes with a set of indices if the columns with
* this indices now have a duplicated name/role or don't anymore.
*
* @author Dominik Halfkann, Gisa Schaefer
*/
class ConfigureDataValidator extends AbstractObservable<Set<Integer>> {
private static final String AND = " " + I18N.getGUILabel("io.dataimport.step.data_column_configuration.error_table.and")
+ " ";
/** map from column role to the indices of columns with this role */
private final Map<String, List<Integer>> columnRoles = new HashMap<String, List<Integer>>();
/** map from column name to the indices of columns with this name */
private final Map<String, List<Integer>> columnNames = new HashMap<String, List<Integer>>();
/** set of column indices that appear in the {@link #parsingErrorList} */
private final Set<Integer> parsingErrorAffectedColumns = new HashSet<>();
/** set of indices of columns that have the same name as some other column(s) */
private final Set<Integer> duplicateNameColumn = new HashSet<Integer>();
/**
* set of indices of columns that had the same name as some other column(s) in the last
* validation
*/
private final Set<Integer> oldDuplicateNameColumn = new HashSet<Integer>();
/** set of indices of columns that have the same role as some other column(s) */
private final Set<Integer> duplicateRoleColumn = new HashSet<Integer>();
/**
* set of indices of columns that had the same role as some other column(s) in the last
* validation
*/
private final Set<Integer> oldDuplicateRoleColumn = new HashSet<Integer>();
/**
* The list of column errors (duplicate names or roles), updated on every call of
* {@link #validate(int)}
*/
private final List<ColumnError> columnErrorList = new LinkedList<>();
/**
* The list of column errors (duplicate names or roles) from the last call
*/
private final List<ColumnError> oldColumnErrorList = new LinkedList<>();
/** Keeps track of which columns are not removed */
private final Set<Integer> selectedColumns = new HashSet<>();
/** The list of parsing errors, only changed by {@link #setParsingErrors(List)} */
private List<ParsingError> parsingErrorList = new ArrayList<ParsingError>();
/** the meta data for the columns */
private List<ColumnMetaData> columnMetaData;
/**
* Initializes the validator with the given columnMetaData.
*
* @param columnMetaData
*/
void init(List<ColumnMetaData> columnMetaData) {
this.columnMetaData = columnMetaData;
columnRoles.clear();
columnNames.clear();
int columnIndex = 0;
for (ColumnMetaData column : columnMetaData) {
addColumnToColumnsMaps(columnIndex, column);
if (!column.isRemoved()) {
selectedColumns.add(columnIndex);
}
columnIndex++;
}
checkForDuplicates();
}
/**
* Adds the column to {@link #columnNames} and {@link #columnRoles}.
*
* @param columnIndex
* the index of the column
* @param column
* the meta data of the column
*/
private void addColumnToColumnsMaps(int columnIndex, ColumnMetaData column) {
if (!column.isRemoved()) {
// add to name map
List<Integer> listForName = columnNames.get(column.getName());
if (listForName == null) {
List<Integer> indexList = new ArrayList<>();
indexList.add(columnIndex);
columnNames.put(column.getName(), indexList);
} else {
listForName.add(columnIndex);
}
// add to role map
String role = column.getRole();
if (role != null) {
List<Integer> listForRole = columnRoles.get(role);
if (listForRole == null) {
List<Integer> indexList = new ArrayList<>();
indexList.add(columnIndex);
columnRoles.put(role, indexList);
} else {
listForRole.add(columnIndex);
}
}
}
}
/**
* Sets the parsing errors.
*
* @param parsingErrors
* the parsing errors to set
*/
void setParsingErrors(List<ParsingError> parsingErrors) {
this.parsingErrorList = parsingErrors;
for (ParsingError error : parsingErrors) {
parsingErrorAffectedColumns.add(error.getColumn());
}
fireUpdate();
}
/**
* @return the {@link ParsingError}s of columns that are not removed
*/
List<ParsingError> getParsingErrors() {
List<ParsingError> errorList = new LinkedList<>();
for (ParsingError error : parsingErrorList) {
if (!columnMetaData.get(error.getColumn()).isRemoved()) {
errorList.add(error);
}
}
return errorList;
}
/**
* @return the list of {@link ColumnError}s
*/
List<ColumnError> getColumnErrors() {
return columnErrorList;
}
/**
* Deletes the columnIndex from the maps and adds it again.
*
* @param columnIndex
* the index of the column to update
* @return
*/
private void updateColumnMaps(int columnIndex) {
deleteColumnIndexFromMaps(columnIndex);
addColumnToColumnsMaps(columnIndex, columnMetaData.get(columnIndex));
}
/**
* Validates the settings for the column with the given columnIndex.
*
* @param columnIndex
* the index of the column to check
*/
void validate(int columnIndex) {
updateColumnMaps(columnIndex);
checkForDuplicates();
checkEmptySelection(columnIndex);
checkIfUpdate(columnIndex);
}
/**
* Checks if all columns are removed.
*/
private void checkEmptySelection(int columnIndex) {
if (columnMetaData.get(columnIndex).isRemoved()) {
selectedColumns.remove(columnIndex);
} else {
selectedColumns.add(columnIndex);
}
if (selectedColumns.isEmpty()) {
columnErrorList.add(new ColumnError(Collections.<Integer> emptyList(), null,
I18N.getGUILabel("io.dataimport.step.data_column_configuration.error_table.no_column_error")));
}
}
/**
* Stores the indices of the columns with duplicate name or role and stores the associated
* errors to the list of {@link ColumnError}s.
*/
private void checkForDuplicates() {
oldDuplicateNameColumn.clear();
oldDuplicateNameColumn.addAll(duplicateNameColumn);
duplicateNameColumn.clear();
oldDuplicateRoleColumn.clear();
oldDuplicateRoleColumn.addAll(duplicateRoleColumn);
duplicateRoleColumn.clear();
oldColumnErrorList.clear();
oldColumnErrorList.addAll(columnErrorList);
columnErrorList.clear();
for (Entry<String, List<Integer>> roleEntry : columnRoles.entrySet()) {
if (roleEntry.getValue().size() > 1) {
duplicateRoleColumn.addAll(roleEntry.getValue());
columnErrorList.add(makeDuplicateRoleError(roleEntry));
}
}
for (Entry<String, List<Integer>> nameEntry : columnNames.entrySet()) {
if (nameEntry.getValue().size() > 1) {
duplicateNameColumn.addAll(nameEntry.getValue());
columnErrorList.add(makeDuplicateNameError(nameEntry));
}
}
}
/**
* Creates an error for the given roleEntry.
*/
private ColumnError makeDuplicateRoleError(Entry<String, List<Integer>> roleEntry) {
final String duplicateRoleMessage = I18N.getGUILabel(
"io.dataimport.step.data_column_configuration.error_table.column_error.duplicate_role_message",
roleEntry.getKey(), listToString(roleEntry.getValue()));
return new ColumnError(roleEntry.getValue(), roleEntry.getKey(), duplicateRoleMessage);
}
/**
* Create an error for the given nameEntry.
*/
private ColumnError makeDuplicateNameError(Entry<String, List<Integer>> nameEntry) {
String duplicatenNameMessage = I18N.getGUILabel(
"io.dataimport.step.data_column_configuration.error_table.column_error.duplicate_name_message",
nameEntry.getKey(), listToString(nameEntry.getValue()));
return new ColumnError(nameEntry.getValue(), nameEntry.getKey(), duplicatenNameMessage);
}
/**
* Converts the integer list to a string where the entries are separated by "," and "and".
*/
private static String listToString(List<Integer> list) {
Collections.sort(list);
StringBuilder builder = new StringBuilder();
for (int i = 0; i < list.size(); i++) {
Integer column = list.get(i) + 1;
if (i < list.size() - 2) {
builder.append(column);
builder.append(", ");
} else if (i == list.size() - 2) {
builder.append(column);
builder.append(AND);
} else {
builder.append(column);
}
}
return builder.toString();
}
/**
* Checks if an update should be fired. First checks if the sets of duplicate name or role
* columns have changed. If this is not the case checks if the column errors have changed or if
* a column changed that has a parsing error.
*
*/
private void checkIfUpdate(int columnIndex) {
if (!oldDuplicateNameColumn.equals(duplicateNameColumn) || !oldDuplicateRoleColumn.equals(duplicateRoleColumn)) {
// if one of the duplicates lists aren't equal, fire update
Set<Integer> columnsUpdate = new HashSet<Integer>();
columnsUpdate.addAll(oldDuplicateNameColumn);
columnsUpdate.addAll(duplicateNameColumn);
columnsUpdate.addAll(oldDuplicateRoleColumn);
columnsUpdate.addAll(duplicateRoleColumn);
fireUpdate(columnsUpdate);
} else if (!oldColumnErrorList.equals(columnErrorList) || parsingErrorAffectedColumns.contains(columnIndex)) {
// fire update with no indices if either the column errors changed or a parsing error
// affected column changed
fireUpdate();
}
}
/**
* Deletes the columnNumber from {@link #columnNames} and {@link #columnRoles}.
*/
private void deleteColumnIndexFromMaps(int columnNumber) {
for (Entry<String, List<Integer>> nameEntry : columnNames.entrySet()) {
if (nameEntry.getValue() != null) {
nameEntry.getValue().remove((Integer) columnNumber);
}
}
for (Entry<String, List<Integer>> roleEntry : columnRoles.entrySet()) {
if (roleEntry.getValue() != null) {
roleEntry.getValue().remove((Integer) columnNumber);
}
}
}
/**
* Whether the column has a name that is also used in another column.
*
* @param column
* the index of the column to check
* @return {@code true} if the name of this column is also used in another column
*/
boolean isDuplicateNameColumn(int column) {
return duplicateNameColumn.contains(column);
}
/**
* Whether the column has a role that is also used in another column.
*
* @param column
* the index of the column to check
* @return {@code true} if the role of this column is also used in another column
*/
boolean isDuplicateRoleColumn(int column) {
return duplicateRoleColumn.contains(column);
}
/**
* Checks if the given column name is already in use.
*
* @param name
* the column name which should be checked
* @return {@code true} if a duplicate entry exists, otherwise {@code false}
*/
boolean isNameUsed(String name) {
List<Integer> columnsWithName = columnNames.get(name);
return columnsWithName != null && !columnsWithName.isEmpty();
}
/**
* Checks if the given role is already in use.
*
* @param role
* the role which should be checked
* @return {@code true} if a duplicate entry exists, otherwise {@code false}
*/
public boolean isRoleUsed(String role) {
List<Integer> columnsWithRole = columnRoles.get(role);
return columnsWithRole != null && !columnsWithRole.isEmpty();
}
}