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