JAL-4090 JAL-1551 spotlessApply
[jalview.git] / src / jalview / io / gff / GffHelperBase.java
index de9212f..0097343 100644 (file)
@@ -20,8 +20,6 @@
  */
 package jalview.io.gff;
 
-import static jalview.io.FeaturesFile.MAP_ATTRIBUTE_PREFIX;
-
 import jalview.analysis.SequenceIdMatcher;
 import jalview.datamodel.AlignedCodonFrame;
 import jalview.datamodel.AlignmentI;
@@ -29,7 +27,6 @@ import jalview.datamodel.MappingType;
 import jalview.datamodel.SequenceDummy;
 import jalview.datamodel.SequenceFeature;
 import jalview.datamodel.SequenceI;
-import jalview.io.FeaturesFile;
 import jalview.util.MapList;
 import jalview.util.StringUtils;
 
@@ -46,9 +43,13 @@ import java.util.Map.Entry;
  */
 public abstract class GffHelperBase implements GffHelperI
 {
-  private static final String COMMA = ",";
+  private static final String INVALID_GFF_ATTRIBUTE_FORMAT = "Invalid GFF attribute format: ";
+
+  protected static final String COMMA = ",";
+
+  protected static final String EQUALS = "=";
 
-  private static final String NOTE = "Note";
+  protected static final String NOTE = "Note";
 
   /*
    * GFF columns 1-9 (zero-indexed):
@@ -113,8 +114,9 @@ public abstract class GffHelperBase implements GffHelperI
      */
     if (!trimMapping(from, to, fromRatio, toRatio))
     {
-      System.err.println("Ignoring mapping from " + Arrays.toString(from)
-              + " to " + Arrays.toString(to) + " as counts don't match!");
+      jalview.bin.Console.errPrintln(
+              "Ignoring mapping from " + Arrays.toString(from) + " to "
+                      + Arrays.toString(to) + " as counts don't match!");
       return null;
     }
 
@@ -165,7 +167,7 @@ public abstract class GffHelperBase implements GffHelperI
       {
         from[1] += fromOverlap / toRatio;
       }
-      System.err.println(Arrays.toString(from));
+      jalview.bin.Console.errPrintln(Arrays.toString(from));
       return true;
     }
     else if (fromOverlap < 0 && fromOverlap % fromRatio == 0)
@@ -184,7 +186,7 @@ public abstract class GffHelperBase implements GffHelperI
       {
         to[1] += fromOverlap / fromRatio;
       }
-      System.err.println(Arrays.toString(to));
+      jalview.bin.Console.errPrintln(Arrays.toString(to));
       return true;
     }
 
@@ -264,29 +266,32 @@ public abstract class GffHelperBase implements GffHelperI
   }
 
   /**
-   * Parses the input line to a map of name / value(s) pairs. For example the line
-   * <br>
+   * Parses the input line to a map of name / value(s) pairs. For example the
+   * line
+   * 
+   * <pre>
    * Notes=Fe-S;Method=manual curation, prediction; source = Pfam; Notes = Metal
-   * <br>
+   * </pre>
+   * 
    * if parsed with delimiter=";" and separators {' ', '='} <br>
    * would return a map with { Notes={Fe=S, Metal}, Method={manual curation,
    * prediction}, source={Pfam}} <br>
    * 
    * This method supports parsing of either GFF2 format (which uses space ' ' as
-   * the name/value delimiter, and allows multiple occurrences of the same name),
-   * or GFF3 format (which uses '=' as the name/value delimiter, and strictly does
-   * not allow repeat occurrences of the same name - but does allow a
-   * comma-separated list of values).
+   * the name/value delimiter, and allows multiple occurrences of the same
+   * name), or GFF3 format (which uses '=' as the name/value delimiter, and
+   * strictly does not allow repeat occurrences of the same name - but does
+   * allow a comma-separated list of values).
    * <p>
    * Returns a (possibly empty) map of lists of values by attribute name.
    * 
    * @param text
    * @param namesDelimiter
-   *                             the major delimiter between name-value pairs
+   *          the major delimiter between name-value pairs
    * @param nameValueSeparator
-   *                             separator used between name and value
+   *          separator used between name and value
    * @param valuesDelimiter
-   *                             delimits a list of more than one value
+   *          delimits a list of more than one value
    * @return
    */
   public static Map<String, List<String>> parseNameValuePairs(String text,
@@ -299,60 +304,58 @@ public abstract class GffHelperBase implements GffHelperI
       return map;
     }
 
