Merge branch 'documentation/JAL-3407_2.11.1_release' into releases/Release_2_11_1_Branch
[jalview.git] / src / jalview / datamodel / features / FeatureMatcher.java
index 1fc0e0f..e9fb9b2 100644 (file)
@@ -1,6 +1,27 @@
+/*
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
+ * 
+ * This file is part of Jalview.
+ * 
+ * Jalview is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License 
+ * as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
+ *  
+ * Jalview is distributed in the hope that it will be useful, but 
+ * WITHOUT ANY WARRANTY; without even the implied warranty 
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
+ * PURPOSE.  See the GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
+ */
 package jalview.datamodel.features;
 
 import jalview.datamodel.SequenceFeature;
+import jalview.util.MessageManager;
 import jalview.util.matcher.Condition;
 import jalview.util.matcher.Matcher;
 import jalview.util.matcher.MatcherI;
@@ -22,6 +43,20 @@ import jalview.util.matcher.MatcherI;
  */
 public class FeatureMatcher implements FeatureMatcherI
 {
+  private static final String SCORE = "Score";
+
+  private static final String LABEL = "Label";
+
+  private static final String SPACE = " ";
+
+  private static final String QUOTE = "'";
+
+  /*
+   * a dummy matcher that comes in useful for the 'add a filter' gui row
+   */
+  public static final FeatureMatcherI NULL_MATCHER = FeatureMatcher
+          .byLabel(Condition.values()[0], "");
+
   private static final String COLON = ":";
 
   /*
@@ -42,12 +77,188 @@ public class FeatureMatcher implements FeatureMatcherI
   final private MatcherI matcher;
 
   /**
+   * 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();
+    if (lower.startsWith(LABEL.toLowerCase()))
+    {
+      byLabel = true;
+    }
+    else if (lower.startsWith(SCORE.toLowerCase()))
+    {
+      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;
+    }
+  }
+
+  /**
    * A factory constructor method for a matcher that applies its match condition
    * to the feature label (description)
    * 
    * @param cond
    * @param pattern
    * @return
+   * @throws NumberFormatException
+   *           if an invalid numeric pattern is supplied
    */
   public static FeatureMatcher byLabel(Condition cond, String pattern)
   {
@@ -62,6 +273,8 @@ public class FeatureMatcher implements FeatureMatcherI
    * @param cond
    * @param pattern
    * @return
+   * @throws NumberFormatException
+   *           if an invalid numeric pattern is supplied
    */
   public static FeatureMatcher byScore(Condition cond, String pattern)
   {
@@ -77,6 +290,8 @@ public class FeatureMatcher implements FeatureMatcherI
    * @param pattern
    * @param attName
    * @return
+   * @throws NumberFormatException
+   *           if an invalid numeric pattern is supplied
    */
   public static FeatureMatcher byAttribute(Condition cond, String pattern,
           String... attName)
@@ -103,7 +318,7 @@ public class FeatureMatcher implements FeatureMatcherI
   }
 
   @Override
-  public String[] getKey()
+  public String[] getAttribute()
   {
     return key;
   }
@@ -122,18 +337,101 @@ public class FeatureMatcher implements FeatureMatcherI
   public String toString()
   {
     StringBuilder sb = new StringBuilder();
-    sb.append(String.join(COLON, key)).append(" ")
-            .append(matcher.getCondition().toString());
+    if (byLabel)
+    {
+      sb.append(MessageManager.getString("label.label"));
+    }
+    else if (byScore)
+    {
+      sb.append(MessageManager.getString("label.score"));
+    }
+    else
+    {
+      sb.append(String.join(COLON, key));
+    }
+
     Condition condition = matcher.getCondition();
+    sb.append(SPACE).append(condition.toString().toLowerCase());
     if (condition.isNumeric())
     {
-      sb.append(" ").append(matcher.getPattern());
+      sb.append(SPACE).append(matcher.getPattern());
     }
     else if (condition.needsAPattern())
     {
-      sb.append(" '").append(matcher.getPattern()).append("'");
+      sb.append(" '").append(matcher.getPattern()).append(QUOTE);
     }
 
     return sb.toString();
   }
+
+  @Override
+  public boolean isByLabel()
+  {
+    return byLabel;
+  }
+
+  @Override
+  public boolean isByScore()
+  {
+    return byScore;
+  }
+
+  @Override
+  public boolean isByAttribute()
+  {
+    return getAttribute() != null;
+  }
+
+  /**
+   * {@inheritDoc} The output of this method should be parseable by method
+   * <code>fromString<code> to restore the original object.
+   */
+  @Override
+  public String toStableString()
+  {
+    StringBuilder sb = new StringBuilder();
+    if (byLabel)
+    {
+      sb.append(LABEL); // no i18n here unlike toString() !
+    }
+    else if (byScore)
+    {
+      sb.append(SCORE);
+    }
+    else
+    {
+      /*
+       * enclose attribute name in quotes if it includes space
+       */
+      String displayName = toAttributeDisplayName(key);
+      if (displayName.contains(SPACE))
+      {
+        sb.append(QUOTE).append(displayName).append(QUOTE);
+      }
+      else
+      {
+        sb.append(displayName);
+      }
+    }
+  
+    Condition condition = matcher.getCondition();
+    sb.append(SPACE).append(condition.getStableName());
+    String pattern = matcher.getPattern();
+    if (condition.needsAPattern())
+    {
+      /*
+       * enclose pattern in quotes if it includes space
+       */
+      if (pattern.contains(SPACE))
+      {
+        sb.append(SPACE).append(QUOTE).append(pattern).append(QUOTE);
+      }
+      else
+      {
+        sb.append(SPACE).append(pattern);
+      }
+    }
+  
+    return sb.toString();
+  }
 }