--- /dev/null
+package jalview.util.matcher;
+
+import java.util.Objects;
+import java.util.regex.Pattern;
+
+/**
+ * A bean to describe one attribute-based filter
+ */
+public class Matcher implements MatcherI
+{
+ /*
+ * the comparison condition
+ */
+ Condition condition;
+
+ /*
+ * the string pattern as entered, or the regex, to compare to
+ * also holds the string form of float value if a numeric condition
+ */
+ String pattern;
+
+ /*
+ * the pattern in upper case, for non-case-sensitive matching
+ */
+ String uppercasePattern;
+
+ /*
+ * the compiled regex if using a pattern match condition
+ * (reserved for possible future enhancement)
+ */
+ Pattern regexPattern;
+
+ /*
+ * the value to compare to for a numerical condition
+ */
+ float value;
+
+ /**
+ * Constructor
+ *
+ * @param cond
+ * @param compareTo
+ * @return
+ * @throws NumberFormatException
+ * if a numerical condition is specified with a non-numeric
+ * comparison value
+ * @throws NullPointerException
+ * if a null condition or comparison string is specified
+ */
+ public Matcher(Condition cond, String compareTo)
+ {
+ Objects.requireNonNull(cond);
+ condition = cond;
+ if (cond.isNumeric())
+ {
+ value = Float.valueOf(compareTo);
+ pattern = String.valueOf(value);
+ uppercasePattern = pattern;
+ }
+ else
+ {
+ pattern = compareTo;
+ if (pattern != null)
+ {
+ uppercasePattern = pattern.toUpperCase();
+ }
+ }
+
+ // if we add regex conditions (e.g. matchesPattern), then
+ // pattern should hold the raw regex, and
+ // regexPattern = Pattern.compile(compareTo);
+ }
+
+ /**
+ * Constructor for a numerical match condition. Note that if a string
+ * comparison condition is specified, this will be converted to a comparison
+ * with the float value as string
+ *
+ * @param cond
+ * @param compareTo
+ */
+ public Matcher(Condition cond, float compareTo)
+ {
+ this(cond, String.valueOf(compareTo));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @SuppressWarnings("incomplete-switch")
+ @Override
+ public boolean matches(String val)
+ {
+ if (condition.isNumeric())
+ {
+ try
+ {
+ /*
+ * treat a null value (no such attribute) as
+ * failing any numerical filter condition
+ */
+ return val == null ? false : matches(Float.valueOf(val));
+ } catch (NumberFormatException e)
+ {
+ return false;
+ }
+ }
+
+ /*
+ * a null value matches a negative condition, fails a positive test
+ */
+ if (val == null)
+ {
+ return condition == Condition.NotContains
+ || condition == Condition.NotMatches
+ || condition == Condition.NotPresent;
+ }
+
+ String upper = val.toUpperCase().trim();
+ boolean matched = false;
+ switch(condition) {
+ case Matches:
+ matched = upper.equals(uppercasePattern);
+ break;
+ case NotMatches:
+ matched = !upper.equals(uppercasePattern);
+ break;
+ case Contains:
+ matched = upper.indexOf(uppercasePattern) > -1;
+ break;
+ case NotContains:
+ matched = upper.indexOf(uppercasePattern) == -1;
+ break;
+ case Present:
+ matched = true;
+ break;
+ default:
+ break;
+ }
+ return matched;
+ }
+
+ /**
+ * Applies a numerical comparison match condition
+ *
+ * @param f
+ * @return
+ */
+ @SuppressWarnings("incomplete-switch")
+ boolean matches(float f)
+ {
+ if (!condition.isNumeric())
+ {
+ return matches(String.valueOf(f));
+ }
+
+ boolean matched = false;
+ switch (condition) {
+ case LT:
+ matched = f < value;
+ break;
+ case LE:
+ matched = f <= value;
+ break;
+ case EQ:
+ matched = f == value;
+ break;
+ case NE:
+ matched = f != value;
+ break;
+ case GT:
+ matched = f > value;
+ break;
+ case GE:
+ matched = f >= value;
+ break;
+ default:
+ break;
+ }
+
+ return matched;
+ }
+
+ /**
+ * A simple hash function that guarantees that when two objects are equal,
+ * they have the same hashcode
+ */
+ @Override
+ public int hashCode()
+ {
+ return pattern.hashCode() + condition.hashCode() + (int) value;
+ }
+
+ /**
+ * equals is overridden so that we can safely remove Matcher objects from
+ * collections (e.g. delete an attribute match condition for a feature colour)
+ */
+ @Override
+ public boolean equals(Object obj)
+ {
+ if (obj == null || !(obj instanceof Matcher))
+ {
+ return false;
+ }
+ Matcher m = (Matcher) obj;
+ if (condition != m.condition || value != m.value)
+ {
+ return false;
+ }
+ if (pattern == null)
+ {
+ return m.pattern == null;
+ }
+ return uppercasePattern.equals(m.uppercasePattern);
+ }
+
+ @Override
+ public Condition getCondition()
+ {
+ return condition;
+ }
+
+ @Override
+ public String getPattern()
+ {
+ return pattern;
+ }
+
+ @Override
+ public float getFloatValue()
+ {
+ return value;
+ }
+
+ @Override
+ public String toString()
+ {
+ StringBuilder sb = new StringBuilder();
+ sb.append(condition.toString()).append(" ");
+ if (condition.isNumeric())
+ {
+ sb.append(pattern);
+ }
+ else
+ {
+ sb.append("'").append(pattern).append("'");
+ }
+
+ return sb.toString();
+ }
+}