+ * A helper method that converts a 'compound' attribute name from its display
+ * form, e.g. CSQ:PolyPhen to array form, e.g. { "CSQ", "PolyPhen" }
+ *
+ * @param attribute
+ * @return
+ */
+ public static String[] fromAttributeDisplayName(String attribute)
+ {
+ return attribute == null ? null : attribute.split(COLON);
+ }
+
+ /**
+ * A helper method that converts a 'compound' attribute name to its display
+ * form, e.g. CSQ:PolyPhen from its array form, e.g. { "CSQ", "PolyPhen" }
+ *
+ * @param attName
+ * @return
+ */
+ public static String toAttributeDisplayName(String[] attName)
+ {
+ return attName == null ? "" : String.join(COLON, attName);
+ }
+
+ /**
+ * A factory constructor that converts a stringified object (as output by
+ * toStableString) to an object instance. Returns null if parsing fails.
+ * <p>
+ * Leniency in parsing (for manually created feature files):
+ * <ul>
+ * <li>keywords Score and Label, and the condition, are not
+ * case-sensitive</li>
+ * <li>quotes around value and pattern are optional if string does not include
+ * a space</li>
+ * </ul>
+ *
+ * @param descriptor
+ * @return
+ */
+ public static FeatureMatcher fromString(final String descriptor)
+ {
+ String invalidFormat = "Invalid matcher format: " + descriptor;
+
+ /*
+ * expect
+ * value condition pattern
+ * where value is Label or Space or attributeName or attName1:attName2
+ * and pattern is a float value as string, or a text string
+ * attribute names or patterns may be quoted (must be if include space)
+ */
+ String attName = null;
+ boolean byScore = false;
+ boolean byLabel = false;
+ Condition cond = null;
+ String pattern = null;
+
+ /*
+ * parse first field (Label / Score / attribute)
+ * optionally in quotes (required if attName includes space)
+ */
+ String leftToParse = descriptor;
+ String firstField = null;
+
+ if (descriptor.startsWith(QUOTE))
+ {
+ // 'Label' / 'Score' / 'attName'
+ int nextQuotePos = descriptor.indexOf(QUOTE, 1);
+ if (nextQuotePos == -1)
+ {
+ System.err.println(invalidFormat);
+ return null;
+ }
+ firstField = descriptor.substring(1, nextQuotePos);
+ leftToParse = descriptor.substring(nextQuotePos + 1).trim();
+ }
+ else
+ {
+ // Label / Score / attName (unquoted)
+ int nextSpacePos = descriptor.indexOf(SPACE);
+ if (nextSpacePos == -1)
+ {
+ System.err.println(invalidFormat);
+ return null;
+ }
+ firstField = descriptor.substring(0, nextSpacePos);
+ leftToParse = descriptor.substring(nextSpacePos + 1).trim();
+ }
+ String lower = firstField.toLowerCase(Locale.ROOT);
+ if (lower.startsWith(LABEL.toLowerCase(Locale.ROOT)))
+ {
+ byLabel = true;
+ }
+ else if (lower.startsWith(SCORE.toLowerCase(Locale.ROOT)))
+ {
+ byScore = true;
+ }
+ else
+ {
+ attName = firstField;
+ }
+
+ /*
+ * next field is the comparison condition
+ * most conditions require a following pattern (optionally quoted)
+ * although some conditions e.g. Present do not
+ */
+ int nextSpacePos = leftToParse.indexOf(SPACE);
+ if (nextSpacePos == -1)
+ {
+ /*
+ * no value following condition - only valid for some conditions
+ */
+ cond = Condition.fromString(leftToParse);
+ if (cond == null || cond.needsAPattern())
+ {
+ System.err.println(invalidFormat);
+ return null;
+ }
+ }
+ else
+ {
+ /*
+ * condition and pattern
+ */
+ cond = Condition.fromString(leftToParse.substring(0, nextSpacePos));
+ leftToParse = leftToParse.substring(nextSpacePos + 1).trim();
+ if (leftToParse.startsWith(QUOTE))
+ {
+ // pattern in quotes
+ if (leftToParse.endsWith(QUOTE))
+ {
+ pattern = leftToParse.substring(1, leftToParse.length() - 1);
+ }
+ else
+ {
+ // unbalanced quote
+ System.err.println(invalidFormat);
+ return null;
+ }
+ }
+ else
+ {
+ // unquoted pattern
+ pattern = leftToParse;
+ }
+ }
+
+ /*
+ * we have parsed out value, condition and pattern
+ * so can now make the FeatureMatcher
+ */
+ try
+ {
+ if (byLabel)
+ {
+ return FeatureMatcher.byLabel(cond, pattern);
+ }
+ else if (byScore)
+ {
+ return FeatureMatcher.byScore(cond, pattern);
+ }
+ else
+ {
+ String[] attNames = FeatureMatcher
+ .fromAttributeDisplayName(attName);
+ return FeatureMatcher.byAttribute(cond, pattern, attNames);
+ }
+ } catch (NumberFormatException e)
+ {
+ // numeric condition with non-numeric pattern
+ return null;
+ }
+ }
+
+ /**