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