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