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