// $HeadURL$ // $Id$ // // Copyright © 2006, 2010, 2011, 2012 by the President and Fellows of Harvard College. // // Screensaver is an open-source project developed by the ICCB-L and NSRB labs // at Harvard Medical School. This software is distributed under the terms of // the GNU General Public License. package edu.harvard.med.screensaver.db; import java.util.Collection; import java.util.Observable; import java.util.Set; import com.google.common.collect.Sets; import org.apache.log4j.Logger; import edu.harvard.med.screensaver.util.NullSafeUtils; /** * @author <a mailto="andrew_tolopko@hms.harvard.edu">Andrew Tolopko</a> * @author <a mailto="john_sullivan@hms.harvard.edu">John Sullivan</a> */ public class Criterion<T> extends Observable { // static members private static Logger log = Logger.getLogger(Criterion.class); public enum OperatorClass { EXTANT, EQUALITY, RANKING, TEXT }; public enum Operator { ANY("", OperatorClass.EXTANT, 1), // equality operators EQUAL("=", OperatorClass.EQUALITY), NOT_EQUAL("<>", OperatorClass.EQUALITY), // ranking operators LESS_THAN("<", OperatorClass.RANKING), LESS_THAN_EQUAL("<=", OperatorClass.RANKING), GREATER_THAN(">", OperatorClass.RANKING), GREATER_THAN_EQUAL(">=", OperatorClass.RANKING), // text operators TEXT_STARTS_WITH("starts with", OperatorClass.TEXT), TEXT_CONTAINS("contains", OperatorClass.TEXT), TEXT_NOT_CONTAINS("doesn't contain", OperatorClass.TEXT), TEXT_LIKE("matches", OperatorClass.TEXT), TEXT_NOT_LIKE("doesn't match", OperatorClass.TEXT), // empty operators EMPTY("blank", OperatorClass.EXTANT, 1), NOT_EMPTY("not blank", OperatorClass.EXTANT, 1); public final static Set<Operator> ALL_OPERATORS = Sets.newHashSet(Operator.values()); public final static Set<Operator> TEXT_OPERATORS = Sets.newHashSet(TEXT_STARTS_WITH, TEXT_CONTAINS, TEXT_NOT_CONTAINS, TEXT_LIKE, TEXT_NOT_LIKE); public final static Set<Operator> COMPARABLE_OPERATORS = Sets.difference(ALL_OPERATORS, TEXT_OPERATORS); public final static Set<Operator> NEGATED_OPERATORS = Sets.newHashSet(NOT_EQUAL, TEXT_NOT_CONTAINS, TEXT_NOT_LIKE); private String _symbol; private OperatorClass _opClass; private int _argumentCount = 2; // operator is binary, by default private Operator(String symbol, OperatorClass opClass) { this(symbol, opClass, 2); } private Operator(String symbol, OperatorClass opClass, int argumentCount) { _symbol = symbol; _opClass = opClass; _argumentCount = argumentCount; } public String getSymbol() { return _symbol; } public String getName() { return name(); } public boolean isUnary() { return _argumentCount == 1; } public OperatorClass getOperatorClass() { return _opClass; } } // instance data members private Operator _operator = Operator.EQUAL; private T _value; private String _regex; // public constructors and methods public Criterion() { } public Criterion(Operator operator, T value) { _operator = operator; _value = value; } public Criterion<T> setOperatorAndValue(Operator operator, T value) { setOperator(operator); setValue(value); return this; } public Operator getOperator() { return _operator; } public void setOperator(Operator operator) { if (NullSafeUtils.nullSafeEquals(_operator, operator)) { return; } _operator = operator; if (_operator.isUnary()) { _value = null; } setChanged(); notifyObservers(); } public T getValue() { return _value; } public void setValue(T value) { // to reduce user-confusion, we'll treat empty strings as undefined criterion if (value instanceof String) { if (value == "") { value = null; } } if (NullSafeUtils.nullSafeEquals(_value, value)) { return; } _value = value; _regex = null; setChanged(); notifyObservers(); } public void reset() { if (_operator.isUnary()) { _operator = Operator.EQUAL; } setValue(null); } /** * Get whether this is an an undefined criterion, which always matches (does * not filter out any data). */ public boolean isUndefined() { return _operator == null || (!_operator.isUnary() && _value == null) || _operator == Operator.ANY; } @SuppressWarnings("unchecked") public boolean matches(Object datum) { Object criterionValue = getValue(); Operator operator = getOperator(); // handle collections by matching on individual elements if (datum instanceof Collection) { assert !!!Operator.NEGATED_OPERATORS.contains(operator) : "operator " + operator.getSymbol() + " cannot be applied to collection-based values"; if (datum != null && ((Collection) datum).isEmpty()) { return matches(null); } else { for (Object elementDatum : (Collection) datum) { if (matches(elementDatum)) { return true; } } return false; } } boolean result; if (isUndefined()) { // non-initialized criterion value means "do not filter" (criterion is undefined by user; // operator negation is not considered return true; } if (operator == Operator.EMPTY || operator == Operator.NOT_EMPTY) { result = datum == null || datum.toString().length() == 0; if (operator == Operator.NOT_EMPTY) { result = !result; } } else if (datum == null) { // null data value can only match with the EMPTY operator; // operator negation is not considered return false; } else if (operator == Operator.EQUAL || operator == Operator.NOT_EQUAL) { result = NullSafeUtils.nullSafeEquals(criterionValue, datum); if (operator == Operator.NOT_EQUAL) { result = !result; } } else if (operator == Operator.GREATER_THAN || operator == Operator.LESS_THAN_EQUAL || operator == Operator.LESS_THAN || operator == Operator.GREATER_THAN_EQUAL) { if (! (datum instanceof Comparable)) { throw new CriterionMatchException("expecting Comparable value", this); } int cmpResult = ((Comparable) datum).compareTo(criterionValue); result = operator == Operator.GREATER_THAN ? cmpResult > 0 : operator == Operator.LESS_THAN ? cmpResult < 0 : operator == Operator.GREATER_THAN_EQUAL ? cmpResult >= 0 : cmpResult <= 0; } else if (operator == Operator.TEXT_LIKE || operator == Operator.TEXT_NOT_LIKE || operator == Operator.TEXT_STARTS_WITH || operator == Operator.TEXT_CONTAINS || operator == Operator.TEXT_NOT_CONTAINS) { if (! (criterionValue instanceof String)) { throw new CriterionMatchException("expecting String criterion value", this); } result = ((String) datum).matches(getRegex((String) criterionValue)); if (operator == Operator.TEXT_NOT_LIKE || operator == Operator.TEXT_NOT_CONTAINS) { result = !result; } } else { throw new CriterionMatchException(operator + " not supported", this); } return result; } private String getRegex(String expr) { if (_regex == null) { Operator textOperator = getOperator(); if (textOperator == Operator.TEXT_STARTS_WITH) { expr = expr + "*"; } else if (textOperator == Operator.TEXT_LIKE || textOperator == Operator.TEXT_NOT_LIKE) { // use expression exactly as provided } else if (textOperator == Operator.TEXT_CONTAINS || textOperator == Operator.TEXT_NOT_CONTAINS) { expr = "*" + expr + "*"; } _regex = convertToRegex(expr); } return _regex; } /** * @param expr search string containing wildcard characters "*" and "?", which can be escaped with a preceding "\" to be treats as literals. */ private String convertToRegex(String expr) { String regex = expr; String lastRegex; // note: loop is necessary to properly handle sequential '?' characters do { lastRegex = regex; regex = regex.replaceAll("([^\\\\])\\?|^\\?", "$1__ANY_ONE__"); regex = regex.replaceAll("([^\\\\])\\*|^\\*", "$1__ANY_MULTI__"); } while (! regex.equals(lastRegex)); // escape any remaining regex special characters in user's expression regex = regex.replaceAll("\\\\\\\\", "\\\\\\\\"); String[] REGEX_CHARACTERS = { ".", "^", "$", "|", "(", ")", "{", "}", "[", "]" }; for (String c : REGEX_CHARACTERS ) { if (regex.contains(c)) { regex = regex.replace(c, "\\" + c); } } // convert user's wildcards to regex equivalents regex = regex.replaceAll("__ANY_ONE__", ".").replaceAll("__ANY_MULTI__", ".*"); regex = "(?i)(?s)" + regex; // case insensitive, single-line mode (i.e., match across all lines) log.debug("regex=" + regex); return regex; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj instanceof Criterion) { Criterion<T> that = (Criterion<T>) obj; return this._operator == that._operator && (this._value == null ? that._value == null : this._value.equals(that._value)); } return false; } public int hashCode() { throw new UnsupportedOperationException("hashCode not implemented"); } public String toString() { return "[" + _operator + " " + getValue() + "]"; } }