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