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 public boolean isByAttribute()
362 return getAttribute() != null;
366 * {@inheritDoc} The output of this method should be parseable by method
367 * <code>fromString<code> to restore the original object.
370 public String toStableString()
372 StringBuilder sb = new StringBuilder();
375 sb.append(LABEL); // no i18n here unlike toString() !
384 * enclose attribute name in quotes if it includes space
386 String displayName = toAttributeDisplayName(key);
387 if (displayName.contains(SPACE))
389 sb.append(QUOTE).append(displayName).append(QUOTE);
393 sb.append(displayName);
397 Condition condition = matcher.getCondition();
398 sb.append(SPACE).append(condition.getStableName());
399 String pattern = matcher.getPattern();
400 if (condition.needsAPattern())
403 * enclose pattern in quotes if it includes space
405 if (pattern.contains(SPACE))
407 sb.append(SPACE).append(QUOTE).append(pattern).append(QUOTE);
411 sb.append(SPACE).append(pattern);
415 return sb.toString();