JAL-3730 retry ping before failing; sysout logging changed to Cache.log
[jalview.git] / src / jalview / ext / ensembl / EnsemblMap.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 java.io.IOException;
24 import java.net.MalformedURLException;
25 import java.net.URL;
26 import java.util.ArrayList;
27 import java.util.Collections;
28 import java.util.Iterator;
29 import java.util.List;
30 import java.util.Map;
31
32 import org.json.simple.parser.ParseException;
33
34 import jalview.bin.Cache;
35 import jalview.datamodel.AlignmentI;
36 import jalview.datamodel.DBRefSource;
37 import jalview.datamodel.GeneLociI;
38 import jalview.datamodel.GeneLocus;
39 import jalview.datamodel.Mapping;
40 import jalview.util.MapList;
41
42 /**
43  * A client for the Ensembl REST service /map endpoint, to convert from
44  * coordinates of one genome assembly to another.
45  * <p>
46  * Note that species and assembly identifiers passed to this class must be valid
47  * in Ensembl. They are not case sensitive.
48  * 
49  * @author gmcarstairs
50  * @see https://rest.ensembl.org/documentation/info/assembly_map
51  * @see https://rest.ensembl.org/info/assembly/human?content-type=text/xml
52  * @see https://rest.ensembl.org/info/species?content-type=text/xml
53  */
54 public class EnsemblMap extends EnsemblRestClient
55 {
56   private static final String MAPPED = "mapped";
57
58   private static final String MAPPINGS = "mappings";
59
60   private static final String CDS = "cds";
61
62   private static final String CDNA = "cdna";
63
64   /**
65    * Default constructor (to use rest.ensembl.org)
66    */
67   public EnsemblMap()
68   {
69     super();
70   }
71
72   /**
73    * Constructor given the target domain to fetch data from
74    * 
75    * @param
76    */
77   public EnsemblMap(String domain)
78   {
79     super(domain);
80   }
81
82   @Override
83   public String getDbName()
84   {
85     return DBRefSource.ENSEMBL;
86   }
87
88   @Override
89   public AlignmentI getSequenceRecords(String queries) throws Exception
90   {
91     return null; // not used
92   }
93
94   /**
95    * Constructs a URL of the format <code>
96    * http://rest.ensembl.org/map/human/GRCh38/17:45051610..45109016:1/GRCh37?content-type=application/json
97    * </code>
98    * 
99    * @param species
100    * @param chromosome
101    * @param fromRef
102    * @param toRef
103    * @param startPos
104    * @param endPos
105    * @return
106    * @throws MalformedURLException
107    */
108   protected URL getAssemblyMapUrl(String species, String chromosome, String fromRef,
109           String toRef, int startPos, int endPos)
110           throws MalformedURLException
111   {
112     /*
113      * start-end might be reverse strand - present forwards to the service
114      */
115     boolean forward = startPos <= endPos;
116     int start = forward ? startPos : endPos;
117     int end = forward ? endPos : startPos;
118     String strand = forward ? "1" : "-1";
119     String url = String.format(
120             "%s/map/%s/%s/%s:%d..%d:%s/%s?content-type=application/json",
121             getDomain(), species, fromRef, chromosome, start, end, strand,
122             toRef);
123     return new URL(url);
124   }
125
126   @Override
127   protected boolean useGetRequest()
128   {
129     return true;
130   }
131
132   @Override
133   protected URL getUrl(List<String> ids) throws MalformedURLException
134   {
135     return null; // not used
136   }
137
138   /**
139    * Calls the REST /map service to get the chromosomal coordinates (start/end)
140    * in 'toRef' that corresponding to the (start/end) queryRange in 'fromRef'
141    * 
142    * @param species
143    * @param chromosome
144    * @param fromRef
145    * @param toRef
146    * @param queryRange
147    * @return
148    * @see http://rest.ensemblgenomes.org/documentation/info/assembly_map
149    */
150   public int[] getAssemblyMapping(String species, String chromosome,
151           String fromRef, String toRef, int[] queryRange)
152   {
153     URL url = null;
154     try
155     {
156       url = getAssemblyMapUrl(species, chromosome, fromRef, toRef, queryRange[0],
157               queryRange[1]);
158       return (parseAssemblyMappingResponse(url));
159     } catch (Throwable t)
160     {
161       Cache.log.error("Error calling " + url + ": " + t.getMessage());
162       return null;
163     }
164   }
165
166   /**
167    * Parses the JSON response from the /map/&lt;species&gt;/ REST service. The
168    * format is (with some fields omitted)
169    * 
170    * <pre>
171    *  {"mappings": 
172    *    [{
173    *       "original": {"end":45109016,"start":45051610},
174    *       "mapped"  : {"end":43186384,"start":43128978} 
175    *  }] }
176    * </pre>
177    * 
178    * @param br
179    * @return
180    */
181   @SuppressWarnings("unchecked")
182   protected int[] parseAssemblyMappingResponse(URL url)
183   {
184     int[] result = null;
185
186     try
187     {
188       Iterator<Object> rvals = (Iterator<Object>) getJSON(url, null, -1, MODE_ITERATOR, MAPPINGS);
189       if (rvals == null)
190       {
191         return null;
192       }
193       while (rvals.hasNext())
194       {
195         // todo check for "mapped"
196         Map<String, Object> val = (Map<String, Object>) rvals.next();
197         Map<String, Object> mapped = (Map<String, Object>) val.get(MAPPED);
198         int start = Integer.parseInt(mapped.get("start").toString());
199         int end = Integer.parseInt(mapped.get("end").toString());
200         String strand = mapped.get("strand").toString();
201         if ("1".equals(strand))
202         {
203           result = new int[] { start, end };
204         }
205         else
206         {
207           result = new int[] { end, start };
208         }
209       }
210     } catch (IOException | ParseException | NumberFormatException e)
211     {
212       // ignore
213     }
214     return result;
215   }
216
217   /**
218    * Calls the REST /map/cds/id service, and returns a DBRefEntry holding the
219    * returned chromosomal coordinates, or returns null if the call fails
220    * 
221    * @param division
222    *          e.g. Ensembl, EnsemblMetazoa
223    * @param accession
224    *          e.g. ENST00000592782, Y55B1AR.1.1
225    * @param start
226    * @param end
227    * @return
228    */
229   public GeneLociI getCdsMapping(String division, String accession,
230           int start, int end)
231   {
232     return getIdMapping(division, accession, start, end, CDS);
233   }
234
235   /**
236    * Calls the REST /map/cdna/id service, and returns a DBRefEntry holding the
237    * returned chromosomal coordinates, or returns null if the call fails
238    * 
239    * @param division
240    *          e.g. Ensembl, EnsemblMetazoa
241    * @param accession
242    *          e.g. ENST00000592782, Y55B1AR.1.1
243    * @param start
244    * @param end
245    * @return
246    */
247   public GeneLociI getCdnaMapping(String division, String accession,
248           int start, int end)
249   {
250     return getIdMapping(division, accession, start, end, CDNA);
251   }
252
253   GeneLociI getIdMapping(String division, String accession, int start,
254           int end, String cdsOrCdna)
255   {
256     URL url = null;
257     try
258     {
259       String domain = new EnsemblInfo().getDomain(division);
260       if (domain != null)
261       {
262         url = getIdMapUrl(domain, accession, start, end, cdsOrCdna);
263         return (parseIdMappingResponse(url, accession, domain));
264       }
265       return null;
266     } catch (Throwable t)
267     {
268       Cache.log.error("Error calling " + url + ": " + t.getMessage());
269       return null;
270     }
271   }
272
273   /**
274    * Constructs a URL to the /map/cds/<id> or /map/cdna/<id> REST service. The
275    * REST call is to either ensembl or ensemblgenomes, as determined from the
276    * division, e.g. Ensembl or EnsemblProtists.
277    * 
278    * @param domain
279    * @param accession
280    * @param start
281    * @param end
282    * @param cdsOrCdna
283    * @return
284    * @throws MalformedURLException
285    */
286   URL getIdMapUrl(String domain, String accession, int start, int end,
287           String cdsOrCdna) throws MalformedURLException
288   {
289     String url = String
290             .format("%s/map/%s/%s/%d..%d?include_original_region=1&content-type=application/json",
291                     domain, cdsOrCdna, accession, start, end);
292     return new URL(url);
293   }
294
295   /**
296    * Parses the JSON response from the /map/cds/ or /map/cdna REST service. The
297    * format is
298    * 
299    * <pre>
300    * {"mappings":
301    *   [
302    *    {"assembly_name":"TAIR10","end":2501311,"seq_region_name":"1","gap":0,
303    *     "strand":-1,"coord_system":"chromosome","rank":0,"start":2501114},
304    *    {"assembly_name":"TAIR10","end":2500815,"seq_region_name":"1","gap":0,
305    *     "strand":-1,"coord_system":"chromosome","rank":0,"start":2500714}
306    *   ]
307    * }
308    * </pre>
309    * 
310    * @param br
311    * @param accession
312    * @param domain
313    * @return
314    */
315   @SuppressWarnings("unchecked")
316 GeneLociI parseIdMappingResponse(URL url, String accession,
317           String domain)
318   {
319
320     try
321     {
322       Iterator<Object> rvals = (Iterator<Object>) getJSON(url, null, -1, MODE_ITERATOR, MAPPINGS);
323       if (rvals == null)
324       {
325         return null;
326       }
327       String assembly = null;
328       String chromosome = null;
329       int fromEnd = 0;
330       List<int[]> regions = new ArrayList<>();
331
332       while (rvals.hasNext())
333       {
334         Map<String, Object> val = (Map<String, Object>) rvals.next();
335         Map<String, Object> original = (Map<String, Object>) val.get("original");
336         fromEnd = Integer.parseInt(original.get("end").toString());
337
338         Map<String, Object> mapped = (Map<String, Object>) val.get(MAPPED);
339         int start = Integer.parseInt(mapped.get("start").toString());
340         int end = Integer.parseInt(mapped.get("end").toString());
341         String ass = mapped.get("assembly_name").toString();
342         if (assembly != null && !assembly.equals(ass))
343         {
344           System.err
345                   .println("EnsemblMap found multiple assemblies - can't resolve");
346           return null;
347         }
348         assembly = ass;
349         String chr = mapped.get("seq_region_name").toString();
350         if (chromosome != null && !chromosome.equals(chr))
351         {
352           System.err
353                   .println("EnsemblMap found multiple chromosomes - can't resolve");
354           return null;
355         }
356         chromosome = chr;
357         String strand = mapped.get("strand").toString();
358         if ("-1".equals(strand))
359         {
360           regions.add(new int[] { end, start });
361         }
362         else
363         {
364           regions.add(new int[] { start, end });
365         }
366       }
367
368       /*
369        * processed all mapped regions on chromosome, assemble the result,
370        * having first fetched the species id for the accession
371        */
372       final String species = new EnsemblLookup(domain)
373               .getSpecies(accession);
374       final String as = assembly;
375       final String chr = chromosome;
376       List<int[]> fromRange = Collections.singletonList(new int[] { 1,
377           fromEnd });
378       Mapping mapping = new Mapping(new MapList(fromRange, regions, 1, 1));
379       return new GeneLocus(species == null ? "" : species, as, chr,
380               mapping);
381     } catch (IOException | ParseException | NumberFormatException e)
382     {
383       // ignore
384     }
385
386     return null;
387   }
388
389 }