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