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