/**
* DataCleaner (community edition)
* Copyright (C) 2014 Neopost - Customer Information Management
*
* 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.beans.filter;
import java.util.Collections;
import javax.inject.Inject;
import javax.inject.Named;
import org.apache.commons.lang3.StringUtils;
import org.apache.metamodel.data.DefaultRow;
import org.apache.metamodel.data.SimpleDataSetHeader;
import org.apache.metamodel.query.FilterItem;
import org.apache.metamodel.query.OperatorType;
import org.apache.metamodel.query.Query;
import org.apache.metamodel.query.SelectItem;
import org.apache.metamodel.schema.Column;
import org.apache.metamodel.schema.ColumnTypeImpl;
import org.apache.metamodel.schema.MutableColumn;
import org.apache.metamodel.util.HasName;
import org.datacleaner.api.Categorized;
import org.datacleaner.api.Configured;
import org.datacleaner.api.Description;
import org.datacleaner.api.Distributed;
import org.datacleaner.api.HasLabelAdvice;
import org.datacleaner.api.Initialize;
import org.datacleaner.api.InputColumn;
import org.datacleaner.api.InputRow;
import org.datacleaner.api.QueryOptimizedFilter;
import org.datacleaner.api.Validate;
import org.datacleaner.components.categories.FilterCategory;
import org.datacleaner.components.convert.ConvertToBooleanTransformer;
import org.datacleaner.components.convert.ConvertToDateTransformer;
import org.datacleaner.components.convert.ConvertToNumberTransformer;
import org.datacleaner.components.convert.ConvertToStringTransformer;
import org.datacleaner.util.ReflectionUtils;
@Named("Compare")
@Description("Compare two values using an operator of your choice. The options available in this filter resemble those "
+ "of a SQL WHERE clause - you can specify columns, fixed values and use operators including the LIKE operator.")
@Categorized(FilterCategory.class)
@Distributed(true)
public class CompareFilter implements QueryOptimizedFilter<CompareFilter.Category>, HasLabelAdvice {
public enum Category {
TRUE, FALSE
}
public enum Operator implements HasName {
LESS_THAN("Less than", OperatorType.LESS_THAN),
LESS_THAN_OR_EQUAL("Less than or equal", OperatorType.LESS_THAN_OR_EQUAL),
EQUALS_TO("Equal", OperatorType.EQUALS_TO),
LIKE("Like", OperatorType.LIKE),
IN("In", OperatorType.IN),
DIFFERENT_FROM("Not equal", OperatorType.DIFFERENT_FROM),
GREATER_THAN_OR_EQUAL("Greater than or equal", OperatorType.GREATER_THAN_OR_EQUAL),
GREATER_THAN("Greater than", OperatorType.GREATER_THAN);
private final OperatorType _operatorType;
private final String _name;
Operator(final String name, final OperatorType operatorType) {
_name = name;
_operatorType = operatorType;
}
public OperatorType getOperatorType() {
return _operatorType;
}
@Override
public String getName() {
return _name;
}
}
@Inject
@Configured(order = 1)
@Description("The column to compare values of")
InputColumn<?> inputColumn;
@Inject
@Configured(order = 2)
Operator operator;
@Inject
@Configured(order = 21, required = false)
@Description("Value to compare with")
String compareValue;
@Inject
@Configured(order = 22, required = false)
@Description("Column holding value to compare with")
InputColumn<?> compareColumn;
private Object compareValueAsOperand;
private SelectItem compareSelectItem;
public CompareFilter() {
}
public CompareFilter(final InputColumn<?> column, final Operator operator, final String compareValue) {
this();
this.inputColumn = column;
this.operator = operator;
this.compareValue = compareValue;
init();
}
public CompareFilter(final InputColumn<?> inputColumn, final Operator operator,
final InputColumn<?> compareColumn) {
this();
this.inputColumn = inputColumn;
this.operator = operator;
this.compareColumn = compareColumn;
init();
}
public void setCompareColumn(final InputColumn<?> compareColumn) {
this.compareColumn = compareColumn;
}
public void setCompareValue(final String compareValue) {
this.compareValue = compareValue;
}
@Validate
public void validate() {
if (compareColumn == null && compareValue == null) {
throw new IllegalStateException("Either 'Compare value' or 'Compare column' needs to be specified.");
}
}
@Initialize
public void init() {
compareValueAsOperand = toOperand(compareValue);
compareSelectItem = new SelectItem(
new MutableColumn("my column", ColumnTypeImpl.convertColumnType(inputColumn.getDataType())));
}
@Override
public String getSuggestedLabel() {
if (inputColumn == null || operator == null) {
return null;
}
if (compareColumn == null && compareValue == null) {
return null;
}
final StringBuilder sb = new StringBuilder();
sb.append(inputColumn.getName());
sb.append(' ');
sb.append(operator.getOperatorType().toSql());
sb.append(' ');
if (compareColumn != null) {
sb.append(compareColumn.getName());
} else {
injectCompareValueLabel(sb);
}
return sb.toString();
}
private void injectCompareValueLabel(final StringBuilder labelPart) {
if (compareValue == null) {
return;
}
final Object operand = toOperand(compareValue);
if (operand instanceof String[] && operator == Operator.IN) {
labelPart.append("('");
labelPart.append(StringUtils.join((String[]) operand, "','"));
labelPart.append("')");
} else if (operand instanceof String) {
labelPart.append('\'');
labelPart.append(operand);
labelPart.append('\'');
} else {
labelPart.append(operand);
}
}
private Object toOperand(final Object value) {
if (value == null) {
return null;
}
final Class<?> dataType = inputColumn.getDataType();
if (ReflectionUtils.isBoolean(dataType)) {
return ConvertToBooleanTransformer.transformValue(value, ConvertToBooleanTransformer.DEFAULT_TRUE_TOKENS,
ConvertToBooleanTransformer.DEFAULT_FALSE_TOKENS);
} else if (ReflectionUtils.isDate(dataType)) {
return ConvertToDateTransformer.getInternalInstance().transformValue(value);
} else if (ReflectionUtils.isNumber(dataType)) {
return ConvertToNumberTransformer.transformValue(value);
} else if (ReflectionUtils.isString(dataType)) {
if (operator == Operator.IN && value instanceof String) {
return csvStringToArray((String) value);
} else {
return ConvertToStringTransformer.transformValue(value);
}
} else {
return value;
}
}
private String[] csvStringToArray(final String csvString) {
return csvString.trim().replaceAll(" *, *", ",").split(",");
}
@Override
public CompareFilter.Category categorize(final InputRow inputRow) {
final Object inputValue = inputRow.getValue(inputColumn);
final Object operand;
if (compareColumn != null) {
final Object compareColumnValue = inputRow.getValue(compareColumn);
operand = toOperand(compareColumnValue);
} else {
operand = compareValueAsOperand;
}
return filter(inputValue, operator, operand);
}
public CompareFilter.Category filter(final Object value, final Operator operator, final Object operand) {
// use MetaModel FilterItem to do the evaluation - it's a bit of a
// detour, but there's a ton of operator/operand combinations to take
// care of which is already done there.
final FilterItem item = new FilterItem(compareSelectItem, operator.getOperatorType(), operand);
final boolean evaluation = item.evaluate(
new DefaultRow(new SimpleDataSetHeader(Collections.singletonList(compareSelectItem)),
new Object[] { value }));
if (evaluation) {
return Category.TRUE;
} else {
return Category.FALSE;
}
}
@Override
public boolean isOptimizable(final Category category) {
return category == Category.TRUE;
}
@Override
public Query optimizeQuery(final Query q, final Category category) {
if (category == Category.TRUE) {
final Column inputPhysicalColumn = inputColumn.getPhysicalColumn();
final Object operand;
if (compareColumn != null) {
final Column physicalCompareColumn = compareColumn.getPhysicalColumn();
operand = new SelectItem(physicalCompareColumn);
} else {
operand = toOperand(compareValue);
}
q.where(inputPhysicalColumn, operator.getOperatorType(), operand);
return q;
}
throw new UnsupportedOperationException();
}
}