label.reset_to_defaults = Reset to defaults
label.oview_calc = Recalculating overview...
label.feature_details = Feature details
+label.matchCondition_contains = Contains
+label.matchCondition_notcontains = Does not contain
+label.matchCondition_matches = Matches
+label.matchCondition_notmatches = Does not match
+label.matchCondition_eq = Is equal to
+label.matchCondition_ne = Is not equal to
+label.matchCondition_lt = Is less than
+label.matchCondition_le = Is less than or equal to
+label.matchCondition_gt = Is greater than
+label.matchCondition_ge = Is greater than or equal to
--- /dev/null
+package jalview.util.matcher;
+
+import jalview.util.MessageManager;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * An enumeration for binary conditions that a user might choose from when
+ * setting filter or match conditions for values
+ */
+public enum Condition
+{
+ Contains(false), NotContains(false), Matches(false), NotMatches(false),
+ EQ(true), NE(true), LT(true), LE(true), GT(true), GE(true);
+
+ private static Map<Condition, String> displayNames = new HashMap<>();
+
+ private boolean numeric;
+
+ Condition(boolean isNumeric)
+ {
+ numeric = isNumeric;
+ }
+
+ /**
+ * Answers true if the condition does a numerical comparison, else false
+ * (string comparison)
+ *
+ * @return
+ */
+ public boolean isNumeric()
+ {
+ return numeric;
+ }
+
+ /**
+ * Answers a display name for the match condition, suitable for showing in
+ * drop-down menus. The value may be internationalized using the resource key
+ * "label.matchCondition_" with the enum name appended.
+ *
+ * @return
+ */
+ @Override
+ public String toString()
+ {
+ String name = displayNames.get(this);
+ if (name != null)
+ {
+ return name;
+ }
+ name = MessageManager
+ .getStringOrReturn("label.matchCondition_", name());
+ displayNames.put(this, name);
+ return name;
+ }
+}
--- /dev/null
+package jalview.util.matcher;
+
+import java.util.function.Function;
+
+public class KeyedMatcher implements KeyedMatcherI
+{
+ private String key;
+
+ private MatcherI matcher;
+
+ /*
+ * an optional second condition
+ */
+ KeyedMatcherI combineWith;
+
+ /*
+ * if true, any second condition is AND-ed with this one
+ * if false,any second condition is OR-ed with this one
+ */
+ boolean combineAnd;
+
+ /**
+ * Constructor given a match condition
+ *
+ * @param m
+ */
+ public KeyedMatcher(String theKey, MatcherI m)
+ {
+ key = theKey;
+ matcher = m;
+ }
+
+ @Override
+ public boolean matches(Function<String, String> valueProvider)
+ {
+ String value = valueProvider.apply(key);
+ boolean matched = matcher.matches(value);
+
+ /*
+ * apply a second condition if there is one, using
+ * lazy evalution of AND and OR combinations
+ */
+ if (combineWith != null)
+ {
+ if (combineAnd && matched)
+ {
+ matched = combineWith.matches(valueProvider);
+ }
+ if (!combineAnd && !matched)
+ {
+ matched = combineWith.matches(valueProvider);
+ }
+ }
+
+ return matched;
+ }
+
+ @Override
+ public KeyedMatcherI and(String key2, MatcherI m)
+ {
+ return combineWith(key2, m, true);
+ }
+
+ @Override
+ public KeyedMatcherI or(String key2, MatcherI m)
+ {
+ return combineWith(key2, m, false);
+ }
+
+ /**
+ * Answers a Matcher that is the logical combination of this one with the
+ * given argument. The two matchers are AND-ed if and is true, else OR-ed.
+ *
+ * @param key2
+ * @param condition2
+ * @param and
+ * @return
+ */
+ KeyedMatcher combineWith(String key2, MatcherI condition2,
+ boolean and)
+ {
+ if (condition2 == null)
+ {
+ return this;
+ }
+
+ KeyedMatcher combined = new KeyedMatcher(key2, condition2);
+ combined.combineWith = this;
+ combined.combineAnd = and;
+
+ return combined;
+ }
+
+ @Override
+ public String getKey()
+ {
+ return key;
+ }
+
+ @Override
+ public MatcherI getMatcher()
+ {
+ return matcher;
+ }
+
+ @Override
+ public KeyedMatcherI getSecondMatcher()
+ {
+ return combineWith;
+ }
+
+ @Override
+ public boolean isAnded()
+ {
+ return combineAnd;
+ }
+
+ /**
+ * Answers a string description of this matcher, suitable for debugging or
+ * logging. The format may change in future.
+ */
+ @Override
+ public String toString()
+ {
+ StringBuilder sb = new StringBuilder();
+ sb.append(key).append(" ").append(matcher.getCondition().name())
+ .append(" ").append(matcher.getPattern());
+
+ if (combineWith != null)
+ {
+ sb.append(" ").append(combineAnd ? "AND (" : "OR (")
+ .append(combineWith.toString()).append(")");
+ }
+ return sb.toString();
+ }
+}
--- /dev/null
+package jalview.util.matcher;
+
+import java.util.function.Function;
+
+public interface KeyedMatcherI
+{
+ boolean matches(Function<String, String> valueProvider);
+
+ /**
+ * Answers a new object that matches the logical AND of this and m
+ *
+ * @param m
+ * @return
+ */
+ KeyedMatcherI and(String key, MatcherI m);
+
+ /**
+ * Answers a new object that matches the logical OR of this and m
+ *
+ * @param m
+ * @return
+ */
+ KeyedMatcherI or(String key, MatcherI m);
+
+ String getKey();
+
+ MatcherI getMatcher();
+
+ KeyedMatcherI getSecondMatcher();
+
+ boolean isAnded();
+}
--- /dev/null
+package jalview.util.matcher;
+
+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 value (upper-cased), or the regex, to compare to
+ * also holds the string form of float value if a numeric condition
+ */
+ String pattern;
+
+ /*
+ * 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
+ * comparision value
+ * @throws NullPointerException
+ * if a null comparison string is specified
+ */
+ public Matcher(Condition cond, String compareTo)
+ {
+ condition = cond;
+ if (cond.isNumeric())
+ {
+ value = Float.valueOf(compareTo);
+ }
+ // pattern matches will be non-case-sensitive
+ pattern = compareTo.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)
+ {
+ condition = cond;
+ value = compareTo;
+ pattern = 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;
+ }
+
+ String upper = val.toUpperCase().trim();
+ boolean matched = false;
+ switch(condition) {
+ case Matches:
+ matched = upper.equals(pattern);
+ break;
+ case NotMatches:
+ matched = !upper.equals(pattern);
+ break;
+ case Contains:
+ matched = upper.indexOf(pattern) > -1;
+ break;
+ case NotContains:
+ matched = upper.indexOf(pattern) == -1;
+ 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;
+ }
+
+ 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 attribut match condition for a feature colour)
+ */
+ @Override
+ public boolean equals(Object obj)
+ {
+ if (obj == null || !(obj instanceof Matcher))
+ {
+ return false;
+ }
+ Matcher m = (Matcher) obj;
+ return condition != m.condition || value != m.value
+ || !pattern.equals(m.pattern);
+ }
+
+ @Override
+ public Condition getCondition()
+ {
+ return condition;
+ }
+
+ @Override
+ public String getPattern()
+ {
+ return pattern;
+ }
+
+ @Override
+ public float getFloatValue()
+ {
+ return value;
+ }
+}
--- /dev/null
+package jalview.util.matcher;
+
+public interface MatcherI
+{
+ /**
+ * Answers true if the given value is matched, else false
+ *
+ * @param s
+ * @return
+ */
+ boolean matches(String s);
+
+ Condition getCondition();
+
+ String getPattern();
+
+ float getFloatValue();
+}
--- /dev/null
+package jalview.util.matcher;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+
+import java.util.function.Function;
+
+import org.testng.annotations.Test;
+
+public class KeyedMatcherTest
+{
+ @Test
+ public void testMatches()
+ {
+ /*
+ * a numeric matcher - MatcherTest covers more conditions
+ */
+ MatcherI m1 = new Matcher(Condition.GE, -2f);
+ KeyedMatcherI km = new KeyedMatcher("AF", m1);
+ assertTrue(km.matches(key -> "-2"));
+ assertTrue(km.matches(key -> "-1"));
+ assertFalse(km.matches(key -> "-3"));
+ assertFalse(km.matches(key -> ""));
+ assertFalse(km.matches(key -> "junk"));
+ assertFalse(km.matches(key -> null));
+
+ /*
+ * a string pattern matcher
+ */
+ MatcherI m2 = new Matcher(Condition.Contains, "Cat");
+ km = new KeyedMatcher("AF", m2);
+ assertTrue(km.matches(key -> "AF".equals(key) ? "raining cats and dogs"
+ : "showers"));
+ }
+
+ @Test
+ public void testAnd()
+ {
+ // condition1: AF value contains "dog" (matches)
+ KeyedMatcherI km1 = new KeyedMatcher("AF", new Matcher(
+ Condition.Contains, "dog"));
+
+ Function<String, String> vp = key -> "AF".equals(key) ? "raining cats and dogs"
+ : "showers";
+ assertTrue(km1.matches(vp));
+
+ // condition 2: CSQ value does not contain "how" (does not match)
+ KeyedMatcherI km2 = km1.and("CSQ", new Matcher(Condition.NotContains,
+ "how"));
+ assertFalse(km2.matches(vp));
+ }
+
+ @Test
+ public void testToString()
+ {
+ KeyedMatcherI km = new KeyedMatcher("AF",
+ new Matcher(Condition.LT, 1.2f));
+ assertEquals(km.toString(), "AF LT 1.2");
+
+ /*
+ * add an AND condition
+ */
+ km = km.and("CLIN_SIG", new Matcher(Condition.NotContains, "path"));
+ assertEquals(km.toString(), "CLIN_SIG NotContains PATH AND (AF LT 1.2)");
+
+ /*
+ * add an OR condition
+ */
+ km = km.or("CSQ", new Matcher(Condition.Contains, "benign"));
+ assertEquals(km.toString(),
+ "CSQ Contains BENIGN OR (CLIN_SIG NotContains PATH AND (AF LT 1.2))");
+ }
+
+ @Test
+ public void testOr()
+ {
+ // condition1: AF value contains "dog" (matches)
+ KeyedMatcherI km1 = new KeyedMatcher("AF", new Matcher(
+ Condition.Contains, "dog"));
+
+ Function<String, String> vp = key -> "AF".equals(key) ? "raining cats and dogs"
+ : "showers";
+ assertTrue(km1.matches(vp));
+
+ // condition 2: CSQ value does not contain "how" (does not match)
+ // the OR combination still passes
+ KeyedMatcherI km2 = km1.or("CSQ", new Matcher(Condition.NotContains,
+ "how"));
+ assertTrue(km2.matches(vp));
+ }
+}
--- /dev/null
+package jalview.util.matcher;
+
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+
+import org.testng.annotations.Test;
+
+public class MatcherTest
+{
+ /**
+ * Tests for float comparison conditions
+ */
+ @Test
+ public void testMatches_float()
+ {
+ /*
+ * EQUALS test
+ */
+ MatcherI m = new Matcher(Condition.EQ, 2f);
+ assertTrue(m.matches("2"));
+ assertTrue(m.matches("2.0"));
+ assertFalse(m.matches("2.01"));
+
+ /*
+ * NOT EQUALS test
+ */
+ m = new Matcher(Condition.NE, 2f);
+ assertFalse(m.matches("2"));
+ assertFalse(m.matches("2.0"));
+ assertTrue(m.matches("2.01"));
+
+ /*
+ * >= test
+ */
+ m = new Matcher(Condition.GE, 2f);
+ assertTrue(m.matches("2"));
+ assertTrue(m.matches("2.1"));
+ assertFalse(m.matches("1.9"));
+
+ /*
+ * > test
+ */
+ m = new Matcher(Condition.GT, 2f);
+ assertFalse(m.matches("2"));
+ assertTrue(m.matches("2.1"));
+ assertFalse(m.matches("1.9"));
+
+ /*
+ * <= test
+ */
+ m = new Matcher(Condition.LE, 2f);
+ assertTrue(m.matches("2"));
+ assertFalse(m.matches("2.1"));
+ assertTrue(m.matches("1.9"));
+
+ /*
+ * < test
+ */
+ m = new Matcher(Condition.LT, 2f);
+ assertFalse(m.matches("2"));
+ assertFalse(m.matches("2.1"));
+ assertTrue(m.matches("1.9"));
+ }
+
+ @Test
+ public void testMatches_floatNullOrInvalid()
+ {
+ for (Condition cond : Condition.values())
+ {
+ if (cond.isNumeric())
+ {
+ MatcherI m = new Matcher(cond, 2f);
+ assertFalse(m.matches(null));
+ assertFalse(m.matches(""));
+ assertFalse(m.matches("two"));
+ }
+ }
+ }
+
+ /**
+ * Tests for string comparison conditions
+ */
+ @Test
+ public void testMatches_pattern()
+ {
+ /*
+ * Contains
+ */
+ MatcherI m = new Matcher(Condition.Contains, "benign");
+ assertTrue(m.matches("benign"));
+ assertTrue(m.matches("MOSTLY BENIGN OBSERVED")); // not case-sensitive
+ assertFalse(m.matches("pathogenic"));
+ assertFalse(m.matches(null));
+
+ /*
+ * does not contain
+ */
+ m = new Matcher(Condition.NotContains, "benign");
+ assertFalse(m.matches("benign"));
+ assertFalse(m.matches("MOSTLY BENIGN OBSERVED")); // not case-sensitive
+ assertTrue(m.matches("pathogenic"));
+ assertTrue(m.matches(null)); // null value passes this condition
+
+ /*
+ * matches
+ */
+ m = new Matcher(Condition.Matches, "benign");
+ assertTrue(m.matches("benign"));
+ assertTrue(m.matches(" Benign ")); // trim before testing
+ assertFalse(m.matches("MOSTLY BENIGN"));
+ assertFalse(m.matches("pathogenic"));
+ assertFalse(m.matches(null));
+
+ /*
+ * does not match
+ */
+ m = new Matcher(Condition.NotMatches, "benign");
+ assertFalse(m.matches("benign"));
+ assertFalse(m.matches(" Benign ")); // trim before testing
+ assertTrue(m.matches("MOSTLY BENIGN"));
+ assertTrue(m.matches("pathogenic"));
+ assertTrue(m.matches(null));
+ }
+
+ /**
+ * If a float is passed with a string condition it gets converted to a string
+ */
+ @Test
+ public void testMatches_floatWithStringCondition()
+ {
+ MatcherI m = new Matcher(Condition.Contains, 1.2e-6f);
+ assertTrue(m.matches("1.2e-6"));
+
+ m = new Matcher(Condition.Contains, 0.0000001f);
+ assertTrue(m.matches("1.0e-7"));
+ assertTrue(m.matches("1.0E-7"));
+ assertFalse(m.matches("0.0000001f"));
+ }
+}