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