1 package jalview.datamodel.features;
3 import jalview.datamodel.SequenceFeature;
4 import jalview.util.MessageManager;
5 import jalview.util.matcher.Condition;
6 import jalview.util.matcher.Matcher;
7 import jalview.util.matcher.MatcherI;
10 * An immutable class that models one or more match conditions, each of which is
11 * applied to the value obtained by lookup given the match key.
13 * For example, the value provider could be a SequenceFeature's attributes map,
14 * and the conditions might be
16 * <li>CSQ contains "pathological"</li>
18 * <li>AF <= 1.0e-5</li>
24 public class FeatureMatcher implements FeatureMatcherI
26 private static final String SCORE = "Score";
28 private static final String LABEL = "Label";
30 private static final String SPACE = " ";
32 private static final String QUOTE = "'";
35 * a dummy matcher that comes in useful for the 'add a filter' gui row
37 public static final FeatureMatcherI NULL_MATCHER = FeatureMatcher
38 .byLabel(Condition.values()[0], "");
40 private static final String COLON = ":";
43 * if true, match is against feature description
45 final private boolean byLabel;
48 * if true, match is against feature score
50 final private boolean byScore;
53 * if not null, match is against feature attribute [sub-attribute]
55 final private String[] key;
57 final private MatcherI matcher;
60 * A helper method that converts a 'compound' attribute name from its display
61 * form, e.g. CSQ:PolyPhen to array form, e.g. { "CSQ", "PolyPhen" }
66 public static String[] fromAttributeDisplayName(String attribute)
68 return attribute == null ? null : attribute.split(COLON);
72 * A helper method that converts a 'compound' attribute name to its display
73 * form, e.g. CSQ:PolyPhen from its array form, e.g. { "CSQ", "PolyPhen" }
78 public static String toAttributeDisplayName(String[] attName)
80 return attName == null ? "" : String.join(COLON, attName);
84 * A factory constructor that converts a stringified object (as output by
85 * toStableString) to an object instance. Returns null if parsing fails.
87 * Leniency in parsing (for manually created feature files):
89 * <li>keywords Score and Label, and the condition, are not
91 * <li>quotes around value and pattern are optional if string does not include
98 public static FeatureMatcher fromString(final String descriptor)
100 String invalidFormat = "Invalid matcher format: " + descriptor;
104 * value condition pattern
105 * where value is Label or Space or attributeName or attName1:attName2
106 * and pattern is a float value as string, or a text string
107 * attribute names or patterns may be quoted (must be if include space)
109 String attName = null;
110 boolean byScore = false;
111 boolean byLabel = false;
112 Condition cond = null;
113 String pattern = null;
116 * parse first field (Label / Score / attribute)
117 * optionally in quotes (required if attName includes space)
119 String leftToParse = descriptor;
120 String firstField = null;
122 if (descriptor.startsWith(QUOTE))
124 // 'Label' / 'Score' / 'attName'
125 int nextQuotePos = descriptor.indexOf(QUOTE, 1);
126 if (nextQuotePos == -1)
128 System.err.println(invalidFormat);
131 firstField = descriptor.substring(1, nextQuotePos);
132 leftToParse = descriptor.substring(nextQuotePos + 1).trim();
136 // Label / Score / attName (unquoted)
137 int nextSpacePos = descriptor.indexOf(SPACE);
138 if (nextSpacePos == -1)
140 System.err.println(invalidFormat);
143 firstField = descriptor.substring(0, nextSpacePos);
144 leftToParse = descriptor.substring(nextSpacePos + 1).trim();
146 String lower = firstField.toLowerCase();
147 if (lower.startsWith(LABEL.toLowerCase()))
151 else if (lower.startsWith(SCORE.toLowerCase()))
157 attName = firstField;
161 * next field is the comparison condition
162 * most conditions require a following pattern (optionally quoted)
163 * although some conditions e.g. Present do not
165 int nextSpacePos = leftToParse.indexOf(SPACE);
166 if (nextSpacePos == -1)
169 * no value following condition - only valid for some conditions
171 cond = Condition.fromString(leftToParse);
172 if (cond == null || cond.needsAPattern())
174 System.err.println(invalidFormat);
181 * condition and pattern
183 cond = Condition.fromString(leftToParse.substring(0, nextSpacePos));
184 leftToParse = leftToParse.substring(nextSpacePos + 1).trim();
185 if (leftToParse.startsWith(QUOTE))
188 if (leftToParse.endsWith(QUOTE))
190 pattern = leftToParse.substring(1, leftToParse.length() - 1);
195 System.err.println(invalidFormat);
202 pattern = leftToParse;
207 * we have parsed out value, condition and pattern
208 * so can now make the FeatureMatcher
214 return FeatureMatcher.byLabel(cond, pattern);
218 return FeatureMatcher.byScore(cond, pattern);
222 String[] attNames = FeatureMatcher
223 .fromAttributeDisplayName(attName);
224 return FeatureMatcher.byAttribute(cond, pattern, attNames);
226 } catch (NumberFormatException e)
228 // numeric condition with non-numeric pattern
234 * A factory constructor method for a matcher that applies its match condition
235 * to the feature label (description)
240 * @throws NumberFormatException
241 * if an invalid numeric pattern is supplied
243 public static FeatureMatcher byLabel(Condition cond, String pattern)
245 return new FeatureMatcher(new Matcher(cond, pattern), true, false,
250 * A factory constructor method for a matcher that applies its match condition
251 * to the feature score
256 * @throws NumberFormatException
257 * if an invalid numeric pattern is supplied
259 public static FeatureMatcher byScore(Condition cond, String pattern)
261 return new FeatureMatcher(new Matcher(cond, pattern), false, true,
266 * A factory constructor method for a matcher that applies its match condition
267 * to the named feature attribute [and optional sub-attribute]
273 * @throws NumberFormatException
274 * if an invalid numeric pattern is supplied
276 public static FeatureMatcher byAttribute(Condition cond, String pattern,
279 return new FeatureMatcher(new Matcher(cond, pattern), false, false,
283 private FeatureMatcher(Matcher m, boolean forLabel, boolean forScore,
292 public boolean matches(SequenceFeature feature)
294 String value = byLabel ? feature.getDescription()
295 : (byScore ? String.valueOf(feature.getScore())
296 : feature.getValueAsString(key));
297 return matcher.matches(value);
301 public String[] getAttribute()
307 public MatcherI getMatcher()
313 * Answers a string description of this matcher, suitable for display, debugging
314 * or logging. The format may change in future.
317 public String toString()
319 StringBuilder sb = new StringBuilder();
322 sb.append(MessageManager.getString("label.label"));
326 sb.append(MessageManager.getString("label.score"));
330 sb.append(String.join(COLON, key));
333 Condition condition = matcher.getCondition();
334 sb.append(SPACE).append(condition.toString().toLowerCase());
335 if (condition.isNumeric())
337 sb.append(SPACE).append(matcher.getPattern());
339 else if (condition.needsAPattern())
341 sb.append(" '").append(matcher.getPattern()).append(QUOTE);
344 return sb.toString();
348 public boolean isByLabel()
354 public boolean isByScore()
360 * {@inheritDoc} The output of this method should be parseable by method
361 * <code>fromString<code> to restore the original object.
364 public String toStableString()
366 StringBuilder sb = new StringBuilder();
369 sb.append(LABEL); // no i18n here unlike toString() !
378 * enclose attribute name in quotes if it includes space
380 String displayName = toAttributeDisplayName(key);
381 if (displayName.contains(SPACE))
383 sb.append(QUOTE).append(displayName).append(QUOTE);
387 sb.append(displayName);
391 Condition condition = matcher.getCondition();
392 sb.append(SPACE).append(condition.getStableName());
393 String pattern = matcher.getPattern();
394 if (condition.needsAPattern())
397 * enclose pattern in quotes if it includes space
399 if (pattern.contains(SPACE))
401 sb.append(SPACE).append(QUOTE).append(pattern).append(QUOTE);
405 sb.append(SPACE).append(pattern);
409 return sb.toString();