JAL-3121 round trip GFF attributes including map-valued attributes
[jalview.git] / src / jalview / io / gff / GffHelperBase.java
index 499fa7b..d034c8d 100644 (file)
@@ -1,3 +1,23 @@
+/*
+ * 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.io.gff;
 
 import jalview.analysis.SequenceIdMatcher;
@@ -7,6 +27,7 @@ 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;
 
@@ -130,8 +151,8 @@ public abstract class GffHelperBase implements GffHelperI
        * restrict from range to make them match up
        * it's kind of arbitrary which end we truncate - here it is the end
        */
-      System.err.print("Truncating mapping from " + Arrays.toString(from)
-              + " to ");
+      System.err.print(
+              "Truncating mapping from " + Arrays.toString(from) + " to ");
       if (from[1] > from[0])
       {
         from[1] -= fromOverlap / toRatio;
@@ -149,8 +170,8 @@ public abstract class GffHelperBase implements GffHelperI
       /*
        * restrict to range to make them match up
        */
-      System.err.print("Truncating mapping to " + Arrays.toString(to)
-              + " to ");
+      System.err.print(
+              "Truncating mapping to " + Arrays.toString(to) + " to ");
       if (to[1] > to[0])
       {
         to[1] -= fromOverlap / fromRatio;
@@ -241,7 +262,8 @@ public abstract class GffHelperBase implements GffHelperI
   /**
    * Parses the input line to a map of name / value(s) pairs. For example the
    * line <br>
-   * Notes=Fe-S;Method=manual curation, prediction; source = Pfam; Notes = Metal <br>
+   * Notes=Fe-S;Method=manual curation, prediction; source = Pfam; Notes = Metal
+   * <br>
    * if parsed with delimiter=";" and separators {' ', '='} <br>
    * would return a map with { Notes={Fe=S, Metal}, Method={manual curation,
    * prediction}, source={Pfam}} <br>
@@ -265,7 +287,7 @@ public abstract class GffHelperBase implements GffHelperI
           String namesDelimiter, char nameValueSeparator,
           String valuesDelimiter)
   {
-    Map<String, List<String>> map = new HashMap<String, List<String>>();
+    Map<String, List<String>> map = new HashMap<>();
     if (text == null || text.trim().length() == 0)
     {
       return map;
@@ -293,7 +315,7 @@ public abstract class GffHelperBase implements GffHelperI
         List<String> vals = map.get(key);
         if (vals == null)
         {
-          vals = new ArrayList<String>();
+          vals = new ArrayList<>();
           map.put(key, vals);
         }
         for (String val : values.split(valuesDelimiter))
@@ -317,21 +339,38 @@ public abstract class GffHelperBase implements GffHelperI
   protected SequenceFeature buildSequenceFeature(String[] gff,
           Map<String, List<String>> attributes)
   {
+    return buildSequenceFeature(gff, TYPE_COL, gff[SOURCE_COL], attributes);
+  }
+
+  /**
+   * @param gff
+   * @param typeColumn
+   * @param group
+   * @param attributes
+   * @return
+   */
+  protected SequenceFeature buildSequenceFeature(String[] gff,
+          int typeColumn, String group, Map<String, List<String>> attributes)
+  {
     try
     {
       int start = Integer.parseInt(gff[START_COL]);
       int end = Integer.parseInt(gff[END_COL]);
-      float score = Float.NaN;
+
+      /*
+       * default 'score' is 0 rather than Float.NaN - see JAL-2554
+       */
+      float score = 0f;
       try
       {
         score = Float.parseFloat(gff[SCORE_COL]);
       } catch (NumberFormatException nfe)
       {
-        // e.g. '.' - leave as NaN to indicate no score
+        // e.g. '.' - leave as zero
       }
 
-      SequenceFeature sf = new SequenceFeature(gff[TYPE_COL],
-              gff[SOURCE_COL], start, end, score, gff[SOURCE_COL]);
+      SequenceFeature sf = new SequenceFeature(gff[typeColumn],
+              gff[SOURCE_COL], start, end, score, group);
 
       sf.setStrand(gff[STRAND_COL]);
 
@@ -340,22 +379,66 @@ public abstract class GffHelperBase implements GffHelperI
       if (attributes != null)
       {
         /*
-         * save 'raw' column 9 to allow roundtrip output as input
-         */
-        sf.setAttributes(gff[ATTRIBUTES_COL]);
-
-        /*
          * Add attributes in column 9 to the sequence feature's 
-         * 'otherData' table; use Note as a best proxy for description
+         * 'otherData' table; use Note as a best proxy for description;
+         * decode any encoded comma, equals, semi-colon as per GFF3 spec
          */
         for (Entry<String, List<String>> attr : attributes.entrySet())
         {
-          String values = StringUtils.listToDelimitedString(
-                  attr.getValue(), ",");
-          sf.setValue(attr.getKey(), values);
-          if (NOTE.equals(attr.getKey()))
+          String key = attr.getKey();
+          List<String> value = attr.getValue();
+          if (key.startsWith(FeaturesFile.MAP_ATTRIBUTE_PREFIX))
+          {
+            /*
+             * e.g. jvmap_CSQ={ALLELE_NUM=1,CDS_position=249,Codons=caG/caT}
+             */
+            String trueKey = key
+                    .substring(FeaturesFile.MAP_ATTRIBUTE_PREFIX.length());
+            if (trueKey.isEmpty() || value.isEmpty()
+                    || !value.get(0).startsWith("{")
+                    || !value.get(value.size() - 1).endsWith("}"))
+            {
+              System.err.println("Malformed GFF data '" + value.toString()
+                      + "' for " + key);
+              continue;
+            }
+            Map<String, String> values = new HashMap<>();
+            for (String entry : value)
+            {
+              if (entry.startsWith("{"))
+              {
+                entry = entry.substring(1);
+              }
+              if (entry.endsWith("}"))
+              {
+                entry = entry.substring(0, entry.length() - 1);
+              }
+              String[] fields = entry.split(",");
+            for (String field : fields)
+            {
+              String[] keyValue = field.split("=");
+              if (keyValue.length == 2)
+              {
+                String theKey = StringUtils.urlDecode(keyValue[0],
+                        GFF_ENCODABLE);
+                String theValue = StringUtils.urlDecode(keyValue[1],
+                        GFF_ENCODABLE);
+                values.put(theKey, theValue);
+              }
+            }
+            }
+            sf.setValue(trueKey, values);
+          }
+          else
           {
-            sf.setDescription(values);
+            String values = StringUtils
+                    .listToDelimitedString(value, ",");
+            values = StringUtils.urlDecode(values, GFF_ENCODABLE);
+            sf.setValue(key, values);
+            if (NOTE.equals(key))
+            {
+              sf.setDescription(values);
+            }
           }
         }
       }
@@ -387,7 +470,8 @@ public abstract class GffHelperBase implements GffHelperI
    * @param toSeq
    * @return
    */
-  protected AlignedCodonFrame getMapping(AlignmentI align, SequenceI fromSeq, SequenceI toSeq)
+  protected AlignedCodonFrame getMapping(AlignmentI align,
+          SequenceI fromSeq, SequenceI toSeq)
   {
     AlignedCodonFrame acf = align.getMapping(fromSeq, toSeq);
     if (acf == null)