From ec44bf310ff6aaa0438f486dadc65f2af385d99d Mon Sep 17 00:00:00 2001 From: gmungoc Date: Fri, 8 Dec 2017 15:13:41 +0000 Subject: [PATCH] JAL-2843 toStableString, fromString for Jalview features file format --- src/jalview/datamodel/features/FeatureMatcher.java | 247 +++++++++++++++++++- .../datamodel/features/FeatureMatcherI.java | 7 + .../datamodel/features/FeatureMatcherSet.java | 166 ++++++++++++- .../datamodel/features/FeatureMatcherSetI.java | 7 + src/jalview/util/matcher/Condition.java | 53 ++++- src/jalview/util/matcher/Matcher.java | 4 +- .../datamodel/features/FeatureMatcherSetTest.java | 137 ++++++++++- .../datamodel/features/FeatureMatcherTest.java | 142 ++++++++++- test/jalview/util/matcher/ConditionTest.java | 43 ++++ 9 files changed, 783 insertions(+), 23 deletions(-) diff --git a/src/jalview/datamodel/features/FeatureMatcher.java b/src/jalview/datamodel/features/FeatureMatcher.java index b86468d..50dd102 100644 --- a/src/jalview/datamodel/features/FeatureMatcher.java +++ b/src/jalview/datamodel/features/FeatureMatcher.java @@ -23,6 +23,14 @@ import jalview.util.matcher.MatcherI; */ public class FeatureMatcher implements FeatureMatcherI { + private static final String SCORE = "Score"; + + private static final String LABEL = "Label"; + + private static final String SPACE = " "; + + private static final String QUOTE = "'"; + /* * a dummy matcher that comes in useful for the 'add a filter' gui row */ @@ -49,12 +57,188 @@ public class FeatureMatcher implements FeatureMatcherI final private MatcherI matcher; /** + * A helper method that converts a 'compound' attribute name from its display + * form, e.g. CSQ:PolyPhen to array form, e.g. { "CSQ", "PolyPhen" } + * + * @param attribute + * @return + */ + public static String[] fromAttributeDisplayName(String attribute) + { + return attribute == null ? null : attribute.split(COLON); + } + + /** + * A helper method that converts a 'compound' attribute name to its display + * form, e.g. CSQ:PolyPhen from its array form, e.g. { "CSQ", "PolyPhen" } + * + * @param attName + * @return + */ + public static String toAttributeDisplayName(String[] attName) + { + return attName == null ? "" : String.join(COLON, attName); + } + + /** + * A factory constructor that converts a stringified object (as output by + * toStableString) to an object instance. Returns null if parsing fails. + *

+ * Leniency in parsing (for manually created feature files): + *

+ * + * @param descriptor + * @return + */ + public static FeatureMatcher fromString(final String descriptor) + { + String invalidFormat = "Invalid matcher format: " + descriptor; + + /* + * expect + * value condition pattern + * where value is Label or Space or attributeName or attName1:attName2 + * and pattern is a float value as string, or a text string + * attribute names or patterns may be quoted (must be if include space) + */ + String attName = null; + boolean byScore = false; + boolean byLabel = false; + Condition cond = null; + String pattern = null; + + /* + * parse first field (Label / Score / attribute) + * optionally in quotes (required if attName includes space) + */ + String leftToParse = descriptor; + String firstField = null; + + if (descriptor.startsWith(QUOTE)) + { + // 'Label' / 'Score' / 'attName' + int nextQuotePos = descriptor.indexOf(QUOTE, 1); + if (nextQuotePos == -1) + { + System.err.println(invalidFormat); + return null; + } + firstField = descriptor.substring(1, nextQuotePos); + leftToParse = descriptor.substring(nextQuotePos + 1).trim(); + } + else + { + // Label / Score / attName (unquoted) + int nextSpacePos = descriptor.indexOf(SPACE); + if (nextSpacePos == -1) + { + System.err.println(invalidFormat); + return null; + } + firstField = descriptor.substring(0, nextSpacePos); + leftToParse = descriptor.substring(nextSpacePos + 1).trim(); + } + String lower = firstField.toLowerCase(); + if (lower.startsWith(LABEL.toLowerCase())) + { + byLabel = true; + } + else if (lower.startsWith(SCORE.toLowerCase())) + { + byScore = true; + } + else + { + attName = firstField; + } + + /* + * next field is the comparison condition + * most conditions require a following pattern (optionally quoted) + * although some conditions e.g. Present do not + */ + int nextSpacePos = leftToParse.indexOf(SPACE); + if (nextSpacePos == -1) + { + /* + * no value following condition - only valid for some conditions + */ + cond = Condition.fromString(leftToParse); + if (cond == null || cond.needsAPattern()) + { + System.err.println(invalidFormat); + return null; + } + } + else + { + /* + * condition and pattern + */ + cond = Condition.fromString(leftToParse.substring(0, nextSpacePos)); + leftToParse = leftToParse.substring(nextSpacePos + 1).trim(); + if (leftToParse.startsWith(QUOTE)) + { + // pattern in quotes + if (leftToParse.endsWith(QUOTE)) + { + pattern = leftToParse.substring(1, leftToParse.length() - 1); + } + else + { + // unbalanced quote + System.err.println(invalidFormat); + return null; + } + } + else + { + // unquoted pattern + pattern = leftToParse; + } + } + + /* + * we have parsed out value, condition and pattern + * so can now make the FeatureMatcher + */ + try + { + if (byLabel) + { + return FeatureMatcher.byLabel(cond, pattern); + } + else if (byScore) + { + return FeatureMatcher.byScore(cond, pattern); + } + else + { + String[] attNames = FeatureMatcher + .fromAttributeDisplayName(attName); + return FeatureMatcher.byAttribute(cond, pattern, attNames); + } + } catch (NumberFormatException e) + { + // numeric condition with non-numeric pattern + return null; + } + } + + /** * A factory constructor method for a matcher that applies its match condition * to the feature label (description) * * @param cond * @param pattern * @return + * @throws NumberFormatException + * if an invalid numeric pattern is supplied */ public static FeatureMatcher byLabel(Condition cond, String pattern) { @@ -69,6 +253,8 @@ public class FeatureMatcher implements FeatureMatcherI * @param cond * @param pattern * @return + * @throws NumberFormatException + * if an invalid numeric pattern is supplied */ public static FeatureMatcher byScore(Condition cond, String pattern) { @@ -84,6 +270,8 @@ public class FeatureMatcher implements FeatureMatcherI * @param pattern * @param attName * @return + * @throws NumberFormatException + * if an invalid numeric pattern is supplied */ public static FeatureMatcher byAttribute(Condition cond, String pattern, String... attName) @@ -143,14 +331,14 @@ public class FeatureMatcher implements FeatureMatcherI } Condition condition = matcher.getCondition(); - sb.append(" ").append(condition.toString().toLowerCase()); + sb.append(SPACE).append(condition.toString().toLowerCase()); if (condition.isNumeric()) { - sb.append(" ").append(matcher.getPattern()); + sb.append(SPACE).append(matcher.getPattern()); } else if (condition.needsAPattern()) { - sb.append(" '").append(matcher.getPattern()).append("'"); + sb.append(" '").append(matcher.getPattern()).append(QUOTE); } return sb.toString(); @@ -167,4 +355,57 @@ public class FeatureMatcher implements FeatureMatcherI { return byScore; } + + /** + * {@inheritDoc} The output of this method should be parseable by method + * fromString to restore the original object. + */ + @Override + public String toStableString() + { + StringBuilder sb = new StringBuilder(); + if (byLabel) + { + sb.append(LABEL); // no i18n here unlike toString() ! + } + else if (byScore) + { + sb.append(SCORE); + } + else + { + /* + * enclose attribute name in quotes if it includes space + */ + String displayName = toAttributeDisplayName(key); + if (displayName.contains(SPACE)) + { + sb.append(QUOTE).append(displayName).append(QUOTE); + } + else + { + sb.append(displayName); + } + } + + Condition condition = matcher.getCondition(); + sb.append(SPACE).append(condition.getStableName()); + String pattern = matcher.getPattern(); + if (condition.needsAPattern()) + { + /* + * enclose pattern in quotes if it includes space + */ + if (pattern.contains(SPACE)) + { + sb.append(SPACE).append(QUOTE).append(pattern).append(QUOTE); + } + else + { + sb.append(SPACE).append(pattern); + } + } + + return sb.toString(); + } } diff --git a/src/jalview/datamodel/features/FeatureMatcherI.java b/src/jalview/datamodel/features/FeatureMatcherI.java index 07b060c..396f500 100644 --- a/src/jalview/datamodel/features/FeatureMatcherI.java +++ b/src/jalview/datamodel/features/FeatureMatcherI.java @@ -48,4 +48,11 @@ public interface FeatureMatcherI * @return */ MatcherI getMatcher(); + + /** + * Answers a string representation of this object suitable for use when + * persisting data, in a format that should not change so can be reliably read + * back. + */ + String toStableString(); } diff --git a/src/jalview/datamodel/features/FeatureMatcherSet.java b/src/jalview/datamodel/features/FeatureMatcherSet.java index eb55387..3d28def 100644 --- a/src/jalview/datamodel/features/FeatureMatcherSet.java +++ b/src/jalview/datamodel/features/FeatureMatcherSet.java @@ -8,6 +8,16 @@ import java.util.List; public class FeatureMatcherSet implements FeatureMatcherSetI { + private static final String OR = "OR"; + + private static final String AND = "AND"; + + private static final String SPACE = " "; + + private static final String CLOSE_BRACKET = ")"; + + private static final String OPEN_BRACKET = "("; + private static final String OR_I18N = MessageManager .getString("label.or"); @@ -19,6 +29,112 @@ public class FeatureMatcherSet implements FeatureMatcherSetI boolean andConditions; /** + * A factory constructor that converts a stringified object (as output by + * toStableString) to an object instance. + * + * Format: + *
    + *
  • (condition1) AND (condition2) AND (condition3)
  • + *
  • or
  • + *
  • (condition1) OR (condition2) OR (condition3)
  • + *
+ * where OR and AND are not case-sensitive, and may not be mixed. Brackets are + * optional if there is only one condition. + * + * @param descriptor + * @return + * @see FeatureMatcher#fromString(String) + */ + public static FeatureMatcherSet fromString(final String descriptor) + { + String invalid = "Invalid descriptor: " + descriptor; + boolean firstCondition = true; + FeatureMatcherSet result = new FeatureMatcherSet(); + + String leftToParse = descriptor.trim(); + + while (leftToParse.length() > 0) + { + /* + * inspect AND or OR condition, check not mixed + */ + boolean and = true; + if (!firstCondition) + { + int spacePos = leftToParse.indexOf(SPACE); + if (spacePos == -1) + { + // trailing junk after a match condition + System.err.println(invalid); + return null; + } + String conjunction = leftToParse.substring(0, spacePos); + leftToParse = leftToParse.substring(spacePos + 1).trim(); + if (conjunction.equalsIgnoreCase(AND)) + { + and = true; + } + else if (conjunction.equalsIgnoreCase(OR)) + { + and = false; + } + else + { + // not an AND or an OR - invalid + System.err.println(invalid); + return null; + } + } + + /* + * now extract the next condition and AND or OR it + */ + String nextCondition = leftToParse; + if (leftToParse.startsWith(OPEN_BRACKET)) + { + int closePos = leftToParse.indexOf(CLOSE_BRACKET); + if (closePos == -1) + { + System.err.println(invalid); + return null; + } + nextCondition = leftToParse.substring(1, closePos); + leftToParse = leftToParse.substring(closePos + 1).trim(); + } + else + { + leftToParse = ""; + } + + FeatureMatcher fm = FeatureMatcher.fromString(nextCondition); + if (fm == null) + { + System.err.println(invalid); + return null; + } + try + { + if (and) + { + result.and(fm); + } + else + { + result.or(fm); + } + firstCondition = false; + } catch (IllegalStateException e) + { + // thrown if OR and AND are mixed + System.err.println(invalid); + return null; + } + + } + return result; + } + + /** * Constructor */ public FeatureMatcherSet() @@ -103,20 +219,34 @@ public class FeatureMatcherSet implements FeatureMatcherSetI return matchConditions; } + /** + * Answers a string representation of this object suitable for display, and + * possibly internationalized. The format is not guaranteed stable and may + * change in future. + */ @Override public String toString() { StringBuilder sb = new StringBuilder(); boolean first = true; + boolean multiple = matchConditions.size() > 1; for (FeatureMatcherI matcher : matchConditions) { if (!first) { String joiner = andConditions ? AND_18N : OR_I18N; - sb.append(" ").append(joiner.toLowerCase()).append(" "); + sb.append(SPACE).append(joiner.toLowerCase()).append(SPACE); } first = false; - sb.append("(").append(matcher.toString()).append(")"); + if (multiple) + { + sb.append(OPEN_BRACKET).append(matcher.toString()) + .append(CLOSE_BRACKET); + } + else + { + sb.append(matcher.toString()); + } } return sb.toString(); } @@ -127,4 +257,36 @@ public class FeatureMatcherSet implements FeatureMatcherSetI return matchConditions == null || matchConditions.isEmpty(); } + /** + * {@inheritDoc} The output of this method should be parseable by method + * fromString to restore the original object. + */ + @Override + public String toStableString() + { + StringBuilder sb = new StringBuilder(); + boolean moreThanOne = matchConditions.size() > 1; + boolean first = true; + + for (FeatureMatcherI matcher : matchConditions) + { + if (!first) + { + String joiner = andConditions ? AND : OR; + sb.append(SPACE).append(joiner).append(SPACE); + } + first = false; + if (moreThanOne) + { + sb.append(OPEN_BRACKET).append(matcher.toStableString()) + .append(CLOSE_BRACKET); + } + else + { + sb.append(matcher.toStableString()); + } + } + return sb.toString(); + } + } diff --git a/src/jalview/datamodel/features/FeatureMatcherSetI.java b/src/jalview/datamodel/features/FeatureMatcherSetI.java index f064770..8a9d675 100644 --- a/src/jalview/datamodel/features/FeatureMatcherSetI.java +++ b/src/jalview/datamodel/features/FeatureMatcherSetI.java @@ -60,4 +60,11 @@ public interface FeatureMatcherSetI * @return */ boolean isEmpty(); + + /** + * Answers a string representation of this object suitable for use when + * persisting data, in a format that should not change so can be reliably read + * back. + */ + String toStableString(); } diff --git a/src/jalview/util/matcher/Condition.java b/src/jalview/util/matcher/Condition.java index 3047802..8816a7f 100644 --- a/src/jalview/util/matcher/Condition.java +++ b/src/jalview/util/matcher/Condition.java @@ -8,19 +8,55 @@ import jalview.util.MessageManager; */ public enum Condition { - Contains(false, true), NotContains(false, true), Matches(false, true), - NotMatches(false, true), Present(false, false), NotPresent(false, false), - EQ(true, true), NE(true, true), LT(true, true), LE(true, true), - GT(true, true), GE(true, true); - + Contains(false, true, "Contains"), + NotContains(false, true, "NotContains"), Matches(false, true, "Matches"), + NotMatches(false, true, "NotMatches"), Present(false, false, "Present"), + NotPresent(false, false, "NotPresent"), EQ(true, true, "EQ"), + NE(true, true, "NE"), LT(true, true, "LT"), LE(true, true, "LE"), + GT(true, true, "GT"), GE(true, true, "GE"); + private boolean numeric; private boolean needsAPattern; - Condition(boolean isNumeric, boolean needsPattern) + /* + * value used to save a Condition to the + * Jalview project file or restore it from project; + * it should not be changed even if enum names change in future + */ + private String stableName; + + /** + * Answers the enum value whose 'stable name' matches the argument (not case + * sensitive), or null if no match + * + * @param stableName + * @return + */ + public static Condition fromString(String stableName) + { + for (Condition c : values()) + { + if (c.stableName.equalsIgnoreCase(stableName)) + { + return c; + } + } + return null; + } + + /** + * Constructor + * + * @param isNumeric + * @param needsPattern + * @param stablename + */ + Condition(boolean isNumeric, boolean needsPattern, String stablename) { numeric = isNumeric; needsAPattern = needsPattern; + stableName = stablename; } /** @@ -45,6 +81,11 @@ public enum Condition return needsAPattern; } + public String getStableName() + { + return stableName; + } + /** * Answers a display name for the match condition, suitable for showing in * drop-down menus. The value may be internationalized using the resource key diff --git a/src/jalview/util/matcher/Matcher.java b/src/jalview/util/matcher/Matcher.java index 353df83..0792509 100644 --- a/src/jalview/util/matcher/Matcher.java +++ b/src/jalview/util/matcher/Matcher.java @@ -42,8 +42,8 @@ public class Matcher implements MatcherI * @param compareTo * @return * @throws NumberFormatException - * if a numerical condition is specified with a non-numeric comparison - * value + * if a numerical condition is specified with a non-numeric + * comparison value * @throws NullPointerException * if a null condition or comparison string is specified */ diff --git a/test/jalview/datamodel/features/FeatureMatcherSetTest.java b/test/jalview/datamodel/features/FeatureMatcherSetTest.java index 56644fd..34bc8fe 100644 --- a/test/jalview/datamodel/features/FeatureMatcherSetTest.java +++ b/test/jalview/datamodel/features/FeatureMatcherSetTest.java @@ -2,6 +2,7 @@ package jalview.datamodel.features; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertNull; import static org.testng.Assert.assertSame; import static org.testng.Assert.assertTrue; import static org.testng.Assert.fail; @@ -127,7 +128,7 @@ public class FeatureMatcherSetTest FeatureMatcherSetI fms = new FeatureMatcherSet(); assertEquals(fms.toString(), ""); fms.and(fm1); - assertEquals(fms.toString(), "(AF < 1.2)"); + assertEquals(fms.toString(), "AF < 1.2"); fms.and(fm2); assertEquals(fms.toString(), "(AF < 1.2) and (CLIN_SIG does not contain 'path')"); @@ -138,7 +139,7 @@ public class FeatureMatcherSetTest fms = new FeatureMatcherSet(); assertEquals(fms.toString(), ""); fms.or(fm1); - assertEquals(fms.toString(), "(AF < 1.2)"); + assertEquals(fms.toString(), "AF < 1.2"); fms.or(fm2); assertEquals(fms.toString(), "(AF < 1.2) or (CLIN_SIG does not contain 'path')"); @@ -282,4 +283,136 @@ public class FeatureMatcherSetTest csq.put("Consequence", "Catastrophic"); assertTrue(fms.matches(sf)); } + + /** + * Tests for toStableString which (unlike toString) does not i18n the + * conditions + * + * @see FeatureMatcherTest#testToStableString() + */ + @Test(groups = "Functional") + public void testToStableString() + { + FeatureMatcherI fm1 = FeatureMatcher.byAttribute(Condition.LT, "1.2", + "AF"); + assertEquals(fm1.toStableString(), "AF LT 1.2"); + + FeatureMatcher fm2 = FeatureMatcher.byAttribute(Condition.NotContains, + "path", "CLIN_SIG"); + assertEquals(fm2.toStableString(), "CLIN_SIG NotContains path"); + + /* + * AND them + */ + FeatureMatcherSetI fms = new FeatureMatcherSet(); + assertEquals(fms.toStableString(), ""); + fms.and(fm1); + // no brackets needed if a single condition + assertEquals(fms.toStableString(), "AF LT 1.2"); + // brackets if more than one condition + fms.and(fm2); + assertEquals(fms.toStableString(), + "(AF LT 1.2) AND (CLIN_SIG NotContains path)"); + + /* + * OR them + */ + fms = new FeatureMatcherSet(); + assertEquals(fms.toStableString(), ""); + fms.or(fm1); + assertEquals(fms.toStableString(), "AF LT 1.2"); + fms.or(fm2); + assertEquals(fms.toStableString(), + "(AF LT 1.2) OR (CLIN_SIG NotContains path)"); + + /* + * attribute or value including space is quoted + */ + FeatureMatcher fm3 = FeatureMatcher.byAttribute(Condition.NotMatches, + "foo bar", "CSQ", "Poly Phen"); + assertEquals(fm3.toStableString(), + "'CSQ:Poly Phen' NotMatches 'foo bar'"); + fms.or(fm3); + assertEquals(fms.toStableString(), + "(AF LT 1.2) OR (CLIN_SIG NotContains path) OR ('CSQ:Poly Phen' NotMatches 'foo bar')"); + + try + { + fms.and(fm1); + fail("Expected exception"); + } catch (IllegalStateException e) + { + // expected + } + } + + /** + * Tests for parsing a string representation of a FeatureMatcherSet + * + * @see FeatureMatcherSetTest#testToStableString() + */ + @Test(groups = "Functional") + public void testFromString() + { + String descriptor = "AF LT 1.2"; + FeatureMatcherSetI fms = FeatureMatcherSet.fromString(descriptor); + + /* + * shortcut asserts by verifying a 'roundtrip', + * which we trust if other tests pass :-) + */ + assertEquals(fms.toStableString(), descriptor); + + // brackets optional, quotes optional, condition case insensitive + fms = FeatureMatcherSet.fromString("('AF' lt '1.2')"); + assertEquals(fms.toStableString(), descriptor); + + descriptor = "(AF LT 1.2) AND (CLIN_SIG NotContains path)"; + fms = FeatureMatcherSet.fromString(descriptor); + assertEquals(fms.toStableString(), descriptor); + + // AND is not case-sensitive + fms = FeatureMatcherSet + .fromString("(AF LT 1.2) and (CLIN_SIG NotContains path)"); + assertEquals(fms.toStableString(), descriptor); + + descriptor = "(AF LT 1.2) OR (CLIN_SIG NotContains path)"; + fms = FeatureMatcherSet.fromString(descriptor); + assertEquals(fms.toStableString(), descriptor); + + // OR is not case-sensitive + fms = FeatureMatcherSet + .fromString("(AF LT 1.2) or (CLIN_SIG NotContains path)"); + assertEquals(fms.toStableString(), descriptor); + + // can get away without brackets on last match condition + fms = FeatureMatcherSet + .fromString("(AF LT 1.2) or CLIN_SIG NotContains path"); + assertEquals(fms.toStableString(), descriptor); + + descriptor = "(AF LT 1.2) OR (CLIN_SIG NotContains path) OR ('CSQ:Poly Phen' NotMatches 'foo bar')"; + fms = FeatureMatcherSet.fromString(descriptor); + assertEquals(fms.toStableString(), descriptor); + + // can't mix OR and AND + descriptor = "(AF LT 1.2) OR (CLIN_SIG NotContains path) AND ('CSQ:Poly Phen' NotMatches 'foo bar')"; + assertNull(FeatureMatcherSet.fromString(descriptor)); + + // can't mix AND and OR + descriptor = "(AF LT 1.2) and (CLIN_SIG NotContains path) or ('CSQ:Poly Phen' NotMatches 'foo bar')"; + assertNull(FeatureMatcherSet.fromString(descriptor)); + + // brackets missing + assertNull(FeatureMatcherSet + .fromString("AF LT 1.2 or CLIN_SIG NotContains path")); + + // invalid conjunction + assertNull(FeatureMatcherSet.fromString("(AF LT 1.2) but (AF GT -2)")); + + // unbalanced quote (1) + assertNull(FeatureMatcherSet.fromString("('AF lt '1.2')")); + + // unbalanced quote (2) + assertNull(FeatureMatcherSet.fromString("('AF' lt '1.2)")); + } } diff --git a/test/jalview/datamodel/features/FeatureMatcherTest.java b/test/jalview/datamodel/features/FeatureMatcherTest.java index 62b03a3..fbbdbd5 100644 --- a/test/jalview/datamodel/features/FeatureMatcherTest.java +++ b/test/jalview/datamodel/features/FeatureMatcherTest.java @@ -3,6 +3,7 @@ package jalview.datamodel.features; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertFalse; import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertSame; import static org.testng.Assert.assertTrue; import jalview.datamodel.SequenceFeature; @@ -15,7 +16,7 @@ import org.testng.annotations.Test; public class FeatureMatcherTest { - @Test + @Test(groups = "Functional") public void testMatches_byLabel() { SequenceFeature sf = new SequenceFeature("Cath", "this is my label", 11, @@ -62,7 +63,7 @@ public class FeatureMatcherTest FeatureMatcher.byLabel(Condition.NotPresent, "").matches(sf)); } - @Test + @Test(groups = "Functional") public void testMatches_byScore() { SequenceFeature sf = new SequenceFeature("Cath", "this is my label", 11, @@ -87,7 +88,8 @@ public class FeatureMatcherTest assertFalse(FeatureMatcher.byScore(Condition.GT, "3.2").matches(sf)); assertTrue(FeatureMatcher.byScore(Condition.GT, "2.2").matches(sf)); } - @Test + + @Test(groups = "Functional") public void testMatches_byAttribute() { /* @@ -127,7 +129,7 @@ public class FeatureMatcherTest assertFalse(fm.matches(sf)); } - @Test + @Test(groups = "Functional") public void testToString() { Locale.setDefault(Locale.ENGLISH); @@ -162,7 +164,7 @@ public class FeatureMatcherTest MessageManager.getString("label.score") + " >= 12.2"); } - @Test + @Test(groups = "Functional") public void testGetAttribute() { FeatureMatcherI fm = FeatureMatcher.byAttribute(Condition.GE, "-2", @@ -184,7 +186,7 @@ public class FeatureMatcherTest assertNull(FeatureMatcher.byScore(Condition.LE, "-1").getAttribute()); } - @Test + @Test(groups = "Functional") public void testIsByLabel() { assertTrue(FeatureMatcher.byLabel(Condition.NotContains, "foo") @@ -194,7 +196,7 @@ public class FeatureMatcherTest .isByLabel()); } - @Test + @Test(groups = "Functional") public void testIsByScore() { assertFalse(FeatureMatcher.byLabel(Condition.NotContains, "foo") @@ -204,7 +206,7 @@ public class FeatureMatcherTest .isByScore()); } - @Test + @Test(groups = "Functional") public void testGetMatcher() { FeatureMatcherI fm = FeatureMatcher.byAttribute(Condition.GE, "-2f", @@ -213,4 +215,128 @@ public class FeatureMatcherTest assertEquals(fm.getMatcher().getFloatValue(), -2F); assertEquals(fm.getMatcher().getPattern(), "-2.0"); } + + @Test(groups = "Functional") + public void testFromString() + { + FeatureMatcherI fm = FeatureMatcher.fromString("'AF' LT 1.2"); + assertFalse(fm.isByLabel()); + assertFalse(fm.isByScore()); + assertEquals(fm.getAttribute(), new String[] { "AF" }); + assertSame(Condition.LT, fm.getMatcher().getCondition()); + assertEquals(fm.getMatcher().getFloatValue(), 1.2f); + assertEquals(fm.getMatcher().getPattern(), "1.2"); + + // quotes are optional, condition is not case sensitive + fm = FeatureMatcher.fromString("AF lt '1.2'"); + assertFalse(fm.isByLabel()); + assertFalse(fm.isByScore()); + assertEquals(fm.getAttribute(), new String[] { "AF" }); + assertSame(Condition.LT, fm.getMatcher().getCondition()); + assertEquals(fm.getMatcher().getFloatValue(), 1.2f); + assertEquals(fm.getMatcher().getPattern(), "1.2"); + + fm = FeatureMatcher.fromString("'AF' Present"); + assertFalse(fm.isByLabel()); + assertFalse(fm.isByScore()); + assertEquals(fm.getAttribute(), new String[] { "AF" }); + assertSame(Condition.Present, fm.getMatcher().getCondition()); + + fm = FeatureMatcher.fromString("CSQ:Consequence contains damaging"); + assertFalse(fm.isByLabel()); + assertFalse(fm.isByScore()); + assertEquals(fm.getAttribute(), new String[] { "CSQ", "Consequence" }); + assertSame(Condition.Contains, fm.getMatcher().getCondition()); + assertEquals(fm.getMatcher().getPattern(), "damaging"); + + // keyword Label is not case sensitive + fm = FeatureMatcher.fromString("LABEL Matches 'foobar'"); + assertTrue(fm.isByLabel()); + assertFalse(fm.isByScore()); + assertNull(fm.getAttribute()); + assertSame(Condition.Matches, fm.getMatcher().getCondition()); + assertEquals(fm.getMatcher().getPattern(), "foobar"); + + fm = FeatureMatcher.fromString("'Label' matches 'foo bar'"); + assertTrue(fm.isByLabel()); + assertFalse(fm.isByScore()); + assertNull(fm.getAttribute()); + assertSame(Condition.Matches, fm.getMatcher().getCondition()); + assertEquals(fm.getMatcher().getPattern(), "foo bar"); + + // quotes optional on pattern + fm = FeatureMatcher.fromString("'Label' matches foo bar"); + assertTrue(fm.isByLabel()); + assertFalse(fm.isByScore()); + assertNull(fm.getAttribute()); + assertSame(Condition.Matches, fm.getMatcher().getCondition()); + assertEquals(fm.getMatcher().getPattern(), "foo bar"); + + fm = FeatureMatcher.fromString("Score GE 12.2"); + assertFalse(fm.isByLabel()); + assertTrue(fm.isByScore()); + assertNull(fm.getAttribute()); + assertSame(Condition.GE, fm.getMatcher().getCondition()); + assertEquals(fm.getMatcher().getPattern(), "12.2"); + assertEquals(fm.getMatcher().getFloatValue(), 12.2f); + + // keyword Score is not case sensitive + fm = FeatureMatcher.fromString("'SCORE' ge '12.2'"); + assertFalse(fm.isByLabel()); + assertTrue(fm.isByScore()); + assertNull(fm.getAttribute()); + assertSame(Condition.GE, fm.getMatcher().getCondition()); + assertEquals(fm.getMatcher().getPattern(), "12.2"); + assertEquals(fm.getMatcher().getFloatValue(), 12.2f); + + // invalid numeric pattern + assertNull(FeatureMatcher.fromString("Score eq twelve")); + // unbalanced opening quote + assertNull(FeatureMatcher.fromString("'Score ge 12.2")); + // unbalanced pattern quote + assertNull(FeatureMatcher.fromString("'Score' ge '12.2")); + // pattern missing + assertNull(FeatureMatcher.fromString("Score ge")); + // condition and pattern missing + assertNull(FeatureMatcher.fromString("Score")); + // everything missing + assertNull(FeatureMatcher.fromString("")); + } + + /** + * Tests for toStableString which (unlike toString) does not i18n the + * conditions + */ + @Test(groups = "Functional") + public void testToStableString() + { + // attribute name not quoted unless it contains space + FeatureMatcherI fm = FeatureMatcher.byAttribute(Condition.LT, "1.2", + "AF"); + assertEquals(fm.toStableString(), "AF LT 1.2"); + + /* + * Present / NotPresent omit the value pattern + */ + fm = FeatureMatcher.byAttribute(Condition.Present, "", "AF"); + assertEquals(fm.toStableString(), "AF Present"); + fm = FeatureMatcher.byAttribute(Condition.NotPresent, "", "AF"); + assertEquals(fm.toStableString(), "AF NotPresent"); + + /* + * by Label + * pattern not quoted unless it contains space + */ + fm = FeatureMatcher.byLabel(Condition.Matches, "foobar"); + assertEquals(fm.toStableString(), "Label Matches foobar"); + + fm = FeatureMatcher.byLabel(Condition.Matches, "foo bar"); + assertEquals(fm.toStableString(), "Label Matches 'foo bar'"); + + /* + * by Score + */ + fm = FeatureMatcher.byScore(Condition.GE, "12.2"); + assertEquals(fm.toStableString(), "Score GE 12.2"); + } } diff --git a/test/jalview/util/matcher/ConditionTest.java b/test/jalview/util/matcher/ConditionTest.java index 883596a..2a12534 100644 --- a/test/jalview/util/matcher/ConditionTest.java +++ b/test/jalview/util/matcher/ConditionTest.java @@ -1,6 +1,7 @@ package jalview.util.matcher; import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNull; import java.util.Locale; @@ -30,4 +31,46 @@ public class ConditionTest */ assertEquals(Condition.NE.toString(), "not ="); } + + @Test(groups = "Functional") + public void testGetStableName() + { + assertEquals(Condition.Contains.getStableName(), "Contains"); + assertEquals(Condition.NotContains.getStableName(), "NotContains"); + assertEquals(Condition.Matches.getStableName(), "Matches"); + assertEquals(Condition.NotMatches.getStableName(), "NotMatches"); + assertEquals(Condition.Present.getStableName(), "Present"); + assertEquals(Condition.NotPresent.getStableName(), "NotPresent"); + assertEquals(Condition.LT.getStableName(), "LT"); + assertEquals(Condition.LE.getStableName(), "LE"); + assertEquals(Condition.GT.getStableName(), "GT"); + assertEquals(Condition.GE.getStableName(), "GE"); + assertEquals(Condition.EQ.getStableName(), "EQ"); + assertEquals(Condition.NE.getStableName(), "NE"); + } + + @Test(groups = "Functional") + public void testFromString() + { + assertEquals(Condition.fromString("Contains"), Condition.Contains); + // not case sensitive + assertEquals(Condition.fromString("contains"), Condition.Contains); + assertEquals(Condition.fromString("CONTAINS"), Condition.Contains); + assertEquals(Condition.fromString("NotContains"), + Condition.NotContains); + assertEquals(Condition.fromString("Matches"), Condition.Matches); + assertEquals(Condition.fromString("NotMatches"), Condition.NotMatches); + assertEquals(Condition.fromString("Present"), Condition.Present); + assertEquals(Condition.fromString("NotPresent"), Condition.NotPresent); + assertEquals(Condition.fromString("LT"), Condition.LT); + assertEquals(Condition.fromString("LE"), Condition.LE); + assertEquals(Condition.fromString("GT"), Condition.GT); + assertEquals(Condition.fromString("GE"), Condition.GE); + assertEquals(Condition.fromString("EQ"), Condition.EQ); + assertEquals(Condition.fromString("NE"), Condition.NE); + + assertNull(Condition.fromString("Equals")); + assertNull(Condition.fromString("")); + assertNull(Condition.fromString(null)); + } } -- 1.7.10.2