JAL-3438 spotless for 2.11.2.0
[jalview.git] / src / jalview / datamodel / features / FeatureMatcherSet.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
28 import java.util.ArrayList;
29 import java.util.List;
30
31 /**
32  * A class that models one or more match conditions, which may be combined with
33  * AND or OR (but not a mixture)
34  * 
35  * @author gmcarstairs
36  */
37 public class FeatureMatcherSet implements FeatureMatcherSetI
38 {
39   private static final String OR = "OR";
40
41   private static final String AND = "AND";
42
43   private static final String SPACE = " ";
44
45   private static final String CLOSE_BRACKET = ")";
46
47   private static final String OPEN_BRACKET = "(";
48
49   private static final String OR_I18N = MessageManager
50           .getString("label.or");
51
52   private static final String AND_18N = MessageManager
53           .getString("label.and");
54
55   List<FeatureMatcherI> matchConditions;
56
57   boolean andConditions;
58
59   /**
60    * A factory constructor that converts a stringified object (as output by
61    * toStableString) to an object instance.
62    * 
63    * Format:
64    * <ul>
65    * <li>(condition1) AND (condition2) AND (condition3)</li>
66    * <li>or</li>
67    * <li>(condition1) OR (condition2) OR (condition3)</li>
68    * </ul>
69    * where OR and AND are not case-sensitive, and may not be mixed. Brackets are
70    * optional if there is only one condition.
71    * 
72    * @param descriptor
73    * @return
74    * @see FeatureMatcher#fromString(String)
75    */
76   public static FeatureMatcherSet fromString(final String descriptor)
77   {
78     String invalid = "Invalid descriptor: " + descriptor;
79     boolean firstCondition = true;
80     FeatureMatcherSet result = new FeatureMatcherSet();
81
82     String leftToParse = descriptor.trim();
83
84     while (leftToParse.length() > 0)
85     {
86       /*
87        * inspect AND or OR condition, check not mixed
88        */
89       boolean and = true;
90       if (!firstCondition)
91       {
92         int spacePos = leftToParse.indexOf(SPACE);
93         if (spacePos == -1)
94         {
95           // trailing junk after a match condition
96           System.err.println(invalid);
97           return null;
98         }
99         String conjunction = leftToParse.substring(0, spacePos);
100         leftToParse = leftToParse.substring(spacePos + 1).trim();
101         if (conjunction.equalsIgnoreCase(AND))
102         {
103           and = true;
104         }
105         else if (conjunction.equalsIgnoreCase(OR))
106         {
107           and = false;
108         }
109         else
110         {
111           // not an AND or an OR - invalid
112           System.err.println(invalid);
113           return null;
114         }
115       }
116
117       /*
118        * now extract the next condition and AND or OR it
119        */
120       String nextCondition = leftToParse;
121       if (leftToParse.startsWith(OPEN_BRACKET))
122       {
123         int closePos = leftToParse.indexOf(CLOSE_BRACKET);
124         if (closePos == -1)
125         {
126           System.err.println(invalid);
127           return null;
128         }
129         nextCondition = leftToParse.substring(1, closePos);
130         leftToParse = leftToParse.substring(closePos + 1).trim();
131       }
132       else
133       {
134         leftToParse = "";
135       }
136
137       FeatureMatcher fm = FeatureMatcher.fromString(nextCondition);
138       if (fm == null)
139       {
140         System.err.println(invalid);
141         return null;
142       }
143       try
144       {
145         if (and)
146         {
147           result.and(fm);
148         }
149         else
150         {
151           result.or(fm);
152         }
153         firstCondition = false;
154       } catch (IllegalStateException e)
155       {
156         // thrown if OR and AND are mixed
157         System.err.println(invalid);
158         return null;
159       }
160
161     }
162     return result;
163   }
164
165   /**
166    * Constructor
167    */
168   public FeatureMatcherSet()
169   {
170     matchConditions = new ArrayList<>();
171   }
172
173   @Override
174   public boolean matches(SequenceFeature feature)
175   {
176     /*
177      * no conditions matches anything
178      */
179     if (matchConditions.isEmpty())
180     {
181       return true;
182     }
183
184     /*
185      * AND until failure
186      */
187     if (andConditions)
188     {
189       for (FeatureMatcherI m : matchConditions)
190       {
191         if (!m.matches(feature))
192         {
193           return false;
194         }
195       }
196       return true;
197     }
198
199     /*
200      * OR until match
201      */
202     for (FeatureMatcherI m : matchConditions)
203     {
204       if (m.matches(feature))
205       {
206         return true;
207       }
208     }
209     return false;
210   }
211
212   @Override
213   public void and(FeatureMatcherI m)
214   {
215     if (!andConditions && matchConditions.size() > 1)
216     {
217       throw new IllegalStateException("Can't add an AND to OR conditions");
218     }
219     matchConditions.add(m);
220     andConditions = true;
221   }
222
223   @Override
224   public void or(FeatureMatcherI m)
225   {
226     if (andConditions && matchConditions.size() > 1)
227     {
228       throw new IllegalStateException("Can't add an OR to AND conditions");
229     }
230     matchConditions.add(m);
231     andConditions = false;
232   }
233
234   @Override
235   public boolean isAnded()
236   {
237     return andConditions;
238   }
239
240   @Override
241   public Iterable<FeatureMatcherI> getMatchers()
242   {
243     return matchConditions;
244   }
245
246   /**
247    * Answers a string representation of this object suitable for display, and
248    * possibly internationalized. The format is not guaranteed stable and may
249    * change in future.
250    */
251   @Override
252   public String toString()
253   {
254     StringBuilder sb = new StringBuilder();
255     boolean first = true;
256     boolean multiple = matchConditions.size() > 1;
257     for (FeatureMatcherI matcher : matchConditions)
258     {
259       if (!first)
260       {
261         String joiner = andConditions ? AND_18N : OR_I18N;
262         sb.append(SPACE).append(joiner.toLowerCase(Locale.ROOT))
263                 .append(SPACE);
264       }
265       first = false;
266       if (multiple)
267       {
268         sb.append(OPEN_BRACKET).append(matcher.toString())
269                 .append(CLOSE_BRACKET);
270       }
271       else
272       {
273         sb.append(matcher.toString());
274       }
275     }
276     return sb.toString();
277   }
278
279   @Override
280   public boolean isEmpty()
281   {
282     return matchConditions == null || matchConditions.isEmpty();
283   }
284
285   /**
286    * {@inheritDoc} The output of this method should be parseable by method
287    * <code>fromString<code> to restore the original object.
288    */
289   @Override
290   public String toStableString()
291   {
292     StringBuilder sb = new StringBuilder();
293     boolean moreThanOne = matchConditions.size() > 1;
294     boolean first = true;
295
296     for (FeatureMatcherI matcher : matchConditions)
297     {
298       if (!first)
299       {
300         String joiner = andConditions ? AND : OR;
301         sb.append(SPACE).append(joiner).append(SPACE);
302       }
303       first = false;
304       if (moreThanOne)
305       {
306         sb.append(OPEN_BRACKET).append(matcher.toStableString())
307                 .append(CLOSE_BRACKET);
308       }
309       else
310       {
311         sb.append(matcher.toStableString());
312       }
313     }
314     return sb.toString();
315   }
316
317 }