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