Merge branch 'bug/JAL-3061_RNA_secondary_structure_annotation_gap_character' into...
authorJim Procter <jprocter@issues.jalview.org>
Fri, 31 Aug 2018 14:04:32 +0000 (15:04 +0100)
committerJim Procter <jprocter@issues.jalview.org>
Fri, 31 Aug 2018 14:04:32 +0000 (15:04 +0100)
examples/rna_ss_test.stk [new file with mode: 0644]
src/jalview/datamodel/AlignmentAnnotation.java
src/jalview/datamodel/Annotation.java
src/jalview/datamodel/SequenceFeature.java
src/jalview/io/StockholmFile.java
test/jalview/io/StockholmFileTest.java

diff --git a/examples/rna_ss_test.stk b/examples/rna_ss_test.stk
new file mode 100644 (file)
index 0000000..429612e
--- /dev/null
@@ -0,0 +1,6 @@
+# STOCKHOLM 1.0
+#=GF ID RNA.SS.TEST
+#=GF TP RNA;
+Test.sequence         GUACAAAAAAAAAA
+#=GC SS_cons          <(EHBheb(E)e)>
+//
index 0098d76..ee9389c 100755 (executable)
@@ -25,6 +25,7 @@ import jalview.analysis.SecStrConsensus.SimpleBP;
 import jalview.analysis.WUSSParseException;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
@@ -164,6 +165,64 @@ public class AlignmentAnnotation
   }
 
   /**
+   * Get the RNA Secondary Structure SequenceFeature Array if present
+   */
+  public SequenceFeature[] getRnaSecondaryStructure()
+  {
+    return this._rnasecstr;
+  }
+
+  /**
+   * Check the RNA Secondary Structure is equivalent to one in given
+   * AlignmentAnnotation param
+   */
+  public boolean rnaSecondaryStructureEquivalent(AlignmentAnnotation that)
+  {
+    return rnaSecondaryStructureEquivalent(that, true);
+  }
+
+  public boolean rnaSecondaryStructureEquivalent(AlignmentAnnotation that, boolean compareType)
+  {
+    SequenceFeature[] thisSfArray = this.getRnaSecondaryStructure();
+    SequenceFeature[] thatSfArray = that.getRnaSecondaryStructure();
+    if (thisSfArray == null || thatSfArray == null)
+    {
+      return thisSfArray == null && thatSfArray == null;
+    }
+    if (thisSfArray.length != thatSfArray.length)
+    {
+      return false;
+    }
+    Arrays.sort(thisSfArray, new SFSortByEnd()); // probably already sorted
+                                                   // like this
+    Arrays.sort(thatSfArray, new SFSortByEnd()); // probably already sorted
+                                                   // like this
+    for (int i=0; i < thisSfArray.length; i++) {
+      SequenceFeature thisSf = thisSfArray[i];
+      SequenceFeature thatSf = thatSfArray[i];
+      if (compareType) {
+        if (thisSf.getType() == null || thatSf.getType() == null) {
+          if (thisSf.getType() == null && thatSf.getType() == null) {
+            continue;
+          } else {
+            return false;
+          }
+        }
+        if (! thisSf.getType().equals(thatSf.getType())) {
+          return false;
+        }
+      }
+      if (!(thisSf.getBegin() == thatSf.getBegin()
+              && thisSf.getEnd() == thatSf.getEnd()))
+      {
+        return false;
+      }
+    }
+    return true;
+
+  }
+
+  /**
    * map of positions in the associated annotation
    */
   private Map<Integer, Annotation> sequenceMapping;
