JAL-3851 Fix getRequestBody()
[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.net.BindException;
28 import java.util.HashMap;
29 import java.util.Map;
30
31 import javax.servlet.ServletInputStream;
32 import javax.servlet.http.HttpServletRequest;
33 import javax.servlet.http.HttpServletResponse;
34
35 import org.eclipse.jetty.server.handler.ContextHandler;
36
37 import jalview.bin.Console;
38 import jalview.httpserver.AbstractRequestHandler;
39 import jalview.httpserver.HttpServer;
40
41 /**
42  * A simple handler to process (or delegate) HTTP requests on /jalview/rest
43  */
44 public class RestHandler extends AbstractRequestHandler
45 {
46
47   public enum Status
48   {
49     STARTED, IN_PROGRESS, FINISHED, ERROR, NOT_RUN
50   }
51
52   public interface EndpointI
53   {
54     public String getPath();
55
56     public String getName();
57
58     public String getParameters();
59
60     public String getDescription();
61
62     public void processEndpoint(HttpServletRequest request,
63             HttpServletResponse response);
64
65   }
66
67   private static final String MY_PATH = "rest";
68
69   private static final String MY_NAME = "Rest";
70
71   private String missingEndpointMessage = null;
72
73   private boolean init = false;
74
75   // map of method names and method handlers
76   private Map<String, EndpointI> endpoints = null;
77
78   protected Map<String, EndpointI> getEndpoints()
79   {
80     return endpoints;
81   }
82
83   /**
84    * Singleton instance of this class
85    */
86   private static RestHandler instance = null;
87
88   /**
89    * Returns the singleton instance of this class
90    * 
91    * @return
92    * @throws BindException
93    */
94   public static RestHandler getInstance() throws BindException
95   {
96     synchronized (RestHandler.class)
97     {
98       if (instance == null)
99       {
100         instance = new RestHandler();
101       }
102     }
103     return instance;
104   }
105
106   /**
107    * Private constructor enforces use of singleton
108    * 
109    * @throws BindException
110    */
111   protected RestHandler() throws BindException
112   {
113     init();
114
115     /*
116      * We don't register the handler here - this is done as a special case in
117      * HttpServer initialisation; to do it here would invite an infinite loop of
118      * RestHandler/HttpServer constructor
119      */
120   }
121
122   /**
123    * Handle a jalview/rest request
124    * 
125    * @throws IOException
126    */
127   @Override
128   protected void processRequest(HttpServletRequest request,
129           HttpServletResponse response) throws IOException
130   {
131     /*
132      * Currently just echoes the request; add helper classes as required to
133      * process requests
134      */
135     // This "pointless" call to request.getInputStream() seems to preserve the
136     // InputStream for use later in getRequestBody.
137     request.getInputStream();
138     if (endpoints == null)
139     {
140       final String queryString = request.getQueryString();
141       final String reply = "REST not yet implemented; received "
142               + request.getMethod() + ": " + request.getRequestURL()
143               + (queryString == null ? "" : "?" + queryString);
144       System.out.println(reply);
145
146       response.setHeader("Cache-Control", "no-cache/no-store");
147       response.setHeader("Content-type", "text/plain");
148       final PrintWriter writer = response.getWriter();
149       writer.write(reply);
150       return;
151     }
152
153     String endpointName = getRequestedEndpointName(request);
154
155     if (!endpoints.containsKey(endpointName)
156             || endpoints.get(endpointName) == null)
157     {
158
159       response.setHeader("Cache-Control", "no-cache/no-store");
160       response.setHeader("Content-type", "text/plain");
161       PrintWriter writer = response.getWriter();
162       writer.write(missingEndpointMessage == null
163               ? "REST endpoint '" + endpointName + "' not defined"
164               : missingEndpointMessage);
165       writer.write("\n");
166       writer.write("Available endpoints are:\n");
167       ContextHandler ch = HttpServer.getInstance().getContextHandler(this);
168       String base = HttpServer.getInstance().getUri().toString();
169       String contextPath = ch == null ? "" : ch.getContextPath();
170       for (String key : endpoints.keySet())
171       {
172         writer.write(base + contextPath + "/" + key + "\n");
173       }
174       response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
175       return;
176     }
177
178     response.setHeader("Cache-Control", "no-cache/no-store");
179     response.setHeader("Content-type", "text/plain");
180     EndpointI ep = endpoints.get(endpointName);
181     ep.processEndpoint(request, response);
182
183     return;
184   }
185
186   /**
187    * Returns a display name for this service
188    */
189   @Override
190   public String getName()
191   {
192     return MY_NAME;
193   }
194
195   /**
196    * Initialise methods
197    * 
198    * @throws BindException
199    */
200   protected void init() throws BindException
201   {
202     init(MY_PATH);
203   }
204
205   protected void init(String path) throws BindException
206   {
207     setPath(path);
208     // Override this in extended class
209     // e.g. registerHandler and addEndpoints
210   }
211
212   protected boolean addEndpoint(EndpointI ep)
213   {
214     if (endpoints == null)
215     {
216       endpoints = new HashMap<>();
217     }
218     AbstractEndpoint e = (AbstractEndpoint) ep;
219     endpoints.put(ep.getPath(), ep);
220     return true;
221   }
222
223   protected String getRequestedEndpointName(HttpServletRequest request)
224   {
225     String pathInfo = request.getPathInfo();
226     int slashpos = pathInfo.indexOf('/', 1);
227     return slashpos > 1 ? pathInfo.substring(1, slashpos)
228             : pathInfo.substring(1);
229   }
230
231   protected String[] getEndpointPathParameters(HttpServletRequest request)
232   {
233     String pathInfo = request.getPathInfo();
234     int slashpos = pathInfo.indexOf('/', 1);
235     return slashpos < 1 ? null
236             : pathInfo.substring(slashpos + 1).split("/");
237   }
238
239   protected void returnError(HttpServletRequest request,
240           HttpServletResponse response, String message)
241   {
242     response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
243     String endpointName = getRequestedEndpointName(request);
244     Console.error(getName() + " error: endpoint " + endpointName
245             + " failed: '" + message + "'");
246     try
247     {
248       PrintWriter writer = response.getWriter();
249       writer.write("Endpoint " + endpointName + ": " + message);
250     } catch (IOException e)
251     {
252       Console.debug("Could not write to REST response for endpoint "
253               + endpointName, e);
254     }
255   }
256
257   protected void returnStatus(HttpServletResponse response, String id,
258           Status status)
259   {
260     try
261     {
262       PrintWriter writer = response.getWriter();
263       if (id != null)
264         writer.write("id=" + id + "\n");
265       if (status != null)
266         writer.write("status=" + status.toString() + "\n");
267     } catch (IOException e)
268     {
269       Console.debug("Could not write status to REST response for id:" + id,
270               e);
271     }
272   }
273
274   protected String getRequestBody(HttpServletRequest request,
275           HttpServletResponse response) throws IOException
276   {
277     StringBuilder sb = new StringBuilder();
278     BufferedReader reader = null;
279     Console.debug("REQUEST=" + request.toString());
280     Console.debug("REQUEST.Content-Lenggth=" + request.getContentLength());
281     try
282     {
283       reader = request.getReader();
284       Console.debug("Using getReader()");
285     } catch (IllegalStateException e)
286     {
287       ServletInputStream is = request.getInputStream();
288       Console.debug("INPUTSTREAM "
289               + (is.isFinished() ? "FINISHED" : "NOT FINISHED"));
290       reader = new BufferedReader(new InputStreamReader(is));
291       Console.debug("Using getInputStream()");
292     }
293     if (reader != null)
294     {
295       try
296       {
297         String line;
298         while ((line = reader.readLine()) != null)
299         {
300           sb.append(line).append('\n');
301         }
302       } finally
303       {
304         reader.close();
305       }
306     }
307     else
308     {
309       returnError(request, response, "Error reading body of HTTP request");
310     }
311     return sb.toString();
312   }
313 }