Clover icon

Coverage Report

  1. Project Clover database Thu Nov 28 2024 11:45:30 GMT
  2. Package jalview.datamodel.features

File FeatureMatcher.java

 

Coverage histogram

../../../img/srcFileCovDistChart10.png
0% of files have more coverage

Code metrics

48
95
15
1
440
251
41
0.43
6.33
15
2.73

Classes

Class Line # Actions
FeatureMatcher 46 95 41
0.9873417698.7%
 

Contributing tests

This file is covered by 32 tests. .

Source view

1    /*
2    * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3    * Copyright (C) $$Year-Rel$$ The Jalview Authors
4    *
5    * This file is part of Jalview.
6    *
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.
11    *
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.
16    *
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.
20    */
21    package jalview.datamodel.features;
22   
23    import java.util.Locale;
24   
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;
30   
31    /**
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.
34    * <p>
35    * For example, the value provider could be a SequenceFeature's attributes map,
36    * and the conditions might be
37    * <ul>
38    * <li>CSQ contains "pathological"</li>
39    * <li>AND</li>
40    * <li>AF <= 1.0e-5</li>
41    * </ul>
42    *
43    * @author gmcarstairs
44    *
45    */
 
46    public class FeatureMatcher implements FeatureMatcherI
47    {
48    private static final String SCORE = "Score";
49   
50    private static final String LABEL = "Label";
51   
52    private static final String SPACE = " ";
53   
54    private static final String QUOTE = "'";
55   
56    /*
57    * a dummy matcher that comes in useful for the 'add a filter' gui row
58    */
59    public static final FeatureMatcherI NULL_MATCHER = FeatureMatcher
60    .byLabel(Condition.values()[0], "");
61   
62    private static final String COLON = ":";
63   
64    /*
65    * if true, match is against feature description
66    */
67    final private boolean byLabel;
68   
69    /*
70    * if true, match is against feature score
71    */
72    final private boolean byScore;
73   
74    /*
75    * if not null, match is against feature attribute [sub-attribute]
76    */
77    final private String[] key;
78   
79    final private MatcherI matcher;
80   
81    /**
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" }
84    *
85    * @param attribute
86    * @return
87    */
 
88  32 toggle public static String[] fromAttributeDisplayName(String attribute)
89    {
90  32 return attribute == null ? null : attribute.split(COLON);
91    }
92   
93    /**
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" }
96    *
97    * @param attName
98    * @return
99    */
 
100  43 toggle public static String toAttributeDisplayName(String[] attName)
101    {
102  43 return attName == null ? "" : String.join(COLON, attName);
103    }
104   
105    /**
106    * A factory constructor that converts a stringified object (as output by
107    * toStableString) to an object instance. Returns null if parsing fails.
108    * <p>
109    * Leniency in parsing (for manually created feature files):
110    * <ul>
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
114    * a space</li>
115    * </ul>
116    *
117    * @param descriptor
118    * @return
119    */
 
120  46 toggle public static FeatureMatcher fromString(final String descriptor)
121    {
122  46 String invalidFormat = "Invalid matcher format: " + descriptor;
123   
124    /*
125    * expect
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)
130    */
131  46 String attName = null;
132  46 boolean byScore = false;
133  46 boolean byLabel = false;
134  46 Condition cond = null;
135  46 String pattern = null;
136   
137    /*
138    * parse first field (Label / Score / attribute)
139    * optionally in quotes (required if attName includes space)
140    */
141  46 String leftToParse = descriptor;
142  46 String firstField = null;
143   
144  46 if (descriptor.startsWith(QUOTE))
145    {
146    // 'Label' / 'Score' / 'attName'
147  13 int nextQuotePos = descriptor.indexOf(QUOTE, 1);
148  13 if (nextQuotePos == -1)
149    {
150  1 jalview.bin.Console.errPrintln(invalidFormat);
151  1 return null;
152    }
153  12 firstField = descriptor.substring(1, nextQuotePos);
154  12 leftToParse = descriptor.substring(nextQuotePos + 1).trim();
155    }
156    else
157    {
158    // Label / Score / attName (unquoted)
159  33 int nextSpacePos = descriptor.indexOf(SPACE);
160  33 if (nextSpacePos == -1)
161    {
162  2 jalview.bin.Console.errPrintln(invalidFormat);
163  2 return null;
164    }
165  31 firstField = descriptor.substring(0, nextSpacePos);
166  31 leftToParse = descriptor.substring(nextSpacePos + 1).trim();
167    }
168  43 String lower = firstField.toLowerCase(Locale.ROOT);
169  43 if (lower.startsWith(LABEL.toLowerCase(Locale.ROOT)))
170    {
171  5 byLabel = true;
172    }
173  38 else if (lower.startsWith(SCORE.toLowerCase(Locale.ROOT)))
174    {
175  8 byScore = true;
176    }
177    else
178    {
179  30 attName = firstField;
180    }
181   
182    /*
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
186    */
187  43 int nextSpacePos = leftToParse.indexOf(SPACE);
188  43 if (nextSpacePos == -1)
189    {
190    /*
191    * no value following condition - only valid for some conditions
192    */
193  3 cond = Condition.fromString(leftToParse);
194  3 if (cond == null || cond.needsAPattern())
195    {
196  2 jalview.bin.Console.errPrintln(invalidFormat);
197  2 return null;
198    }
199    }
200    else
201    {
202    /*
203    * condition and pattern
204    */
205  40 cond = Condition.fromString(leftToParse.substring(0, nextSpacePos));
206  40 leftToParse = leftToParse.substring(nextSpacePos + 1).trim();
207  40 if (leftToParse.startsWith(QUOTE))
208    {
209    // pattern in quotes
210  11 if (leftToParse.endsWith(QUOTE))
211    {
212  9 pattern = leftToParse.substring(1, leftToParse.length() - 1);
213    }
214    else
215    {
216    // unbalanced quote
217  2 jalview.bin.Console.errPrintln(invalidFormat);
218  2 return null;
219    }
220    }
221    else
222    {
223    // unquoted pattern
224  29 pattern = leftToParse;
225    }
226    }
227   
228    /*
229    * we have parsed out value, condition and pattern
230    * so can now make the FeatureMatcher
231    */
232  39 try
233    {
234  39 if (byLabel)
235    {
236  5 return FeatureMatcher.byLabel(cond, pattern);
237    }
238  34 else if (byScore)
239    {
240  6 return FeatureMatcher.byScore(cond, pattern);
241    }
242    else
243    {
244  28 String[] attNames = FeatureMatcher
245    .fromAttributeDisplayName(attName);
246  28 return FeatureMatcher.byAttribute(cond, pattern, attNames);
247    }
248    } catch (NumberFormatException e)
249    {
250    // numeric condition with non-numeric pattern
251  2 return null;
252    }
253    }
254   
255    /**
256    * A factory constructor method for a matcher that applies its match condition
257    * to the feature label (description)
258    *
259    * @param cond
260    * @param pattern
261    * @return
262    * @throws NumberFormatException
263    * if an invalid numeric pattern is supplied
264    */
 
265  31 toggle public static FeatureMatcher byLabel(Condition cond, String pattern)
266    {
267  31 return new FeatureMatcher(new Matcher(cond, pattern), true, false,
268    null);
269    }
270   
271    /**
272    * A factory constructor method for a matcher that applies its match condition
273    * to the feature score
274    *
275    * @param cond
276    * @param pattern
277    * @return
278    * @throws NumberFormatException
279    * if an invalid numeric pattern is supplied
280    */
 
281  37 toggle public static FeatureMatcher byScore(Condition cond, String pattern)
282    {
283  37 return new FeatureMatcher(new Matcher(cond, pattern), false, true,
284    null);
285    }
286   
287    /**
288    * A factory constructor method for a matcher that applies its match condition
289    * to the named feature attribute [and optional sub-attribute]
290    *
291    * @param cond
292    * @param pattern
293    * @param attName
294    * @return
295    * @throws NumberFormatException
296    * if an invalid numeric pattern is supplied
297    */
 
298  75 toggle public static FeatureMatcher byAttribute(Condition cond, String pattern,
299    String... attName)
300    {
301  75 return new FeatureMatcher(new Matcher(cond, pattern), false, false,
302    attName);
303    }
304   
 
305  141 toggle private FeatureMatcher(Matcher m, boolean forLabel, boolean forScore,
306    String[] theKey)
307    {
308  141 key = theKey;
309  141 matcher = m;
310  141 byLabel = forLabel;
311  141 byScore = forScore;
312    }
313   
 
314  94 toggle @Override
315    public boolean matches(SequenceFeature feature)
316    {
317  94 String value = byLabel ? feature.getDescription()
318  81 : (byScore ? String.valueOf(feature.getScore())
319    : feature.getValueAsString(key));
320  94 return matcher.matches(value);
321    }
322   
 
323  31 toggle @Override
324    public String[] getAttribute()
325    {
326  31 return key;
327    }
328   
 
329  39 toggle @Override
330    public MatcherI getMatcher()
331    {
332  39 return matcher;
333    }
334   
335    /**
336    * Answers a string description of this matcher, suitable for display,
337    * debugging or logging. The format may change in future.
338    */
 
339  13 toggle @Override
340    public String toString()
341    {
342  13 StringBuilder sb = new StringBuilder();
343  13 if (byLabel)
344    {
345  1 sb.append(MessageManager.getString("label.label"));
346    }
347  12 else if (byScore)
348    {
349  1 sb.append(MessageManager.getString("label.score"));
350    }
351    else
352    {
353  11 sb.append(String.join(COLON, key));
354    }
355   
356  13 Condition condition = matcher.getCondition();
357  13 sb.append(SPACE).append(condition.toString().toLowerCase(Locale.ROOT));
358  13 if (condition.isNumeric())
359    {
360  7 sb.append(SPACE).append(matcher.getPattern());
361    }
362  6 else if (condition.needsAPattern())
363    {
364  4 sb.append(" '").append(matcher.getPattern()).append(QUOTE);
365    }
366   
367  13 return sb.toString();
368    }
369   
 
370  19 toggle @Override
371    public boolean isByLabel()
372    {
373  19 return byLabel;
374    }
375   
 
376  17 toggle @Override
377    public boolean isByScore()
378    {
379  17 return byScore;
380    }
381   
 
382  13 toggle @Override
383    public boolean isByAttribute()
384    {
385  13 return getAttribute() != null;
386    }
387   
388    /**
389    * {@inheritDoc} The output of this method should be parseable by method
390    * <code>fromString<code> to restore the original object.
391    */
 
392  47 toggle @Override
393    public String toStableString()
394    {
395  47 StringBuilder sb = new StringBuilder();
396  47 if (byLabel)
397    {
398  5 sb.append(LABEL); // no i18n here unlike toString() !
399    }
400  42 else if (byScore)
401    {
402  6 sb.append(SCORE);
403    }
404    else
405    {
406    /*
407    * enclose attribute name in quotes if it includes space
408    */
409  36 String displayName = toAttributeDisplayName(key);
410  36 if (displayName.contains(SPACE))
411    {
412  3 sb.append(QUOTE).append(displayName).append(QUOTE);
413    }
414    else
415    {
416  33 sb.append(displayName);
417    }
418    }
419   
420  47 Condition condition = matcher.getCondition();
421  47 sb.append(SPACE).append(condition.getStableName());
422  47 String pattern = matcher.getPattern();
423  47 if (condition.needsAPattern())
424    {
425    /*
426    * enclose pattern in quotes if it includes space
427    */
428  43 if (pattern.contains(SPACE))
429    {
430  4 sb.append(SPACE).append(QUOTE).append(pattern).append(QUOTE);
431    }
432    else
433    {
434  39 sb.append(SPACE).append(pattern);
435    }
436    }
437   
438  47 return sb.toString();
439    }
440    }