c967d3e08c5f430604c73985ab293dbc0441f76d
[jalview.git] / src / jalview / rest / RestHandler.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
4  * 
5  * This file is part of Jalview.
6  * 
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License 
9  * as published by the Free Software Foundation, either version 3
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 package jalview.rest;
22
23 import java.io.BufferedReader;
24 import java.io.IOException;
25 import java.io.InputStreamReader;
26 import java.io.PrintWriter;
27 import java.lang.reflect.InvocationTargetException;
28 import java.net.BindException;
29 import java.util.HashMap;
30 import java.util.Map;
31
32 import javax.servlet.ServletInputStream;
33 import javax.servlet.http.HttpServletRequest;
34 import javax.servlet.http.HttpServletResponse;
35
36 import org.eclipse.jetty.server.handler.ContextHandler;
37
38 import jalview.bin.Console;
39 import jalview.httpserver.AbstractRequestHandler;
40 import jalview.httpserver.HttpServer;
41
42 /**
43  * A simple handler to process (or delegate) HTTP requests on /jalview/rest
44  */
45 public class RestHandler extends AbstractRequestHandler
46 {
47
48   public enum Status
49   {
50     STARTED, IN_PROGRESS, FINISHED, ERROR, NOT_RUN
51   }
52
53   public interface EndpointI
54   {
55     public String getPath();
56
57     public String getName();
58
59     public String getParameters();
60
61     public String getDescription();
62
63     public void processEndpoint(HttpServletRequest request,
64             HttpServletResponse response);
65
66   }
67
68   private static final String MY_PATH = "rest";
69
70   private static final String MY_NAME = "Rest";
71
72   private String missingEndpointMessage = null;
73
74   private boolean init = false;
75
76   // map of method names and method handlers
77   private Map<String, AbstractEndpoint> endpoints = null;
78
79   protected Map<String, AbstractEndpoint> getEndpoints()
80   {
81     return endpoints;
82   }
83
84   protected AbstractEndpoint getNewEndpoint(String name)
85   {
86     if (getEndpoints() == null)
87     {
88       return null;
89     }
90     try
91     {
92       return getEndpoints().get(name).getClass()
93               .getDeclaredConstructor(API.class).newInstance(this);
94     } catch (InstantiationException | IllegalAccessException
95             | IllegalArgumentException | InvocationTargetException
96             | NoSuchMethodException | SecurityException e)
97     {
98       Console.debug("Could not instantiate new endpoint '" + name + "'", e);
99     }
100     return null;
101   }
102
103   /**
104    * Singleton instance of this class
105    */
106   private static RestHandler instance = null;
107
108   /**
109    * Returns the singleton instance of this class
110    * 
111    * @return
112    * @throws BindException
113    */
114   public static RestHandler getInstance() throws BindException
115   {
116     synchronized (RestHandler.class)
117     {
118       if (instance == null)
119       {
120         instance = new RestHandler();
121       }
122     }
123     return instance;
124   }
125
126   /**
127    * Private constructor enforces use of singleton
128    * 
129    * @throws BindException
130    */
131   protected RestHandler() throws BindException
132   {
133     init();
134
135     /*
136      * We don't register the handler here - this is done as a special case in
137      * HttpServer initialisation; to do it here would invite an infinite loop of
138      * RestHandler/HttpServer constructor
139      */
140   }
141
142   /**
143    * Handle a jalview/rest request
144    * 
145    * @throws IOException
146    */
147   @Override
148   protected void processRequest(HttpServletRequest request,
149           HttpServletResponse response) throws IOException
150   {
151     /*
152      * Currently just echoes the request; add helper classes as required to
153      * process requests
154      */
155     // This "pointless" call to request.getInputStream() seems to preserve the
156     // InputStream for use later in getRequestBody.
157     request.getInputStream();
158
159     String remoteAddr = request.getRemoteAddr();
160     if (!("127.0.0.1".equals(remoteAddr) || "localhost".equals(remoteAddr)))
161     {
162       returnError(request, response, "Not authorised: " + remoteAddr,
163               HttpServletResponse.SC_UNAUTHORIZED);
164       return;
165     }
166     if (getEndpoints() == null)
167     {
168       final String queryString = request.getQueryString();
169       final String reply = "REST not yet implemented; received "
170               + request.getMethod() + ": " + request.getRequestURL()
171               + (queryString == null ? "" : "?" + queryString);
172       Console.error(reply);
173
174       response.setHeader("Cache-Control", "no-cache/no-store");
175       response.setHeader("Content-type", "text/plain");
176       final PrintWriter writer = response.getWriter();
177       writer.write(reply);
178       return;
179     }
180
181     String endpointName = getRequestedEndpointName(request);
182     Console.debug(endpointName);
183
184     if (!getEndpoints().containsKey(endpointName)
185             || getEndpoints().get(endpointName) == null)
186     {
187       Console.debug(
188               "RestHandler did not find endpoint '" + endpointName + "'");
189
190       response.setHeader("Cache-Control", "no-cache/no-store");
191       response.setHeader("Content-type", "text/plain");
192       PrintWriter writer = response.getWriter();
193       writer.write(missingEndpointMessage == null
194               ? "REST endpoint '" + endpointName + "' not defined"
195               : missingEndpointMessage);
196       writer.write("\n");
197       writer.write("Available endpoints are:\n");
198       ContextHandler ch = HttpServer.getInstance().getContextHandler(this);
199       String base = HttpServer.getInstance().getUri().toString();
200       String contextPath = ch == null ? "" : ch.getContextPath();
201       for (String key : getEndpoints().keySet())
202       {
203         writer.write(base + contextPath + "/" + key + "\n");
204       }
205       response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
206       return;
207     }
208
209     response.setHeader("Cache-Control", "no-cache/no-store");
210     response.setHeader("Content-type", "text/plain");
211     EndpointI ep = getNewEndpoint(endpointName);
212     ep.processEndpoint(request, response);
213
214     return;
215   }
216
217   /**
218    * Returns a display name for this service
219    */
220   @Override
221   public String getName()
222   {
223     return MY_NAME;
224   }
225
226   /**
227    * Initialise methods
228    * 
229    * @throws BindException
230    */
231   protected void init() throws BindException
232   {
233     init(MY_PATH);
234   }
235
236   protected void init(String path) throws BindException
237   {
238     setPath(path);
239     // Override this in extended class
240     // e.g. registerHandler and addEndpoints
241   }
242
243   protected void addEndpoint(AbstractEndpoint ep)
244   {
245     if (getEndpoints() == null)
246     {
247       endpoints = new HashMap<>();
248     }
249     endpoints.put(ep.getPath(), ep);
250     Console.debug("REST API, added endpoint '" + ep.getPath() + "'");
251   }
252
253   protected String getRequestedEndpointName(HttpServletRequest request)
254   {
255     String pathInfo = request.getPathInfo();
256     int slashpos = pathInfo.indexOf('/', 1);
257     return slashpos > 1 ? pathInfo.substring(1, slashpos)
258             : pathInfo.substring(1);
259   }
260
261   protected void returnError(HttpServletRequest request,
262           HttpServletResponse response, String message)
263   {
264     returnError(request, response, message,
265             HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
266   }
267
268   protected void returnError(HttpServletRequest request,
269           HttpServletResponse response, String message, int statusCode)
270   {
271     response.setStatus(statusCode);
272     String endpointName = getRequestedEndpointName(request);
273     Console.error(getName() + " error: endpoint " + endpointName
274             + " failed: '" + message + "'");
275     try
276     {
277       PrintWriter writer = response.getWriter();
278       writer.write("Endpoint " + endpointName + ": " + message);
279     } catch (IOException e)
280     {
281       Console.debug("Could not write to REST response for endpoint "
282               + endpointName, e);
283     }
284   }
285
286   protected void returnStatus(HttpServletResponse response, String id,
287           Status status)
288   {
289     try
290     {
291       PrintWriter writer = response.getWriter();
292       if (id != null)
293         writer.write("id=" + id + "\n");
294       if (status != null)
295         writer.write("status=" + status.toString() + "\n");
296     } catch (IOException e)
297     {
298       Console.debug("Could not write status to REST response for id:" + id,
299               e);
300     }
301   }
302
303   protected String getRequestBody(HttpServletRequest request,
304           HttpServletResponse response) throws IOException
305   {
306     StringBuilder sb = new StringBuilder();
307     BufferedReader reader = null;
308     Console.debug("REQUEST=" + request.toString());
309     Console.debug("REQUEST.Content-Length=" + request.getContentLength());
310     try
311     {
312       reader = request.getReader();
313       Console.debug("Using getReader()");
314     } catch (IllegalStateException e)
315     {
316       ServletInputStream is = request.getInputStream();
317       reader = new BufferedReader(new InputStreamReader(is));
318       Console.debug("Using getInputStream()");
319     }
320     if (reader != null)
321     {
322       try
323       {
324         String line;
325         while ((line = reader.readLine()) != null)
326         {
327           sb.append(line).append('\n');
328         }
329       } finally
330       {
331         reader.close();
332       }
333     }
334     else
335     {
336       returnError(request, response, "Error reading body of HTTP request");
337     }
338     return sb.toString();
339   }
340 }