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