Class |
Line # |
Actions |
|||
---|---|---|---|---|---|
FeatureMatcher | 46 | 95 | 41 |
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 | import jalview.util.matcher.Condition; | |
28 | import jalview.util.matcher.Matcher; | |
29 | import jalview.util.matcher.MatcherI; | |
30 | ||
31 | /** | |
32 | * An immutable class that models one or more match conditions, each of which is | |
33 | * applied to the value obtained by lookup given the match key. | |
34 | * <p> | |
35 | * For example, the value provider could be a SequenceFeature's attributes map, | |
36 | * and the conditions might be | |
37 | * <ul> | |
38 | * <li>CSQ contains "pathological"</li> | |
39 | * <li>AND</li> | |
40 | * <li>AF <= 1.0e-5</li> | |
41 | * </ul> | |
42 | * | |
43 | * @author gmcarstairs | |
44 | * | |
45 | */ | |
46 | public class FeatureMatcher implements FeatureMatcherI | |
47 | { | |
48 | private static final String SCORE = "Score"; | |
49 | ||
50 | private static final String LABEL = "Label"; | |
51 | ||
52 | private static final String SPACE = " "; | |
53 | ||
54 | private static final String QUOTE = "'"; | |
55 | ||
56 | /* | |
57 | * a dummy matcher that comes in useful for the 'add a filter' gui row | |
58 | */ | |
59 | public static final FeatureMatcherI NULL_MATCHER = FeatureMatcher | |
60 | .byLabel(Condition.values()[0], ""); | |
61 | ||
62 | private static final String COLON = ":"; | |
63 | ||
64 | /* | |
65 | * if true, match is against feature description | |
66 | */ | |
67 | final private boolean byLabel; | |
68 | ||
69 | /* | |
70 | * if true, match is against feature score | |
71 | */ | |
72 | final private boolean byScore; | |
73 | ||
74 | /* | |
75 | * if not null, match is against feature attribute [sub-attribute] | |
76 | */ | |
77 | final private String[] key; | |
78 | ||
79 | final private MatcherI matcher; | |
80 | ||
81 | /** | |
82 | * A helper method that converts a 'compound' attribute name from its display | |
83 | * form, e.g. CSQ:PolyPhen to array form, e.g. { "CSQ", "PolyPhen" } | |
84 | * | |
85 | * @param attribute | |
86 | * @return | |
87 | */ | |
88 | 32 | public static String[] fromAttributeDisplayName(String attribute) |
89 | { | |
90 | 32 | return attribute == null ? null : attribute.split(COLON); |
91 | } | |
92 | ||
93 | /** | |
94 | * A helper method that converts a 'compound' attribute name to its display | |
95 | * form, e.g. CSQ:PolyPhen from its array form, e.g. { "CSQ", "PolyPhen" } | |
96 | * | |
97 | * @param attName | |
98 | * @return | |
99 | */ | |
100 | 43 | public static String toAttributeDisplayName(String[] attName) |
101 | { | |
102 | 43 | return attName == null ? "" : String.join(COLON, attName); |
103 | } | |
104 | ||
105 | /** | |
106 | * A factory constructor that converts a stringified object (as output by | |
107 | * toStableString) to an object instance. Returns null if parsing fails. | |
108 | * <p> | |
109 | * Leniency in parsing (for manually created feature files): | |
110 | * <ul> | |
111 | * <li>keywords Score and Label, and the condition, are not | |
112 | * case-sensitive</li> | |
113 | * <li>quotes around value and pattern are optional if string does not include | |
114 | * a space</li> | |
115 | * </ul> | |
116 | * | |
117 | * @param descriptor | |
118 | * @return | |
119 | */ | |
120 | 46 | public static FeatureMatcher fromString(final String descriptor) |
121 | { | |
122 | 46 | String invalidFormat = "Invalid matcher format: " + descriptor; |
123 | ||
124 | /* | |
125 | * expect | |
126 | * value condition pattern | |
127 | * where value is Label or Space or attributeName or attName1:attName2 | |
128 | * and pattern is a float value as string, or a text string | |
129 | * attribute names or patterns may be quoted (must be if include space) | |
130 | */ | |
131 | 46 | String attName = null; |
132 | 46 | boolean byScore = false; |
133 | 46 | boolean byLabel = false; |
134 | 46 | Condition cond = null; |
135 | 46 | String pattern = null; |
136 | ||
137 | /* | |
138 | * parse first field (Label / Score / attribute) | |
139 | * optionally in quotes (required if attName includes space) | |
140 | */ | |
141 | 46 | String leftToParse = descriptor; |
142 | 46 | String firstField = null; |
143 | ||
144 | 46 | if (descriptor.startsWith(QUOTE)) |
145 | { | |
146 | // 'Label' / 'Score' / 'attName' | |
147 | 13 | int nextQuotePos = descriptor.indexOf(QUOTE, 1); |
148 | 13 | if (nextQuotePos == -1) |
149 | { | |
150 | 1 | jalview.bin.Console.errPrintln(invalidFormat); |
151 | 1 | return null; |
152 | } | |
153 | 12 | firstField = descriptor.substring(1, nextQuotePos); |
154 | 12 | leftToParse = descriptor.substring(nextQuotePos + 1).trim(); |
155 | } | |
156 | else | |
157 | { | |
158 | // Label / Score / attName (unquoted) | |
159 | 33 | int nextSpacePos = descriptor.indexOf(SPACE); |
160 | 33 | if (nextSpacePos == -1) |
161 | { | |
162 | 2 | jalview.bin.Console.errPrintln(invalidFormat); |
163 | 2 | return null; |
164 | } | |
165 | 31 | firstField = descriptor.substring(0, nextSpacePos); |
166 | 31 | leftToParse = descriptor.substring(nextSpacePos + 1).trim(); |
167 | } | |
168 | 43 | String lower = firstField.toLowerCase(Locale.ROOT); |
169 | 43 | if (lower.startsWith(LABEL.toLowerCase(Locale.ROOT))) |
170 | { | |
171 | 5 | byLabel = true; |
172 | } | |
173 | 38 | else if (lower.startsWith(SCORE.toLowerCase(Locale.ROOT))) |
174 | { | |
175 | 8 | byScore = true; |
176 | } | |
177 | else | |
178 | { | |
179 | 30 | attName = firstField; |
180 | } | |
181 | ||
182 | /* | |
183 | * next field is the comparison condition | |
184 | * most conditions require a following pattern (optionally quoted) | |
185 | * although some conditions e.g. Present do not | |
186 | */ | |
187 | 43 | int nextSpacePos = leftToParse.indexOf(SPACE); |
188 | 43 | if (nextSpacePos == -1) |
189 | { | |
190 | /* | |
191 | * no value following condition - only valid for some conditions | |
192 | */ | |
193 | 3 | cond = Condition.fromString(leftToParse); |
194 | 3 | if (cond == null || cond.needsAPattern()) |
195 | { | |
196 | 2 | jalview.bin.Console.errPrintln(invalidFormat); |
197 | 2 | return null; |
198 | } | |
199 | } | |
200 | else | |
201 | { | |
202 | /* | |
203 | * condition and pattern | |
204 | */ | |
205 | 40 | cond = Condition.fromString(leftToParse.substring(0, nextSpacePos)); |
206 | 40 | leftToParse = leftToParse.substring(nextSpacePos + 1).trim(); |
207 | 40 | if (leftToParse.startsWith(QUOTE)) |
208 | { | |
209 | // pattern in quotes | |
210 | 11 | if (leftToParse.endsWith(QUOTE)) |
211 | { | |
212 | 9 | pattern = leftToParse.substring(1, leftToParse.length() - 1); |
213 | } | |
214 | else | |
215 | { | |
216 | // unbalanced quote | |
217 | 2 | jalview.bin.Console.errPrintln(invalidFormat); |
218 | 2 | return null; |
219 | } | |
220 | } | |
221 | else | |
222 | { | |
223 | // unquoted pattern | |
224 | 29 | pattern = leftToParse; |
225 | } | |
226 | } | |
227 | ||
228 | /* | |
229 | * we have parsed out value, condition and pattern | |
230 | * so can now make the FeatureMatcher | |
231 | */ | |
232 | 39 | try |
233 | { | |
234 | 39 | if (byLabel) |
235 | { | |
236 | 5 | return FeatureMatcher.byLabel(cond, pattern); |
237 | } | |
238 | 34 | else if (byScore) |
239 | { | |
240 | 6 | return FeatureMatcher.byScore(cond, pattern); |
241 | } | |
242 | else | |
243 | { | |
244 | 28 | String[] attNames = FeatureMatcher |
245 | .fromAttributeDisplayName(attName); | |
246 | 28 | return FeatureMatcher.byAttribute(cond, pattern, attNames); |
247 | } | |
248 | } catch (NumberFormatException e) | |
249 | { | |
250 | // numeric condition with non-numeric pattern | |
251 | 2 | return null; |
252 | } | |
253 | } | |
254 | ||
255 | /** | |
256 | * A factory constructor method for a matcher that applies its match condition | |
257 | * to the feature label (description) | |
258 | * | |
259 | * @param cond | |
260 | * @param pattern | |
261 | * @return | |
262 | * @throws NumberFormatException | |
263 | * if an invalid numeric pattern is supplied | |
264 | */ | |
265 | 31 | public static FeatureMatcher byLabel(Condition cond, String pattern) |
266 | { | |
267 | 31 | return new FeatureMatcher(new Matcher(cond, pattern), true, false, |
268 | null); | |
269 | } | |
270 | ||
271 | /** | |
272 | * A factory constructor method for a matcher that applies its match condition | |
273 | * to the feature score | |
274 | * | |
275 | * @param cond | |
276 | * @param pattern | |
277 | * @return | |
278 | * @throws NumberFormatException | |
279 | * if an invalid numeric pattern is supplied | |
280 | */ | |
281 | 37 | public static FeatureMatcher byScore(Condition cond, String pattern) |
282 | { | |
283 | 37 | return new FeatureMatcher(new Matcher(cond, pattern), false, true, |
284 | null); | |
285 | } | |
286 | ||
287 | /** | |
288 | * A factory constructor method for a matcher that applies its match condition | |
289 | * to the named feature attribute [and optional sub-attribute] | |
290 | * | |
291 | * @param cond | |
292 | * @param pattern | |
293 | * @param attName | |
294 | * @return | |
295 | * @throws NumberFormatException | |
296 | * if an invalid numeric pattern is supplied | |
297 | */ | |
298 | 75 | public static FeatureMatcher byAttribute(Condition cond, String pattern, |
299 | String... attName) | |
300 | { | |
301 | 75 | return new FeatureMatcher(new Matcher(cond, pattern), false, false, |
302 | attName); | |
303 | } | |
304 | ||
305 | 141 | private FeatureMatcher(Matcher m, boolean forLabel, boolean forScore, |
306 | String[] theKey) | |
307 | { | |
308 | 141 | key = theKey; |
309 | 141 | matcher = m; |
310 | 141 | byLabel = forLabel; |
311 | 141 | byScore = forScore; |
312 | } | |
313 | ||
314 | 94 | @Override |
315 | public boolean matches(SequenceFeature feature) | |
316 | { | |
317 | 94 | String value = byLabel ? feature.getDescription() |
318 | 81 | : (byScore ? String.valueOf(feature.getScore()) |
319 | : feature.getValueAsString(key)); | |
320 | 94 | return matcher.matches(value); |
321 | } | |
322 | ||
323 | 31 | @Override |
324 | public String[] getAttribute() | |
325 | { | |
326 | 31 | return key; |
327 | } | |
328 | ||
329 | 39 | @Override |
330 | public MatcherI getMatcher() | |
331 | { | |
332 | 39 | return matcher; |
333 | } | |
334 | ||
335 | /** | |
336 | * Answers a string description of this matcher, suitable for display, | |
337 | * debugging or logging. The format may change in future. | |
338 | */ | |
339 | 13 | @Override |
340 | public String toString() | |
341 | { | |
342 | 13 | StringBuilder sb = new StringBuilder(); |
343 | 13 | if (byLabel) |
344 | { | |
345 | 1 | sb.append(MessageManager.getString("label.label")); |
346 | } | |
347 | 12 | else if (byScore) |
348 | { | |
349 | 1 | sb.append(MessageManager.getString("label.score")); |
350 | } | |
351 | else | |
352 | { | |
353 | 11 | sb.append(String.join(COLON, key)); |
354 | } | |
355 | ||
356 | 13 | Condition condition = matcher.getCondition(); |
357 | 13 | sb.append(SPACE).append(condition.toString().toLowerCase(Locale.ROOT)); |
358 | 13 | if (condition.isNumeric()) |
359 | { | |
360 | 7 | sb.append(SPACE).append(matcher.getPattern()); |
361 | } | |
362 | 6 | else if (condition.needsAPattern()) |
363 | { | |
364 | 4 | sb.append(" '").append(matcher.getPattern()).append(QUOTE); |
365 | } | |
366 | ||
367 | 13 | return sb.toString(); |
368 | } | |
369 | ||
370 | 19 | @Override |
371 | public boolean isByLabel() | |
372 | { | |
373 | 19 | return byLabel; |
374 | } | |
375 | ||
376 | 17 | @Override |
377 | public boolean isByScore() | |
378 | { | |
379 | 17 | return byScore; |
380 | } | |
381 | ||
382 | 13 | @Override |
383 | public boolean isByAttribute() | |
384 | { | |
385 | 13 | return getAttribute() != null; |
386 | } | |
387 | ||
388 | /** | |
389 | * {@inheritDoc} The output of this method should be parseable by method | |
390 | * <code>fromString<code> to restore the original object. | |
391 | */ | |
392 | 47 | @Override |
393 | public String toStableString() | |
394 | { | |
395 | 47 | StringBuilder sb = new StringBuilder(); |
396 | 47 | if (byLabel) |
397 | { | |
398 | 5 | sb.append(LABEL); // no i18n here unlike toString() ! |
399 | } | |
400 | 42 | else if (byScore) |
401 | { | |
402 | 6 | sb.append(SCORE); |
403 | } | |
404 | else | |
405 | { | |
406 | /* | |
407 | * enclose attribute name in quotes if it includes space | |
408 | */ | |
409 | 36 | String displayName = toAttributeDisplayName(key); |
410 | 36 | if (displayName.contains(SPACE)) |
411 | { | |
412 | 3 | sb.append(QUOTE).append(displayName).append(QUOTE); |
413 | } | |
414 | else | |
415 | { | |
416 | 33 | sb.append(displayName); |
417 | } | |
418 | } | |
419 | ||
420 | 47 | Condition condition = matcher.getCondition(); |
421 | 47 | sb.append(SPACE).append(condition.getStableName()); |
422 | 47 | String pattern = matcher.getPattern(); |
423 | 47 | if (condition.needsAPattern()) |
424 | { | |
425 | /* | |
426 | * enclose pattern in quotes if it includes space | |
427 | */ | |
428 | 43 | if (pattern.contains(SPACE)) |
429 | { | |
430 | 4 | sb.append(SPACE).append(QUOTE).append(pattern).append(QUOTE); |
431 | } | |
432 | else | |
433 | { | |
434 | 39 | sb.append(SPACE).append(pattern); |
435 | } | |
436 | } | |
437 | ||
438 | 47 | return sb.toString(); |
439 | } | |
440 | } |