package jalview.util.matcher; import java.util.Objects; /** * A bean to describe one attribute-based filter */ public class Matcher implements MatcherI { public enum PatternType { String, Integer, Float } /* * the comparison condition */ private final Condition condition; /* * the string pattern as entered, to compare to */ private String pattern; /* * the pattern in upper case, for non-case-sensitive matching */ private final String uppercasePattern; /* * the compiled regex if using a pattern match condition * (possible future enhancement) */ // private Pattern regexPattern; /* * the value to compare to for a numerical condition with a float pattern */ private float floatValue = 0F; /* * the value to compare to for a numerical condition with an integer pattern */ private long longValue = 0L; private PatternType patternType; /** * 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()) { try { longValue = Long.valueOf(compareTo); pattern = String.valueOf(longValue); patternType = PatternType.Integer; } catch (NumberFormatException e) { floatValue = Float.valueOf(compareTo); pattern = String.valueOf(floatValue); patternType = PatternType.Float; } } else { pattern = compareTo; patternType = PatternType.String; } uppercasePattern = pattern == null ? null : 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 float-valued 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)); } /** * Constructor for an integer-valued numerical match condition. Note that if a * string comparison condition is specified, this will be converted to a * comparison with the integer value as string * * @param cond * @param compareTo */ public Matcher(Condition cond, long compareTo) { this(cond, String.valueOf(compareTo)); } /** * {@inheritDoc} */ @Override public boolean matches(String compareTo) { if (compareTo == null) { return matchesNull(); } boolean matched = false; switch (patternType) { case Float: matched = matchesFloat(compareTo, floatValue); break; case Integer: matched = matchesLong(compareTo); break; default: matched = matchesString(compareTo); break; } return matched; } /** * Executes a non-case-sensitive string comparison to the given value, after * trimming it. Returns true if the test passes, false if it fails. * * @param compareTo * @return */ boolean matchesString(String compareTo) { boolean matched = false; String upper = compareTo.toUpperCase().trim(); 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; } /** * Performs a numerical comparison match condition test against a float value * * @param testee * @param compareTo * @return */ boolean matchesFloat(String testee, float compareTo) { if (!condition.isNumeric()) { // failsafe, shouldn't happen return matches(testee); } float f = 0f; try { f = Float.valueOf(testee); } catch (NumberFormatException e) { return false; } boolean matched = false; switch (condition) { case LT: matched = f < compareTo; break; case LE: matched = f <= compareTo; break; case EQ: matched = f == compareTo; break; case NE: matched = f != compareTo; break; case GT: matched = f > compareTo; break; case GE: matched = f >= compareTo; 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) floatValue; } /** * 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 || floatValue != m.floatValue || longValue != m.longValue) { 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 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(); } /** * Performs a numerical comparison match condition test against an integer * value * * @param compareTo * @return */ boolean matchesLong(String compareTo) { if (!condition.isNumeric()) { // failsafe, shouldn't happen return matches(String.valueOf(compareTo)); } long val = 0L; try { val = Long.valueOf(compareTo); } catch (NumberFormatException e) { /* * try the presented value as a float instead */ return matchesFloat(compareTo, longValue); } boolean matched = false; switch (condition) { case LT: matched = val < longValue; break; case LE: matched = val <= longValue; break; case EQ: matched = val == longValue; break; case NE: matched = val != longValue; break; case GT: matched = val > longValue; break; case GE: matched = val >= longValue; break; default: break; } return matched; } /** * Tests whether a null value matches the condition. The rule is that any * numeric condition is failed, and only 'negative' string conditions are * matched. So for example
* {@code null contains "damaging"}
* fails, but
* {@code null does not contain "damaging"}
* passes. */ boolean matchesNull() { if (condition.isNumeric()) { return false; } else { return condition == Condition.NotContains || condition == Condition.NotMatches || condition == Condition.NotPresent; } } }