-    for (String pair : text.trim().split(namesDelimiter))
+    /*
+     * split by major delimiter (; for GFF3)
+     */
+    for (String nameValuePair : text.trim().split(namesDelimiter))
     {
-      pair = pair.trim();
-      if (pair.length() == 0)
+      nameValuePair = nameValuePair.trim();
+      if (nameValuePair.length() == 0)
       {
         continue;
       }
 
-      int sepPos = pair.indexOf(nameValueSeparator);
+      /*
+       * find name/value separator (= for GFF3)
+       */
+      int sepPos = nameValuePair.indexOf(nameValueSeparator);
       if (sepPos == -1)
       {
         // no name=value found
         continue;
       }
 
-      String key = pair.substring(0, sepPos).trim();
-      String values = pair.substring(sepPos + 1).trim();
-      if (values.length() > 0)
+      String name = nameValuePair.substring(0, sepPos).trim();
+      String values = nameValuePair.substring(sepPos + 1).trim();
+      if (values.isEmpty())
       {
-        List<String> vals = map.get(key);
-        if (vals == null)
-        {
-          vals = new ArrayList<>();
-          map.put(key, vals);
-        }
+        continue;
+      }
 
-        /*
-         * special case: formatted as jvmap_AttName={a=b,c=d,...}
-         * save the value within { } for parsing at a later stage
-         */
-        if (key.startsWith(MAP_ATTRIBUTE_PREFIX))
-        {
+      List<String> vals = map.get(name);
+      if (vals == null)
+      {
+        vals = new ArrayList<>();
+        map.put(name, vals);
+      }
 
-          if (key.length() > MAP_ATTRIBUTE_PREFIX.length()
-                  && values.startsWith("{")
-                  && values.endsWith("}"))
-          {
-            vals.add(values.substring(1, values.length() - 1));
-          }
-          else
-          {
-            System.err.println("Malformed GFF data '" + values.toString()
-                    + "' for " + key);
-          }
-        }
-        else
+      /*
+       * if 'values' contains more name/value separators, parse as a map
+       * (nested sub-attribute values)
+       */
+      if (values.indexOf(nameValueSeparator) != -1)
+      {
+        vals.add(values);
+      }
+      else
+      {
+        for (String val : values.split(valuesDelimiter))
         {
-          for (String val : values.split(valuesDelimiter))
-          {
-            vals.add(val);
-          }
+          vals.add(val);
         }
       }
     }
+
     return map;
   }
 
