JAL-1705 check Retry-After response header
[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   String getDomain()
73   {
74     return domain;
75   }
76
77   void setDomain(String d)
78   {
79     domain = d;
80   }
81
82   public boolean isTranscriptIdentifier(String query)
83   {
84     return query == null ? false : TRANSCRIPT_REGEX.search(query);
85   }
86
87   public boolean isGeneIdentifier(String query)
88   {
89     return query == null ? false : GENE_REGEX.search(query);
90   }
91
92   @Override
93   public boolean queryInProgress()
94   {
95     return inProgress;
96   }
97
98   @Override
99   public StringBuffer getRawRecords()
100   {
101     return null;
102   }
103
104   /**
105    * Returns the URL for the client http request
106    * 
107    * @param ids
108    * @return
109    * @throws MalformedURLException
110    */
111   protected abstract URL getUrl(List<String> ids)
112           throws MalformedURLException;
113
114   /**
115    * Returns true if client uses GET method, false if it uses POST
116    * 
117    * @return
118    */
119   protected abstract boolean useGetRequest();
120
121   /**
122    * Return the desired value for the Content-Type request header
123    * 
124    * @param multipleIds
125    * 
126    * @return
127    * @see https://github.com/Ensembl/ensembl-rest/wiki/HTTP-Headers
128    */
129   protected abstract String getRequestMimeType(boolean multipleIds);
130
131   /**
132    * Return the desired value for the Accept request header
133    * 
134    * @return
135    * @see https://github.com/Ensembl/ensembl-rest/wiki/HTTP-Headers
136    */
137   protected abstract String getResponseMimeType();
138
139   /**
140    * Tries to connect to Ensembl's REST 'ping' endpoint, and returns true if
141    * successful, else false
142    * 
143    * @return
144    */
145   private boolean checkEnsembl()
146   {
147     try
148     {
149       // note this format works for both ensembl and ensemblgenomes
150       // info/ping.json works for ensembl only (March 2016)
151       URL ping = new URL(getDomain()
152               + "/info/ping?content-type=application/json");
153       HttpURLConnection conn = (HttpURLConnection) ping.openConnection();
154       int rc = conn.getResponseCode();
155       conn.disconnect();
156       if (rc >= 200 && rc < 300)
157       {
158         return true;
159       }
160     } catch (Throwable t)
161     {
162       System.err.println("Error connecting to " + PING_URL + ": "
163               + t.getMessage());
164     }
165     return false;
166   }
167
168   /**
169    * returns a reader to a Fasta response from the Ensembl sequence endpoint
170    * 
171    * @param ids
172    * @return
173    * @throws IOException
174    */
175   protected FileParse getSequenceReader(List<String> ids)
176           throws IOException
177   {
178     URL url = getUrl(ids);
179   
180     BufferedReader reader = getHttpResponse(url, ids);
181     FileParse fp = new FileParse(reader, url.toString(), "HTTP_POST");
182     return fp;
183   }
184
185   /**
186    * Writes the HTTP request and gets the response as a reader.
187    * 
188    * @param url
189    * @param ids
190    *          written as Json POST body if more than one
191    * @return
192    * @throws IOException
193    *           if response code was not 200, or other I/O error
194    */
195   protected BufferedReader getHttpResponse(URL url, List<String> ids)
196           throws IOException
197   {
198     // long now = System.currentTimeMillis();
199     HttpURLConnection connection = (HttpURLConnection) url.openConnection();
200   
201     /*
202      * POST method allows multiple queries in one request; it is supported for
203      * sequence queries, but not for overlap
204      */
205     boolean multipleIds = ids.size() > 1;// useGetRequest();
206     connection.setRequestMethod(multipleIds ? HttpMethod.POST
207             : HttpMethod.GET);
208     connection.setRequestProperty("Content-Type",
209             getRequestMimeType(multipleIds));
210     connection.setRequestProperty("Accept", getResponseMimeType());
211
212     connection.setUseCaches(false);
213     connection.setDoInput(true);
214     connection.setDoOutput(multipleIds);
215
216     if (multipleIds)
217     {
218       writePostBody(connection, ids);
219     }
220   
221     InputStream response = connection.getInputStream();
222     int responseCode = connection.getResponseCode();
223   
224     if (responseCode != 200)
225     {
226       /*
227        * note: a GET request for an invalid id returns an error code e.g. 415
228        * but POST request returns 200 and an empty Fasta response 
229        */
230       throw new IOException(
231               "Response code was not 200. Detected response was "
232                       + responseCode);
233     }
234     // System.out.println(getClass().getName() + " took "
235     // + (System.currentTimeMillis() - now) + "ms to fetch");
236
237     checkRateLimits(connection);
238   
239     BufferedReader reader = null;
240     reader = new BufferedReader(new InputStreamReader(response, "UTF-8"));
241     return reader;
242   }
243
244   /**
245    * Inspect response headers for any sign of server overload and respect any
246    * 'retry-after' directive
247    * 
248    * @see https://github.com/Ensembl/ensembl-rest/wiki/Rate-Limits
249    * @param connection
250    */
251   void checkRateLimits(HttpURLConnection connection)
252   {
253     // number of requests allowed per time interval:
254     String limit = connection.getHeaderField("X-RateLimit-Limit");
255     // length of quota time interval in seconds:
256     // String period = connection.getHeaderField("X-RateLimit-Period");
257     // seconds remaining until usage quota is reset:
258     String reset = connection.getHeaderField("X-RateLimit-Reset");
259     // number of requests remaining from quota for current period:
260     String remaining = connection.getHeaderField("X-RateLimit-Remaining");
261     // number of seconds to wait before retrying (if remaining == 0)
262     String retryDelay = connection.getHeaderField("Retry-After");
263
264     // to test:
265     // retryDelay = "5";
266
267     if (retryDelay != null)
268     {
269       System.err.println("Ensembl REST service rate limit exceeded, wait "
270               + retryDelay + " seconds before retrying");
271       try
272       {
273         retryAfter = System.currentTimeMillis()
274                 + (1000 * Integer.valueOf(retryDelay));
275       } catch (NumberFormatException e)
276       {
277         System.err.println("Unexpected value for Retry-After: "
278                 + retryDelay);
279       }
280     }
281     else
282     {
283       retryAfter = 0;
284       // debug:
285       // System.out.println(String.format(
286       // "%s Ensembl requests remaining of %s (reset in %ss)",
287       // remaining, limit, reset));
288     }
289   }
290   /**
291    * Rechecks if Ensembl is responding, unless the last check was successful and
292    * the retest interval has not yet elapsed. Returns true if Ensembl is up,
293    * else false.
294    * 
295    * @return
296    */
297   protected boolean isEnsemblAvailable()
298   {
299     long now = System.currentTimeMillis();
300
301     /*
302      * check if we are waiting for 'Retry-After' to expire
303      */
304     if (retryAfter > now)
305     {
306       System.err.println("Still " + (1 + (retryAfter - now) / 1000)
307               + " secs to wait before retrying Ensembl");
308       return false;
309     }
310     else
311     {
312       retryAfter = 0;
313     }
314
315     boolean retest = now - lastCheck > RETEST_INTERVAL;
316     if (ensemblRestAvailable && !retest)
317     {
318       return true;
319     }
320     ensemblRestAvailable = checkEnsembl();
321     lastCheck = now;
322     return ensemblRestAvailable;
323   }
324
325   /**
326    * Constructs, writes and flushes the POST body of the request, containing the
327    * query ids in JSON format
328    * 
329    * @param connection
330    * @param ids
331    * @throws IOException
332    */
333   protected void writePostBody(HttpURLConnection connection,
334           List<String> ids) throws IOException
335   {
336     boolean first;
337     StringBuilder postBody = new StringBuilder(64);
338     postBody.append("{\"ids\":[");
339     first = true;
340     for (String id : ids)
341     {
342       if (!first)
343       {
344         postBody.append(",");
345       }
346       first = false;
347       postBody.append("\"");
348       postBody.append(id.trim());
349       postBody.append("\"");
350     }
351     postBody.append("]}");
352     byte[] thepostbody = postBody.toString().getBytes();
353     connection.setRequestProperty("Content-Length",
354             Integer.toString(thepostbody.length));
355     DataOutputStream wr = new DataOutputStream(connection.getOutputStream());
356     wr.write(thepostbody);
357     wr.flush();
358     wr.close();
359   }
360
361 }