JAL-3397 impl.IntervalStore and nonc.IntervalStore unified api
[jalview.git] / src / jalview / ext / ensembl / EnsemblFeatures.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 package jalview.ext.ensembl;
22
23 import jalview.datamodel.Alignment;
24 import jalview.datamodel.AlignmentI;
25 import jalview.datamodel.Sequence;
26 import jalview.datamodel.SequenceFeature;
27 import jalview.datamodel.SequenceI;
28 import jalview.io.gff.SequenceOntologyI;
29 import jalview.util.JSONUtils;
30
31 import java.io.IOException;
32 import java.net.MalformedURLException;
33 import java.net.URL;
34 import java.util.ArrayList;
35 import java.util.Iterator;
36 import java.util.List;
37 import java.util.Map;
38
39 import org.json.simple.parser.ParseException;
40
41 /**
42  * A client for fetching and processing Ensembl feature data in GFF format by
43  * calling the overlap REST service
44  * 
45  * @author gmcarstairs
46  * @see http://rest.ensembl.org/documentation/info/overlap_id
47  */
48 class EnsemblFeatures extends EnsemblRestClient
49 {
50   /*
51    * The default features to retrieve from Ensembl
52    * can override in getSequenceRecords parameter
53    */
54   private EnsemblFeatureType[] featuresWanted = { EnsemblFeatureType.cds,
55       EnsemblFeatureType.exon, EnsemblFeatureType.variation };
56
57   /**
58    * Default constructor (to use rest.ensembl.org)
59    */
60   public EnsemblFeatures()
61   {
62     super();
63   }
64
65   /**
66    * Constructor given the target domain to fetch data from
67    * 
68    * @param d
69    */
70   public EnsemblFeatures(String d)
71   {
72     super(d);
73   }
74
75   @Override
76   public String getDbName()
77   {
78     return "ENSEMBL (features)";
79   }
80
81   /**
82    * Makes a query to the REST overlap endpoint for the given sequence
83    * identifier. This returns an 'alignment' consisting of one 'dummy sequence'
84    * (the genomic sequence for which overlap features are returned by the
85    * service). This sequence will have on it sequence features which are the
86    * real information of interest, such as CDS regions or sequence variations.
87    */
88   @Override
89   public AlignmentI getSequenceRecords(String query) throws IOException
90   {
91     // TODO: use a vararg String... for getSequenceRecords instead?
92           
93     List<String> queries = new ArrayList<>();
94     queries.add(query);
95     SequenceI seq = parseFeaturesJson(queries);
96     return new Alignment(new SequenceI[] { seq });
97   }
98
99   /**
100    * Parses the JSON response into Jalview sequence features and attaches them
101    * to a dummy sequence
102    * 
103    * @param br
104    * @return
105    */
106   @SuppressWarnings("unchecked")
107   private SequenceI parseFeaturesJson(List<String> queries)
108   {
109     SequenceI seq = new Sequence("Dummy", "");
110     try
111     {
112       Iterator<Object> rvals = (Iterator<Object>) getJSON(null, queries, -1, MODE_ITERATOR, null);
113       if (rvals == null)
114       {
115           return null;
116       }
117       while (rvals.hasNext())
118       {           
119         try
120         {
121           Map<String, Object> obj = (Map<String, Object>) rvals.next();
122           String type = obj.get("feature_type").toString();
123           int start = Integer.parseInt(obj.get("start").toString());
124           int end = Integer.parseInt(obj.get("end").toString());
125           String source = obj.get("source").toString();
126           String strand = obj.get("strand").toString();
127           String alleles = JSONUtils
128                   .arrayToStringList((List<Object>) obj.get("alleles"));
129           String clinSig = JSONUtils
130                   .arrayToStringList(
131                           (List<Object>) obj.get("clinical_significance"));
132
133           /*
134            * convert 'variation' to 'sequence_variant', and 'cds' to 'CDS'
135            * so as to have a valid SO term for the feature type
136            * ('gene', 'exon', 'transcript' don't need any conversion)
137            */
138           if ("variation".equals(type))
139           {
140             type = SequenceOntologyI.SEQUENCE_VARIANT;
141           }
142           else if (SequenceOntologyI.CDS.equalsIgnoreCase((type)))
143           {
144             type = SequenceOntologyI.CDS;
145           }
146
147           String desc = getFirstNotNull(obj, "alleles", "external_name",
148                   JSON_ID);
149           SequenceFeature sf = new SequenceFeature(type, desc, start, end,
150                   source);
151           sf.setStrand("1".equals(strand) ? "+" : "-");
152           setFeatureAttribute(sf, obj, "id");
153           setFeatureAttribute(sf, obj, "Parent");
154           setFeatureAttribute(sf, obj, "consequence_type");
155           sf.setValue("alleles", alleles);
156           sf.setValue("clinical_significance", clinSig);
157
158           seq.addSequenceFeature(sf);
159           
160         } catch (Throwable t)
161         {
162           // ignore - keep trying other features
163         }
164       }
165     } catch (ParseException | IOException e)
166     {
167         e.printStackTrace();
168       // ignore
169     }
170
171     return seq;
172   }
173
174   /**
175    * Returns the first non-null attribute found (if any) as a string, formatted
176    * suitably for display as feature description or tooltip. Answers null if
177    * none of the attribute keys is present.
178    * 
179    * @param obj
180    * @param keys
181    * @return
182    */
183   @SuppressWarnings("unchecked")
184   protected String getFirstNotNull(Map<String, Object> obj, String... keys)
185   {
186     for (String key : keys)
187     {
188       Object val = obj.get(key);
189       if (val != null)
190       {
191         String s = val instanceof List<?>
192                 ? JSONUtils.arrayToStringList((List<Object>) val)
193                 : val.toString();
194         if (!s.isEmpty())
195         {
196           return s;
197         }
198       }
199     }
200     return null;
201   }
202
203   /**
204    * A helper method that reads the 'key' entry in the JSON object, and if not
205    * null, sets its string value as an attribute on the sequence feature
206    * 
207    * @param sf
208    * @param obj
209    * @param key
210    */
211   protected void setFeatureAttribute(SequenceFeature sf, Map<String, Object> obj,
212           String key)
213   {
214     Object object = obj.get(key);
215     if (object != null)
216     {
217       sf.setValue(key, object.toString());
218     }
219   }
220
221   /**
222    * Returns a URL for the REST overlap endpoint
223    * 
224    * @param ids
225    * @return
226    */
227   @Override
228   protected URL getUrl(List<String> ids) throws MalformedURLException
229   {
230     StringBuffer urlstring = new StringBuffer(128);
231     urlstring.append(getDomain()).append("/overlap/id/").append(ids.get(0));
232
233     // @see https://github.com/Ensembl/ensembl-rest/wiki/Output-formats
234     urlstring.append("?content-type=" + getResponseMimeType());
235
236     /*
237      * specify object_type=gene in case is shared by transcript and/or protein;
238      * currently only fetching features for gene sequences;
239      * refactor in future if needed to fetch for transcripts
240      */
241     urlstring.append("&").append(OBJECT_TYPE).append("=")
242             .append(OBJECT_TYPE_GENE);
243
244     /*
245      * specify  features to retrieve
246      * @see http://rest.ensembl.org/documentation/info/overlap_id
247      * could make the list a configurable entry in .jalview_properties
248      */
249     for (EnsemblFeatureType feature : featuresWanted)
250     {
251       urlstring.append("&feature=").append(feature.name());
252     }
253
254     return new URL(urlstring.toString());
255   }
256
257   @Override
258   protected boolean useGetRequest()
259   {
260     return true;
261   }
262
263   /**
264    * Returns the MIME type for GFF3. For GET requests the Content-type header
265    * describes the required encoding of the response.
266    */
267   @Override
268   protected String getRequestMimeType()
269   {
270     return "application/json";
271   }
272
273   /**
274    * Returns the MIME type wanted for the response
275    */
276   @Override
277   protected String getResponseMimeType()
278   {
279     return "application/json";
280   }
281
282   /**
283    * Overloaded method that allows a list of features to retrieve to be
284    * specified
285    * 
286    * @param accId
287    * @param features
288    * @return
289    * @throws IOException
290    */
291   protected AlignmentI getSequenceRecords(String accId,
292           EnsemblFeatureType[] features) throws IOException
293   {
294     featuresWanted = features;
295     return getSequenceRecords(accId);
296   }
297 }