JAL-1705 regular expression updates, tests, other refactoring
[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   protected final static String ENSEMBL_REST = "http://rest.ensembl.org";
27
28   protected static final String SEQUENCE_ID_URL = ENSEMBL_REST
29           + "/sequence/id";
30
31   // @see https://github.com/Ensembl/ensembl-rest/wiki/Output-formats
32   private static final String PING_URL = "http://rest.ensembl.org/info/ping.json";
33
34   private final static long RETEST_INTERVAL = 10000L; // 10 seconds
35
36   private static final Regex TRANSCRIPT_REGEX = new Regex(
37             "(ENS)([A-Z]{3}|)T[0-9]{11}$");
38
39   private static final Regex GENE_REGEX = new Regex(
40             "(ENS)([A-Z]{3}|)G[0-9]{11}$");
41
42   private static boolean ensemblRestAvailable = false;
43
44   private static long lastCheck = -1;
45
46   protected volatile boolean inProgress = false;
47
48   public static boolean isTranscriptIdentifier(String query)
49   {
50     return query == null ? false : TRANSCRIPT_REGEX.search(query);
51   }
52
53   public static boolean isGeneIdentifier(String query)
54   {
55     return query == null ? false : GENE_REGEX.search(query);
56   }
57
58   @Override
59   public boolean queryInProgress()
60   {
61     return inProgress;
62   }
63
64   @Override
65   public StringBuffer getRawRecords()
66   {
67     return null;
68   }
69
70   /**
71    * Returns the URL for the client http request
72    * 
73    * @param ids
74    * @return
75    * @throws MalformedURLException
76    */
77   protected abstract URL getUrl(List<String> ids)
78           throws MalformedURLException;
79
80   /**
81    * Returns true if client uses GET method, false if it uses POST
82    * 
83    * @return
84    */
85   protected abstract boolean useGetRequest();
86
87   /**
88    * Return the desired value for the Content-Type request header
89    * 
90    * @param multipleIds
91    * 
92    * @return
93    * @see https://github.com/Ensembl/ensembl-rest/wiki/HTTP-Headers
94    */
95   protected abstract String getRequestMimeType(boolean multipleIds);
96
97   /**
98    * Return the desired value for the Accept request header
99    * 
100    * @return
101    * @see https://github.com/Ensembl/ensembl-rest/wiki/HTTP-Headers
102    */
103   protected abstract String getResponseMimeType();
104
105   /**
106    * Tries to connect to Ensembl's REST 'ping' endpoint, and returns true if
107    * successful, else false
108    * 
109    * @return
110    */
111   private boolean checkEnsembl()
112   {
113     try
114     {
115       URL ping = new URL(PING_URL);
116       HttpURLConnection conn = (HttpURLConnection) ping.openConnection();
117       int rc = conn.getResponseCode();
118       conn.disconnect();
119       if (rc >= 200 && rc < 300)
120       {
121         return true;
122       }
123     } catch (Throwable t)
124     {
125       System.err.println("Error connecting to " + PING_URL + ": "
126               + t.getMessage());
127     }
128     return false;
129   }
130
131   /**
132    * returns a reader to a Fasta response from the Ensembl sequence endpoint
133    * 
134    * @param ids
135    * @return
136    * @throws IOException
137    */
138   protected FileParse getSequenceReader(List<String> ids)
139           throws IOException
140   {
141     URL url = getUrl(ids);
142   
143     BufferedReader reader = getHttpResponse(url, ids);
144     FileParse fp = new FileParse(reader, url.toString(), "HTTP_POST");
145     return fp;
146   }
147
148   /**
149    * Writes the HTTP request and gets the response as a reader.
150    * 
151    * @param url
152    * @param ids
153    *          written as Json POST body if more than one
154    * @return
155    * @throws IOException
156    *           if response code was not 200, or other I/O error
157    */
158   protected BufferedReader getHttpResponse(URL url, List<String> ids)
159           throws IOException
160   {
161     // long now = System.currentTimeMillis();
162     HttpURLConnection connection = (HttpURLConnection) url.openConnection();
163   
164     /*
165      * POST method allows multiple queries in one request; it is supported for
166      * sequence queries, but not for overlap
167      */
168     boolean multipleIds = ids.size() > 1;// useGetRequest();
169     connection.setRequestMethod(multipleIds ? HttpMethod.POST
170             : HttpMethod.GET);
171     connection.setRequestProperty("Content-Type",
172             getRequestMimeType(multipleIds));
173     connection.setRequestProperty("Accept", getResponseMimeType());
174
175     connection.setUseCaches(false);
176     connection.setDoInput(true);
177     connection.setDoOutput(multipleIds);
178
179     if (multipleIds)
180     {
181       writePostBody(connection, ids);
182     }
183   
184     InputStream response = connection.getInputStream();
185     int responseCode = connection.getResponseCode();
186   
187     if (responseCode != 200)
188     {
189       /*
190        * note: a GET request for an invalid id returns an error code e.g. 415
191        * but POST request returns 200 and an empty Fasta response 
192        */
193       throw new IOException(
194               "Response code was not 200. Detected response was "
195                       + responseCode);
196     }
197     // System.out.println(getClass().getName() + " took "
198     // + (System.currentTimeMillis() - now) + "ms to fetch");
199   
200     BufferedReader reader = null;
201     reader = new BufferedReader(new InputStreamReader(response, "UTF-8"));
202     return reader;
203   }
204
205   /**
206    * Rechecks if Ensembl is responding, unless the last check was successful and
207    * the retest interval has not yet elapsed. Returns true if Ensembl is up,
208    * else false.
209    * 
210    * @return
211    */
212   protected boolean isEnsemblAvailable()
213   {
214     long now = System.currentTimeMillis();
215     boolean retest = now - lastCheck > RETEST_INTERVAL;
216     if (ensemblRestAvailable && !retest)
217     {
218       return true;
219     }
220     ensemblRestAvailable = checkEnsembl();
221     lastCheck = now;
222     return ensemblRestAvailable;
223   }
224
225   /**
226    * Constructs, writes and flushes the POST body of the request, containing the
227    * query ids in JSON format
228    * 
229    * @param connection
230    * @param ids
231    * @throws IOException
232    */
233   protected void writePostBody(HttpURLConnection connection,
234           List<String> ids) throws IOException
235   {
236     boolean first;
237     StringBuilder postBody = new StringBuilder(64);
238     postBody.append("{\"ids\":[");
239     first = true;
240     for (String id : ids)
241     {
242       if (!first)
243       {
244         postBody.append(",");
245       }
246       first = false;
247       postBody.append("\"");
248       postBody.append(id.trim());
249       postBody.append("\"");
250     }
251     postBody.append("]}");
252     byte[] thepostbody = postBody.toString().getBytes();
253     connection.setRequestProperty("Content-Length",
254             Integer.toString(thepostbody.length));
255     DataOutputStream wr = new DataOutputStream(connection.getOutputStream());
256     wr.write(thepostbody);
257     wr.flush();
258     wr.close();
259   }
260
261 }