JAL-3851 Allow specifying a sequence index in highlight
[jalview.git] / src / jalview / rest / RestHandler.java
index 88f23f4..c967d3e 100644 (file)
+/*
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
+ * 
+ * This file is part of Jalview.
+ * 
+ * Jalview is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License 
+ * as published by the Free Software Foundation, either version 3
+ * of the License, or (at your option) any later version.
+ *  
+ * Jalview is distributed in the hope that it will be useful, but 
+ * WITHOUT ANY WARRANTY; without even the implied warranty 
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
+ * PURPOSE.  See the GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
+ */
 package jalview.rest;
 
+import java.io.BufferedReader;
 import java.io.IOException;
+import java.io.InputStreamReader;
 import java.io.PrintWriter;
+import java.lang.reflect.InvocationTargetException;
 import java.net.BindException;
+import java.util.HashMap;
+import java.util.Map;
 
+import javax.servlet.ServletInputStream;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
+import org.eclipse.jetty.server.handler.ContextHandler;
+
+import jalview.bin.Console;
 import jalview.httpserver.AbstractRequestHandler;
+import jalview.httpserver.HttpServer;
 
 /**
  * A simple handler to process (or delegate) HTTP requests on /jalview/rest
  */
 public class RestHandler extends AbstractRequestHandler
 {
+
+  public enum Status
+  {
+    STARTED, IN_PROGRESS, FINISHED, ERROR, NOT_RUN
+  }
+
+  public interface EndpointI
+  {
+    public String getPath();
+
+    public String getName();
+
+    public String getParameters();
+
+    public String getDescription();
+
+    public void processEndpoint(HttpServletRequest request,
+            HttpServletResponse response);
+
+  }
+
   private static final String MY_PATH = "rest";
 
   private static final String MY_NAME = "Rest";
 
+  private String missingEndpointMessage = null;
+
+  private boolean init = false;
+
+  // map of method names and method handlers
+  private Map<String, AbstractEndpoint> endpoints = null;
+
+  protected Map<String, AbstractEndpoint> getEndpoints()
+  {
+    return endpoints;
+  }
+
+  protected AbstractEndpoint getNewEndpoint(String name)
+  {
+    if (getEndpoints() == null)
+    {
+      return null;
+    }
+    try
+    {
+      return getEndpoints().get(name).getClass()
+              .getDeclaredConstructor(API.class).newInstance(this);
+    } catch (InstantiationException | IllegalAccessException
+            | IllegalArgumentException | InvocationTargetException
+            | NoSuchMethodException | SecurityException e)
+    {
+      Console.debug("Could not instantiate new endpoint '" + name + "'", e);
+    }
+    return null;
+  }
+
   /**
    * Singleton instance of this class
    */
@@ -46,9 +128,9 @@ public class RestHandler extends AbstractRequestHandler
    * 
    * @throws BindException
    */
-  private RestHandler() throws BindException
+  protected RestHandler() throws BindException
   {
-    setPath(MY_PATH);
+    init();
 
     /*
      * We don't register the handler here - this is done as a special case in
@@ -64,27 +146,72 @@ public class RestHandler extends AbstractRequestHandler
    */
   @Override
   protected void processRequest(HttpServletRequest request,
-          HttpServletResponse response)
+          HttpServletResponse response) throws IOException
   {
     /*
      * Currently just echoes the request; add helper classes as required to
      * process requests
      */
-    final String queryString = request.getQueryString();
-    final String reply = "REST not yet implemented; received "
-            + request.getRequestURL()
-            + (queryString == null ? "" : "?" + queryString);
-    System.out.println(reply);
+    // This "pointless" call to request.getInputStream() seems to preserve the
+    // InputStream for use later in getRequestBody.
+    request.getInputStream();
 
-    try
+    String remoteAddr = request.getRemoteAddr();
+    if (!("127.0.0.1".equals(remoteAddr) || "localhost".equals(remoteAddr)))
+    {
+      returnError(request, response, "Not authorised: " + remoteAddr,
+              HttpServletResponse.SC_UNAUTHORIZED);
+      return;
+    }
+    if (getEndpoints() == null)
     {
+      final String queryString = request.getQueryString();
+      final String reply = "REST not yet implemented; received "
+              + request.getMethod() + ": " + request.getRequestURL()
+              + (queryString == null ? "" : "?" + queryString);
+      Console.error(reply);
+
+      response.setHeader("Cache-Control", "no-cache/no-store");
+      response.setHeader("Content-type", "text/plain");
       final PrintWriter writer = response.getWriter();
       writer.write(reply);
-      writer.close();
-    } catch (IOException e)
+      return;
+    }
+
+    String endpointName = getRequestedEndpointName(request);
+    Console.debug(endpointName);
+
+    if (!getEndpoints().containsKey(endpointName)
+            || getEndpoints().get(endpointName) == null)
     {
-      System.err.println("Error writing REST response: " + e.getMessage());
+      Console.debug(
+              "RestHandler did not find endpoint '" + endpointName + "'");
+
+      response.setHeader("Cache-Control", "no-cache/no-store");
+      response.setHeader("Content-type", "text/plain");
+      PrintWriter writer = response.getWriter();
+      writer.write(missingEndpointMessage == null
+              ? "REST endpoint '" + endpointName + "' not defined"
+              : missingEndpointMessage);
+      writer.write("\n");
+      writer.write("Available endpoints are:\n");
+      ContextHandler ch = HttpServer.getInstance().getContextHandler(this);
+      String base = HttpServer.getInstance().getUri().toString();
+      String contextPath = ch == null ? "" : ch.getContextPath();
+      for (String key : getEndpoints().keySet())
+      {
+        writer.write(base + contextPath + "/" + key + "\n");
+      }
+      response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+      return;
     }
+
+    response.setHeader("Cache-Control", "no-cache/no-store");
+    response.setHeader("Content-type", "text/plain");
+    EndpointI ep = getNewEndpoint(endpointName);
+    ep.processEndpoint(request, response);
+
+    return;
   }
 
   /**
@@ -96,4 +223,118 @@ public class RestHandler extends AbstractRequestHandler
     return MY_NAME;
   }
 
-}
+  /**
+   * Initialise methods
+   * 
+   * @throws BindException
+   */
+  protected void init() throws BindException
+  {
+    init(MY_PATH);
+  }
+
+  protected void init(String path) throws BindException
+  {
+    setPath(path);
+    // Override this in extended class
+    // e.g. registerHandler and addEndpoints
+  }
+
+  protected void addEndpoint(AbstractEndpoint ep)
+  {
+    if (getEndpoints() == null)
+    {
+      endpoints = new HashMap<>();
+    }
+    endpoints.put(ep.getPath(), ep);
+    Console.debug("REST API, added endpoint '" + ep.getPath() + "'");
+  }
+
+  protected String getRequestedEndpointName(HttpServletRequest request)
+  {
+    String pathInfo = request.getPathInfo();
+    int slashpos = pathInfo.indexOf('/', 1);
+    return slashpos > 1 ? pathInfo.substring(1, slashpos)
+            : pathInfo.substring(1);
+  }
+
+  protected void returnError(HttpServletRequest request,
+          HttpServletResponse response, String message)
+  {
+    returnError(request, response, message,
+            HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+  }
+
+  protected void returnError(HttpServletRequest request,
+          HttpServletResponse response, String message, int statusCode)
+  {
+    response.setStatus(statusCode);
+    String endpointName = getRequestedEndpointName(request);
+    Console.error(getName() + " error: endpoint " + endpointName
+            + " failed: '" + message + "'");
+    try
+    {
+      PrintWriter writer = response.getWriter();
+      writer.write("Endpoint " + endpointName + ": " + message);
+    } catch (IOException e)
+    {
+      Console.debug("Could not write to REST response for endpoint "
+              + endpointName, e);
+    }
+  }
+
+  protected void returnStatus(HttpServletResponse response, String id,
+          Status status)
+  {
+    try
+    {
+      PrintWriter writer = response.getWriter();
+      if (id != null)
+        writer.write("id=" + id + "\n");
+      if (status != null)
+        writer.write("status=" + status.toString() + "\n");
+    } catch (IOException e)
+    {
+      Console.debug("Could not write status to REST response for id:" + id,
+              e);
+    }
+  }
+
+  protected String getRequestBody(HttpServletRequest request,
+          HttpServletResponse response) throws IOException
+  {
+    StringBuilder sb = new StringBuilder();
+    BufferedReader reader = null;
+    Console.debug("REQUEST=" + request.toString());
+    Console.debug("REQUEST.Content-Length=" + request.getContentLength());
+    try
+    {
+      reader = request.getReader();
+      Console.debug("Using getReader()");
+    } catch (IllegalStateException e)
+    {
+      ServletInputStream is = request.getInputStream();
+      reader = new BufferedReader(new InputStreamReader(is));
+      Console.debug("Using getInputStream()");
+    }
+    if (reader != null)
+    {
+      try
+      {
+        String line;
+        while ((line = reader.readLine()) != null)
+        {
+          sb.append(line).append('\n');
+        }
+      } finally
+      {
+        reader.close();
+      }
+    }
+    else
+    {
+      returnError(request, response, "Error reading body of HTTP request");
+    }
+    return sb.toString();
+  }
+}
\ No newline at end of file