JAL-1705 xref ENST to correct (ENSP) accession id
[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   /**
335    * Returns a list of database names to be used when fetching cross-references.
336    * Specifically, the names are used to filter data returned by the Ensembl
337    * xrefs REST service on the value in field 'dbname'.
338    * 
339    * @return
340    */
341   protected List<String> getCrossReferenceDatabases()
342   {
343     return CROSS_REFERENCES;
344   }
345
346   /**
347    * Fetches sequences for the list of accession ids and adds them to the
348    * alignment. Returns the extended (or created) alignment.
349    * 
350    * @param ids
351    * @param alignment
352    * @return
353    * @throws JalviewException
354    * @throws IOException
355    */
356   protected AlignmentI fetchSequences(List<String> ids, AlignmentI alignment)
357           throws JalviewException, IOException
358   {
359     if (!isEnsemblAvailable())
360     {
361       inProgress = false;
362       throw new JalviewException("ENSEMBL Rest API not available.");
363     }
364     FileParse fp = getSequenceReader(ids);
365     FastaFile fr = new FastaFile(fp);
366     if (fr.hasWarningMessage())
367     {
368       System.out.println(String.format(
369               "Warning when retrieving %d ids %s\n%s", ids.size(),
370               ids.toString(), fr.getWarningMessage()));
371     }
372     else if (fr.getSeqs().size() != ids.size())
373     {
374       System.out.println(String.format(
375               "Only retrieved %d sequences for %d query strings", fr
376                       .getSeqs().size(), ids.size()));
377     }
378
379     if (fr.getSeqs().size() == 1 && fr.getSeqs().get(0).getLength() == 0)
380     {
381       /*
382        * POST request has returned an empty FASTA file e.g. for invalid id
383        */
384       throw new IOException("No data returned for " + ids);
385     }
386
387     if (fr.getSeqs().size() > 0)
388     {
389       AlignmentI seqal = new Alignment(
390               fr.getSeqsAsArray());
391       for (SequenceI sq:seqal.getSequences())
392       {
393         if (sq.getDescription() == null)
394         {
395           sq.setDescription(getDbName());
396         }
397         String name = sq.getName();
398         if (ids.contains(name)
399                 || ids.contains(name.replace("ENSP", "ENST")))
400         {
401           DBRefUtils.parseToDbRef(sq, DBRefSource.ENSEMBL, "0", name);
402         }
403       }
404       if (alignment == null)
405       {
406         alignment = seqal;
407       }
408       else
409       {
410         alignment.append(seqal);
411       }
412     }
413     return alignment;
414   }
415
416   /**
417    * Returns the URL for the REST call
418    * 
419    * @return
420    * @throws MalformedURLException
421    */
422   @Override
423   protected URL getUrl(List<String> ids) throws MalformedURLException
424   {
425     /*
426      * a single id is included in the URL path
427      * multiple ids go in the POST body instead
428      */
429     StringBuffer urlstring = new StringBuffer(128);
430     urlstring.append(getDomain() + "/sequence/id");
431     if (ids.size() == 1)
432     {
433       urlstring.append("/").append(ids.get(0));
434     }
435     // @see https://github.com/Ensembl/ensembl-rest/wiki/Output-formats
436     urlstring.append("?type=").append(getSourceEnsemblType().getType());
437     urlstring.append(("&Accept=text/x-fasta"));
438
439     URL url = new URL(urlstring.toString());
440     return url;
441   }
442
443   /**
444    * A sequence/id POST request currently allows up to 50 queries
445    * 
446    * @see http://rest.ensembl.org/documentation/info/sequence_id_post
447    */
448   @Override
449   public int getMaximumQueryCount()
450   {
451     return 50;
452   }
453
454   @Override
455   protected boolean useGetRequest()
456   {
457     return false;
458   }
459
460   @Override
461   protected String getRequestMimeType(boolean multipleIds)
462   {
463     return multipleIds ? "application/json" : "text/x-fasta";
464   }
465
466   @Override
467   protected String getResponseMimeType()
468   {
469     return "text/x-fasta";
470   }
471
472   /**
473    * 
474    * @return the configured sequence return type for this source
475    */
476   protected abstract EnsemblSeqType getSourceEnsemblType();
477
478   /**
479    * Returns a list of [start, end] genomic ranges corresponding to the sequence
480    * being retrieved.
481    * 
482    * The correspondence between the frames of reference is made by locating
483    * those features on the genomic sequence which identify the retrieved
484    * sequence. Specifically
485    * <ul>
486    * <li>genomic sequence is identified by "transcript" features with
487    * ID=transcript:transcriptId</li>
488    * <li>cdna sequence is identified by "exon" features with
489    * Parent=transcript:transcriptId</li>
490    * <li>cds sequence is identified by "CDS" features with
491    * Parent=transcript:transcriptId</li>
492    * </ul>
493    * 
494    * The returned ranges are sorted to run forwards (for positive strand) or
495    * backwards (for negative strand). Aborts and returns null if both positive
496    * and negative strand are found (this should not normally happen).
497    * 
498    * @param sourceSequence
499    * @param accId
500    * @param start
501    *          the start position of the sequence we are mapping to
502    * @return
503    */
504   protected MapList getGenomicRangesFromFeatures(SequenceI sourceSequence,
505           String accId, int start)
506   {
507     SequenceFeature[] sfs = sourceSequence.getSequenceFeatures();
508     if (sfs == null)
509     {
510       return null;
511     }
512
513     /*
514      * generously initial size for number of cds regions
515      * (worst case titin Q8WZ42 has c. 313 exons)
516      */
517     List<int[]> regions = new ArrayList<int[]>(100);
518     int mappedLength = 0;
519     int direction = 1; // forward
520     boolean directionSet = false;
521   
522     for (SequenceFeature sf : sfs)
523     {
524       /*
525        * accept the target feature type or a specialisation of it
526        * (e.g. coding_exon for exon)
527        */
528       if (identifiesSequence(sf, accId))
529       {
530         int strand = sf.getStrand();
531         strand = strand == 0 ? 1 : strand; // treat unknown as forward
532
533         if (directionSet && strand != direction)
534         {
535           // abort - mix of forward and backward
536           System.err.println("Error: forward and backward strand for "
537                   + accId);
538             return null;
539           }
540           direction = strand;
541           directionSet = true;
542   
543           /*
544            * add to CDS ranges, semi-sorted forwards/backwards
545            */
546           if (strand < 0)
547           {
548             regions.add(0, new int[] { sf.getEnd(), sf.getBegin() });
549           }
550           else
551           {
552           regions.add(new int[] { sf.getBegin(), sf.getEnd() });
553         }
554         mappedLength += Math.abs(sf.getEnd() - sf.getBegin() + 1);
555
556         if (!isSpliceable())
557         {
558           /*
559            * 'gene' sequence is contiguous so we can stop as soon as its
560            * identifying feature has been found
561            */
562           break;
563         }
564       }
565     }
566   
567     if (regions.isEmpty())
568     {
569       System.out.println("Failed to identify target sequence for " + accId
570               + " from genomic features");
571       return null;
572     }
573
574     /*
575      * a final sort is needed since Ensembl returns CDS sorted within source
576      * (havana / ensembl_havana)
577      */
578     Collections.sort(regions, new RangeSorter(direction == 1));
579   
580     List<int[]> to = Arrays.asList(new int[] { start,
581         start + mappedLength - 1 });
582   
583     return new MapList(regions, to, 1, 1);
584   }
585
586   /**
587    * Answers true if the sequence being retrieved may occupy discontiguous
588    * regions on the genomic sequence.
589    */
590   protected boolean isSpliceable()
591   {
592     return true;
593   }
594
595   /**
596    * Returns true if the sequence feature marks positions of the genomic
597    * sequence feature which are within the sequence being retrieved. For
598    * example, an 'exon' feature whose parent is the target transcript marks the
599    * cdna positions of the transcript.
600    * 
601    * @param sf
602    * @param accId
603    * @return
604    */
605   protected abstract boolean identifiesSequence(SequenceFeature sf,
606           String accId);
607
608   /**
609    * Transfers the sequence feature to the target sequence, locating its start
610    * and end range based on the mapping. Features which do not overlap the
611    * target sequence are ignored.
612    * 
613    * @param sf
614    * @param targetSequence
615    * @param mapping
616    *          mapping from the sequence feature's coordinates to the target
617    *          sequence
618    * @param forwardStrand
619    */
620   protected void transferFeature(SequenceFeature sf,
621           SequenceI targetSequence, MapList mapping, boolean forwardStrand)
622   {
623     int start = sf.getBegin();
624     int end = sf.getEnd();
625     int[] mappedRange = mapping.locateInTo(start, end);
626   
627     if (mappedRange != null)
628     {
629       SequenceFeature copy = new SequenceFeature(sf);
630       copy.setBegin(Math.min(mappedRange[0], mappedRange[1]));
631       copy.setEnd(Math.max(mappedRange[0], mappedRange[1]));
632       targetSequence.addSequenceFeature(copy);
633
634       /*
635        * for sequence_variant on reverse strand, have to convert the allele
636        * values to their complements
637        */
638       if (!forwardStrand
639               && SequenceOntologyFactory.getInstance().isA(sf.getType(),
640                       SequenceOntologyI.SEQUENCE_VARIANT))
641       {
642         reverseComplementAlleles(copy);
643       }
644     }
645   }
646
647   /**
648    * Change the 'alleles' value of a feature by converting to complementary
649    * bases, and also update the feature description to match
650    * 
651    * @param sf
652    */
653   static void reverseComplementAlleles(SequenceFeature sf)
654   {
655     final String alleles = (String) sf.getValue(ALLELES);
656     if (alleles == null)
657     {
658       return;
659     }
660     StringBuilder complement = new StringBuilder(alleles.length());
661     for (String allele : alleles.split(","))
662     {
663       reverseComplementAllele(complement, allele);
664     }
665     String comp = complement.toString();
666     sf.setValue(ALLELES, comp);
667     sf.setDescription(comp);
668
669     /*
670      * replace value of "alleles=" in sf.ATTRIBUTES as well
671      * so 'output as GFF' shows reverse complement alleles
672      */
673     String atts = sf.getAttributes();
674     if (atts != null)
675     {
676       atts = atts.replace(ALLELES + "=" + alleles, ALLELES + "=" + comp);
677       sf.setAttributes(atts);
678     }
679   }
680
681   /**
682    * Makes the 'reverse complement' of the given allele and appends it to the
683    * buffer, after a comma separator if not the first
684    * 
685    * @param complement
686    * @param allele
687    */
688   static void reverseComplementAllele(StringBuilder complement,
689           String allele)
690   {
691     if (complement.length() > 0)
692     {
693       complement.append(",");
694     }
695     if ("HGMD_MUTATION".equalsIgnoreCase(allele))
696     {
697       complement.append(allele);
698     }
699     else
700     {
701       char[] alleles = allele.toCharArray();
702       for (int i = alleles.length - 1; i >= 0; i--)
703       {
704         complement.append(Dna.getComplement(alleles[i]));
705       }
706     }
707   }
708
709   /**
710    * Transfers features from sourceSequence to targetSequence
711    * 
712    * @param accessionId
713    * @param sourceSequence
714    * @param targetSequence
715    * @return true if any features were transferred, else false
716    */
717   protected boolean transferFeatures(String accessionId,
718           SequenceI sourceSequence, SequenceI targetSequence)
719   {
720     if (sourceSequence == null || targetSequence == null)
721     {
722       return false;
723     }
724
725     // long start = System.currentTimeMillis();
726     SequenceFeature[] sfs = sourceSequence.getSequenceFeatures();
727     MapList mapping = getGenomicRangesFromFeatures(sourceSequence, accessionId,
728             targetSequence.getStart());
729     if (mapping == null)
730     {
731       return false;
732     }
733
734     boolean result = transferFeatures(sfs, targetSequence, mapping,
735             accessionId);
736     // System.out.println("transferFeatures (" + (sfs.length) + " --> "
737     // + targetSequence.getSequenceFeatures().length + ") to "
738     // + targetSequence.getName()
739     // + " took " + (System.currentTimeMillis() - start) + "ms");
740     return result;
741   }
742
743   /**
744    * Transfer features to the target sequence. The start/end positions are
745    * converted using the mapping. Features which do not overlap are ignored.
746    * Features whose parent is not the specified identifier are also ignored.
747    * 
748    * @param features
749    * @param targetSequence
750    * @param mapping
751    * @param parentId
752    * @return
753    */
754   protected boolean transferFeatures(SequenceFeature[] features,
755           SequenceI targetSequence, MapList mapping, String parentId)
756   {
757     final boolean forwardStrand = mapping.isFromForwardStrand();
758
759     /*
760      * sort features by start position (which corresponds to end
761      * position descending if reverse strand) so as to add them in
762      * 'forwards' order to the target sequence
763      */
764     sortFeatures(features, forwardStrand);
765
766     boolean transferred = false;
767     for (SequenceFeature sf : features)
768     {
769       if (retainFeature(sf, parentId))
770       {
771         transferFeature(sf, targetSequence, mapping, forwardStrand);
772         transferred = true;
773       }
774     }
775     return transferred;
776   }
777
778   /**
779    * Sort features by start position ascending (if on forward strand), or end
780    * position descending (if on reverse strand)
781    * 
782    * @param features
783    * @param forwardStrand
784    */
785   protected static void sortFeatures(SequenceFeature[] features,
786           final boolean forwardStrand)
787   {
788     Arrays.sort(features, new Comparator<SequenceFeature>()
789     {
790       @Override
791       public int compare(SequenceFeature o1, SequenceFeature o2)
792       {
793         if (forwardStrand)
794         {
795           return Integer.compare(o1.getBegin(), o2.getBegin());
796         }
797         else
798         {
799           return Integer.compare(o2.getEnd(), o1.getEnd());
800         }
801       }
802     });
803   }
804
805   /**
806    * Answers true if the feature type is one we want to keep for the sequence.
807    * Some features are only retrieved in order to identify the sequence range,
808    * and may then be discarded as redundant information (e.g. "CDS" feature for
809    * a CDS sequence).
810    */
811   @SuppressWarnings("unused")
812   protected boolean retainFeature(SequenceFeature sf, String accessionId)
813   {
814     return true; // override as required
815   }
816
817   /**
818    * Answers true if the feature has a Parent which refers to the given
819    * accession id, or if the feature has no parent. Answers false if the
820    * feature's Parent is for a different accession id.
821    * 
822    * @param sf
823    * @param identifier
824    * @return
825    */
826   protected boolean featureMayBelong(SequenceFeature sf, String identifier)
827   {
828     String parent = (String) sf.getValue(PARENT);
829     // using contains to allow for prefix "gene:", "transcript:" etc
830     if (parent != null && !parent.contains(identifier))
831     {
832       // this genomic feature belongs to a different transcript
833       return false;
834     }
835     return true;
836   }
837
838   @Override
839   public String getDescription()
840   {
841     return "Ensembl " + getSourceEnsemblType().getType()
842             + " sequence with variant features";
843   }
844
845   /**
846    * Returns a (possibly empty) list of features on the sequence which have the
847    * specified sequence ontology type (or a sub-type of it), and the given
848    * identifier as parent
849    * 
850    * @param sequence
851    * @param type
852    * @param parentId
853    * @return
854    */
855   protected List<SequenceFeature> findFeatures(SequenceI sequence,
856           String type, String parentId)
857   {
858     List<SequenceFeature> result = new ArrayList<SequenceFeature>();
859     
860     SequenceFeature[] sfs = sequence.getSequenceFeatures();
861     if (sfs != null) {
862       SequenceOntologyI so = SequenceOntologyFactory.getInstance();
863       for (SequenceFeature sf :sfs) {
864         if (so.isA(sf.getType(), type))
865         {
866           String parent = (String) sf.getValue(PARENT);
867           if (parent.equals(parentId))
868           {
869             result.add(sf);
870           }
871         }
872       }
873     }
874     return result;
875   }
876
877   /**
878    * Answers true if the feature type is either 'NMD_transcript_variant' or
879    * 'transcript' or one of its sub-types in the Sequence Ontology. This is
880    * needed because NMD_transcript_variant behaves like 'transcript' in Ensembl
881    * although strictly speaking it is not (it is a sub-type of
882    * sequence_variant).
883    * 
884    * @param featureType
885    * @return
886    */
887   public static boolean isTranscript(String featureType)
888   {
889     return SequenceOntologyI.NMD_TRANSCRIPT_VARIANT.equals(featureType)
890             || SequenceOntologyFactory.getInstance().isA(featureType,
891                     SequenceOntologyI.TRANSCRIPT);
892   }
893 }