@@ -294,6 +353,7 @@ public class AlignmentAnnotation
     char firstChar = 0;
     for (int i = 0; i < annotations.length; i++)
     {
+      // DEBUG System.out.println(i + ": " + annotations[i]);
       if (annotations[i] == null)
       {
         continue;
@@ -301,12 +361,15 @@ public class AlignmentAnnotation
       if (annotations[i].secondaryStructure == 'H'
               || annotations[i].secondaryStructure == 'E')
       {
+        // DEBUG System.out.println( "/H|E/ '" +
+        // annotations[i].secondaryStructure + "'");
         hasIcons |= true;
       }
       else
       // Check for RNA secondary structure
       {
-        // System.out.println(annotations[i].secondaryStructure);
+        // DEBUG System.out.println( "/else/ '" +
+        // annotations[i].secondaryStructure + "'");
         // TODO: 2.8.2 should this ss symbol validation check be a function in
         // RNA/ResidueProperties ?
         if (annotations[i].secondaryStructure == '('
@@ -317,10 +380,12 @@ public class AlignmentAnnotation
                 || annotations[i].secondaryStructure == 'B'
                 || annotations[i].secondaryStructure == 'C'
                 || annotations[i].secondaryStructure == 'D'
-                || annotations[i].secondaryStructure == 'E'
+                // || annotations[i].secondaryStructure == 'E' // ambiguous on
+                // its own -- already checked above
                 || annotations[i].secondaryStructure == 'F'
                 || annotations[i].secondaryStructure == 'G'
-                || annotations[i].secondaryStructure == 'H'
+                // || annotations[i].secondaryStructure == 'H' // ambiguous on
+                // its own -- already checked above
                 || annotations[i].secondaryStructure == 'I'
                 || annotations[i].secondaryStructure == 'J'
                 || annotations[i].secondaryStructure == 'K'
@@ -367,7 +432,7 @@ public class AlignmentAnnotation
         // &&
         // annotations[i].displayCharacter.charAt(0)==annotations[i].secondaryStructure
                 firstChar != ' ' && firstChar != '$' && firstChar != 0xCE
-                && firstChar != '(' && firstChar != '[' && firstChar != '>'
+                && firstChar != '(' && firstChar != '[' && firstChar != '<'
                 && firstChar != '{' && firstChar != 'A' && firstChar != 'B'
                 && firstChar != 'C' && firstChar != 'D' && firstChar != 'E'
                 && firstChar != 'F' && firstChar != 'G' && firstChar != 'H'
@@ -1648,4 +1713,5 @@ public class AlignmentAnnotation
     }
     return aa;
   }
+
 }
index ae29417..f6919cd 100755 (executable)
@@ -210,7 +210,10 @@ public class Annotation
     return ((value == 0f)
             && ((description == null) || (description.trim().length() == 0))
             && ((displayCharacter == null)
-                    || (displayCharacter.trim().length() == 0))
+                    || (displayCharacter.trim().length() == 0)
+                    || (displayCharacter.equals(" ."))) // RNA Stockholm blank
+                                                        // displayCharacter can
+                                                        // end up like this
             && (secondaryStructure == '\0' || (secondaryStructure == ' '))
             && colour == null);
   }
index 9c4087e..f17bd33 100755 (executable)
@@ -22,6 +22,7 @@ package jalview.datamodel;
 
 import jalview.datamodel.features.FeatureLocationI;
 
