JAL-1705 add DBRefEntry for 'self' to retrieved transcripts so they are
[jalview.git] / src / jalview / ext / ensembl / EnsemblSeqProxy.java
1 package jalview.ext.ensembl;
2
3 import jalview.analysis.AlignmentUtils;
4 import jalview.analysis.Dna;
5 import jalview.datamodel.Alignment;
6 import jalview.datamodel.AlignmentI;
7 import jalview.datamodel.DBRefEntry;
8 import jalview.datamodel.DBRefSource;
9 import jalview.datamodel.Mapping;
10 import jalview.datamodel.SequenceFeature;
11 import jalview.datamodel.SequenceI;
12 import jalview.exceptions.JalviewException;
13 import jalview.io.FastaFile;
14 import jalview.io.FileParse;
15 import jalview.io.gff.SequenceOntologyFactory;
16 import jalview.io.gff.SequenceOntologyI;
17 import jalview.util.DBRefUtils;
18 import jalview.util.MapList;
19
20 import java.io.IOException;
21 import java.net.MalformedURLException;
22 import java.net.URL;
23 import java.util.ArrayList;
24 import java.util.Arrays;
25 import java.util.Collections;
26 import java.util.Comparator;
27 import java.util.List;
28
29 /**
30  * Base class for Ensembl sequence fetchers
31  * 
32  * @see http://rest.ensembl.org/documentation/info/sequence_id
33  * @author gmcarstairs
34  */
35 public abstract class EnsemblSeqProxy extends EnsemblRestClient
36 {
37   private static final String ALLELES = "alleles";
38
39   private static final List<String> CROSS_REFERENCES = Arrays
40           .asList(new String[] { "CCDS", "Uniprot/SWISSPROT",
41               "Uniprot/SPTREMBL" });
42
43   protected static final String CONSEQUENCE_TYPE = "consequence_type";
44
45   protected static final String PARENT = "Parent";
46
47   protected static final String ID = "ID";
48
49   protected static final String NAME = "Name";
50
51   protected static final String DESCRIPTION = "description";
52
53   /*
54    * enum for 'type' parameter to the /sequence REST service
55    */
56   public enum EnsemblSeqType
57   {
58     /**
59      * type=genomic to fetch full dna including introns
60      */
61     GENOMIC("genomic"),
62
63     /**
64      * type=cdna to fetch dna including UTRs
65      */
66     CDNA("cdna"),
67
68     /**
69      * type=cds to fetch coding dna excluding UTRs
70      */
71     CDS("cds"),
72
73     /**
74      * type=protein to fetch peptide product sequence
75      */
76     PROTEIN("protein");
77
78     /*
79      * the value of the 'type' parameter to fetch this version of 
80      * an Ensembl sequence
81      */
82     private String type;
83
84     EnsemblSeqType(String t)
85     {
86       type = t;
87     }
88
89     public String getType()
90     {
91       return type;
92     }
93
94   }
95
96   /**
97    * A comparator to sort ranges into ascending start position order
98    */
99   private class RangeSorter implements Comparator<int[]>
100   {
101     boolean forwards;
102
103     RangeSorter(boolean forward)
104     {
105       forwards = forward;
106     }
107
108     @Override
109     public int compare(int[] o1, int[] o2)
110     {
111       return (forwards ? 1 : -1) * Integer.compare(o1[0], o2[0]);
112     }
113
114   }
115
116   /**
117    * Default constructor (to use rest.ensembl.org)
118    */
119   public EnsemblSeqProxy()
120   {
121     super();
122   }
123
124   /**
125    * Constructor given the target domain to fetch data from
126    */
127   public EnsemblSeqProxy(String d)
128   {
129     super(d);
130   }
131
132   /**
133    * Makes the sequence queries to Ensembl's REST service and returns an
134    * alignment consisting of the returned sequences.
135    */
136   @Override
137   public AlignmentI getSequenceRecords(String query) throws Exception
138   {
139     // TODO use a String... query vararg instead?
140
141     // danger: accession separator used as a regex here, a string elsewhere
142     // in this case it is ok (it is just a space), but (e.g.) '\' would not be
143     List<String> allIds = Arrays.asList(query
144             .split(getAccessionSeparator()));
145     AlignmentI alignment = null;
146     inProgress = true;
147
148     /*
149      * execute queries, if necessary in batches of the
150      * maximum allowed number of ids
151      */
152     int maxQueryCount = getMaximumQueryCount();
153     for (int v = 0, vSize = allIds.size(); v < vSize; v += maxQueryCount)
154     {
155       int p = Math.min(vSize, v + maxQueryCount);
156       List<String> ids = allIds.subList(v, p);
157       try
158       {
159         alignment = fetchSequences(ids, alignment);
160       } catch (Throwable r)
161       {
162         inProgress = false;
163         String msg = "Aborting ID retrieval after " + v
164                 + " chunks. Unexpected problem (" + r.getLocalizedMessage()
165                 + ")";
166         System.err.println(msg);
167         break;
168       }
169     }
170
171     if (alignment == null)
172     {
173       return null;
174     }
175
176     /*
177      * fetch and transfer genomic sequence features,
178      * fetch protein product and add as cross-reference
179      */
180     for (String accId : allIds)
181     {
182       addFeaturesAndProduct(accId, alignment);
183     }
184
185     for (SequenceI seq : alignment.getSequences())
186     {
187       getCrossReferences(seq);
188     }
189
190     return alignment;
191   }
192
193   /**
194    * Fetches Ensembl features using the /overlap REST endpoint, and adds them to
195    * the sequence in the alignment. Also fetches the protein product, maps it
196    * from the CDS features of the sequence, and saves it as a cross-reference of
197    * the dna sequence.
198    * 
199    * @param accId
200    * @param alignment
201    */
202   protected void addFeaturesAndProduct(String accId, AlignmentI alignment)
203   {
204     if (alignment == null)
205     {
206       return;
207     }
208
209     try
210     {
211       /*
212        * get 'dummy' genomic sequence with exon, cds and variation features
213        */
214       SequenceI genomicSequence = null;
215       EnsemblFeatures gffFetcher = new EnsemblFeatures(getDomain());
216       EnsemblFeatureType[] features = getFeaturesToFetch();
217       AlignmentI geneFeatures = gffFetcher.getSequenceRecords(accId,
218               features);
219       if (geneFeatures.getHeight() > 0)
220       {
221         genomicSequence = geneFeatures.getSequenceAt(0);
222       }
223       if (genomicSequence != null)
224       {
225         /*
226          * transfer features to the query sequence
227          */
228         SequenceI querySeq = alignment.findName(accId);
229         if (transferFeatures(accId, genomicSequence, querySeq))
230         {
231
232           /*
233            * fetch and map protein product, and add it as a cross-reference
234            * of the retrieved sequence
235            */
236           addProteinProduct(querySeq);
237         }
238       }
239     } catch (IOException e)
240     {
241       System.err.println("Error transferring Ensembl features: "
242               + e.getMessage());
243     }
244   }
245
246   /**
247    * Returns those sequence feature types to fetch from Ensembl. We may want
248    * features either because they are of interest to the user, or as means to
249    * identify the locations of the sequence on the genomic sequence (CDS
250    * features identify CDS, exon features identify cDNA etc).
251    * 
252    * @return
253    */
254   protected abstract EnsemblFeatureType[] getFeaturesToFetch();
255
256   /**
257    * Fetches and maps the protein product, and adds it as a cross-reference of
258    * the retrieved sequence
259    */
260   protected void addProteinProduct(SequenceI querySeq)
261   {
262     String accId = querySeq.getName();
263     try
264     {
265       AlignmentI protein = new EnsemblProtein(getDomain())
266               .getSequenceRecords(accId);
267       if (protein == null || protein.getHeight() == 0)
268       {
269         System.out.println("No protein product found for " + accId);
270         return;
271       }
272       SequenceI proteinSeq = protein.getSequenceAt(0);
273
274       /*
275        * need dataset sequences (to be the subject of mappings)
276        */
277       proteinSeq.createDatasetSequence();
278       querySeq.createDatasetSequence();
279
280       MapList mapList = AlignmentUtils.mapCdsToProtein(querySeq, proteinSeq);
281       if (mapList != null)
282       {
283         // clunky: ensure Uniprot xref if we have one is on mapped sequence
284         SequenceI ds = proteinSeq.getDatasetSequence();
285         ds.setSourceDBRef(proteinSeq.getSourceDBRef());
286
287         Mapping map = new Mapping(ds, mapList);
288         DBRefEntry dbr = new DBRefEntry(getDbSource(), getDbVersion(),
289                 proteinSeq.getName(), map);
290         querySeq.getDatasetSequence().addDBRef(dbr);
291         
292         /*
293          * copy exon features to protein, compute peptide variants from dna 
294          * variants and add as features on the protein sequence ta-da
295          */
296         AlignmentUtils.computeProteinFeatures(querySeq, proteinSeq, mapList);
297       }
298     } catch (Exception e)
299     {
300       System.err
301               .println(String.format("Error retrieving protein for %s: %s",
302                       accId, e.getMessage()));
303     }
304   }
305
306   /**
307    * Get database xrefs from Ensembl, and attach them to the sequence
308    * 
309    * @param seq
310    */
311   protected void getCrossReferences(SequenceI seq)
312   {
313     while (seq.getDatasetSequence() != null)
314     {
315       seq = seq.getDatasetSequence();
316     }
317
318     EnsemblXref xrefFetcher = new EnsemblXref(getDomain());
319     List<DBRefEntry> xrefs = xrefFetcher.getCrossReferences(seq.getName(),
320             getCrossReferenceDatabases());
321     for (DBRefEntry xref : xrefs)
322     {
323       seq.addDBRef(xref);
324       /*
325        * Save any Uniprot xref to be the reference for SIFTS mapping
326        */
327       if (DBRefSource.UNIPROT.equals(xref.getSource()))
328       {
329         seq.setSourceDBRef(xref);
330       }
331     }
332
333     /*
334      * and add a reference to itself
335      */
336     DBRefEntry self = new DBRefEntry(getDbSource(), "0", seq.getName());
337     seq.addDBRef(self);
338   }
339
340   /**
341    * Returns a list of database names to be used when fetching cross-references.
342    * Specifically, the names are used to filter data returned by the Ensembl
343    * xrefs REST service on the value in field 'dbname'.
344    * 
345    * @return
346    */
347   protected List<String> getCrossReferenceDatabases()
348   {
349     return CROSS_REFERENCES;
350   }
351
352   /**
353    * Fetches sequences for the list of accession ids and adds them to the
354    * alignment. Returns the extended (or created) alignment.
355    * 
356    * @param ids
357    * @param alignment
358    * @return
359    * @throws JalviewException
360    * @throws IOException
361    */
362   protected AlignmentI fetchSequences(List<String> ids, AlignmentI alignment)
363           throws JalviewException, IOException
364   {
365     if (!isEnsemblAvailable())
366     {
367       inProgress = false;
368       throw new JalviewException("ENSEMBL Rest API not available.");
369     }
370     FileParse fp = getSequenceReader(ids);
371     FastaFile fr = new FastaFile(fp);
372     if (fr.hasWarningMessage())
373     {
374       System.out.println(String.format(
375               "Warning when retrieving %d ids %s\n%s", ids.size(),
376               ids.toString(), fr.getWarningMessage()));
377     }
378     else if (fr.getSeqs().size() != ids.size())
379     {
380       System.out.println(String.format(
381               "Only retrieved %d sequences for %d query strings", fr
382                       .getSeqs().size(), ids.size()));
383     }
384
385     if (fr.getSeqs().size() == 1 && fr.getSeqs().get(0).getLength() == 0)
386     {
387       /*
388        * POST request has returned an empty FASTA file e.g. for invalid id
389        */
390       throw new IOException("No data returned for " + ids);
391     }
392
393     if (fr.getSeqs().size() > 0)
394     {
395       AlignmentI seqal = new Alignment(
396               fr.getSeqsAsArray());
397       for (SequenceI sq:seqal.getSequences())
398       {
399         if (sq.getDescription() == null)
400         {
401           sq.setDescription(getDbName());
402         }
403         String name = sq.getName();
404         if (ids.contains(name)
405                 || ids.contains(name.replace("ENSP", "ENST")))
406         {
407           DBRefUtils.parseToDbRef(sq, DBRefSource.ENSEMBL, "0", name);
408         }
409       }
410       if (alignment == null)
411       {
412         alignment = seqal;
413       }
414       else
415       {
416         alignment.append(seqal);
417       }
418     }
419     return alignment;
420   }
421
422   /**
423    * Returns the URL for the REST call
424    * 
425    * @return
426    * @throws MalformedURLException
427    */
428   @Override
429   protected URL getUrl(List<String> ids) throws MalformedURLException
430   {
431     /*
432      * a single id is included in the URL path
433      * multiple ids go in the POST body instead
434      */
435     StringBuffer urlstring = new StringBuffer(128);
436     urlstring.append(getDomain() + "/sequence/id");
437     if (ids.size() == 1)
438     {
439       urlstring.append("/").append(ids.get(0));
440     }
441     // @see https://github.com/Ensembl/ensembl-rest/wiki/Output-formats
442     urlstring.append("?type=").append(getSourceEnsemblType().getType());
443     urlstring.append(("&Accept=text/x-fasta"));
444
445     URL url = new URL(urlstring.toString());
446     return url;
447   }
448
449   /**
450    * A sequence/id POST request currently allows up to 50 queries
451    * 
452    * @see http://rest.ensembl.org/documentation/info/sequence_id_post
453    */
454   @Override
455   public int getMaximumQueryCount()
456   {
457     return 50;
458   }
459
460   @Override
461   protected boolean useGetRequest()
462   {
463     return false;
464   }
465
466   @Override
467   protected String getRequestMimeType(boolean multipleIds)
468   {
469     return multipleIds ? "application/json" : "text/x-fasta";
470   }
471
472   @Override
473   protected String getResponseMimeType()
474   {
475     return "text/x-fasta";
476   }
477
478   /**
479    * 
480    * @return the configured sequence return type for this source
481    */
482   protected abstract EnsemblSeqType getSourceEnsemblType();
483
484   /**
485    * Returns a list of [start, end] genomic ranges corresponding to the sequence
486    * being retrieved.
487    * 
488    * The correspondence between the frames of reference is made by locating
489    * those features on the genomic sequence which identify the retrieved
490    * sequence. Specifically
491    * <ul>
492    * <li>genomic sequence is identified by "transcript" features with
493    * ID=transcript:transcriptId</li>
494    * <li>cdna sequence is identified by "exon" features with
495    * Parent=transcript:transcriptId</li>
496    * <li>cds sequence is identified by "CDS" features with
497    * Parent=transcript:transcriptId</li>
498    * </ul>
499    * 
500    * The returned ranges are sorted to run forwards (for positive strand) or
501    * backwards (for negative strand). Aborts and returns null if both positive
502    * and negative strand are found (this should not normally happen).
503    * 
504    * @param sourceSequence
505    * @param accId
506    * @param start
507    *          the start position of the sequence we are mapping to
508    * @return
509    */
510   protected MapList getGenomicRangesFromFeatures(SequenceI sourceSequence,
511           String accId, int start)
512   {
513     SequenceFeature[] sfs = sourceSequence.getSequenceFeatures();
514     if (sfs == null)
515     {
516       return null;
517     }
518
519     /*
520      * generously initial size for number of cds regions
521      * (worst case titin Q8WZ42 has c. 313 exons)
522      */
523     List<int[]> regions = new ArrayList<int[]>(100);
524     int mappedLength = 0;
525     int direction = 1; // forward
526     boolean directionSet = false;
527   
528     for (SequenceFeature sf : sfs)
529     {
530       /*
531        * accept the target feature type or a specialisation of it
532        * (e.g. coding_exon for exon)
533        */
534       if (identifiesSequence(sf, accId))
535       {
536         int strand = sf.getStrand();
537         strand = strand == 0 ? 1 : strand; // treat unknown as forward
538
539         if (directionSet && strand != direction)
540         {
541           // abort - mix of forward and backward
542           System.err.println("Error: forward and backward strand for "
543                   + accId);
544             return null;
545           }
546           direction = strand;
547           directionSet = true;
548   
549           /*
550            * add to CDS ranges, semi-sorted forwards/backwards
551            */
552           if (strand < 0)
553           {
554             regions.add(0, new int[] { sf.getEnd(), sf.getBegin() });
555           }
556           else
557           {
558           regions.add(new int[] { sf.getBegin(), sf.getEnd() });
559         }
560         mappedLength += Math.abs(sf.getEnd() - sf.getBegin() + 1);
561
562         if (!isSpliceable())
563         {
564           /*
565            * 'gene' sequence is contiguous so we can stop as soon as its
566            * identifying feature has been found
567            */
568           break;
569         }
570       }
571     }
572   
573     if (regions.isEmpty())
574     {
575       System.out.println("Failed to identify target sequence for " + accId
576               + " from genomic features");
577       return null;
578     }
579
580     /*
581      * a final sort is needed since Ensembl returns CDS sorted within source
582      * (havana / ensembl_havana)
583      */
584     Collections.sort(regions, new RangeSorter(direction == 1));
585   
586     List<int[]> to = Arrays.asList(new int[] { start,
587         start + mappedLength - 1 });
588   
589     return new MapList(regions, to, 1, 1);
590   }
591
592   /**
593    * Answers true if the sequence being retrieved may occupy discontiguous
594    * regions on the genomic sequence.
595    */
596   protected boolean isSpliceable()
597   {
598     return true;
599   }
600
601   /**
602    * Returns true if the sequence feature marks positions of the genomic
603    * sequence feature which are within the sequence being retrieved. For
604    * example, an 'exon' feature whose parent is the target transcript marks the
605    * cdna positions of the transcript.
606    * 
607    * @param sf
608    * @param accId
609    * @return
610    */
611   protected abstract boolean identifiesSequence(SequenceFeature sf,
612           String accId);
613
614   /**
615    * Transfers the sequence feature to the target sequence, locating its start
616    * and end range based on the mapping. Features which do not overlap the
617    * target sequence are ignored.
618    * 
619    * @param sf
620    * @param targetSequence
621    * @param mapping
622    *          mapping from the sequence feature's coordinates to the target
623    *          sequence
624    * @param forwardStrand
625    */
626   protected void transferFeature(SequenceFeature sf,
627           SequenceI targetSequence, MapList mapping, boolean forwardStrand)
628   {
629     int start = sf.getBegin();
630     int end = sf.getEnd();
631     int[] mappedRange = mapping.locateInTo(start, end);
632   
633     if (mappedRange != null)
634     {
635       SequenceFeature copy = new SequenceFeature(sf);
636       copy.setBegin(Math.min(mappedRange[0], mappedRange[1]));
637       copy.setEnd(Math.max(mappedRange[0], mappedRange[1]));
638       targetSequence.addSequenceFeature(copy);
639
640       /*
641        * for sequence_variant on reverse strand, have to convert the allele
642        * values to their complements
643        */
644       if (!forwardStrand
645               && SequenceOntologyFactory.getInstance().isA(sf.getType(),
646                       SequenceOntologyI.SEQUENCE_VARIANT))
647       {
648         reverseComplementAlleles(copy);
649       }
650     }
651   }
652
653   /**
654    * Change the 'alleles' value of a feature by converting to complementary
655    * bases, and also update the feature description to match
656    * 
657    * @param sf
658    */
659   static void reverseComplementAlleles(SequenceFeature sf)
660   {
661     final String alleles = (String) sf.getValue(ALLELES);
662     if (alleles == null)
663     {
664       return;
665     }
666     StringBuilder complement = new StringBuilder(alleles.length());
667     for (String allele : alleles.split(","))
668     {
669       reverseComplementAllele(complement, allele);
670     }
671     String comp = complement.toString();
672     sf.setValue(ALLELES, comp);
673     sf.setDescription(comp);
674
675     /*
676      * replace value of "alleles=" in sf.ATTRIBUTES as well
677      * so 'output as GFF' shows reverse complement alleles
678      */
679     String atts = sf.getAttributes();
680     if (atts != null)
681     {
682       atts = atts.replace(ALLELES + "=" + alleles, ALLELES + "=" + comp);
683       sf.setAttributes(atts);
684     }
685   }
686
687   /**
688    * Makes the 'reverse complement' of the given allele and appends it to the
689    * buffer, after a comma separator if not the first
690    * 
691    * @param complement
692    * @param allele
693    */
694   static void reverseComplementAllele(StringBuilder complement,
695           String allele)
696   {
697     if (complement.length() > 0)
698     {
699       complement.append(",");
700     }
701     if ("HGMD_MUTATION".equalsIgnoreCase(allele))
702     {
703       complement.append(allele);
704     }
705     else
706     {
707       char[] alleles = allele.toCharArray();
708       for (int i = alleles.length - 1; i >= 0; i--)
709       {
710         complement.append(Dna.getComplement(alleles[i]));
711       }
712     }
713   }
714
715   /**
716    * Transfers features from sourceSequence to targetSequence
717    * 
718    * @param accessionId
719    * @param sourceSequence
720    * @param targetSequence
721    * @return true if any features were transferred, else false
722    */
723   protected boolean transferFeatures(String accessionId,
724           SequenceI sourceSequence, SequenceI targetSequence)
725   {
726     if (sourceSequence == null || targetSequence == null)
727     {
728       return false;
729     }
730
731     // long start = System.currentTimeMillis();
732     SequenceFeature[] sfs = sourceSequence.getSequenceFeatures();
733     MapList mapping = getGenomicRangesFromFeatures(sourceSequence, accessionId,
734             targetSequence.getStart());
735     if (mapping == null)
736     {
737       return false;
738     }
739
740     boolean result = transferFeatures(sfs, targetSequence, mapping,
741             accessionId);
742     // System.out.println("transferFeatures (" + (sfs.length) + " --> "
743     // + targetSequence.getSequenceFeatures().length + ") to "
744     // + targetSequence.getName()
745     // + " took " + (System.currentTimeMillis() - start) + "ms");
746     return result;
747   }
748
749   /**
750    * Transfer features to the target sequence. The start/end positions are
751    * converted using the mapping. Features which do not overlap are ignored.
752    * Features whose parent is not the specified identifier are also ignored.
753    * 
754    * @param features
755    * @param targetSequence
756    * @param mapping
757    * @param parentId
758    * @return
759    */
760   protected boolean transferFeatures(SequenceFeature[] features,
761           SequenceI targetSequence, MapList mapping, String parentId)
762   {
763     final boolean forwardStrand = mapping.isFromForwardStrand();
764
765     /*
766      * sort features by start position (which corresponds to end
767      * position descending if reverse strand) so as to add them in
768      * 'forwards' order to the target sequence
769      */
770     sortFeatures(features, forwardStrand);
771
772     boolean transferred = false;
773     for (SequenceFeature sf : features)
774     {
775       if (retainFeature(sf, parentId))
776       {
777         transferFeature(sf, targetSequence, mapping, forwardStrand);
778         transferred = true;
779       }
780     }
781     return transferred;
782   }
783
784   /**
785    * Sort features by start position ascending (if on forward strand), or end
786    * position descending (if on reverse strand)
787    * 
788    * @param features
789    * @param forwardStrand
790    */
791   protected static void sortFeatures(SequenceFeature[] features,
792           final boolean forwardStrand)
793   {
794     Arrays.sort(features, new Comparator<SequenceFeature>()
795     {
796       @Override
797       public int compare(SequenceFeature o1, SequenceFeature o2)
798       {
799         if (forwardStrand)
800         {
801           return Integer.compare(o1.getBegin(), o2.getBegin());
802         }
803         else
804         {
805           return Integer.compare(o2.getEnd(), o1.getEnd());
806         }
807       }
808     });
809   }
810
811   /**
812    * Answers true if the feature type is one we want to keep for the sequence.
813    * Some features are only retrieved in order to identify the sequence range,
814    * and may then be discarded as redundant information (e.g. "CDS" feature for
815    * a CDS sequence).
816    */
817   @SuppressWarnings("unused")
818   protected boolean retainFeature(SequenceFeature sf, String accessionId)
819   {
820     return true; // override as required
821   }
822
823   /**
824    * Answers true if the feature has a Parent which refers to the given
825    * accession id, or if the feature has no parent. Answers false if the
826    * feature's Parent is for a different accession id.
827    * 
828    * @param sf
829    * @param identifier
830    * @return
831    */
832   protected boolean featureMayBelong(SequenceFeature sf, String identifier)
833   {
834     String parent = (String) sf.getValue(PARENT);
835     // using contains to allow for prefix "gene:", "transcript:" etc
836     if (parent != null && !parent.contains(identifier))
837     {
838       // this genomic feature belongs to a different transcript
839       return false;
840     }
841     return true;
842   }
843
844   @Override
845   public String getDescription()
846   {
847     return "Ensembl " + getSourceEnsemblType().getType()
848             + " sequence with variant features";
849   }
850
851   /**
852    * Returns a (possibly empty) list of features on the sequence which have the
853    * specified sequence ontology type (or a sub-type of it), and the given
854    * identifier as parent
855    * 
856    * @param sequence
857    * @param type
858    * @param parentId
859    * @return
860    */
861   protected List<SequenceFeature> findFeatures(SequenceI sequence,
862           String type, String parentId)
863   {
864     List<SequenceFeature> result = new ArrayList<SequenceFeature>();
865     
866     SequenceFeature[] sfs = sequence.getSequenceFeatures();
867     if (sfs != null) {
868       SequenceOntologyI so = SequenceOntologyFactory.getInstance();
869       for (SequenceFeature sf :sfs) {
870         if (so.isA(sf.getType(), type))
871         {
872           String parent = (String) sf.getValue(PARENT);
873           if (parent.equals(parentId))
874           {
875             result.add(sf);
876           }
877         }
878       }
879     }
880     return result;
881   }
882
883   /**
884    * Answers true if the feature type is either 'NMD_transcript_variant' or
885    * 'transcript' or one of its sub-types in the Sequence Ontology. This is
886    * needed because NMD_transcript_variant behaves like 'transcript' in Ensembl
887    * although strictly speaking it is not (it is a sub-type of
888    * sequence_variant).
889    * 
890    * @param featureType
891    * @return
892    */
893   public static boolean isTranscript(String featureType)
894   {
895     return SequenceOntologyI.NMD_TRANSCRIPT_VARIANT.equals(featureType)
896             || SequenceOntologyFactory.getInstance().isA(featureType,
897                     SequenceOntologyI.TRANSCRIPT);
898   }
899 }