6a564f10e9655a90f91cc5a9f0fa35d27f12b9ff
[jalview.git] / src / jalview / ext / ensembl / EnsemblRestClient.java
1 package jalview.ext.ensembl;
2
3 import jalview.io.FileParse;
4
5 import java.io.BufferedReader;
6 import java.io.DataOutputStream;
7 import java.io.IOException;
8 import java.io.InputStream;
9 import java.io.InputStreamReader;
10 import java.net.HttpURLConnection;
11 import java.net.MalformedURLException;
12 import java.net.URL;
13 import java.util.List;
14
15 import javax.ws.rs.HttpMethod;
16
17 import com.stevesoft.pat.Regex;
18
19 /**
20  * Base class for Ensembl REST service clients
21  * 
22  * @author gmcarstairs
23  */
24 abstract class EnsemblRestClient extends EnsemblSequenceFetcher
25 {
26   private final static String ENSEMBL_REST = "http://rest.ensembl.org";
27
28   protected final static String ENSEMBL_GENOMES_REST = "http://rest.ensemblgenomes.org";
29
30   // @see https://github.com/Ensembl/ensembl-rest/wiki/Output-formats
31   private static final String PING_URL = "http://rest.ensembl.org/info/ping.json";
32
33   private final static long RETEST_INTERVAL = 10000L; // 10 seconds
34
35   private static final Regex TRANSCRIPT_REGEX = new Regex(
36             "(ENS)([A-Z]{3}|)T[0-9]{11}$");
37
38   private static final Regex GENE_REGEX = new Regex(
39             "(ENS)([A-Z]{3}|)G[0-9]{11}$");
40
41   private String domain = ENSEMBL_REST;
42
43   private static boolean ensemblRestAvailable = false;
44
45   private static long lastCheck = -1;
46
47   /*
48    * absolute time to wait till if we overloaded the REST service
49    */
50   private static long retryAfter;
51
52   protected volatile boolean inProgress = false;
53
54   /**
55    * Default constructor to use rest.ensembl.org
56    */
57   public EnsemblRestClient()
58   {
59     this(ENSEMBL_REST);
60   }
61
62   /**
63    * Constructor given the target domain to fetch data from
64    * 
65    * @param d
66    */
67   public EnsemblRestClient(String d)
68   {
69     domain = d;
70   }
71
72   /**
73    * Returns the domain name to query e.g. http://rest.ensembl.org or
74    * http://rest.ensemblgenomes.org
75    * 
76    * @return
77    */
78   String getDomain()
79   {
80     return domain;
81   }
82
83   void setDomain(String d)
84   {
85     domain = d;
86   }
87
88   /**
89    * Answers true if the query matches the regular expression pattern for an
90    * Ensembl transcript stable identifier
91    * 
92    * @param query
93    * @return
94    */
95   public boolean isTranscriptIdentifier(String query)
96   {
97     return query == null ? false : TRANSCRIPT_REGEX.search(query);
98   }
99
100   /**
101    * Answers true if the query matches the regular expression pattern for an
102    * Ensembl gene stable identifier
103    * 
104    * @param query
105    * @return
106    */
107   public boolean isGeneIdentifier(String query)
108   {
109     return query == null ? false : GENE_REGEX.search(query);
110   }
111
112   @Override
113   public boolean queryInProgress()
114   {
115     return inProgress;
116   }
117
118   @Override
119   public StringBuffer getRawRecords()
120   {
121     return null;
122   }
123
124   /**
125    * Returns the URL for the client http request
126    * 
127    * @param ids
128    * @return
129    * @throws MalformedURLException
130    */
131   protected abstract URL getUrl(List<String> ids)
132           throws MalformedURLException;
133
134   /**
135    * Returns true if client uses GET method, false if it uses POST
136    * 
137    * @return
138    */
139   protected abstract boolean useGetRequest();
140
141   /**
142    * Return the desired value for the Content-Type request header
143    * 
144    * @param multipleIds
145    * 
146    * @return
147    * @see https://github.com/Ensembl/ensembl-rest/wiki/HTTP-Headers
148    */
149   protected abstract String getRequestMimeType(boolean multipleIds);
150
151   /**
152    * Return the desired value for the Accept request header
153    * 
154    * @return
155    * @see https://github.com/Ensembl/ensembl-rest/wiki/HTTP-Headers
156    */
157   protected abstract String getResponseMimeType();
158
159   /**
160    * Tries to connect to Ensembl's REST 'ping' endpoint, and returns true if
161    * successful, else false
162    * 
163    * @return
164    */
165   private boolean checkEnsembl()
166   {
167     try
168     {
169       // note this format works for both ensembl and ensemblgenomes
170       // info/ping.json works for ensembl only (March 2016)
171       URL ping = new URL(getDomain()
172               + "/info/ping?content-type=application/json");
173       HttpURLConnection conn = (HttpURLConnection) ping.openConnection();
174       int rc = conn.getResponseCode();
175       conn.disconnect();
176       if (rc >= 200 && rc < 300)
177       {
178         return true;
179       }
180     } catch (Throwable t)
181     {
182       System.err.println("Error connecting to " + PING_URL + ": "
183               + t.getMessage());
184     }
185     return false;
186   }
187
188   /**
189    * returns a reader to a Fasta response from the Ensembl sequence endpoint
190    * 
191    * @param ids
192    * @return
193    * @throws IOException
194    */
195   protected FileParse getSequenceReader(List<String> ids)
196           throws IOException
197   {
198     URL url = getUrl(ids);
199   
200     BufferedReader reader = getHttpResponse(url, ids);
201     FileParse fp = new FileParse(reader, url.toString(), "HTTP_POST");
202     return fp;
203   }
204
205   /**
206    * Writes the HTTP request and gets the response as a reader.
207    * 
208    * @param url
209    * @param ids
210    *          written as Json POST body if more than one
211    * @return
212    * @throws IOException
213    *           if response code was not 200, or other I/O error
214    */
215   protected BufferedReader getHttpResponse(URL url, List<String> ids)
216           throws IOException
217   {
218     // long now = System.currentTimeMillis();
219     HttpURLConnection connection = (HttpURLConnection) url.openConnection();
220   
221     /*
222      * POST method allows multiple queries in one request; it is supported for
223      * sequence queries, but not for overlap
224      */
225     boolean multipleIds = ids.size() > 1;// useGetRequest();
226     connection.setRequestMethod(multipleIds ? HttpMethod.POST
227             : HttpMethod.GET);
228     connection.setRequestProperty("Content-Type",
229             getRequestMimeType(multipleIds));
230     connection.setRequestProperty("Accept", getResponseMimeType());
231
232     connection.setUseCaches(false);
233     connection.setDoInput(true);
234     connection.setDoOutput(multipleIds);
235
236     if (multipleIds)
237     {
238       writePostBody(connection, ids);
239     }
240   
241     InputStream response = connection.getInputStream();
242     int responseCode = connection.getResponseCode();
243   
244     if (responseCode != 200)
245     {
246       /*
247        * note: a GET request for an invalid id returns an error code e.g. 415
248        * but POST request returns 200 and an empty Fasta response 
249        */
250       throw new IOException(
251               "Response code was not 200. Detected response was "
252                       + responseCode);
253     }
254     // System.out.println(getClass().getName() + " took "
255     // + (System.currentTimeMillis() - now) + "ms to fetch");
256
257     checkRateLimits(connection);
258   
259     BufferedReader reader = null;
260     reader = new BufferedReader(new InputStreamReader(response, "UTF-8"));
261     return reader;
262   }
263
264   /**
265    * Inspect response headers for any sign of server overload and respect any
266    * 'retry-after' directive
267    * 
268    * @see https://github.com/Ensembl/ensembl-rest/wiki/Rate-Limits
269    * @param connection
270    */
271   void checkRateLimits(HttpURLConnection connection)
272   {
273     // number of requests allowed per time interval:
274     String limit = connection.getHeaderField("X-RateLimit-Limit");
275     // length of quota time interval in seconds:
276     // String period = connection.getHeaderField("X-RateLimit-Period");
277     // seconds remaining until usage quota is reset:
278     String reset = connection.getHeaderField("X-RateLimit-Reset");
279     // number of requests remaining from quota for current period:
280     String remaining = connection.getHeaderField("X-RateLimit-Remaining");
281     // number of seconds to wait before retrying (if remaining == 0)
282     String retryDelay = connection.getHeaderField("Retry-After");
283
284     // to test:
285     // retryDelay = "5";
286
287     if (retryDelay != null)
288     {
289       System.err.println("Ensembl REST service rate limit exceeded, wait "
290               + retryDelay + " seconds before retrying");
291       try
292       {
293         retryAfter = System.currentTimeMillis()
294                 + (1000 * Integer.valueOf(retryDelay));
295       } catch (NumberFormatException e)
296       {
297         System.err.println("Unexpected value for Retry-After: "
298                 + retryDelay);
299       }
300     }
301     else
302     {
303       retryAfter = 0;
304       // debug:
305       // System.out.println(String.format(
306       // "%s Ensembl requests remaining of %s (reset in %ss)",
307       // remaining, limit, reset));
308     }
309   }
310   /**
311    * Rechecks if Ensembl is responding, unless the last check was successful and
312    * the retest interval has not yet elapsed. Returns true if Ensembl is up,
313    * else false.
314    * 
315    * @return
316    */
317   protected boolean isEnsemblAvailable()
318   {
319     long now = System.currentTimeMillis();
320
321     /*
322      * check if we are waiting for 'Retry-After' to expire
323      */
324     if (retryAfter > now)
325     {
326       System.err.println("Still " + (1 + (retryAfter - now) / 1000)
327               + " secs to wait before retrying Ensembl");
328       return false;
329     }
330     else
331     {
332       retryAfter = 0;
333     }
334
335     boolean retest = now - lastCheck > RETEST_INTERVAL;
336     if (ensemblRestAvailable && !retest)
337     {
338       return true;
339     }
340     ensemblRestAvailable = checkEnsembl();
341     lastCheck = now;
342     return ensemblRestAvailable;
343   }
344
345   /**
346    * Constructs, writes and flushes the POST body of the request, containing the
347    * query ids in JSON format
348    * 
349    * @param connection
350    * @param ids
351    * @throws IOException
352    */
353   protected void writePostBody(HttpURLConnection connection,
354           List<String> ids) throws IOException
355   {
356     boolean first;
357     StringBuilder postBody = new StringBuilder(64);
358     postBody.append("{\"ids\":[");
359     first = true;
360     for (String id : ids)
361     {
362       if (!first)
363       {
364         postBody.append(",");
365       }
366       first = false;
367       postBody.append("\"");
368       postBody.append(id.trim());
369       postBody.append("\"");
370     }
371     postBody.append("]}");
372     byte[] thepostbody = postBody.toString().getBytes();
373     connection.setRequestProperty("Content-Length",
374             Integer.toString(thepostbody.length));
375     DataOutputStream wr = new DataOutputStream(connection.getOutputStream());
376     wr.write(thepostbody);
377     wr.flush();
378     wr.close();
379   }
380
381 }