+import java.util.Comparator;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Map.Entry;
@@ -157,7 +158,7 @@ public class SequenceFeature implements FeatureLocationI
 
     if (sf.otherDetails != null)
     {
-      otherDetails = new HashMap<String, Object>();
+      otherDetails = new HashMap<>();
       for (Entry<String, Object> entry : sf.otherDetails.entrySet())
       {
         otherDetails.put(entry.getKey(), entry.getValue());
@@ -165,7 +166,7 @@ public class SequenceFeature implements FeatureLocationI
     }
     if (sf.links != null && sf.links.size() > 0)
     {
-      links = new Vector<String>();
+      links = new Vector<>();
       for (int i = 0, iSize = sf.links.size(); i < iSize; i++)
       {
         links.addElement(sf.links.elementAt(i));
@@ -332,7 +333,7 @@ public class SequenceFeature implements FeatureLocationI
   {
     if (links == null)
     {
-      links = new Vector<String>();
+      links = new Vector<>();
     }
 
     if (!links.contains(labelLink))
@@ -394,7 +395,7 @@ public class SequenceFeature implements FeatureLocationI
     {
       if (otherDetails == null)
       {
-        otherDetails = new HashMap<String, Object>();
+        otherDetails = new HashMap<>();
       }
 
       otherDetails.put(key, value);
@@ -536,3 +537,21 @@ public class SequenceFeature implements FeatureLocationI
     return begin == 0 && end == 0;
   }
 }
+
+class SFSortByEnd implements Comparator<SequenceFeature>
+{
+  @Override
+  public int compare(SequenceFeature a, SequenceFeature b)
+  {
+    return a.getEnd() - b.getEnd();
+  }
+}
+
+class SFSortByBegin implements Comparator<SequenceFeature>
+{
+  @Override
+  public int compare(SequenceFeature a, SequenceFeature b)
+  {
+    return a.getBegin() - b.getBegin();
+  }
+}
index f5b5177..0e73af1 100644 (file)
@@ -83,6 +83,14 @@ public class StockholmFile extends AlignFile
   public static final Regex DETECT_BRACKETS = new Regex(
           "(<|>|\\[|\\]|\\(|\\)|\\{|\\})");
 
+  // WUSS extended symbols. Avoid ambiguity with protein SS annotations by using NOT_RNASS first.
+  public static final String RNASS_BRACKETS = "<>[](){}AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz";
+
+  // use the following regex to decide an annotations (whole) line is NOT an RNA
+  // SS (it contains only E,H,e,h and other non-brace/non-alpha chars)
+  private static final Regex NOT_RNASS = new Regex(
+          "^[^<>[\\](){}A-DF-Za-df-z]*$");
+
   StringBuffer out; // output buffer
 
   AlignmentI al;
@@ -197,7 +205,7 @@ public class StockholmFile extends AlignFile
     String version;
     // String id;
     Hashtable seqAnn = new Hashtable(); // Sequence related annotations
-    LinkedHashMap<String, String> seqs = new LinkedHashMap<String, String>();
+    LinkedHashMap<String, String> seqs = new LinkedHashMap<>();
     Regex p, r, rend, s, x;
     // Temporary line for processing RNA annotation
     // String RNAannot = "";
@@ -658,7 +666,7 @@ public class StockholmFile extends AlignFile
               strucAnn = new Hashtable();
             }
 
-            Vector<AlignmentAnnotation> newStruc = new Vector<AlignmentAnnotation>();
+            Vector<AlignmentAnnotation> newStruc = new Vector<>();
             parseAnnotationRow(newStruc, type, ns);
             for (AlignmentAnnotation alan : newStruc)
             {
@@ -710,7 +718,7 @@ public class StockholmFile extends AlignFile
   private void guessDatabaseFor(Sequence seqO, String dbr, String dbsource)
   {
     DBRefEntry dbrf = null;
-    List<DBRefEntry> dbrs = new ArrayList<DBRefEntry>();
+    List<DBRefEntry> dbrs = new ArrayList<>();
     String seqdb = "Unknown", sdbac = "" + dbr;
     int st = -1, en = -1, p;
     if ((st = sdbac.indexOf("/")) > -1)
@@ -824,9 +832,14 @@ public class StockholmFile extends AlignFile
     }
     boolean ss = false, posterior = false;
     type = id2type(type);
+
+    boolean isrnass = false;
     if (type.equalsIgnoreCase("secondary structure"))
     {
       ss = true;
+      isrnass = !NOT_RNASS.search(annots); // sorry about the double negative
+                                           // here (it's easier for dealing with
+                                           // other non-alpha-non-brace chars)
     }
     if (type.equalsIgnoreCase("posterior probability"))
     {
@@ -844,7 +857,7 @@ public class StockholmFile extends AlignFile
       {
         // if (" .-_".indexOf(pos) == -1)
         {
-          if (DETECT_BRACKETS.search(pos))
+          if (isrnass && RNASS_BRACKETS.indexOf(pos) >= 0)
           {
             ann.secondaryStructure = Rna.getRNASecStrucState(pos).charAt(0);
             ann.displayCharacter = "" + pos.charAt(0);
@@ -1114,22 +1127,36 @@ public class StockholmFile extends AlignFile
     String ch = (annot == null)
             ? ((sequenceI == null) ? "-"
                     : Character.toString(sequenceI.getCharAt(k)))
-            : annot.displayCharacter;
+            : (annot.displayCharacter == null
+                    ? String.valueOf(annot.secondaryStructure)
+                    : annot.displayCharacter);
+    if (ch == null)
+    {
+      ch = " ";
+    }
     if (key != null && key.equals("SS"))
     {
+      char ssannotchar = ' ';
+      boolean charset = false;
       if (annot == null)
       {
         // sensible gap character
-        return ' ';
+        ssannotchar = ' ';
+        charset = true;
       }
       else
       {
         // valid secondary structure AND no alternative label (e.g. ' B')
         if (annot.secondaryStructure > ' ' && ch.length() < 2)
         {
-          return annot.secondaryStructure;
+          ssannotchar = annot.secondaryStructure;
+          charset = true;
         }
       }
+      if (charset)
+      {
+        return (ssannotchar == ' ' && isrna) ? '.' : ssannotchar;
+      }
     }
 
     if (ch.length() == 0)
@@ -1144,7 +1171,9 @@ public class StockholmFile extends AlignFile
     {
       seq = ch.charAt(1);
     }
-    return seq;
+
+    return (seq == ' ' && key != null && key.equals("SS") && isrna) ? '.'
+            : seq;
   }
 
   public String print()
index 4273e6c..e86c8ad 100644 (file)
@@ -38,6 +38,8 @@ import java.util.BitSet;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 import org.testng.Assert;
 import org.testng.annotations.BeforeClass;
@@ -54,7 +56,8 @@ public class StockholmFileTest
   }
 
   static String PfamFile = "examples/PF00111_seed.stk",
-          RfamFile = "examples/RF00031_folded.stk";
+          RfamFile = "examples/RF00031_folded.stk",
+          RnaSSTestFile = "examples/rna_ss_test.stk";
 
   @Test(groups = { "Functional" })
   public void pfamFileIO() throws Exception
@@ -230,8 +233,8 @@ public class StockholmFileTest
     // we might want to revise this in future
     int aa_new_size = (aa_new == null ? 0 : aa_new.length);
     int aa_original_size = (aa_original == null ? 0 : aa_original.length);
-    Map<Integer, BitSet> orig_groups = new HashMap<Integer, BitSet>();
-    Map<Integer, BitSet> new_groups = new HashMap<Integer, BitSet>();
+    Map<Integer, BitSet> orig_groups = new HashMap<>();
+    Map<Integer, BitSet> new_groups = new HashMap<>();
 
     if (aa_new != null && aa_original != null)
     {
@@ -623,13 +626,13 @@ public class StockholmFileTest
   {
     for (char ch : new char[] { '{', '}', '[', ']', '(', ')', '<', '>' })
     {
-      Assert.assertTrue(StockholmFile.DETECT_BRACKETS.matchAt("" + ch, 0),
-              "Didn't recognise " + ch + " as a WUSS bracket");
+      Assert.assertTrue(StockholmFile.RNASS_BRACKETS.indexOf(ch) >= 0,
+              "Didn't recognise '" + ch + "' as a WUSS bracket");
     }
-    for (char ch : new char[] { '@', '!', 'V', 'Q', '*', ' ', '-', '.' })
+    for (char ch : new char[] { '@', '!', '*', ' ', '-', '.' })
     {
-      Assert.assertFalse(StockholmFile.DETECT_BRACKETS.matchAt("" + ch, 0),
-              "Shouldn't recognise " + ch + " as a WUSS bracket");
+      Assert.assertFalse(StockholmFile.RNASS_BRACKETS.indexOf(ch) >= 0,
+              "Shouldn't recognise '" + ch + "' as a WUSS bracket");
     }
   }
   private static void roundTripSSForRNA(String aliFile, String annFile)
@@ -654,4 +657,191 @@ public class StockholmFileTest
     testAlignmentEquivalence(al, newAl, true, true, true);
 
   }
+
+  // this is the single sequence alignment and the SS annotations equivalent to
+  // the ones in file RnaSSTestFile
+  String aliFileRnaSS = ">Test.sequence/1-14\n"
+          + "GUACAAAAAAAAAA";
+  String annFileRnaSSAlphaChars = "JALVIEW_ANNOTATION\n"
+          + "# Created: Thu Aug 02 14:54:57 BST 2018\n" + "\n"
+          + "NO_GRAPH\tSecondary Structure\tSecondary Structure\t<,<|(,(|E,E|H,H|B,B|h,h|e,e|b,b|(,(|E,E|),)|e,e|),)|>,>|\t2.0\n"
+          + "\n"
+          + "ROWPROPERTIES\tSecondary Structure\tscaletofit=true\tshowalllabs=true\tcentrelabs=false\n"
+          + "\n" + "\n" + "ALIGNMENT\tID=RNA.SS.TEST\tTP=RNA;";
+  String wrongAnnFileRnaSSAlphaChars = "JALVIEW_ANNOTATION\n"
+          + "# Created: Thu Aug 02 14:54:57 BST 2018\n" + "\n"
+          + "NO_GRAPH\tSecondary Structure\tSecondary Structure\t<,<|(,(|H,H|E,E|B,B|h,h|e,e|b,b|(,(|E,E|),)|e,e|),)|>,>|\t2.0\n"
+          + "\n"
+          + "ROWPROPERTIES\tSecondary Structure\tscaletofit=true\tshowalllabs=true\tcentrelabs=false\n"
+          + "\n" + "\n" + "ALIGNMENT\tID=RNA.SS.TEST\tTP=RNA;";
+  @Test(groups = { "Functional" })
+  public void stockholmFileRnaSSAlphaChars() throws Exception
+  {
+    AppletFormatAdapter af = new AppletFormatAdapter();
+    AlignmentI al = af.readFile(RnaSSTestFile, DataSourceType.FILE,
+            jalview.io.FileFormat.Stockholm);
+    Iterable<AlignmentAnnotation> aai = al.findAnnotations(null, null,
+            "Secondary Structure");
+    AlignmentAnnotation aa = aai.iterator().next();
+    Assert.assertTrue(aa.isRNA(),
+            "'" + RnaSSTestFile + "' not recognised as RNA SS");
+    Assert.assertTrue(aa.isValidStruc(),
+            "'" + RnaSSTestFile + "' not recognised as valid structure");
+    Annotation[] as = aa.annotations;
+    char[] As = new char[as.length];
+    for (int i = 0; i < as.length; i++)
+    {
+      As[i] = as[i].secondaryStructure;
+    }
+    char[] shouldBe = { '<', '(', 'E', 'H', 'B', 'h', 'e', 'b', '(', 'E',
+        ')', 'e', ')', '>' };
+    Assert.assertTrue(
+            Arrays.equals(As, shouldBe),
+            "Annotation is " + new String(As) + " but should be "
+                    + new String(shouldBe));
+
+    // this should result in the same RNA SS Annotations
+    AlignmentI newAl = new AppletFormatAdapter().readFile(
+            aliFileRnaSS,
+            DataSourceType.PASTE, jalview.io.FileFormat.Fasta);
+    AnnotationFile aaf = new AnnotationFile();
+    aaf.readAnnotationFile(newAl, annFileRnaSSAlphaChars,
+            DataSourceType.PASTE);
+
+    Assert.assertTrue(
+            testRnaSSAnnotationsEquivalent(al.getAlignmentAnnotation()[0],
+                    newAl.getAlignmentAnnotation()[0]),
+            "RNA SS Annotations SHOULD be pair-wise equivalent (but apparently aren't): \n"
+                    + "RNA SS A 1:" + al.getAlignmentAnnotation()[0] + "\n"
+                    + "RNA SS A 2:" + newAl.getAlignmentAnnotation()[0]);
+
+    // this should NOT result in the same RNA SS Annotations
+    newAl = new AppletFormatAdapter().readFile(
+            aliFileRnaSS, DataSourceType.PASTE,
+            jalview.io.FileFormat.Fasta);
+    aaf = new AnnotationFile();
+    aaf.readAnnotationFile(newAl, wrongAnnFileRnaSSAlphaChars,
+            DataSourceType.PASTE);
+
+    boolean mismatch = testRnaSSAnnotationsEquivalent(al.getAlignmentAnnotation()[0],
+            newAl.getAlignmentAnnotation()[0]);
+    Assert.assertFalse(mismatch,
+            "RNA SS Annotations SHOULD NOT be pair-wise equivalent (but apparently are): \n"
+                    + "RNA SS A 1:" + al.getAlignmentAnnotation()[0] + "\n"
+                    + "RNA SS A 2:" + newAl.getAlignmentAnnotation()[0]);
+  }
+
+  private static boolean testRnaSSAnnotationsEquivalent(
+          AlignmentAnnotation a1,
+          AlignmentAnnotation a2)
+  {
+    return a1.rnaSecondaryStructureEquivalent(a2);
+  }
+
+  String annFileRnaSSWithSpaceChars = "JALVIEW_ANNOTATION\n"
+          + "# Created: Thu Aug 02 14:54:57 BST 2018\n" + "\n"
+          + "NO_GRAPH\tSecondary Structure\tSecondary Structure\t<,<|.,.|H,H| , |B,B|h,h| , |b,b|(,(|E,E|.,.|e,e|),)|>,>|\t2.0\n"
+          + "\n"
+          + "ROWPROPERTIES\tSecondary Structure\tscaletofit=true\tshowalllabs=true\tcentrelabs=false\n"
+          + "\n" + "\n" + "ALIGNMENT\tID=RNA.SS.TEST\tTP=RNA;";
+  String annFileRnaSSWithoutSpaceChars = "JALVIEW_ANNOTATION\n"
+          + "# Created: Thu Aug 02 14:54:57 BST 2018\n" + "\n"
+          + "NO_GRAPH\tSecondary Structure\tSecondary Structure\t<,<|.,.|H,H|.,.|B,B|h,h|.,.|b,b|(,(|E,E|.,.|e,e|),)|>,>|\t2.0\n"
+          + "\n"
+          + "ROWPROPERTIES\tSecondary Structure\tscaletofit=true\tshowalllabs=true\tcentrelabs=false\n"
+          + "\n" + "\n" + "ALIGNMENT\tID=RNA.SS.TEST\tTP=RNA;";
+
+  String wrongAnnFileRnaSSWithoutSpaceChars = "JALVIEW_ANNOTATION\n"
+          + "# Created: Thu Aug 02 14:54:57 BST 2018\n" + "\n"
+          + "NO_GRAPH\tSecondary Structure\tSecondary Structure\t<,<|.,.|H,H|Z,Z|B,B|h,h|z,z|b,b|(,(|E,E|.,.|e,e|),)|>,>|\t2.0\n"
+          + "\n"
+          + "ROWPROPERTIES\tSecondary Structure\tscaletofit=true\tshowalllabs=true\tcentrelabs=false\n"
+          + "\n" + "\n" + "ALIGNMENT\tID=RNA.SS.TEST\tTP=RNA;";
+
+  @Test(groups = { "Functional" })
+  public void stockholmFileRnaSSSpaceChars() throws Exception
+  {
+    AlignmentI alWithSpaces = new AppletFormatAdapter().readFile(
+            aliFileRnaSS, DataSourceType.PASTE,
+            jalview.io.FileFormat.Fasta);
+    AnnotationFile afWithSpaces = new AnnotationFile();
+    afWithSpaces.readAnnotationFile(alWithSpaces,
+            annFileRnaSSWithSpaceChars, DataSourceType.PASTE);
+
+    Iterable<AlignmentAnnotation> aaiWithSpaces = alWithSpaces
+            .findAnnotations(null, null, "Secondary Structure");
+    AlignmentAnnotation aaWithSpaces = aaiWithSpaces.iterator().next();
+    Assert.assertTrue(aaWithSpaces.isRNA(),
+            "'" + aaWithSpaces + "' not recognised as RNA SS");
+    Assert.assertTrue(aaWithSpaces.isValidStruc(),
+            "'" + aaWithSpaces + "' not recognised as valid structure");
+    Annotation[] annWithSpaces = aaWithSpaces.annotations;
+    char[] As = new char[annWithSpaces.length];
+    for (int i = 0; i < annWithSpaces.length; i++)
+    {
+      As[i] = annWithSpaces[i].secondaryStructure;
+    }
+    // check all spaces and dots are spaces in the internal representation
+    char[] shouldBe = { '<', ' ', 'H', ' ', 'B', 'h', ' ', 'b', '(', 'E',
+        ' ', 'e', ')', '>' };
+    Assert.assertTrue(Arrays.equals(As, shouldBe), "Annotation is "
+            + new String(As) + " but should be " + new String(shouldBe));
+
+    // this should result in the same RNA SS Annotations
+    AlignmentI alWithoutSpaces = new AppletFormatAdapter().readFile(
+            aliFileRnaSS, DataSourceType.PASTE,
+            jalview.io.FileFormat.Fasta);
+    AnnotationFile afWithoutSpaces = new AnnotationFile();
+    afWithoutSpaces.readAnnotationFile(alWithoutSpaces,
+            annFileRnaSSWithoutSpaceChars,
+            DataSourceType.PASTE);
+
+    Assert.assertTrue(
+            testRnaSSAnnotationsEquivalent(
+                    alWithSpaces.getAlignmentAnnotation()[0],
+                    alWithoutSpaces.getAlignmentAnnotation()[0]),
+            "RNA SS Annotations SHOULD be pair-wise equivalent (but apparently aren't): \n"
+                    + "RNA SS A 1:"
+                    + alWithSpaces.getAlignmentAnnotation()[0]
+                            .getRnaSecondaryStructure()
+                    + "\n" + "RNA SS A 2:"
+                    + alWithoutSpaces.getAlignmentAnnotation()[0]
+                            .getRnaSecondaryStructure());
+
+    // this should NOT result in the same RNA SS Annotations
+    AlignmentI wrongAlWithoutSpaces = new AppletFormatAdapter().readFile(
+            aliFileRnaSS, DataSourceType.PASTE,
+            jalview.io.FileFormat.Fasta);
+    AnnotationFile wrongAfWithoutSpaces = new AnnotationFile();
+    wrongAfWithoutSpaces.readAnnotationFile(wrongAlWithoutSpaces,
+            wrongAnnFileRnaSSWithoutSpaceChars,
+            DataSourceType.PASTE);
+
+    Assert.assertFalse(
+            testRnaSSAnnotationsEquivalent(
+                    alWithSpaces.getAlignmentAnnotation()[0],
+                    wrongAlWithoutSpaces.getAlignmentAnnotation()[0]),
+            "RNA SS Annotations SHOULD NOT be pair-wise equivalent (but apparently are): \n"
+                    + "RNA SS A 1:"
+                    + alWithSpaces.getAlignmentAnnotation()[0]
+                            .getRnaSecondaryStructure()
+                    + "\n" + "RNA SS A 2:"
+                    + wrongAlWithoutSpaces.getAlignmentAnnotation()[0]
+                            .getRnaSecondaryStructure());
+
+    // check no spaces in the output
+    // TODO: create a better 'save as <format>' pattern
+    alWithSpaces.getAlignmentAnnotation()[0].visible = true;
+    StockholmFile sf = new StockholmFile(alWithSpaces);
+
+    String stockholmFile = sf.print(alWithSpaces.getSequencesArray(), true);
+    Pattern noSpacesInRnaSSAnnotation = Pattern
+            .compile("\\n#=GC SS_cons\\s+\\S{14}\\n");
+    Matcher m = noSpacesInRnaSSAnnotation.matcher(stockholmFile);
+    boolean matches = m.find();
+    Assert.assertTrue(matches,
+            "StockholmFile output does not contain expected output (may contain spaces):\n"
+                    + stockholmFile);
+
+  }
 }