JAL-2344 override to ignore (obsolete?) JalviewFileView
[jalview.git] / src / jalview / ext / ensembl / EnsemblRestClient.java
1 package jalview.ext.ensembl;
2
3 import jalview.io.DataSourceType;
4 import jalview.io.FileParse;
5 import jalview.util.StringUtils;
6
7 import java.io.BufferedReader;
8 import java.io.DataOutputStream;
9 import java.io.IOException;
10 import java.io.InputStream;
11 import java.io.InputStreamReader;
12 import java.net.HttpURLConnection;
13 import java.net.MalformedURLException;
14 import java.net.URL;
15 import java.util.HashMap;
16 import java.util.List;
17 import java.util.Map;
18
19 import javax.ws.rs.HttpMethod;
20
21 import org.json.simple.JSONArray;
22 import org.json.simple.JSONObject;
23 import org.json.simple.parser.JSONParser;
24
25 import com.stevesoft.pat.Regex;
26
27 /**
28  * Base class for Ensembl REST service clients
29  * 
30  * @author gmcarstairs
31  */
32 abstract class EnsemblRestClient extends EnsemblSequenceFetcher
33 {
34   /*
35    * update these constants when Jalview has been checked / updated for
36    * changes to Ensembl REST API
37    * @see https://github.com/Ensembl/ensembl-rest/wiki/Change-log
38    */
39   private static final String LATEST_ENSEMBLGENOMES_REST_VERSION = "4.6";
40
41   private static final String LATEST_ENSEMBL_REST_VERSION = "4.6";
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     FileParse fp = new FileParse(reader, url.toString(), DataSourceType.URL);
213     return fp;
214   }
215
216   /**
217    * Writes the HTTP request and gets the response as a reader.
218    * 
219    * @param url
220    * @param ids
221    *          written as Json POST body if more than one
222    * @return
223    * @throws IOException
224    *           if response code was not 200, or other I/O error
225    */
226   protected BufferedReader getHttpResponse(URL url, List<String> ids)
227           throws IOException
228   {
229     // long now = System.currentTimeMillis();
230     HttpURLConnection connection = (HttpURLConnection) url.openConnection();
231   
232     /*
233      * POST method allows multiple queries in one request; it is supported for
234      * sequence queries, but not for overlap
235      */
236     boolean multipleIds = ids != null && ids.size() > 1;
237     connection.setRequestMethod(multipleIds ? HttpMethod.POST
238             : HttpMethod.GET);
239     connection.setRequestProperty("Content-Type",
240             getRequestMimeType(multipleIds));
241     connection.setRequestProperty("Accept", getResponseMimeType());
242
243     connection.setUseCaches(false);
244     connection.setDoInput(true);
245     connection.setDoOutput(multipleIds);
246
247     if (multipleIds)
248     {
249       writePostBody(connection, ids);
250     }
251   
252     InputStream response = connection.getInputStream();
253     int responseCode = connection.getResponseCode();
254   
255     if (responseCode != 200)
256     {
257       /*
258        * note: a GET request for an invalid id returns an error code e.g. 415
259        * but POST request returns 200 and an empty Fasta response 
260        */
261       throw new IOException(
262               "Response code was not 200. Detected response was "
263                       + responseCode);
264     }
265     // System.out.println(getClass().getName() + " took "
266     // + (System.currentTimeMillis() - now) + "ms to fetch");
267
268     checkRateLimits(connection);
269   
270     BufferedReader reader = null;
271     reader = new BufferedReader(new InputStreamReader(response, "UTF-8"));
272     return reader;
273   }
274
275   /**
276    * Inspect response headers for any sign of server overload and respect any
277    * 'retry-after' directive
278    * 
279    * @see https://github.com/Ensembl/ensembl-rest/wiki/Rate-Limits
280    * @param connection
281    */
282   void checkRateLimits(HttpURLConnection connection)
283   {
284     // number of requests allowed per time interval:
285     String limit = connection.getHeaderField("X-RateLimit-Limit");
286     // length of quota time interval in seconds:
287     // String period = connection.getHeaderField("X-RateLimit-Period");
288     // seconds remaining until usage quota is reset:
289     String reset = connection.getHeaderField("X-RateLimit-Reset");
290     // number of requests remaining from quota for current period:
291     String remaining = connection.getHeaderField("X-RateLimit-Remaining");
292     // number of seconds to wait before retrying (if remaining == 0)
293     String retryDelay = connection.getHeaderField("Retry-After");
294
295     // to test:
296     // retryDelay = "5";
297
298     EnsemblInfo info = domainData.get(getDomain());
299     if (retryDelay != null)
300     {
301       System.err.println("Ensembl REST service rate limit exceeded, wait "
302               + retryDelay + " seconds before retrying");
303       try
304       {
305         info.retryAfter = System.currentTimeMillis()
306                 + (1000 * Integer.valueOf(retryDelay));
307       } catch (NumberFormatException e)
308       {
309         System.err.println("Unexpected value for Retry-After: "
310                 + retryDelay);
311       }
312     }
313     else
314     {
315       info.retryAfter = 0;
316       // debug:
317       // System.out.println(String.format(
318       // "%s Ensembl requests remaining of %s (reset in %ss)",
319       // remaining, limit, reset));
320     }
321   }
322   
323   /**
324    * Rechecks if Ensembl is responding, unless the last check was successful and
325    * the retest interval has not yet elapsed. Returns true if Ensembl is up,
326    * else false. Also retrieves and saves the current version of Ensembl data
327    * and REST services at intervals.
328    * 
329    * @return
330    */
331   protected boolean isEnsemblAvailable()
332   {
333     EnsemblInfo info = domainData.get(getDomain());
334
335     long now = System.currentTimeMillis();
336
337     /*
338      * check if we are waiting for 'Retry-After' to expire
339      */
340     if (info.retryAfter > now)
341     {
342       System.err.println("Still " + (1 + (info.retryAfter - now) / 1000)
343               + " secs to wait before retrying Ensembl");
344       return false;
345     }
346     else
347     {
348       info.retryAfter = 0;
349     }
350
351     /*
352      * recheck if Ensembl is up if it was down, or the recheck period has elapsed
353      */
354     boolean retestAvailability = (now - info.lastAvailableCheckTime) > AVAILABILITY_RETEST_INTERVAL;
355     if (!info.restAvailable || retestAvailability)
356     {
357       info.restAvailable = checkEnsembl();
358       info.lastAvailableCheckTime = now;
359     }
360
361     /*
362      * refetch Ensembl versions if the recheck period has elapsed
363      */
364     boolean refetchVersion = (now - info.lastVersionCheckTime) > VERSION_RETEST_INTERVAL;
365     if (refetchVersion)
366     {
367       checkEnsemblRestVersion();
368       checkEnsemblDataVersion();
369       info.lastVersionCheckTime = now;
370     }
371
372     return info.restAvailable;
373   }
374
375   /**
376    * Constructs, writes and flushes the POST body of the request, containing the
377    * query ids in JSON format
378    * 
379    * @param connection
380    * @param ids
381    * @throws IOException
382    */
383   protected void writePostBody(HttpURLConnection connection,
384           List<String> ids) throws IOException
385   {
386     boolean first;
387     StringBuilder postBody = new StringBuilder(64);
388     postBody.append("{\"ids\":[");
389     first = true;
390     for (String id : ids)
391     {
392       if (!first)
393       {
394         postBody.append(",");
395       }
396       first = false;
397       postBody.append("\"");
398       postBody.append(id.trim());
399       postBody.append("\"");
400     }
401     postBody.append("]}");
402     byte[] thepostbody = postBody.toString().getBytes();
403     connection.setRequestProperty("Content-Length",
404             Integer.toString(thepostbody.length));
405     DataOutputStream wr = new DataOutputStream(connection.getOutputStream());
406     wr.write(thepostbody);
407     wr.flush();
408     wr.close();
409   }
410
411   /**
412    * Fetches and checks Ensembl's REST version number
413    * 
414    * @return
415    */
416   private void checkEnsemblRestVersion()
417   {
418     EnsemblInfo info = domainData.get(getDomain());
419
420     JSONParser jp = new JSONParser();
421     URL url = null;
422     try
423     {
424       url = new URL(getDomain()
425               + "/info/rest?content-type=application/json");
426       BufferedReader br = getHttpResponse(url, null);
427       JSONObject val = (JSONObject) jp.parse(br);
428       String version = val.get("release").toString();
429       String majorVersion = version.substring(0, version.indexOf("."));
430       String expected = info.expectedRestVersion;
431       String expectedMajorVersion = expected.substring(0,
432               expected.indexOf("."));
433       info.restMajorVersionMismatch = false;
434       try
435       {
436         /*
437          * if actual REST major version is ahead of what we expect,
438          * record this in case we want to warn the user
439          */
440         if (Float.valueOf(majorVersion) > Float
441                 .valueOf(expectedMajorVersion))
442         {
443           info.restMajorVersionMismatch = true;
444         }
445       } catch (NumberFormatException e)
446       {
447         System.err.println("Error in REST version: " + e.toString());
448       }
449
450       /*
451        * check if REST version is later than what Jalview has tested against,
452        * if so warn; we don't worry if it is earlier (this indicates Jalview has
453        * been tested in advance against the next pending REST version)
454        */
455       boolean laterVersion = StringUtils.compareVersions(version, expected) == 1;
456       if (laterVersion)
457       {
458         System.err.println(String.format(
459                 "Expected %s REST version %s but found %s, see %s",
460                 getDbSource(), expected, version, REST_CHANGE_LOG));
461       }
462       info.restVersion = version;
463     } catch (Throwable t)
464     {
465       System.err.println("Error checking Ensembl REST version: "
466               + t.getMessage());
467     }
468   }
469
470   public boolean isRestMajorVersionMismatch()
471   {
472     return domainData.get(getDomain()).restMajorVersionMismatch;
473   }
474
475   /**
476    * Fetches and checks Ensembl's data version number
477    * 
478    * @return
479    */
480   private void checkEnsemblDataVersion()
481   {
482     JSONParser jp = new JSONParser();
483     URL url = null;
484     try
485     {
486       url = new URL(getDomain()
487               + "/info/data?content-type=application/json");
488       BufferedReader br = getHttpResponse(url, null);
489       JSONObject val = (JSONObject) jp.parse(br);
490       JSONArray versions = (JSONArray) val.get("releases");
491       domainData.get(getDomain()).dataVersion = versions.get(0).toString();
492     } catch (Throwable t)
493     {
494       System.err.println("Error checking Ensembl data version: "
495               + t.getMessage());
496     }
497   }
498
499   public String getEnsemblDataVersion()
500   {
501     return domainData.get(getDomain()).dataVersion;
502   }
503
504   @Override
505   public String getDbVersion()
506   {
507     return getEnsemblDataVersion();
508   }
509
510 }