JAL-3851 re-engineering endpoints as separate classes
authorBen Soares <b.soares@dundee.ac.uk>
Thu, 2 Sep 2021 13:26:01 +0000 (14:26 +0100)
committerBen Soares <b.soares@dundee.ac.uk>
Thu, 2 Sep 2021 13:26:01 +0000 (14:26 +0100)
src/jalview/rest/Endpoint.java [new file with mode: 0644]
src/jalview/rest/EndpointAsync.java [new file with mode: 0644]
src/jalview/rest/FetchSequenceEndpoint.java [new file with mode: 0644]
src/jalview/rest/OpenSequenceEndpoint.java [new file with mode: 0644]

diff --git a/src/jalview/rest/Endpoint.java b/src/jalview/rest/Endpoint.java
new file mode 100644 (file)
index 0000000..5ee2320
--- /dev/null
@@ -0,0 +1,86 @@
+package jalview.rest;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.PrintWriter;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import jalview.bin.Cache;
+import jalview.rest.RestHandler.EndpointI;
+
+public abstract class Endpoint implements EndpointI
+{
+  private String name = null;
+
+  private API api = null;
+
+  public void setName(String name)
+  {
+    this.name = name;
+  }
+
+  public String getName()
+  {
+    return this.name;
+  }
+
+  protected API getAPI()
+  {
+    return this.api;
+  }
+
+  public abstract void processEndpoint(HttpServletRequest request,
+          HttpServletResponse response);
+
+  /*
+   * Shared methods below here
+   */
+
+  protected String[] getEndpointPathParameters(HttpServletRequest request)
+  {
+    String pathInfo = request.getPathInfo();
+    int slashpos = pathInfo.indexOf('/', 1);
+    return slashpos < 1 ? null
+            : pathInfo.substring(slashpos + 1).split("/");
+  }
+
+  protected void returnError(HttpServletRequest request,
+          HttpServletResponse response, String message)
+  {
+    response.setStatus(500); // set this to something better
+    String endpointName = getName();
+    Cache.error(getAPI().getName() + " error: endpoint " + endpointName
+            + " failed: '" + message + "'");
+    try
+    {
+      PrintWriter writer = response.getWriter();
+      writer.write("Endpoint " + endpointName + ": " + message);
+      writer.close();
+    } catch (IOException e)
+    {
+      Cache.debug(e);
+    }
+  }
+
+  protected String getRequestBody(HttpServletRequest request)
+          throws IOException
+  {
+    StringBuilder sb = new StringBuilder();
+    BufferedReader reader = request.getReader();
+    try
+    {
+      String line;
+      while ((line = reader.readLine()) != null)
+      {
+        sb.append(line).append('\n');
+      }
+    } finally
+    {
+      reader.close();
+    }
+    return sb.toString();
+  }
+
+}
\ No newline at end of file
diff --git a/src/jalview/rest/EndpointAsync.java b/src/jalview/rest/EndpointAsync.java
new file mode 100644 (file)
index 0000000..511d723
--- /dev/null
@@ -0,0 +1,185 @@
+package jalview.rest;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.concurrent.CompletableFuture;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import jalview.bin.Cache;
+import jalview.rest.RestHandler.Status;
+
+public abstract class EndpointAsync extends Endpoint
+{
+  private String name = null;
+
+  private String idExtension = null;
+
+  private String id = null;
+
+  private CompletableFuture<Void> cf = null;
+
+  protected void setCompletableFuture(CompletableFuture<Void> cf)
+  {
+    this.cf = cf;
+  }
+
+  protected CompletableFuture<Void> getCompletableFuture()
+  {
+    return this.cf;
+  }
+
+  protected void setId(String idExtension)
+  {
+    this.id = getName() + "::" + idExtension;
+  }
+
+  protected String getId()
+  {
+    return this.id;
+  }
+
+  /*
+   * Override the three methods
+   * initialise (get parameters, set id (extension), set cf if using an existing one)
+   * process (what to do in the cf if not using an existing one)
+   * finalise (extra stuff to do at the end of the first call to this)
+   */
+  protected void initialise(HttpServletRequest request,
+          HttpServletResponse response)
+  {
+    // should be overridden
+    // must setId(request, extension)
+    setId(request, null);
+  }
+
+  protected abstract void process(HttpServletRequest request,
+          HttpServletResponse response);
+
+  protected void finalise(HttpServletRequest request,
+          HttpServletResponse response)
+  {
+    // can be Overridden
+  }
+
+  protected void passCompletableFuture()
+  {
+    // Override this if you want to use an existing CompletableFuture
+  }
+
+  @Override
+  public void processEndpoint(HttpServletRequest request,
+          HttpServletResponse response)
+  {
+    initialise(request, response);
+    final String id = getId();
+
+    if (checkStatus(request, response))
+      return;
+
+    passCompletableFuture();
+    if (getCompletableFuture() == null)
+    {
+      setCompletableFuture(CompletableFuture.runAsync(() -> {
+        this.process(request, response);
+      }));
+    }
+    finaliseCompletableFuture();
+
+    finalise(request, response);
+
+    returnStatus(response);
+    changeStatus(Status.IN_PROGRESS);
+  }
+
+  /*
+   * Shared methods below here
+   */
+
+  protected String setId(HttpServletRequest request, String extension)
+  {
+    String idString = request.getParameter("id");
+    if (idString == null)
+    {
+      setId(extension);
+      idString = getId();
+    }
+    return idString;
+  }
+
+  protected void changeStatus(Status status)
+  {
+    String id = getId();
+    // don't change a job's status if it has finished or died
+    if (API.getStatusMap().get(id) == Status.FINISHED
+            || API.getStatusMap().get(id) == Status.ERROR)
+      return;
+    API.getStatusMap().put(id, status);
+  }
+
+  protected Status getStatus()
+  {
+    return API.getStatusMap().get(getId());
+  }
+
+  protected void returnStatus(HttpServletResponse response)
+  {
+    String id = getId();
+    try
+    {
+      PrintWriter writer = response.getWriter();
+      if (id != null)
+        writer.write("id=" + id + "\n");
+      if (API.getRequestMap().get(id) != null)
+        writer.write(
+                "request=" + API.getRequestMap().get(id).toString() + "\n");
+      if (API.getStatusMap().get(id) != null)
+      {
+        if (API.getStatusMap().get(id) == Status.ERROR)
+          response.setStatus(500);
+        writer.write(
+                "status=" + API.getStatusMap().get(id).toString() + "\n");
+      }
+    } catch (IOException e)
+    {
+      Cache.debug(e);
+    }
+  }
+
+  protected boolean checkStatus(HttpServletRequest request,
+          HttpServletResponse response)
+  {
+    String id = getId();
+    Status status = API.getStatusMap().get(id);
+    if (status == null)
+    {
+      API.getStatusMap().put(id, Status.STARTED);
+      API.getRequestMap().put(id, request.getRequestURI());
+    }
+    else
+    {
+      returnStatus(response);
+      return true;
+    }
+    return false;
+  }
+
+  protected void finaliseCompletableFuture()
+  {
+    String id = getId();
+    cf.whenComplete((Void, e) -> {
+      if (e != null)
+      {
+        Cache.error("Endpoint job " + id + " did not complete");
+        Cache.debug(e);
+        changeStatus(Status.ERROR);
+      }
+      else
+      {
+        Cache.info("Endpoint job " + id + " completed successfully");
+        changeStatus(Status.FINISHED);
+      }
+    });
+  }
+}
diff --git a/src/jalview/rest/FetchSequenceEndpoint.java b/src/jalview/rest/FetchSequenceEndpoint.java
new file mode 100644 (file)
index 0000000..ef102e7
--- /dev/null
@@ -0,0 +1,48 @@
+package jalview.rest;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import jalview.gui.Desktop;
+import jalview.gui.SequenceFetcher;
+import jalview.util.DBRefUtils;
+
+public class FetchSequenceEndpoint extends EndpointAsync
+{
+  protected String name = "fetchsequence";
+
+  @Override
+  protected void initialise(HttpServletRequest request,
+          HttpServletResponse response)
+  {
+    // note that endpointName should always be "fetchsequence"
+
+    String[] parameters = getEndpointPathParameters(request);
+
+    // check we can run fetchsequence
+    if (parameters.length < 2)
+    {
+      returnError(request, response,
+              "requires 2 path parameters: dbname, ids");
+      return;
+    }
+
+    String dbName = parameters[0];
+    String dbId = parameters[1];
+
+    setId(request, dbName + "::" + dbId);
+    if (checkStatus(request, response))
+      return;
+
+    String db = DBRefUtils.getCanonicalName(dbName);
+    Desktop desktop = Desktop.instance;
+    SequenceFetcher sf = new SequenceFetcher(desktop, db, dbId);
+    setCompletableFuture(sf.ok_actionPerformed(true));
+  }
+
+  protected void process(HttpServletRequest request,
+          HttpServletResponse response)
+  {
+    // all the work being done by the SequenceFetcher!
+  }
+}
diff --git a/src/jalview/rest/OpenSequenceEndpoint.java b/src/jalview/rest/OpenSequenceEndpoint.java
new file mode 100644 (file)
index 0000000..52671ac
--- /dev/null
@@ -0,0 +1,164 @@
+package jalview.rest;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.xml.bind.DatatypeConverter;
+
+import jalview.bin.Cache;
+import jalview.gui.CutAndPasteTransfer;
+import jalview.gui.Desktop;
+
+public class OpenSequenceEndpoint extends EndpointAsync
+{
+  protected String name = "opensequence";
+
+  private String fileString;
+
+  private String urlString;
+
+  private String method;
+
+  private String dataString;
+
+  private String body;
+
+  private String content;
+
+  private boolean post;
+
+  private boolean data;
+
+  String access = null;
+
+  String ref = null;
+
+  @Override
+  protected void initialise(HttpServletRequest request,
+          HttpServletResponse response)
+  {
+    fileString = request.getParameter("file");
+    urlString = request.getParameter("url");
+    method = request.getMethod().toLowerCase();
+    dataString = request.getParameter("data");
+    body = null;
+    post = method.equalsIgnoreCase("post");
+    data = dataString != null;
+
+    if (post)
+    {
+      access = "post";
+      try
+      {
+        body = getRequestBody(request);
+      } catch (IOException e)
+      {
+        returnError(request, response, "could not read POST body");
+        Cache.debug(e);
+        return;
+      }
+      // for ref see md5 later
+    }
+    else if (data)
+    {
+      access = "data";
+      // for ref see md5 later
+    }
+    else if (fileString != null)
+    {
+      access = "file";
+      ref = fileString;
+    }
+    else if (urlString != null)
+    {
+      access = "url";
+      ref = urlString;
+    }
+
+    if (access == null)
+    {
+      returnError(request, response,
+              "requires POST body or one of parameters 'data', 'file' or 'url'");
+      return;
+    }
+
+    // final content used in Future
+    final String content;
+    if (post || data)
+    {
+      content = post ? body : dataString;
+      try
+      {
+        MessageDigest md5 = MessageDigest.getInstance("MD5");
+        md5.update(content.getBytes());
+        byte[] digest = md5.digest();
+        ref = DatatypeConverter.printBase64Binary(digest).toLowerCase();
+      } catch (NoSuchAlgorithmException e)
+      {
+        Cache.debug(e);
+      }
+    }
+    else
+    {
+      content = null;
+    }
+
+    setId(request, access + "::" + ref);
+  }
+
+  protected void process(HttpServletRequest request,
+          HttpServletResponse response)
+  {
+    if (post || data)
+    {
+      // Sequence file contents being posted
+      // use File -> Input Alignment -> from Textbox
+      CutAndPasteTransfer cap = new CutAndPasteTransfer();
+      cap.setText(content);
+      cap.ok_actionPerformed(null);
+      cap.cancel_actionPerformed(null);
+    }
+    else if (fileString != null)
+    {
+      // Sequence file on filesystem
+      // use File -> Input Alignment -> From File
+      URL url = null;
+      File file = null;
+      try
+      {
+        url = new URL(fileString);
+        file = new File(url.toURI());
+      } catch (MalformedURLException | URISyntaxException e)
+      {
+        returnError(request, response,
+                "could not resolve file='" + fileString + "'");
+        Cache.debug(e);
+        return;
+      }
+      if (!file.exists())
+      {
+        returnError(request, response,
+                "file='" + fileString + "' does not exist");
+        return;
+      }
+      Desktop.instance.openFile(file, null, null);
+    }
+    else if (urlString != null)
+    {
+      boolean success = Desktop.instance.loadUrl(urlString, null);
+      if (!success)
+      {
+        returnError(request, response,
+                "url='" + urlString + "' could not be opened");
+        return;
+      }
+    }
+  }
+}