JAL-2105 update Ensembl REST version to 4.7
[jalview.git] / src / jalview / ext / ensembl / EnsemblRestClient.java
1 package jalview.ext.ensembl;
2
3 import jalview.io.FileParse;
4 import jalview.util.StringUtils;
5
6 import java.io.BufferedReader;
7 import java.io.DataOutputStream;
8 import java.io.IOException;
9 import java.io.InputStream;
10 import java.io.InputStreamReader;
11 import java.net.HttpURLConnection;
12 import java.net.MalformedURLException;
13 import java.net.URL;
14 import java.util.HashMap;
15 import java.util.List;
16 import java.util.Map;
17
18 import javax.ws.rs.HttpMethod;
19
20 import org.json.simple.JSONArray;
21 import org.json.simple.JSONObject;
22 import org.json.simple.parser.JSONParser;
23
24 import com.stevesoft.pat.Regex;
25
26 /**
27  * Base class for Ensembl REST service clients
28  * 
29  * @author gmcarstairs
30  */
31 abstract class EnsemblRestClient extends EnsemblSequenceFetcher
32 {
33   /*
34    * update these constants when Jalview has been checked / updated for
35    * changes to Ensembl REST API
36    * @see https://github.com/Ensembl/ensembl-rest/wiki/Change-log
37    * @see http://rest.ensembl.org/info/rest?content-type=application/json
38    */
39   private static final String LATEST_ENSEMBLGENOMES_REST_VERSION = "4.6";
40
41   private static final String LATEST_ENSEMBL_REST_VERSION = "4.7";
42
43   private static final String REST_CHANGE_LOG = "https://github.com/Ensembl/ensembl-rest/wiki/Change-log";
44
45   private static Map<String, EnsemblInfo> domainData;
46
47   // @see https://github.com/Ensembl/ensembl-rest/wiki/Output-formats
48   private static final String PING_URL = "http://rest.ensembl.org/info/ping.json";
49
50   private final static long AVAILABILITY_RETEST_INTERVAL = 10000L; // 10 seconds
51
52   private final static long VERSION_RETEST_INTERVAL = 1000L * 3600; // 1 hr
53
54   private static final Regex TRANSCRIPT_REGEX = new Regex(
55             "(ENS)([A-Z]{3}|)T[0-9]{11}$");
56
57   private static final Regex GENE_REGEX = new Regex(
58             "(ENS)([A-Z]{3}|)G[0-9]{11}$");
59
60   static
61   {
62     domainData = new HashMap<String, EnsemblInfo>();
63     domainData.put(ENSEMBL_REST, new EnsemblInfo(ENSEMBL_REST,
64             LATEST_ENSEMBL_REST_VERSION));
65     domainData.put(ENSEMBL_GENOMES_REST, new EnsemblInfo(
66             ENSEMBL_GENOMES_REST, LATEST_ENSEMBLGENOMES_REST_VERSION));
67   }
68
69   protected volatile boolean inProgress = false;
70
71   /**
72    * Default constructor to use rest.ensembl.org
73    */
74   public EnsemblRestClient()
75   {
76     this(ENSEMBL_REST);
77   }
78
79   /**
80    * Constructor given the target domain to fetch data from
81    * 
82    * @param d
83    */
84   public EnsemblRestClient(String d)
85   {
86     setDomain(d);
87   }
88
89   /**
90    * Answers true if the query matches the regular expression pattern for an
91    * Ensembl transcript stable identifier
92    * 
93    * @param query
94    * @return
95    */
96   public boolean isTranscriptIdentifier(String query)
97   {
98     return query == null ? false : TRANSCRIPT_REGEX.search(query);
99   }
100
101   /**
102    * Answers true if the query matches the regular expression pattern for an
103    * Ensembl gene stable identifier
104    * 
105    * @param query
106    * @return
107    */
108   public boolean isGeneIdentifier(String query)
109   {
110     return query == null ? false : GENE_REGEX.search(query);
111   }
112
113   @Override
114   public boolean queryInProgress()
115   {
116     return inProgress;
117   }
118
119   @Override
120   public StringBuffer getRawRecords()
121   {
122     return null;
123   }
124
125   /**
126    * Returns the URL for the client http request
127    * 
128    * @param ids
129    * @return
130    * @throws MalformedURLException
131    */
132   protected abstract URL getUrl(List<String> ids)
133           throws MalformedURLException;
134
135   /**
136    * Returns true if client uses GET method, false if it uses POST
137    * 
138    * @return
139    */
140   protected abstract boolean useGetRequest();
141
142   /**
143    * Return the desired value for the Content-Type request header
144    * 
145    * @param multipleIds
146    * 
147    * @return
148    * @see https://github.com/Ensembl/ensembl-rest/wiki/HTTP-Headers
149    */
150   protected abstract String getRequestMimeType(boolean multipleIds);
151
152   /**
153    * Return the desired value for the Accept request header
154    * 
155    * @return
156    * @see https://github.com/Ensembl/ensembl-rest/wiki/HTTP-Headers
157    */
158   protected abstract String getResponseMimeType();
159
160   /**
161    * Checks Ensembl's REST 'ping' endpoint, and returns true if response
162    * indicates available, else false
163    * 
164    * @see http://rest.ensembl.org/documentation/info/ping
165    * @return
166    */
167   private boolean checkEnsembl()
168   {
169     HttpURLConnection conn = null;
170     try
171     {
172       // note this format works for both ensembl and ensemblgenomes
173       // info/ping.json works for ensembl only (March 2016)
174       URL ping = new URL(getDomain()
175               + "/info/ping?content-type=application/json");
176
177       /*
178        * expect {"ping":1} if ok
179        */
180       BufferedReader br = getHttpResponse(ping, null);
181       JSONParser jp = new JSONParser();
182       JSONObject val = (JSONObject) jp.parse(br);
183       String pingString = val.get("ping").toString();
184       return pingString != null;
185     } catch (Throwable t)
186     {
187       System.err.println("Error connecting to " + PING_URL + ": "
188               + t.getMessage());
189     } finally
190     {
191       if (conn != null)
192       {
193         conn.disconnect();
194       }
195     }
196     return false;
197   }
198
199   /**
200    * returns a reader to a Fasta response from the Ensembl sequence endpoint
201    * 
202    * @param ids
203    * @return
204    * @throws IOException
205    */
206   protected FileParse getSequenceReader(List<String> ids)
207           throws IOException
208   {
209     URL url = getUrl(ids);
210   
211     BufferedReader reader = getHttpResponse(url, ids);
212     if (reader == null)
213     {
214       // request failed
215       return null;
216     }
217     FileParse fp = new FileParse(reader, url.toString(), "HTTP_POST");
218     return fp;
219   }
220
221   /**
222    * Writes the HTTP request and gets the response as a reader.
223    * 
224    * @param url
225    * @param ids
226    *          written as Json POST body if more than one
227    * @return
228    * @throws IOException
229    *           if response code was not 200, or other I/O error
230    */
231   protected BufferedReader getHttpResponse(URL url, List<String> ids)
232           throws IOException
233   {
234     // long now = System.currentTimeMillis();
235     HttpURLConnection connection = (HttpURLConnection) url.openConnection();
236   
237     /*
238      * POST method allows multiple queries in one request; it is supported for
239      * sequence queries, but not for overlap
240      */
241     boolean multipleIds = ids != null && ids.size() > 1;
242     connection.setRequestMethod(multipleIds ? HttpMethod.POST
243             : HttpMethod.GET);
244     connection.setRequestProperty("Content-Type",
245             getRequestMimeType(multipleIds));
246     connection.setRequestProperty("Accept", getResponseMimeType());
247
248     connection.setUseCaches(false);
249     connection.setDoInput(true);
250     connection.setDoOutput(multipleIds);
251
252     if (multipleIds)
253     {
254       writePostBody(connection, ids);
255     }
256   
257     int responseCode = connection.getResponseCode();
258   
259     if (responseCode != 200)
260     {
261       /*
262        * note: a GET request for an invalid id returns an error code e.g. 415
263        * but POST request returns 200 and an empty Fasta response 
264        */
265       System.err.println("Response code " + responseCode + " for " + url);
266       return null;
267     }
268     // get content
269     InputStream response = connection.getInputStream();
270
271     // System.out.println(getClass().getName() + " took "
272     // + (System.currentTimeMillis() - now) + "ms to fetch");
273
274     checkRateLimits(connection);
275   
276     BufferedReader reader = null;
277     reader = new BufferedReader(new InputStreamReader(response, "UTF-8"));
278     return reader;
279   }
280
281   /**
282    * Inspect response headers for any sign of server overload and respect any
283    * 'retry-after' directive
284    * 
285    * @see https://github.com/Ensembl/ensembl-rest/wiki/Rate-Limits
286    * @param connection
287    */
288   void checkRateLimits(HttpURLConnection connection)
289   {
290     // number of requests allowed per time interval:
291     String limit = connection.getHeaderField("X-RateLimit-Limit");
292     // length of quota time interval in seconds:
293     // String period = connection.getHeaderField("X-RateLimit-Period");
294     // seconds remaining until usage quota is reset:
295     String reset = connection.getHeaderField("X-RateLimit-Reset");
296     // number of requests remaining from quota for current period:
297     String remaining = connection.getHeaderField("X-RateLimit-Remaining");
298     // number of seconds to wait before retrying (if remaining == 0)
299     String retryDelay = connection.getHeaderField("Retry-After");
300
301     // to test:
302     // retryDelay = "5";
303
304     EnsemblInfo info = domainData.get(getDomain());
305     if (retryDelay != null)
306     {
307       System.err.println("Ensembl REST service rate limit exceeded, wait "
308               + retryDelay + " seconds before retrying");
309       try
310       {
311         info.retryAfter = System.currentTimeMillis()
312                 + (1000 * Integer.valueOf(retryDelay));
313       } catch (NumberFormatException e)
314       {
315         System.err.println("Unexpected value for Retry-After: "
316                 + retryDelay);
317       }
318     }
319     else
320     {
321       info.retryAfter = 0;
322       // debug:
323       // System.out.println(String.format(
324       // "%s Ensembl requests remaining of %s (reset in %ss)",
325       // remaining, limit, reset));
326     }
327   }
328   
329   /**
330    * Rechecks if Ensembl is responding, unless the last check was successful and
331    * the retest interval has not yet elapsed. Returns true if Ensembl is up,
332    * else false. Also retrieves and saves the current version of Ensembl data
333    * and REST services at intervals.
334    * 
335    * @return
336    */
337   protected boolean isEnsemblAvailable()
338   {
339     EnsemblInfo info = domainData.get(getDomain());
340
341     long now = System.currentTimeMillis();
342
343     /*
344      * check if we are waiting for 'Retry-After' to expire
345      */
346     if (info.retryAfter > now)
347     {
348       System.err.println("Still " + (1 + (info.retryAfter - now) / 1000)
349               + " secs to wait before retrying Ensembl");
350       return false;
351     }
352     else
353     {
354       info.retryAfter = 0;
355     }
356
357     /*
358      * recheck if Ensembl is up if it was down, or the recheck period has elapsed
359      */
360     boolean retestAvailability = (now - info.lastAvailableCheckTime) > AVAILABILITY_RETEST_INTERVAL;
361     if (!info.restAvailable || retestAvailability)
362     {
363       info.restAvailable = checkEnsembl();
364       info.lastAvailableCheckTime = now;
365     }
366
367     /*
368      * refetch Ensembl versions if the recheck period has elapsed
369      */
370     boolean refetchVersion = (now - info.lastVersionCheckTime) > VERSION_RETEST_INTERVAL;
371     if (refetchVersion)
372     {
373       checkEnsemblRestVersion();
374       checkEnsemblDataVersion();
375       info.lastVersionCheckTime = now;
376     }
377
378     return info.restAvailable;
379   }
380
381   /**
382    * Constructs, writes and flushes the POST body of the request, containing the
383    * query ids in JSON format
384    * 
385    * @param connection
386    * @param ids
387    * @throws IOException
388    */
389   protected void writePostBody(HttpURLConnection connection,
390           List<String> ids) throws IOException
391   {
392     boolean first;
393     StringBuilder postBody = new StringBuilder(64);
394     postBody.append("{\"ids\":[");
395     first = true;
396     for (String id : ids)
397     {
398       if (!first)
399       {
400         postBody.append(",");
401       }
402       first = false;
403       postBody.append("\"");
404       postBody.append(id.trim());
405       postBody.append("\"");
406     }
407     postBody.append("]}");
408     byte[] thepostbody = postBody.toString().getBytes();
409     connection.setRequestProperty("Content-Length",
410             Integer.toString(thepostbody.length));
411     DataOutputStream wr = new DataOutputStream(connection.getOutputStream());
412     wr.write(thepostbody);
413     wr.flush();
414     wr.close();
415   }
416
417   /**
418    * Fetches and checks Ensembl's REST version number
419    * 
420    * @return
421    */
422   private void checkEnsemblRestVersion()
423   {
424     EnsemblInfo info = domainData.get(getDomain());
425
426     JSONParser jp = new JSONParser();
427     URL url = null;
428     try
429     {
430       url = new URL(getDomain()
431               + "/info/rest?content-type=application/json");
432       BufferedReader br = getHttpResponse(url, null);
433       JSONObject val = (JSONObject) jp.parse(br);
434       String version = val.get("release").toString();
435       String majorVersion = version.substring(0, version.indexOf("."));
436       String expected = info.expectedRestVersion;
437       String expectedMajorVersion = expected.substring(0,
438               expected.indexOf("."));
439       info.restMajorVersionMismatch = false;
440       try
441       {
442         /*
443          * if actual REST major version is ahead of what we expect,
444          * record this in case we want to warn the user
445          */
446         if (Float.valueOf(majorVersion) > Float
447                 .valueOf(expectedMajorVersion))
448         {
449           info.restMajorVersionMismatch = true;
450         }
451       } catch (NumberFormatException e)
452       {
453         System.err.println("Error in REST version: " + e.toString());
454       }
455
456       /*
457        * check if REST version is later than what Jalview has tested against,
458        * if so warn; we don't worry if it is earlier (this indicates Jalview has
459        * been tested in advance against the next pending REST version)
460        */
461       boolean laterVersion = StringUtils.compareVersions(version, expected) == 1;
462       if (laterVersion)
463       {
464         System.err.println(String.format(
465                 "Expected %s REST version %s but found %s, see %s",
466                 getDbSource(), expected, version, REST_CHANGE_LOG));
467       }
468       info.restVersion = version;
469     } catch (Throwable t)
470     {
471       System.err.println("Error checking Ensembl REST version: "
472               + t.getMessage());
473     }
474   }
475
476   public boolean isRestMajorVersionMismatch()
477   {
478     return domainData.get(getDomain()).restMajorVersionMismatch;
479   }
480
481   /**
482    * Fetches and checks Ensembl's data version number
483    * 
484    * @return
485    */
486   private void checkEnsemblDataVersion()
487   {
488     JSONParser jp = new JSONParser();
489     URL url = null;
490     try
491     {
492       url = new URL(getDomain()
493               + "/info/data?content-type=application/json");
494       BufferedReader br = getHttpResponse(url, null);
495       JSONObject val = (JSONObject) jp.parse(br);
496       JSONArray versions = (JSONArray) val.get("releases");
497       domainData.get(getDomain()).dataVersion = versions.get(0).toString();
498     } catch (Throwable t)
499     {
500       System.err.println("Error checking Ensembl data version: "
501               + t.getMessage());
502     }
503   }
504
505   public String getEnsemblDataVersion()
506   {
507     return domainData.get(getDomain()).dataVersion;
508   }
509
510   @Override
511   public String getDbVersion()
512   {
513     return getEnsemblDataVersion();
514   }
515
516 }