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