@@ -379,7 +382,8 @@ public abstract class GffHelperBase implements GffHelperI
    * @return
    */
   protected SequenceFeature buildSequenceFeature(String[] gff,
-          int typeColumn, String group, Map<String, List<String>> attributes)
+          int typeColumn, String group,
+          Map<String, List<String>> attributes)
   {
     try
     {
@@ -416,10 +420,12 @@ public abstract class GffHelperBase implements GffHelperI
         {
           String key = attr.getKey();
           List<String> values = attr.getValue();
-          if (key.startsWith(FeaturesFile.MAP_ATTRIBUTE_PREFIX))
+          if (values.size() == 1 && values.get(0).contains(EQUALS))
           {
-            key = key.substring(FeaturesFile.MAP_ATTRIBUTE_PREFIX.length());
-            Map<String, String> valueMap = parseAttributeMap(values);
+            /*
+             * 'value' is actually nested subattributes as x=a,y=b,z=c
+             */
+            Map<String, String> valueMap = parseAttributeMap(values.get(0));
             sf.setValue(key, valueMap);
           }
           else
@@ -439,37 +445,107 @@ public abstract class GffHelperBase implements GffHelperI
       return sf;
     } catch (NumberFormatException nfe)
     {
-      System.err.println("Invalid number in gff: " + nfe.getMessage());
+      jalview.bin.Console
+              .errPrintln("Invalid number in gff: " + nfe.getMessage());
       return null;
     }
   }
 
   /**
-   * Parses one or more list of comma-separated key=value pairs into a Map of
-   * {key, value}
+   * Parses a (GFF3 format) list of comma-separated key=value pairs into a Map
+   * of {@code key,
+   * value} <br>
+   * An input string like {@code a=b,c,d=e,f=g,h} is parsed to
+   * 
+   * <pre>
+   * a = "b,c"
+   * d = "e"
+   * f = "g,h"
+   * </pre>
+   * 
+   * @param s
    * 
-   * @param values
    * @return
    */
-  protected Map<String, String> parseAttributeMap(List<String> values)
+  protected static Map<String, String> parseAttributeMap(String s)
   {
     Map<String, String> map = new HashMap<>();
-    for (String entry : values)
+    String[] fields = s.split(EQUALS);
+
+    /*
+     * format validation
+     */
+    boolean valid = true;
+    if (fields.length < 2)
+    {
+      /*
+       * need at least A=B here
+       */
+      valid = false;
+    }
+    else if (fields[0].isEmpty() || fields[0].contains(COMMA))
+    {
+      /*
+       * A,B=C is not a valid start, nor is =C
+       */
+      valid = false;
+    }
+    else
     {
-      String[] fields = entry.split(COMMA);
-      for (String field : fields)
+      for (int i = 1; i < fields.length - 1; i++)
       {
-        String[] keyValue = field.split("=");
-        if (keyValue.length == 2)
+        if (fields[i].isEmpty() || !fields[i].contains(COMMA))
         {
-          String theKey = StringUtils.urlDecode(keyValue[0],
-                  GFF_ENCODABLE);
-          String theValue = StringUtils.urlDecode(keyValue[1],
-                  GFF_ENCODABLE);
-          map.put(theKey, theValue);
+          /*
+           * intermediate tokens must include value,name
+           */
+          valid = false;
         }
       }
     }
+
+    if (!valid)
+    {
+      jalview.bin.Console.errPrintln(INVALID_GFF_ATTRIBUTE_FORMAT + s);
+      return map;
+    }
+
+    int i = 0;
+    while (i < fields.length - 1)
+    {
+      boolean lastPair = i == fields.length - 2;
+      String before = fields[i];
+      String after = fields[i + 1];
+
+      /*
+       * if 'key' looks like a,b,c then the last token is the
+       * key
+       */
+      String theKey = before.contains(COMMA)
+              ? before.substring(before.lastIndexOf(COMMA) + 1)
+              : before;
+
+      theKey = theKey.trim();
+      if (theKey.isEmpty())
+      {
+        jalview.bin.Console.errPrintln(INVALID_GFF_ATTRIBUTE_FORMAT + s);
+        map.clear();
+        return map;
+      }
+
+      /*
+       * if 'value' looks like a,b,c then all but the last token is the value,
+       * unless this is the last field (no more = to follow), in which case
+       * all of it makes up the value
+       */
+      String theValue = after.contains(COMMA) && !lastPair
+              ? after.substring(0, after.lastIndexOf(COMMA))
+              : after;
+      map.put(StringUtils.urlDecode(theKey, GFF_ENCODABLE),
+              StringUtils.urlDecode(theValue, GFF_ENCODABLE));
+      i += 1;
+    }
+
     return map;
   }