2 * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3 * Copyright (C) $$Year-Rel$$ The Jalview Authors
5 * This file is part of Jalview.
7 * Jalview is free software: you can redistribute it and/or
8 * modify it under the terms of the GNU General Public License
9 * as published by the Free Software Foundation, either version 3
10 * of the License, or (at your option) any later version.
12 * Jalview is distributed in the hope that it will be useful, but
13 * WITHOUT ANY WARRANTY; without even the implied warranty
14 * of MERCHANTABILITY or FITNESS FOR A PARTICULAR
15 * PURPOSE. See the GNU General Public License for more details.
17 * You should have received a copy of the GNU General Public License
18 * along with Jalview. If not, see <http://www.gnu.org/licenses/>.
19 * The Jalview Authors are detailed in the 'AUTHORS' file.
21 package jalview.datamodel.features;
23 import java.util.Locale;
25 import jalview.datamodel.SequenceFeature;
26 import jalview.util.MessageManager;
27 import jalview.util.matcher.Condition;
28 import jalview.util.matcher.Matcher;
29 import jalview.util.matcher.MatcherI;
32 * An immutable class that models one or more match conditions, each of which is
33 * applied to the value obtained by lookup given the match key.
35 * For example, the value provider could be a SequenceFeature's attributes map,
36 * and the conditions might be
38 * <li>CSQ contains "pathological"</li>
40 * <li>AF <= 1.0e-5</li>
46 public class FeatureMatcher implements FeatureMatcherI
48 private static final String SCORE = "Score";
50 private static final String LABEL = "Label";
52 private static final String SPACE = " ";
54 private static final String QUOTE = "'";
57 * a dummy matcher that comes in useful for the 'add a filter' gui row
59 public static final FeatureMatcherI NULL_MATCHER = FeatureMatcher
60 .byLabel(Condition.values()[0], "");
62 private static final String COLON = ":";
65 * if true, match is against feature description
67 final private boolean byLabel;
70 * if true, match is against feature score
72 final private boolean byScore;
75 * if not null, match is against feature attribute [sub-attribute]
77 final private String[] key;
79 final private MatcherI matcher;
82 * A helper method that converts a 'compound' attribute name from its display
83 * form, e.g. CSQ:PolyPhen to array form, e.g. { "CSQ", "PolyPhen" }
88 public static String[] fromAttributeDisplayName(String attribute)
90 return attribute == null ? null : attribute.split(COLON);
94 * A helper method that converts a 'compound' attribute name to its display
95 * form, e.g. CSQ:PolyPhen from its array form, e.g. { "CSQ", "PolyPhen" }
100 public static String toAttributeDisplayName(String[] attName)
102 return attName == null ? "" : String.join(COLON, attName);
106 * A factory constructor that converts a stringified object (as output by
107 * toStableString) to an object instance. Returns null if parsing fails.
109 * Leniency in parsing (for manually created feature files):
111 * <li>keywords Score and Label, and the condition, are not
112 * case-sensitive</li>
113 * <li>quotes around value and pattern are optional if string does not include
120 public static FeatureMatcher fromString(final String descriptor)
122 String invalidFormat = "Invalid matcher format: " + descriptor;
126 * value condition pattern
127 * where value is Label or Space or attributeName or attName1:attName2
128 * and pattern is a float value as string, or a text string
129 * attribute names or patterns may be quoted (must be if include space)
131 String attName = null;
132 boolean byScore = false;
133 boolean byLabel = false;
134 Condition cond = null;
135 String pattern = null;
138 * parse first field (Label / Score / attribute)
139 * optionally in quotes (required if attName includes space)
141 String leftToParse = descriptor;
142 String firstField = null;
144 if (descriptor.startsWith(QUOTE))
146 // 'Label' / 'Score' / 'attName'
147 int nextQuotePos = descriptor.indexOf(QUOTE, 1);
148 if (nextQuotePos == -1)
150 System.err.println(invalidFormat);
153 firstField = descriptor.substring(1, nextQuotePos);
154 leftToParse = descriptor.substring(nextQuotePos + 1).trim();
158 // Label / Score / attName (unquoted)
159 int nextSpacePos = descriptor.indexOf(SPACE);
160 if (nextSpacePos == -1)
162 System.err.println(invalidFormat);
165 firstField = descriptor.substring(0, nextSpacePos);
166 leftToParse = descriptor.substring(nextSpacePos + 1).trim();
168 String lower = firstField.toLowerCase(Locale.ROOT);
169 if (lower.startsWith(LABEL.toLowerCase(Locale.ROOT)))
173 else if (lower.startsWith(SCORE.toLowerCase(Locale.ROOT)))
179 attName = firstField;
183 * next field is the comparison condition
184 * most conditions require a following pattern (optionally quoted)
185 * although some conditions e.g. Present do not
187 int nextSpacePos = leftToParse.indexOf(SPACE);
188 if (nextSpacePos == -1)
191 * no value following condition - only valid for some conditions
193 cond = Condition.fromString(leftToParse);
194 if (cond == null || cond.needsAPattern())
196 System.err.println(invalidFormat);
203 * condition and pattern
205 cond = Condition.fromString(leftToParse.substring(0, nextSpacePos));
206 leftToParse = leftToParse.substring(nextSpacePos + 1).trim();
207 if (leftToParse.startsWith(QUOTE))
210 if (leftToParse.endsWith(QUOTE))
212 pattern = leftToParse.substring(1, leftToParse.length() - 1);
217 System.err.println(invalidFormat);
224 pattern = leftToParse;
229 * we have parsed out value, condition and pattern
230 * so can now make the FeatureMatcher
236 return FeatureMatcher.byLabel(cond, pattern);
240 return FeatureMatcher.byScore(cond, pattern);
244 String[] attNames = FeatureMatcher
245 .fromAttributeDisplayName(attName);
246 return FeatureMatcher.byAttribute(cond, pattern, attNames);
248 } catch (NumberFormatException e)
250 // numeric condition with non-numeric pattern
256 * A factory constructor method for a matcher that applies its match condition
257 * to the feature label (description)
262 * @throws NumberFormatException
263 * if an invalid numeric pattern is supplied
265 public static FeatureMatcher byLabel(Condition cond, String pattern)
267 return new FeatureMatcher(new Matcher(cond, pattern), true, false,
272 * A factory constructor method for a matcher that applies its match condition
273 * to the feature score
278 * @throws NumberFormatException
279 * if an invalid numeric pattern is supplied
281 public static FeatureMatcher byScore(Condition cond, String pattern)
283 return new FeatureMatcher(new Matcher(cond, pattern), false, true,
288 * A factory constructor method for a matcher that applies its match condition
289 * to the named feature attribute [and optional sub-attribute]
295 * @throws NumberFormatException
296 * if an invalid numeric pattern is supplied
298 public static FeatureMatcher byAttribute(Condition cond, String pattern,
301 return new FeatureMatcher(new Matcher(cond, pattern), false, false,
305 private FeatureMatcher(Matcher m, boolean forLabel, boolean forScore,
315 public boolean matches(SequenceFeature feature)
317 String value = byLabel ? feature.getDescription()
318 : (byScore ? String.valueOf(feature.getScore())
319 : feature.getValueAsString(key));
320 return matcher.matches(value);
324 public String[] getAttribute()
330 public MatcherI getMatcher()
336 * Answers a string description of this matcher, suitable for display,
337 * debugging or logging. The format may change in future.
340 public String toString()
342 StringBuilder sb = new StringBuilder();
345 sb.append(MessageManager.getString("label.label"));
349 sb.append(MessageManager.getString("label.score"));
353 sb.append(String.join(COLON, key));
356 Condition condition = matcher.getCondition();
357 sb.append(SPACE).append(condition.toString().toLowerCase(Locale.ROOT));
358 if (condition.isNumeric())
360 sb.append(SPACE).append(matcher.getPattern());
362 else if (condition.needsAPattern())
364 sb.append(" '").append(matcher.getPattern()).append(QUOTE);
367 return sb.toString();
371 public boolean isByLabel()
377 public boolean isByScore()
383 public boolean isByAttribute()
385 return getAttribute() != null;
389 * {@inheritDoc} The output of this method should be parseable by method
390 * <code>fromString<code> to restore the original object.
393 public String toStableString()
395 StringBuilder sb = new StringBuilder();
398 sb.append(LABEL); // no i18n here unlike toString() !
407 * enclose attribute name in quotes if it includes space
409 String displayName = toAttributeDisplayName(key);
410 if (displayName.contains(SPACE))
412 sb.append(QUOTE).append(displayName).append(QUOTE);
416 sb.append(displayName);
420 Condition condition = matcher.getCondition();
421 sb.append(SPACE).append(condition.getStableName());
422 String pattern = matcher.getPattern();
423 if (condition.needsAPattern())
426 * enclose pattern in quotes if it includes space
428 if (pattern.contains(SPACE))
430 sb.append(SPACE).append(QUOTE).append(pattern).append(QUOTE);
434 sb.append(SPACE).append(pattern);
438 return sb.toString();