69f80f2fb091b9eeee05de48102288735aaf4356
[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 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   public static String[] fromAttributeDisplayName(String attribute)
89   {
90     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   public static String toAttributeDisplayName(String[] attName)
101   {
102     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   public static FeatureMatcher fromString(final String descriptor)
121   {
122     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     String attName = null;
132     boolean byScore = false;
133     boolean byLabel = false;
134     Condition cond = null;
135     String pattern = null;
136
137     /*
138      * parse first field (Label / Score / attribute)
139      * optionally in quotes (required if attName includes space)
140      */
141     String leftToParse = descriptor;
142     String firstField = null;
143
144     if (descriptor.startsWith(QUOTE))
145     {
146       // 'Label' / 'Score' / 'attName'
147       int nextQuotePos = descriptor.indexOf(QUOTE, 1);
148       if (nextQuotePos == -1)
149       {
150         System.err.println(invalidFormat);
151         return null;
152       }
153       firstField = descriptor.substring(1, nextQuotePos);
154       leftToParse = descriptor.substring(nextQuotePos + 1).trim();
155     }
156     else
157     {
158       // Label / Score / attName (unquoted)
159       int nextSpacePos = descriptor.indexOf(SPACE);
160       if (nextSpacePos == -1)
161       {
162         System.err.println(invalidFormat);
163         return null;
164       }
165       firstField = descriptor.substring(0, nextSpacePos);
166       leftToParse = descriptor.substring(nextSpacePos + 1).trim();
167     }
168     String lower = firstField.toLowerCase(Locale.ROOT);
169     if (lower.startsWith(LABEL.toLowerCase(Locale.ROOT)))
170     {
171       byLabel = true;
172     }
173     else if (lower.startsWith(SCORE.toLowerCase(Locale.ROOT)))
174     {
175       byScore = true;
176     }
177     else
178     {
179       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     int nextSpacePos = leftToParse.indexOf(SPACE);
188     if (nextSpacePos == -1)
189     {
190       /*
191        * no value following condition - only valid for some conditions
192        */
193       cond = Condition.fromString(leftToParse);
194       if (cond == null || cond.needsAPattern())
195       {
196         System.err.println(invalidFormat);
197         return null;
198       }
199     }
200     else
201     {
202       /*
203        * condition and pattern
204        */
205       cond = Condition.fromString(leftToParse.substring(0, nextSpacePos));
206       leftToParse = leftToParse.substring(nextSpacePos + 1).trim();
207       if (leftToParse.startsWith(QUOTE))
208       {
209         // pattern in quotes
210         if (leftToParse.endsWith(QUOTE))
211         {
212           pattern = leftToParse.substring(1, leftToParse.length() - 1);
213         }
214         else
215         {
216           // unbalanced quote
217           System.err.println(invalidFormat);
218           return null;
219         }
220       }
221       else
222       {
223         // unquoted pattern
224         pattern = leftToParse;
225       }
226     }
227
228     /*
229      * we have parsed out value, condition and pattern
230      * so can now make the FeatureMatcher
231      */
232     try
233     {
234       if (byLabel)
235       {
236         return FeatureMatcher.byLabel(cond, pattern);
237       }
238       else if (byScore)
239       {
240         return FeatureMatcher.byScore(cond, pattern);
241       }
242       else
243       {
244         String[] attNames = FeatureMatcher
245                 .fromAttributeDisplayName(attName);
246         return FeatureMatcher.byAttribute(cond, pattern, attNames);
247       }
248     } catch (NumberFormatException e)
249     {
250       // numeric condition with non-numeric pattern
251       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   public static FeatureMatcher byLabel(Condition cond, String pattern)
266   {
267     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   public static FeatureMatcher byScore(Condition cond, String pattern)
282   {
283     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   public static FeatureMatcher byAttribute(Condition cond, String pattern,
299           String... attName)
300   {
301     return new FeatureMatcher(new Matcher(cond, pattern), false, false,
302             attName);
303   }
304
305   private FeatureMatcher(Matcher m, boolean forLabel, boolean forScore,
306           String[] theKey)
307   {
308     key = theKey;
309     matcher = m;
310     byLabel = forLabel;
311     byScore = forScore;
312   }
313   @Override
314   public boolean matches(SequenceFeature feature)
315   {
316     String value = byLabel ? feature.getDescription()
317             : (byScore ? String.valueOf(feature.getScore())
318                     : feature.getValueAsString(key));
319     return matcher.matches(value);
320   }
321
322   @Override
323   public String[] getAttribute()
324   {
325     return key;
326   }
327
328   @Override
329   public MatcherI getMatcher()
330   {
331     return matcher;
332   }
333
334   /**
335    * Answers a string description of this matcher, suitable for display, debugging
336    * or logging. The format may change in future.
337    */
338   @Override
339   public String toString()
340   {
341     StringBuilder sb = new StringBuilder();
342     if (byLabel)
343     {
344       sb.append(MessageManager.getString("label.label"));
345     }
346     else if (byScore)
347     {
348       sb.append(MessageManager.getString("label.score"));
349     }
350     else
351     {
352       sb.append(String.join(COLON, key));
353     }
354
355     Condition condition = matcher.getCondition();
356     sb.append(SPACE).append(condition.toString().toLowerCase(Locale.ROOT));
357     if (condition.isNumeric())
358     {
359       sb.append(SPACE).append(matcher.getPattern());
360     }
361     else if (condition.needsAPattern())
362     {
363       sb.append(" '").append(matcher.getPattern()).append(QUOTE);
364     }
365
366     return sb.toString();
367   }
368
369   @Override
370   public boolean isByLabel()
371   {
372     return byLabel;
373   }
374
375   @Override
376   public boolean isByScore()
377   {
378     return byScore;
379   }
380
381   @Override
382   public boolean isByAttribute()
383   {
384     return getAttribute() != null;
385   }
386
387   /**
388    * {@inheritDoc} The output of this method should be parseable by method
389    * <code>fromString<code> to restore the original object.
390    */
391   @Override
392   public String toStableString()
393   {
394     StringBuilder sb = new StringBuilder();
395     if (byLabel)
396     {
397       sb.append(LABEL); // no i18n here unlike toString() !
398     }
399     else if (byScore)
400     {
401       sb.append(SCORE);
402     }
403     else
404     {
405       /*
406        * enclose attribute name in quotes if it includes space
407        */
408       String displayName = toAttributeDisplayName(key);
409       if (displayName.contains(SPACE))
410       {
411         sb.append(QUOTE).append(displayName).append(QUOTE);
412       }
413       else
414       {
415         sb.append(displayName);
416       }
417     }
418   
419     Condition condition = matcher.getCondition();
420     sb.append(SPACE).append(condition.getStableName());
421     String pattern = matcher.getPattern();
422     if (condition.needsAPattern())
423     {
424       /*
425        * enclose pattern in quotes if it includes space
426        */
427       if (pattern.contains(SPACE))
428       {
429         sb.append(SPACE).append(QUOTE).append(pattern).append(QUOTE);
430       }
431       else
432       {
433         sb.append(SPACE).append(pattern);
434       }
435     }
436   
437     return sb.toString();
438   }
439 }