JAL-2994 put ‘\’ first in character class clause of regex - otherwise doesn’t compile...
[jalview.git] / src / jalview / io / StockholmFile.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
4  * 
5  * This file is part of Jalview.
6  * 
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License 
9  * as published by the Free Software Foundation, either version 3
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 /*
22  * This extension was written by Benjamin Schuster-Boeckler at sanger.ac.uk
23  */
24 package jalview.io;
25
26 import jalview.analysis.Rna;
27 import jalview.datamodel.AlignmentAnnotation;
28 import jalview.datamodel.AlignmentI;
29 import jalview.datamodel.Annotation;
30 import jalview.datamodel.DBRefEntry;
31 import jalview.datamodel.DBRefSource;
32 import jalview.datamodel.Mapping;
33 import jalview.datamodel.Sequence;
34 import jalview.datamodel.SequenceFeature;
35 import jalview.datamodel.SequenceI;
36 import jalview.schemes.ResidueProperties;
37 import jalview.util.Comparison;
38 import jalview.util.Format;
39 import jalview.util.MessageManager;
40
41 import java.io.BufferedReader;
42 import java.io.FileReader;
43 import java.io.IOException;
44 import java.util.ArrayList;
45 import java.util.Enumeration;
46 import java.util.HashMap;
47 import java.util.Hashtable;
48 import java.util.LinkedHashMap;
49 import java.util.List;
50 import java.util.Map;
51 import java.util.Map.Entry;
52 import java.util.Vector;
53
54 import com.stevesoft.pat.Regex;
55
56 import fr.orsay.lri.varna.exceptions.ExceptionUnmatchedClosingParentheses;
57 import fr.orsay.lri.varna.factories.RNAFactory;
58 import fr.orsay.lri.varna.models.rna.RNA;
59
60 // import org.apache.log4j.*;
61
62 /**
63  * This class is supposed to parse a Stockholm format file into Jalview There
64  * are TODOs in this class: we do not know what the database source and version
65  * is for the file when parsing the #GS= AC tag which associates accessions with
66  * sequences. Database references are also not parsed correctly: a separate
67  * reference string parser must be added to parse the database reference form
68  * into Jalview's local representation.
69  * 
70  * @author bsb at sanger.ac.uk
71  * @author Natasha Shersnev (Dundee, UK) (Stockholm file writer)
72  * @author Lauren Lui (UCSC, USA) (RNA secondary structure annotation import as
73  *         stockholm)
74  * @author Anne Menard (Paris, FR) (VARNA parsing of Stockholm file data)
75  * @version 0.3 + jalview mods
76  * 
77  */
78 public class StockholmFile extends AlignFile
79 {
80   private static final String ANNOTATION = "annotation";
81
82   private static final char UNDERSCORE = '_';
83
84   // private static final Regex OPEN_PAREN = new Regex("(<|\\[)", "(");
85   // private static final Regex CLOSE_PAREN = new Regex("(>|\\])", ")");
86
87   public static final Regex DETECT_BRACKETS = new Regex(
88           "(<|>|\\[|\\]|\\(|\\)|\\{|\\})");
89
90   /*
91    * lookup table of Stockholm 'feature' (annotation) types
92    * see http://sonnhammer.sbc.su.se/Stockholm.html
93    */
94   private static Map<String, String> featureTypes = null;
95
96   static
97   {
98     featureTypes = new HashMap<>();
99     featureTypes.put("SS", "Secondary Structure");
100     featureTypes.put("SA", "Surface Accessibility");
101     featureTypes.put("TM", "transmembrane");
102     featureTypes.put("PP", "Posterior Probability");
103     featureTypes.put("LI", "ligand binding");
104     featureTypes.put("AS", "active site");
105     featureTypes.put("IN", "intron");
106     featureTypes.put("IR", "interacting residue");
107     featureTypes.put("AC", "accession");
108     featureTypes.put("OS", "organism");
109     featureTypes.put("CL", "class");
110     featureTypes.put("DE", "description");
111     featureTypes.put("DR", "reference");
112     featureTypes.put("LO", "look");
113     featureTypes.put("RF", "Reference Positions");
114   }
115
116   private AlignmentI al;
117
118   public StockholmFile()
119   {
120   }
121
122   /**
123    * Creates a new StockholmFile object for output
124    */
125   public StockholmFile(AlignmentI al)
126   {
127     this.al = al;
128   }
129
130   public StockholmFile(String inFile, DataSourceType type)
131           throws IOException
132   {
133     super(inFile, type);
134   }
135
136   public StockholmFile(FileParse source) throws IOException
137   {
138     super(source);
139   }
140
141   /**
142    * Answers the readable description for a (case-sensitive) annotation type
143    * code, for example "Reference Positions" for "RF". Returns the type code if
144    * no description is found.
145    * 
146    * @param id
147    * @return
148    */
149   public static String typeToDescription(String id)
150   {
151     if (featureTypes.containsKey(id))
152     {
153       return featureTypes.get(id);
154     }
155     System.err.println(
156             "Warning : Unknown Stockholm annotation type code " + id);
157     return id;
158   }
159
160   /**
161    * Answers the annotation type code for a (non-case-sensitive) readable
162    * description, for example "RF" for "Reference Positions" (or null if not
163    * found)
164    * 
165    * @param description
166    * @return
167    */
168   public static String descriptionToType(String description)
169   {
170     for (Entry<String, String> entry : featureTypes.entrySet())
171     {
172       if (entry.getValue().equalsIgnoreCase(description))
173       {
174         return entry.getKey();
175       }
176     }
177     System.err.println(
178             "Warning : Unknown Stockholm annotation type: " + description);
179     return null;
180   }
181
182   @Override
183   public void initData()
184   {
185     super.initData();
186   }
187
188   /**
189    * Parse a file in Stockholm format into Jalview's data model using VARNA
190    * 
191    * @throws IOException
192    *           If there is an error with the input file
193    */
194   public void parse_with_VARNA(java.io.File inFile) throws IOException
195   {
196     FileReader fr = null;
197     fr = new FileReader(inFile);
198
199     BufferedReader r = new BufferedReader(fr);
200     List<RNA> result = null;
201     try
202     {
203       result = RNAFactory.loadSecStrStockholm(r);
204     } catch (ExceptionUnmatchedClosingParentheses umcp)
205     {
206       errormessage = "Unmatched parentheses in annotation. Aborting ("
207               + umcp.getMessage() + ")";
208       throw new IOException(umcp);
209     }
210     // DEBUG System.out.println("this is the secondary scructure:"
211     // +result.size());
212     SequenceI[] seqs = new SequenceI[result.size()];
213     String id = null;
214     for (int i = 0; i < result.size(); i++)
215     {
216       // DEBUG System.err.println("Processing i'th sequence in Stockholm file")
217       RNA current = result.get(i);
218
219       String seq = current.getSeq();
220       String rna = current.getStructDBN(true);
221       // DEBUG System.out.println(seq);
222       // DEBUG System.err.println(rna);
223       int begin = 0;
224       int end = seq.length() - 1;
225       id = safeName(getDataName());
226       seqs[i] = new Sequence(id, seq, begin, end);
227       String[] annot = new String[rna.length()];
228       Annotation[] ann = new Annotation[rna.length()];
229       for (int j = 0; j < rna.length(); j++)
230       {
231         annot[j] = rna.substring(j, j + 1);
232
233       }
234
235       for (int k = 0; k < rna.length(); k++)
236       {
237         ann[k] = new Annotation(annot[k], "",
238                 Rna.getRNASecStrucState(annot[k]).charAt(0), 0f);
239
240       }
241       AlignmentAnnotation align = new AlignmentAnnotation("Sec. str.",
242               current.getID(), ann);
243
244       seqs[i].addAlignmentAnnotation(align);
245       seqs[i].setRNA(result.get(i));
246       this.annotations.addElement(align);
247     }
248     this.setSeqs(seqs);
249
250   }
251
252   /**
253    * Parse a file in Stockholm format into Jalview's data model. The file has to
254    * be passed at construction time
255    * 
256    * @throws IOException
257    *           If there is an error with the input file
258    */
259   @Override
260   public void parse() throws IOException
261   {
262     StringBuffer treeString = new StringBuffer();
263     String treeName = null;
264     // --------------- Variable Definitions -------------------
265     String line;
266     String version;
267     // String id;
268     Hashtable seqAnn = new Hashtable(); // Sequence related annotations
269     LinkedHashMap<String, String> seqs = new LinkedHashMap<>();
270     Regex p, r, rend, s, x;
271     // Temporary line for processing RNA annotation
272     // String RNAannot = "";
273
274     // ------------------ Parsing File ----------------------
275     // First, we have to check that this file has STOCKHOLM format, i.e. the
276     // first line must match
277
278     r = new Regex("# STOCKHOLM ([\\d\\.]+)");
279     if (!r.search(nextLine()))
280     {
281       throw new IOException(MessageManager
282               .getString("exception.stockholm_invalid_format"));
283     }
284     else
285     {
286       version = r.stringMatched(1);
287
288       // logger.debug("Stockholm version: " + version);
289     }
290
291     // We define some Regexes here that will be used regularly later
292     rend = new Regex("^\\s*\\/\\/"); // Find the end of an alignment
293     p = new Regex("(\\S+)\\/(\\d+)\\-(\\d+)"); // split sequence id in
294     // id/from/to
295     s = new Regex("(\\S+)\\s+(\\S*)\\s+(.*)"); // Parses annotation subtype
296     r = new Regex("#=(G[FSRC]?)\\s+(.*)"); // Finds any annotation line
297     x = new Regex("(\\S+)\\s+(\\S+)"); // split id from sequence
298
299     // Convert all bracket types to parentheses (necessary for passing to VARNA)
300     Regex openparen = new Regex("(<|\\[)", "(");
301     Regex closeparen = new Regex("(>|\\])", ")");
302
303     // Detect if file is RNA by looking for bracket types
304     // Regex detectbrackets = new Regex("(<|>|\\[|\\]|\\(|\\))");
305
306     rend.optimize();
307     p.optimize();
308     s.optimize();
309     r.optimize();
310     x.optimize();
311     openparen.optimize();
312     closeparen.optimize();
313
314     while ((line = nextLine()) != null)
315     {
316       if (line.length() == 0)
317       {
318         continue;
319       }
320       if (rend.search(line))
321       {
322         // End of the alignment, pass stuff back
323         this.noSeqs = seqs.size();
324
325         String seqdb, dbsource = null;
326         Regex pf = new Regex("PF[0-9]{5}(.*)"); // Finds AC for Pfam
327         Regex rf = new Regex("RF[0-9]{5}(.*)"); // Finds AC for Rfam
328         if (getAlignmentProperty("AC") != null)
329         {
330           String dbType = getAlignmentProperty("AC").toString();
331           if (pf.search(dbType))
332           {
333             // PFAM Alignment - so references are typically from Uniprot
334             dbsource = "PFAM";
335           }
336           else if (rf.search(dbType))
337           {
338             dbsource = "RFAM";
339           }
340         }
341         // logger.debug("Number of sequences: " + this.noSeqs);
342         for (Map.Entry<String, String> skey : seqs.entrySet())
343         {
344           // logger.debug("Processing sequence " + acc);
345           String acc = skey.getKey();
346           String seq = skey.getValue();
347           if (maxLength < seq.length())
348           {
349             maxLength = seq.length();
350           }
351           int start = 1;
352           int end = -1;
353           String sid = acc;
354           /*
355            * Retrieve hash of annotations for this accession Associate
356            * Annotation with accession
357            */
358           Hashtable accAnnotations = null;
359
360           if (seqAnn != null && seqAnn.containsKey(acc))
361           {
362             accAnnotations = (Hashtable) seqAnn.remove(acc);
363             // TODO: add structures to sequence
364           }
365
366           // Split accession in id and from/to
367           if (p.search(acc))
368           {
369             sid = p.stringMatched(1);
370             start = Integer.parseInt(p.stringMatched(2));
371             end = Integer.parseInt(p.stringMatched(3));
372           }
373           // logger.debug(sid + ", " + start + ", " + end);
374
375           Sequence seqO = new Sequence(sid, seq, start, end);
376           // Add Description (if any)
377           if (accAnnotations != null && accAnnotations.containsKey("DE"))
378           {
379             String desc = (String) accAnnotations.get("DE");
380             seqO.setDescription((desc == null) ? "" : desc);
381           }
382           // Add DB References (if any)
383           if (accAnnotations != null && accAnnotations.containsKey("DR"))
384           {
385             String dbr = (String) accAnnotations.get("DR");
386             if (dbr != null && dbr.indexOf(";") > -1)
387             {
388               String src = dbr.substring(0, dbr.indexOf(";"));
389               String acn = dbr.substring(dbr.indexOf(";") + 1);
390               jalview.util.DBRefUtils.parseToDbRef(seqO, src, "0", acn);
391             }
392           }
393
394           if (accAnnotations != null && accAnnotations.containsKey("AC"))
395           {
396             if (dbsource != null)
397             {
398               String dbr = (String) accAnnotations.get("AC");
399               if (dbr != null)
400               {
401                 // we could get very clever here - but for now - just try to
402                 // guess accession type from source of alignment plus structure
403                 // of accession
404                 guessDatabaseFor(seqO, dbr, dbsource);
405
406               }
407             }
408             // else - do what ? add the data anyway and prompt the user to
409             // specify what references these are ?
410           }
411
412           Hashtable features = null;
413           // We need to adjust the positions of all features to account for gaps
414           try
415           {
416             features = (Hashtable) accAnnotations.remove("features");
417           } catch (java.lang.NullPointerException e)
418           {
419             // loggerwarn("Getting Features for " + acc + ": " +
420             // e.getMessage());
421             // continue;
422           }
423           // if we have features
424           if (features != null)
425           {
426             int posmap[] = seqO.findPositionMap();
427             Enumeration i = features.keys();
428             while (i.hasMoreElements())
429             {
430               // TODO: parse out secondary structure annotation as annotation
431               // row
432               // TODO: parse out scores as annotation row
433               // TODO: map coding region to core jalview feature types
434               String type = i.nextElement().toString();
435               Hashtable content = (Hashtable) features.remove(type);
436
437               // add alignment annotation for this feature
438               String key = descriptionToType(type);
439
440               /*
441                * have we added annotation rows for this type ?
442                */
443               boolean annotsAdded = false;
444               if (key != null)
445               {
446                 if (accAnnotations != null
447                         && accAnnotations.containsKey(key))
448                 {
449                   Vector vv = (Vector) accAnnotations.get(key);
450                   for (int ii = 0; ii < vv.size(); ii++)
451                   {
452                     annotsAdded = true;
453                     AlignmentAnnotation an = (AlignmentAnnotation) vv
454                             .elementAt(ii);
455                     seqO.addAlignmentAnnotation(an);
456                     annotations.add(an);
457                   }
458                 }
459               }
460
461               Enumeration j = content.keys();
462               while (j.hasMoreElements())
463               {
464                 String desc = j.nextElement().toString();
465                 if (ANNOTATION.equals(desc) && annotsAdded)
466                 {
467                   // don't add features if we already added an annotation row
468                   continue;
469                 }
470                 String ns = content.get(desc).toString();
471                 char[] byChar = ns.toCharArray();
472                 for (int k = 0; k < byChar.length; k++)
473                 {
474                   char c = byChar[k];
475                   if (!(c == ' ' || c == '_' || c == '-' || c == '.')) // PFAM
476                   // uses
477                   // '.'
478                   // for
479                   // feature
480                   // background
481                   {
482                     int new_pos = posmap[k]; // look up nearest seqeunce
483                     // position to this column
484                     SequenceFeature feat = new SequenceFeature(type, desc,
485                             new_pos, new_pos, null);
486
487                     seqO.addSequenceFeature(feat);
488                   }
489                 }
490               }
491
492             }
493
494           }
495           // garbage collect
496
497           // logger.debug("Adding seq " + acc + " from " + start + " to " + end
498           // + ": " + seq);
499           this.seqs.addElement(seqO);
500         }
501         return; // finished parsing this segment of source
502       }
503       else if (!r.search(line))
504       {
505         // System.err.println("Found sequence line: " + line);
506
507         // Split sequence in sequence and accession parts
508         if (!x.search(line))
509         {
510           // logger.error("Could not parse sequence line: " + line);
511           throw new IOException(MessageManager.formatMessage(
512                   "exception.couldnt_parse_sequence_line", new String[]
513                   { line }));
514         }
515         String ns = seqs.get(x.stringMatched(1));
516         if (ns == null)
517         {
518           ns = "";
519         }
520         ns += x.stringMatched(2);
521
522         seqs.put(x.stringMatched(1), ns);
523       }
524       else
525       {
526         String annType = r.stringMatched(1);
527         String annContent = r.stringMatched(2);
528
529         // System.err.println("type:" + annType + " content: " + annContent);
530
531         if (annType.equals("GF"))
532         {
533           /*
534            * Generic per-File annotation, free text Magic features: #=GF NH
535            * <tree in New Hampshire eXtended format> #=GF TN <Unique identifier
536            * for the next tree> Pfam descriptions: 7. DESCRIPTION OF FIELDS
537            * 
538            * Compulsory fields: ------------------
539            * 
540            * AC Accession number: Accession number in form PFxxxxx.version or
541            * PBxxxxxx. ID Identification: One word name for family. DE
542            * Definition: Short description of family. AU Author: Authors of the
543            * entry. SE Source of seed: The source suggesting the seed members
544            * belong to one family. GA Gathering method: Search threshold to
545            * build the full alignment. TC Trusted Cutoff: Lowest sequence score
546            * and domain score of match in the full alignment. NC Noise Cutoff:
547            * Highest sequence score and domain score of match not in full
548            * alignment. TP Type: Type of family -- presently Family, Domain,
549            * Motif or Repeat. SQ Sequence: Number of sequences in alignment. AM
550            * Alignment Method The order ls and fs hits are aligned to the model
551            * to build the full align. // End of alignment.
552            * 
553            * Optional fields: ----------------
554            * 
555            * DC Database Comment: Comment about database reference. DR Database
556            * Reference: Reference to external database. RC Reference Comment:
557            * Comment about literature reference. RN Reference Number: Reference
558            * Number. RM Reference Medline: Eight digit medline UI number. RT
559            * Reference Title: Reference Title. RA Reference Author: Reference
560            * Author RL Reference Location: Journal location. PI Previous
561            * identifier: Record of all previous ID lines. KW Keywords: Keywords.
562            * CC Comment: Comments. NE Pfam accession: Indicates a nested domain.
563            * NL Location: Location of nested domains - sequence ID, start and
564            * end of insert.
565            * 
566            * Obsolete fields: ----------- AL Alignment method of seed: The
567            * method used to align the seed members.
568            */
569           // Let's save the annotations, maybe we'll be able to do something
570           // with them later...
571           Regex an = new Regex("(\\w+)\\s*(.*)");
572           if (an.search(annContent))
573           {
574             if (an.stringMatched(1).equals("NH"))
575             {
576               treeString.append(an.stringMatched(2));
577             }
578             else if (an.stringMatched(1).equals("TN"))
579             {
580               if (treeString.length() > 0)
581               {
582                 if (treeName == null)
583                 {
584                   treeName = "Tree " + (getTreeCount() + 1);
585                 }
586                 addNewickTree(treeName, treeString.toString());
587               }
588               treeName = an.stringMatched(2);
589               treeString = new StringBuffer();
590             }
591             setAlignmentProperty(an.stringMatched(1), an.stringMatched(2));
592           }
593         }
594         else if (annType.equals("GS"))
595         {
596           // Generic per-Sequence annotation, free text
597           /*
598            * Pfam uses these features: Feature Description ---------------------
599            * ----------- AC <accession> ACcession number DE <freetext>
600            * DEscription DR <db>; <accession>; Database Reference OS <organism>
601            * OrganiSm (species) OC <clade> Organism Classification (clade, etc.)
602            * LO <look> Look (Color, etc.)
603            */
604           if (s.search(annContent))
605           {
606             String acc = s.stringMatched(1);
607             String type = s.stringMatched(2);
608             String content = s.stringMatched(3);
609             // TODO: store DR in a vector.
610             // TODO: store AC according to generic file db annotation.
611             Hashtable ann;
612             if (seqAnn.containsKey(acc))
613             {
614               ann = (Hashtable) seqAnn.get(acc);
615             }
616             else
617             {
618               ann = new Hashtable();
619             }
620             ann.put(type, content);
621             seqAnn.put(acc, ann);
622           }
623           else
624           {
625             // throw new IOException("Error parsing " + line);
626             System.err.println(">> missing annotation: " + line);
627           }
628         }
629         else if (annType.equals("GC"))
630         {
631           // Generic per-Column annotation, exactly 1 char per column
632           // always need a label.
633           if (x.search(annContent))
634           {
635             // parse out and create alignment annotation directly.
636             parseAnnotationRow(annotations, x.stringMatched(1),
637                     x.stringMatched(2));
638           }
639         }
640         else if (annType.equals("GR"))
641         {
642           // Generic per-Sequence AND per-Column markup, exactly 1 char per
643           // column
644           /*
645            * Feature Description Markup letters ------- -----------
646            * -------------- SS Secondary Structure [HGIEBTSCX] SA Surface
647            * Accessibility [0-9X] (0=0%-10%; ...; 9=90%-100%) TM TransMembrane
648            * [Mio] PP Posterior Probability [0-9*] (0=0.00-0.05; 1=0.05-0.15;
649            * *=0.95-1.00) LI LIgand binding [*] AS Active Site [*] IN INtron (in
650            * or after) [0-2]
651            */
652           if (s.search(annContent))
653           {
654             String acc = s.stringMatched(1);
655             String type = s.stringMatched(2);
656             String oseq = s.stringMatched(3);
657             /*
658              * copy of annotation field that may be processed into whitespace chunks
659              */
660             String seq = new String(oseq);
661
662             Hashtable ann;
663             // Get an object with all the annotations for this sequence
664             if (seqAnn.containsKey(acc))
665             {
666               // logger.debug("Found annotations for " + acc);
667               ann = (Hashtable) seqAnn.get(acc);
668             }
669             else
670             {
671               // logger.debug("Creating new annotations holder for " + acc);
672               ann = new Hashtable();
673               seqAnn.put(acc, ann);
674             }
675
676             // // start of block for appending annotation lines for wrapped
677             // stokchholm file
678             // TODO test structure, call parseAnnotationRow with vector from
679             // hashtable for specific sequence
680
681             Hashtable features;
682             // Get an object with all the content for an annotation
683             if (ann.containsKey("features"))
684             {
685               // logger.debug("Found features for " + acc);
686               features = (Hashtable) ann.get("features");
687             }
688             else
689             {
690               // logger.debug("Creating new features holder for " + acc);
691               features = new Hashtable();
692               ann.put("features", features);
693             }
694
695             Hashtable content;
696             if (features.containsKey(StockholmFile.typeToDescription(type)))
697             {
698               // logger.debug("Found content for " + this.id2type(type));
699               content = (Hashtable) features
700                       .get(StockholmFile.typeToDescription(type));
701             }
702             else
703             {
704               // logger.debug("Creating new content holder for " +
705               // this.id2type(type));
706               content = new Hashtable();
707               features.put(StockholmFile.typeToDescription(type), content);
708             }
709             String ns = (String) content.get(ANNOTATION);
710
711             if (ns == null)
712             {
713               ns = "";
714             }
715             // finally, append the annotation line
716             ns += seq;
717             content.put(ANNOTATION, ns);
718             // // end of wrapped annotation block.
719             // // Now a new row is created with the current set of data
720
721             Hashtable strucAnn;
722             if (seqAnn.containsKey(acc))
723             {
724               strucAnn = (Hashtable) seqAnn.get(acc);
725             }
726             else
727             {
728               strucAnn = new Hashtable();
729             }
730
731             Vector<AlignmentAnnotation> newStruc = new Vector<>();
732             parseAnnotationRow(newStruc, type, ns);
733             for (AlignmentAnnotation alan : newStruc)
734             {
735               alan.visible = false;
736             }
737             // new annotation overwrites any existing annotation...
738
739             strucAnn.put(type, newStruc);
740             seqAnn.put(acc, strucAnn);
741           }
742           // }
743           else
744           {
745             System.err.println(
746                     "Warning - couldn't parse sequence annotation row line:\n"
747                             + line);
748             // throw new IOException("Error parsing " + line);
749           }
750         }
751         else
752         {
753           throw new IOException(MessageManager.formatMessage(
754                   "exception.unknown_annotation_detected", new String[]
755                   { annType, annContent }));
756         }
757       }
758     }
759     if (treeString.length() > 0)
760     {
761       if (treeName == null)
762       {
763         treeName = "Tree " + (1 + getTreeCount());
764       }
765       addNewickTree(treeName, treeString.toString());
766     }
767   }
768
769   /**
770    * Demangle an accession string and guess the originating sequence database
771    * for a given sequence
772    * 
773    * @param seqO
774    *          sequence to be annotated
775    * @param dbr
776    *          Accession string for sequence
777    * @param dbsource
778    *          source database for alignment (PFAM or RFAM)
779    */
780   private void guessDatabaseFor(Sequence seqO, String dbr, String dbsource)
781   {
782     DBRefEntry dbrf = null;
783     List<DBRefEntry> dbrs = new ArrayList<>();
784     String seqdb = "Unknown", sdbac = "" + dbr;
785     int st = -1, en = -1, p;
786     if ((st = sdbac.indexOf("/")) > -1)
787     {
788       String num, range = sdbac.substring(st + 1);
789       sdbac = sdbac.substring(0, st);
790       if ((p = range.indexOf("-")) > -1)
791       {
792         p++;
793         if (p < range.length())
794         {
795           num = range.substring(p).trim();
796           try
797           {
798             en = Integer.parseInt(num);
799           } catch (NumberFormatException x)
800           {
801             // could warn here that index is invalid
802             en = -1;
803           }
804         }
805       }
806       else
807       {
808         p = range.length();
809       }
810       num = range.substring(0, p).trim();
811       try
812       {
813         st = Integer.parseInt(num);
814       } catch (NumberFormatException x)
815       {
816         // could warn here that index is invalid
817         st = -1;
818       }
819     }
820     if (dbsource.equals("PFAM"))
821     {
822       seqdb = "UNIPROT";
823       if (sdbac.indexOf(".") > -1)
824       {
825         // strip of last subdomain
826         sdbac = sdbac.substring(0, sdbac.indexOf("."));
827         dbrf = jalview.util.DBRefUtils.parseToDbRef(seqO, seqdb, dbsource,
828                 sdbac);
829         if (dbrf != null)
830         {
831           dbrs.add(dbrf);
832         }
833       }
834       dbrf = jalview.util.DBRefUtils.parseToDbRef(seqO, dbsource, dbsource,
835               dbr);
836       if (dbr != null)
837       {
838         dbrs.add(dbrf);
839       }
840     }
841     else
842     {
843       seqdb = "EMBL"; // total guess - could be ENA, or something else these
844                       // days
845       if (sdbac.indexOf(".") > -1)
846       {
847         // strip off last subdomain
848         sdbac = sdbac.substring(0, sdbac.indexOf("."));
849         dbrf = jalview.util.DBRefUtils.parseToDbRef(seqO, seqdb, dbsource,
850                 sdbac);
851         if (dbrf != null)
852         {
853           dbrs.add(dbrf);
854         }
855       }
856
857       dbrf = jalview.util.DBRefUtils.parseToDbRef(seqO, dbsource, dbsource,
858               dbr);
859       if (dbrf != null)
860       {
861         dbrs.add(dbrf);
862       }
863     }
864     if (st != -1 && en != -1)
865     {
866       for (DBRefEntry d : dbrs)
867       {
868         jalview.util.MapList mp = new jalview.util.MapList(
869                 new int[]
870                 { seqO.getStart(), seqO.getEnd() }, new int[] { st, en }, 1,
871                 1);
872         jalview.datamodel.Mapping mping = new Mapping(mp);
873         d.setMap(mping);
874       }
875     }
876   }
877
878   protected static AlignmentAnnotation parseAnnotationRow(
879           Vector<AlignmentAnnotation> annotation, String label,
880           String annots)
881   {
882     // String convert1 = OPEN_PAREN.replaceAll(annots);
883     // String convert2 = CLOSE_PAREN.replaceAll(convert1);
884     // annots = convert2;
885
886     String type = label;
887     if (label.contains("_cons"))
888     {
889       type = (label.indexOf("_cons") == label.length() - 5)
890               ? label.substring(0, label.length() - 5)
891               : label;
892     }
893     boolean ss = false, posterior = false;
894     type = typeToDescription(type);
895     if (type.equalsIgnoreCase("secondary structure"))
896     {
897       ss = true;
898     }
899     if (type.equalsIgnoreCase("posterior probability"))
900     {
901       posterior = true;
902     }
903     // decide on secondary structure or not.
904     Annotation[] els = new Annotation[annots.length()];
905     for (int i = 0; i < annots.length(); i++)
906     {
907       String pos = annots.substring(i, i + 1);
908       if (UNDERSCORE == pos.charAt(0))
909       {
910         pos = " ";
911       }
912       Annotation ann;
913       ann = new Annotation(pos, "", ' ', 0f); // 0f is 'valid' null - will not
914       // be written out
915       if (ss)
916       {
917         // if (" .-_".indexOf(pos) == -1)
918         {
919           if (DETECT_BRACKETS.search(pos))
920           {
921             ann.secondaryStructure = Rna.getRNASecStrucState(pos).charAt(0);
922             ann.displayCharacter = "" + pos.charAt(0);
923           }
924           else
925           {
926             ann.secondaryStructure = ResidueProperties.getDssp3state(pos)
927                     .charAt(0);
928
929             if (ann.secondaryStructure == pos.charAt(0))
930             {
931               ann.displayCharacter = ""; // null; // " ";
932             }
933             else
934             {
935               ann.displayCharacter = " " + ann.displayCharacter;
936             }
937           }
938         }
939
940       }
941       if (posterior && !ann.isWhitespace()
942               && !Comparison.isGap(pos.charAt(0)))
943       {
944         float val = 0;
945         // symbol encodes values - 0..*==0..10
946         if (pos.charAt(0) == '*')
947         {
948           val = 10;
949         }
950         else
951         {
952           val = pos.charAt(0) - '0';
953           if (val > 9)
954           {
955             val = 10;
956           }
957         }
958         ann.value = val;
959       }
960
961       els[i] = ann;
962     }
963     AlignmentAnnotation annot = null;
964     Enumeration<AlignmentAnnotation> e = annotation.elements();
965     while (e.hasMoreElements())
966     {
967       annot = e.nextElement();
968       if (annot.label.equals(type))
969       {
970         break;
971       }
972       annot = null;
973     }
974     if (annot == null)
975     {
976       annot = new AlignmentAnnotation(type, type, els);
977       annotation.addElement(annot);
978     }
979     else
980     {
981       Annotation[] anns = new Annotation[annot.annotations.length
982               + els.length];
983       System.arraycopy(annot.annotations, 0, anns, 0,
984               annot.annotations.length);
985       System.arraycopy(els, 0, anns, annot.annotations.length, els.length);
986       annot.annotations = anns;
987       // System.out.println("else: ");
988     }
989     return annot;
990   }
991
992   @Override
993   public String print(final SequenceI[] sequences, boolean jvSuffix)
994   {
995     StringBuilder out = new StringBuilder();
996     out.append("# STOCKHOLM 1.0");
997     out.append(newline);
998
999     int maxIdWidth = 0;
1000     for (SequenceI seq : sequences)
1001     {
1002       if (seq != null)
1003       {
1004         String formattedId = printId(seq, jvSuffix);
1005         maxIdWidth = Math.max(maxIdWidth, formattedId.length());
1006       }
1007     }
1008     maxIdWidth += 9;
1009
1010     /*
1011      * generic alignment properties
1012      */
1013     Hashtable props = al.getProperties();
1014     if (props != null)
1015     {
1016       for (Object key : props.keySet())
1017       {
1018         out.append(String.format("#=GF %s %s", key.toString(),
1019                 props.get(key).toString()));
1020         out.append(newline);
1021       }
1022     }
1023
1024     /*
1025      * output database accessions as #=GS (per sequence annotation)
1026      * PFAM or RFAM are output as AC <accession number>
1027      * others are output as DR <dbname> ; <accession>
1028      */
1029     Format formatter = new Format("%-" + (maxIdWidth - 2) + "s");
1030     for (SequenceI seq : sequences)
1031     {
1032       if (seq != null)
1033       {
1034         DBRefEntry[] dbRefs = seq.getDBRefs();
1035         if (dbRefs != null)
1036         {
1037           String idField = formatter
1038                   .form("#=GS " + printId(seq, jvSuffix) + " ");
1039           for (DBRefEntry dbRef : dbRefs)
1040           {
1041             out.append(idField);
1042             printDbRef(out, dbRef);
1043           }
1044         }
1045       }
1046     }
1047
1048     /*
1049      * output annotations
1050      */
1051     for (SequenceI seq : sequences)
1052     {
1053       if (seq != null)
1054       {
1055         AlignmentAnnotation[] alAnot = seq.getAnnotation();
1056         if (alAnot != null)
1057         {
1058           for (int j = 0; j < alAnot.length; j++)
1059           {
1060             AlignmentAnnotation ann = alAnot[j];
1061             String key = descriptionToType(ann.label);
1062             boolean isrna = ann.isValidStruc();
1063             if (isrna)
1064             {
1065               /*
1066                * output as secondary structure if there is 
1067                * RNA secondary structure on the annotation
1068                */
1069               key = "SS";
1070             }
1071             if (key == null)
1072             {
1073               continue;
1074             }
1075
1076             out.append(new Format("%-" + maxIdWidth + "s").form(
1077                     "#=GR " + printId(seq, jvSuffix) + " " + key + " "));
1078             Annotation[] anns = ann.annotations;
1079             StringBuilder seqString = new StringBuilder();
1080             for (int k = 0; k < anns.length; k++)
1081             {
1082               seqString
1083                       .append(getAnnotationCharacter(key, k, anns[k], seq));
1084             }
1085             out.append(seqString.toString());
1086             out.append(newline);
1087           }
1088         }
1089
1090         out.append(new Format("%-" + maxIdWidth + "s")
1091                 .form(printId(seq, jvSuffix) + " "));
1092         out.append(seq.getSequenceAsString());
1093         out.append(newline);
1094       }
1095     }
1096
1097     /*
1098      * output alignment annotation (but not auto-calculated or sequence-related)
1099      */
1100     if (al.getAlignmentAnnotation() != null)
1101     {
1102       for (int ia = 0; ia < al.getAlignmentAnnotation().length; ia++)
1103       {
1104         AlignmentAnnotation aa = al.getAlignmentAnnotation()[ia];
1105         if (aa.autoCalculated || !aa.visible || aa.sequenceRef != null)
1106         {
1107           continue;
1108         }
1109         String label = aa.label;
1110         String key = "";
1111         if (aa.label.equals("seq"))
1112         {
1113           label = "seq_cons";
1114         }
1115         else
1116         {
1117           key = descriptionToType(aa.label);
1118           if ("RF".equals(key))
1119           {
1120             label = key;
1121           }
1122           else if (key != null)
1123           {
1124             label = key + "_cons";
1125           }
1126         }
1127         label = label.replace(" ", "_");
1128
1129         out.append(
1130                 new Format("%-" + maxIdWidth + "s")
1131                         .form("#=GC " + label + " "));
1132         StringBuilder sb = new StringBuilder(aa.annotations.length);
1133         for (int j = 0; j < aa.annotations.length; j++)
1134         {
1135           sb.append(
1136                   getAnnotationCharacter(key, j, aa.annotations[j], null));
1137         }
1138         out.append(sb.toString());
1139         out.append(newline);
1140       }
1141     }
1142
1143     out.append("//");
1144     out.append(newline);
1145
1146     return out.toString();
1147   }
1148
1149   /**
1150    * A helper method that appends a formatted dbref to the output buffer
1151    * 
1152    * @param out
1153    * @param dbRef
1154    */
1155   protected void printDbRef(StringBuilder out, DBRefEntry dbRef)
1156   {
1157     String db = dbRef.getSource();
1158     String acc = dbRef.getAccessionId();
1159     if (DBRefSource.PFAM.equalsIgnoreCase(db)
1160             || DBRefSource.RFAM.equalsIgnoreCase(db))
1161     {
1162       out.append(" AC " + acc);
1163     }
1164     else
1165     {
1166       out.append(" DR " + db + " ; " + acc);
1167     }
1168     out.append(newline);
1169   }
1170
1171   /**
1172    * Returns an annotation character to add to the output row
1173    * 
1174    * @param seq
1175    * @param key
1176    * @param k
1177    * @param ann
1178    * @param sequenceI
1179    */
1180   static char getAnnotationCharacter(String key, int k, Annotation annot,
1181           SequenceI sequenceI)
1182   {
1183     char seq = ' ';
1184     String ch = (annot == null)
1185             ? ((sequenceI == null) ? "-"
1186                     : Character.toString(sequenceI.getCharAt(k)))
1187             : annot.displayCharacter;
1188     if (key != null && key.equals("SS"))
1189     {
1190       if (annot == null)
1191       {
1192         // Stockholm format requires underscore, not space
1193         return UNDERSCORE;
1194       }
1195       else
1196       {
1197         // valid secondary structure AND no alternative label (e.g. ' B')
1198         if (annot.secondaryStructure > ' ' && ch.length() < 2)
1199         {
1200           return annot.secondaryStructure;
1201         }
1202       }
1203     }
1204
1205     if (ch.length() == 0)
1206     {
1207       seq = '.';
1208     }
1209     else if (ch.length() == 1)
1210     {
1211       seq = ch.charAt(0);
1212     }
1213     else if (ch.length() > 1)
1214     {
1215       seq = ch.charAt(1);
1216     }
1217     return seq;
1218   }
1219
1220   /**
1221    * make a friendly ID string.
1222    * 
1223    * @param dataName
1224    * @return truncated dataName to after last '/'
1225    */
1226   private String safeName(String dataName)
1227   {
1228     int b = 0;
1229     while ((b = dataName.indexOf("/")) > -1 && b < dataName.length())
1230     {
1231       dataName = dataName.substring(b + 1).trim();
1232
1233     }
1234     int e = (dataName.length() - dataName.indexOf(".")) + 1;
1235     dataName = dataName.substring(1, e).trim();
1236     return dataName;
1237   }
1238 }