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