e43ead2deb9e035823b4d130897c68c0a9281723
[jalview.git] / src / jalview / util / matcher / Matcher.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.util.matcher;
22
23 import java.util.Locale;
24
25 import java.util.Objects;
26
27 /**
28  * A bean to describe one attribute-based filter
29  */
30 public class Matcher implements MatcherI
31 {
32   public enum PatternType
33   {
34     String, Integer, Float
35   }
36
37   /*
38    * the comparison condition
39    */
40   private final Condition condition;
41
42   /*
43    * the string pattern as entered, to compare to
44    */
45   private String pattern;
46
47   /*
48    * the pattern in upper case, for non-case-sensitive matching
49    */
50   private final String uppercasePattern;
51
52   /*
53    * the compiled regex if using a pattern match condition
54    * (possible future enhancement)
55    */
56   // private Pattern regexPattern;
57
58   /*
59    * the value to compare to for a numerical condition with a float pattern
60    */
61   private float floatValue = 0F;
62
63   /*
64    * the value to compare to for a numerical condition with an integer pattern
65    */
66   private long longValue = 0L;
67
68   private PatternType patternType;
69
70   /**
71    * Constructor
72    * 
73    * @param cond
74    * @param compareTo
75    * @return
76    * @throws NumberFormatException
77    *           if a numerical condition is specified with a non-numeric
78    *           comparison value
79    * @throws NullPointerException
80    *           if a null condition or comparison string is specified
81    */
82   public Matcher(Condition cond, String compareTo)
83   {
84     Objects.requireNonNull(cond);
85     condition = cond;
86
87     if (cond.isNumeric())
88     {
89       try
90       {
91         longValue = Long.valueOf(compareTo);
92         pattern = String.valueOf(longValue);
93         patternType = PatternType.Integer;
94       } catch (NumberFormatException e)
95       {
96         floatValue = Float.valueOf(compareTo);
97         pattern = String.valueOf(floatValue);
98         patternType = PatternType.Float;
99       }
100     }
101     else
102     {
103       pattern = compareTo;
104       patternType = PatternType.String;
105     }
106
107     uppercasePattern = pattern == null ? null : pattern.toUpperCase(Locale.ROOT);
108
109     // if we add regex conditions (e.g. matchesPattern), then
110     // pattern should hold the raw regex, and
111     // regexPattern = Pattern.compile(compareTo);
112   }
113
114   /**
115    * Constructor for a float-valued numerical match condition. Note that if a
116    * string comparison condition is specified, this will be converted to a
117    * comparison with the float value as string
118    * 
119    * @param cond
120    * @param compareTo
121    */
122   public Matcher(Condition cond, float compareTo)
123   {
124     this(cond, String.valueOf(compareTo));
125   }
126
127   /**
128    * Constructor for an integer-valued numerical match condition. Note that if a
129    * string comparison condition is specified, this will be converted to a
130    * comparison with the integer value as string
131    * 
132    * @param cond
133    * @param compareTo
134    */
135   public Matcher(Condition cond, long compareTo)
136   {
137     this(cond, String.valueOf(compareTo));
138   }
139
140   /**
141    * {@inheritDoc}
142    */
143   @Override
144   public boolean matches(String compareTo)
145   {
146     if (compareTo == null)
147     {
148       return matchesNull();
149     }
150
151     boolean matched = false;
152     switch (patternType)
153     {
154     case Float:
155       matched = matchesFloat(compareTo, floatValue);
156       break;
157     case Integer:
158       matched = matchesLong(compareTo);
159       break;
160     default:
161       matched = matchesString(compareTo);
162       break;
163     }
164     return matched;
165   }
166
167   /**
168    * Executes a non-case-sensitive string comparison to the given value, after
169    * trimming it. Returns true if the test passes, false if it fails.
170    * 
171    * @param compareTo
172    * @return
173    */
174   boolean matchesString(String compareTo)
175   {
176     boolean matched = false;
177     String upper = compareTo.toUpperCase(Locale.ROOT).trim();
178     switch(condition) {
179     case Matches:
180       matched = upper.equals(uppercasePattern);
181       break;
182     case NotMatches:
183       matched = !upper.equals(uppercasePattern);
184       break;
185     case Contains:
186       matched = upper.indexOf(uppercasePattern) > -1;
187       break;
188     case NotContains:
189       matched = upper.indexOf(uppercasePattern) == -1;
190       break;
191     case Present:
192       matched = true;
193       break;
194     default:
195       break;
196     }
197     return matched;
198   }
199
200   /**
201    * Performs a numerical comparison match condition test against a float value
202    * 
203    * @param testee
204    * @param compareTo
205    * @return
206    */
207   boolean matchesFloat(String testee, float compareTo)
208   {
209     if (!condition.isNumeric())
210     {
211       // failsafe, shouldn't happen
212       return matches(testee);
213     }
214
215     float f = 0f;
216     try
217     {
218       f = Float.valueOf(testee);
219     } catch (NumberFormatException e)
220     {
221       return false;
222     }
223     
224     boolean matched = false;
225     switch (condition) {
226     case LT:
227       matched = f < compareTo;
228       break;
229     case LE:
230       matched = f <= compareTo;
231       break;
232     case EQ:
233       matched = f == compareTo;
234       break;
235     case NE:
236       matched = f != compareTo;
237       break;
238     case GT:
239       matched = f > compareTo;
240       break;
241     case GE:
242       matched = f >= compareTo;
243       break;
244     default:
245       break;
246     }
247
248     return matched;
249   }
250
251   /**
252    * A simple hash function that guarantees that when two objects are equal,
253    * they have the same hashcode
254    */
255   @Override
256   public int hashCode()
257   {
258     return pattern.hashCode() + condition.hashCode() + (int) floatValue;
259   }
260
261   /**
262    * equals is overridden so that we can safely remove Matcher objects from
263    * collections (e.g. delete an attribute match condition for a feature colour)
264    */
265   @Override
266   public boolean equals(Object obj)
267   {
268     if (obj == null || !(obj instanceof Matcher))
269     {
270       return false;
271     }
272     Matcher m = (Matcher) obj;
273     if (condition != m.condition || floatValue != m.floatValue
274             || longValue != m.longValue)
275     {
276       return false;
277     }
278     if (pattern == null)
279     {
280       return m.pattern == null;
281     }
282     return uppercasePattern.equals(m.uppercasePattern);
283   }
284
285   @Override
286   public Condition getCondition()
287   {
288     return condition;
289   }
290
291   @Override
292   public String getPattern()
293   {
294     return pattern;
295   }
296
297   @Override
298   public String toString()
299   {
300     StringBuilder sb = new StringBuilder();
301     sb.append(condition.toString()).append(" ");
302     if (condition.isNumeric())
303     {
304       sb.append(pattern);
305     }
306     else
307     {
308       sb.append("'").append(pattern).append("'");
309     }
310
311     return sb.toString();
312   }
313
314   /**
315    * Performs a numerical comparison match condition test against an integer
316    * value
317    * 
318    * @param compareTo
319    * @return
320    */
321   boolean matchesLong(String compareTo)
322   {
323     if (!condition.isNumeric())
324     {
325       // failsafe, shouldn't happen
326       return matches(String.valueOf(compareTo));
327     }
328
329     long val = 0L;
330     try
331     {
332       val = Long.valueOf(compareTo);
333     } catch (NumberFormatException e)
334     {
335       /*
336        * try the presented value as a float instead
337        */
338       return matchesFloat(compareTo, longValue);
339     }
340     
341     boolean matched = false;
342     switch (condition) {
343     case LT:
344       matched = val < longValue;
345       break;
346     case LE:
347       matched = val <= longValue;
348       break;
349     case EQ:
350       matched = val == longValue;
351       break;
352     case NE:
353       matched = val != longValue;
354       break;
355     case GT:
356       matched = val > longValue;
357       break;
358     case GE:
359       matched = val >= longValue;
360       break;
361     default:
362       break;
363     }
364   
365     return matched;
366   }
367
368   /**
369    * Tests whether a null value matches the condition. The rule is that any
370    * numeric condition is failed, and only 'negative' string conditions are
371    * matched. So for example <br>
372    * {@code null contains "damaging"}<br>
373    * fails, but <br>
374    * {@code null does not contain "damaging"}</br>
375    * passes.
376    */
377   boolean matchesNull()
378   {
379     if (condition.isNumeric())
380     {
381       return false;
382     }
383     else
384     {
385       return condition == Condition.NotContains
386               || condition == Condition.NotMatches
387               || condition == Condition.NotPresent;
388     }
389   }
390 }