JAL-3851 API handling of rest calls
authorBen Soares <b.soares@dundee.ac.uk>
Thu, 2 Sep 2021 13:25:15 +0000 (14:25 +0100)
committerBen Soares <b.soares@dundee.ac.uk>
Thu, 2 Sep 2021 13:25:15 +0000 (14:25 +0100)
src/jalview/bin/Jalview.java
src/jalview/gui/Desktop.java
src/jalview/gui/SequenceFetcher.java
src/jalview/httpserver/HttpServer.java
src/jalview/rest/API.java [new file with mode: 0644]
src/jalview/rest/RestHandler.java

index 4c21624..3810d3f 100755 (executable)
@@ -27,6 +27,7 @@ import java.io.IOException;
 import java.io.InputStreamReader;
 import java.io.OutputStreamWriter;
 import java.io.PrintWriter;
+import java.net.BindException;
 import java.net.MalformedURLException;
 import java.net.URI;
 import java.net.URISyntaxException;
@@ -54,6 +55,7 @@ import jalview.ext.so.SequenceOntology;
 import jalview.gui.AlignFrame;
 import jalview.gui.Desktop;
 import jalview.gui.PromptUserConfig;
+import jalview.httpserver.HttpServer;
 import jalview.io.AppletFormatAdapter;
 import jalview.io.BioJsHTMLOutput;
 import jalview.io.DataSourceType;
@@ -66,6 +68,7 @@ import jalview.io.HtmlSvgOutput;
 import jalview.io.IdentifyFile;
 import jalview.io.NewickFile;
 import jalview.io.gff.SequenceOntologyFactory;
+import jalview.rest.API;
 import jalview.schemes.ColourSchemeI;
 import jalview.schemes.ColourSchemeProperty;
 import jalview.util.ChannelProperties;
@@ -475,6 +478,36 @@ public class Jalview
       }
     }
 
+    // set the jetty port if suggested
+    String sPort = aparser.getValue("serverport");
+    if (sPort != null)
+    {
+      int port = 0;
+      try
+      {
+        port = Integer.parseInt(sPort);
+        HttpServer.setSuggestedPort(port);
+        Cache.info("Set suggested server port to " + port);
+      } catch (NumberFormatException e)
+      {
+        Cache.warn("server_port '" + sPort + "' not parseable as Integer");
+      }
+    }
+    // Start a TestListener
+    if (aparser.contains("genomeapi"))
+    {
+      try
+      {
+        API gb = API.getInstance();
+        Cache.info(gb.getName() + " started at "
+                + HttpServer.getInstance().getUri().toString());
+      } catch (BindException e)
+      {
+        Cache.warn("Could not open a genomeapi");
+        Cache.error(e);
+      }
+    }
+
     // Move any new getdown-launcher-new.jar into place over old
     // getdown-launcher.jar
     String appdirString = System.getProperty("getdownappdir");
index 195a313..b50ff07 100644 (file)
@@ -398,16 +398,17 @@ public class Desktop extends jalview.jbgui.GDesktop
             + Cache.getProperty("VERSION"));
 
     /**
-     * Set taskbar "grouped windows" name for linux desktops (works in GNOME and KDE).
-     * This uses sun.awt.X11.XToolkit.awtAppClassName which is not officially documented or
-     * guaranteed to exist, so we access it via reflection.
-     * There appear to be unfathomable criteria about what this string can contain, and it if doesn't
-     * meet those criteria then "java" (KDE) or "jalview-bin-Jalview" (GNOME) is used.
-     * "Jalview", "Jalview Develop" and "Jalview Test" seem okay, but "Jalview non-release" does not.
-     * The reflection access may generate a warning:
-     * WARNING: An illegal reflective access operation has occurred
-     * WARNING: Illegal reflective access by jalview.gui.Desktop () to field sun.awt.X11.XToolkit.awtAppClassName
-     * which I don't think can be avoided.
+     * Set taskbar "grouped windows" name for linux desktops (works in GNOME and
+     * KDE). This uses sun.awt.X11.XToolkit.awtAppClassName which is not
+     * officially documented or guaranteed to exist, so we access it via
+     * reflection. There appear to be unfathomable criteria about what this
+     * string can contain, and it if doesn't meet those criteria then "java"
+     * (KDE) or "jalview-bin-Jalview" (GNOME) is used. "Jalview", "Jalview
+     * Develop" and "Jalview Test" seem okay, but "Jalview non-release" does
+     * not. The reflection access may generate a warning: WARNING: An illegal
+     * reflective access operation has occurred WARNING: Illegal reflective
+     * access by jalview.gui.Desktop () to field
+     * sun.awt.X11.XToolkit.awtAppClassName which I don't think can be avoided.
      */
     if (Platform.isLinux())
     {
@@ -417,7 +418,8 @@ public class Desktop extends jalview.jbgui.GDesktop
         Field[] declaredFields = xToolkit.getClass().getDeclaredFields();
         Field awtAppClassNameField = null;
 
-        if (Arrays.stream(declaredFields).anyMatch(f -> f.getName().equals("awtAppClassName")))
+        if (Arrays.stream(declaredFields)
+                .anyMatch(f -> f.getName().equals("awtAppClassName")))
         {
           awtAppClassNameField = xToolkit.getClass()
                   .getDeclaredField("awtAppClassName");
@@ -441,8 +443,9 @@ public class Desktop extends jalview.jbgui.GDesktop
     }
 
     /**
-     * APQHandlers sets handlers for About, Preferences and Quit actions peculiar to macOS's application menu.
-     * APQHandlers will check to see if a handler is supported before setting it.
+     * APQHandlers sets handlers for About, Preferences and Quit actions
+     * peculiar to macOS's application menu. APQHandlers will check to see if a
+     * handler is supported before setting it.
      */
     try
     {
@@ -1187,33 +1190,38 @@ public class Desktop extends jalview.jbgui.GDesktop
       {
         File selectedFile = chooser.getSelectedFile();
         Cache.setProperty("LAST_DIRECTORY", selectedFile.getParent());
-
         FileFormatI format = chooser.getSelectedFormat();
-
-        /*
-         * Call IdentifyFile to verify the file contains what its extension implies.
-         * Skip this step for dynamically added file formats, because
-         * IdentifyFile does not know how to recognise them.
-         */
-        if (FileFormats.getInstance().isIdentifiable(format))
-        {
-          try
-          {
-            format = new IdentifyFile().identify(selectedFile,
-                    DataSourceType.FILE);
-          } catch (FileFormatException e)
-          {
-            // format = null; //??
-          }
-        }
-
-        new FileLoader().LoadFile(viewport, selectedFile,
-                DataSourceType.FILE, format);
+        openFile(selectedFile, format, viewport);
       }
     });
     chooser.showOpenDialog(this);
   }
 
+  public void openFile(File selectedFile, FileFormatI format,
+          AlignViewport viewport)
+  {
+
+    /*
+     * Call IdentifyFile to verify the file contains what its extension implies.
+     * Skip this step for dynamically added file formats, because
+     * IdentifyFile does not know how to recognise them.
+     */
+    if (FileFormats.getInstance().isIdentifiable(format))
+    {
+      try
+      {
+        format = new IdentifyFile().identify(selectedFile,
+                DataSourceType.FILE);
+      } catch (FileFormatException e)
+      {
+        // format = null; //??
+      }
+    }
+
+    new FileLoader().LoadFile(viewport, selectedFile, DataSourceType.FILE,
+            format);
+  }
+
   /**
    * Shows a dialog for input of a URL at which to retrieve alignment data
    * 
@@ -1277,51 +1285,14 @@ public class Desktop extends jalview.jbgui.GDesktop
                 : ((JComboBox<String>) history).getEditor().getItem()
                         .toString().trim());
 
-        if (url.toLowerCase().endsWith(".jar"))
-        {
-          if (viewport != null)
-          {
-            new FileLoader().LoadFile(viewport, url, DataSourceType.URL,
-                    FileFormat.Jalview);
-          }
-          else
-          {
-            new FileLoader().LoadFile(url, DataSourceType.URL,
-                    FileFormat.Jalview);
-          }
-        }
-        else
+        if (!loadUrl(url, viewport))
         {
-          FileFormatI format = null;
-          try
-          {
-            format = new IdentifyFile().identify(url, DataSourceType.URL);
-          } catch (FileFormatException e)
-          {
-            // TODO revise error handling, distinguish between
-            // URL not found and response not valid
-          }
-
-          if (format == null)
-          {
-            String msg = MessageManager
-                    .formatMessage("label.couldnt_locate", url);
-            JvOptionPane.showInternalMessageDialog(Desktop.desktop, msg,
-                    MessageManager.getString("label.url_not_found"),
-                    JvOptionPane.WARNING_MESSAGE);
-
-            return;
-          }
 
-          if (viewport != null)
-          {
-            new FileLoader().LoadFile(viewport, url, DataSourceType.URL,
-                    format);
-          }
-          else
-          {
-            new FileLoader().LoadFile(url, DataSourceType.URL, format);
-          }
+          String msg = MessageManager.formatMessage("label.couldnt_locate",
+                  url);
+          JvOptionPane.showInternalMessageDialog(Desktop.desktop, msg,
+                  MessageManager.getString("label.url_not_found"),
+                  JvOptionPane.WARNING_MESSAGE);
         }
       }
     };
@@ -1334,6 +1305,34 @@ public class Desktop extends jalview.jbgui.GDesktop
                     MessageManager.getString("action.ok"));
   }
 
+  public boolean loadUrl(String url, AlignViewport viewport)
+  {
+    FileFormatI format = null;
+    try
+    {
+      format = new IdentifyFile().identify(url, DataSourceType.URL);
+    } catch (FileFormatException e)
+    {
+      // TODO revise error handling, distinguish between
+      // URL not found and response not valid
+    }
+
+    if (format == null)
+    {
+      return false;
+    }
+
+    if (viewport != null)
+    {
+      new FileLoader().LoadFile(viewport, url, DataSourceType.URL, format);
+    }
+    else
+    {
+      new FileLoader().LoadFile(url, DataSourceType.URL, format);
+    }
+    return true;
+  }
+
   /**
    * Opens the CutAndPaste window for the user to paste an alignment in to
    * 
index 8b5d3b7..bba5ac0 100755 (executable)
  */
 package jalview.gui;
 
-import jalview.api.FeatureSettingsModelI;
-import jalview.bin.Cache;
-import jalview.datamodel.AlignmentI;
-import jalview.datamodel.DBRefEntry;
-import jalview.datamodel.SequenceI;
-import jalview.fts.core.GFTSPanel;
-import jalview.fts.service.pdb.PDBFTSPanel;
-import jalview.fts.service.uniprot.UniprotFTSPanel;
-import jalview.io.FileFormatI;
-import jalview.io.gff.SequenceOntologyI;
-import jalview.util.DBRefUtils;
-import jalview.util.MessageManager;
-import jalview.util.Platform;
-import jalview.ws.seqfetcher.DbSourceProxy;
-
 import java.awt.BorderLayout;
 import java.awt.Font;
 import java.awt.event.ActionEvent;
@@ -46,6 +31,7 @@ import java.util.Arrays;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
+import java.util.concurrent.CompletableFuture;
 
 import javax.swing.JButton;
 import javax.swing.JCheckBox;
@@ -57,6 +43,21 @@ import javax.swing.JScrollPane;
 import javax.swing.JTextArea;
 import javax.swing.SwingConstants;
 
+import jalview.api.FeatureSettingsModelI;
+import jalview.bin.Cache;
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.DBRefEntry;
+import jalview.datamodel.SequenceI;
+import jalview.fts.core.GFTSPanel;
+import jalview.fts.service.pdb.PDBFTSPanel;
+import jalview.fts.service.uniprot.UniprotFTSPanel;
+import jalview.io.FileFormatI;
+import jalview.io.gff.SequenceOntologyI;
+import jalview.util.DBRefUtils;
+import jalview.util.MessageManager;
+import jalview.util.Platform;
+import jalview.ws.seqfetcher.DbSourceProxy;
+
 /**
  * A panel where the use may choose a database source, and enter one or more
  * accessions, to retrieve entries from the database.
@@ -145,8 +146,8 @@ public class SequenceFetcher extends JPanel implements Runnable
 
     frame = new JInternalFrame();
     frame.setContentPane(this);
-    Desktop.addInternalFrame(frame, getFrameTitle(), true, 400, 
-               Platform.isAMacAndNotJS() ? 240 : 180);
+    Desktop.addInternalFrame(frame, getFrameTitle(), true, 400,
+            Platform.isAMacAndNotJS() ? 240 : 180);
   }
 
   private String getFrameTitle()
@@ -414,6 +415,11 @@ public class SequenceFetcher extends JPanel implements Runnable
    */
   public void ok_actionPerformed()
   {
+    ok_actionPerformed(false);
+  }
+
+  public CompletableFuture ok_actionPerformed(boolean returnFuture)
+  {
     /*
      * tidy inputs and check there is something to search for
      */
@@ -424,9 +430,9 @@ public class SequenceFetcher extends JPanel implements Runnable
       text = text.replace(",", ";");
     }
     text = text.replaceAll("(\\s|[; ])+", ";");
-    if (!t0.equals(text)) 
+    if (!t0.equals(text))
     {
-         textArea.setText(text);
+      textArea.setText(text);
     }
     if (text.isEmpty())
     {
@@ -434,14 +440,14 @@ public class SequenceFetcher extends JPanel implements Runnable
       showErrorMessage(
               "Please enter a (semi-colon separated list of) database id(s)");
       resetDialog();
-      return;
+      return null;
     }
     if (database.getSelectedIndex() == 0)
     {
       // todo i18n
       showErrorMessage("Please choose a database");
       resetDialog();
-      return;
+      return null;
     }
 
     exampleBtn.setEnabled(false);
@@ -450,8 +456,10 @@ public class SequenceFetcher extends JPanel implements Runnable
     closeBtn.setEnabled(false);
     backBtn.setEnabled(false);
 
-    Thread worker = new Thread(this);
-    worker.start();
+    CompletableFuture<Void> worker = CompletableFuture
+            .runAsync(new Thread(this));
+
+    return returnFuture ? worker : null;
   }
 
   private void resetDialog()
@@ -761,12 +769,12 @@ public class SequenceFetcher extends JPanel implements Runnable
 
     for (String q : queries)
     {
-       // BH 2019.01.25 dbr is never used.
-//      DBRefEntry dbr = new DBRefEntry();
-//      dbr.setSource(proxy.getDbSource());
-//      dbr.setVersion(null);
+      // BH 2019.01.25 dbr is never used.
+      // DBRefEntry dbr = new DBRefEntry();
+      // dbr.setSource(proxy.getDbSource());
+      // dbr.setVersion(null);
       String accId = proxy.getAccessionIdFromQuery(q);
-//      dbr.setAccessionId(accId);
+      // dbr.setAccessionId(accId);
       boolean rfound = false;
       for (int r = 0, nr = rs.length; r < nr; r++)
       {
index a18d38d..397f1a8 100644 (file)
@@ -20,8 +20,6 @@
  */
 package jalview.httpserver;
 
-import jalview.rest.RestHandler;
-
 import java.net.BindException;
 import java.net.URI;
 import java.util.Collections;
@@ -31,6 +29,7 @@ import java.util.Map;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
+import org.eclipse.jetty.server.Connector;
 import org.eclipse.jetty.server.Handler;
 import org.eclipse.jetty.server.Server;
 import org.eclipse.jetty.server.ServerConnector;
@@ -38,6 +37,8 @@ import org.eclipse.jetty.server.handler.ContextHandler;
 import org.eclipse.jetty.server.handler.HandlerCollection;
 import org.eclipse.jetty.util.thread.QueuedThreadPool;
 
+import jalview.rest.RestHandler;
+
 /**
  * An HttpServer built on Jetty. To use it
  * <ul>
@@ -82,6 +83,14 @@ public class HttpServer
    */
   private URI contextRoot;
 
+  /*
+   * The port of the server.  This can be set before starting the instance
+   * as a suggested port to use (it is not guaranteed).
+   * The value will be set to the actual port being used after the instance
+   * is started.
+   */
+  private static int PORT = 0;
+
   /**
    * Returns the singleton instance of this class.
    * 
@@ -126,8 +135,9 @@ public class HttpServer
     try
     {
       /*
-       * Create a server with a small number of threads; jetty will allocate a
-       * free port
+       * Create a server with a small number of threads;
+       * If PORT has been set then jetty will try and use this, otherwise
+       * jetty will allocate a free port
        */
       QueuedThreadPool tp = new QueuedThreadPool(4, 1); // max, min
       server = new Server(tp);
@@ -135,6 +145,10 @@ public class HttpServer
       ServerConnector connector = new ServerConnector(server, 0, 2);
       // restrict to localhost
       connector.setHost("localhost");
+      if (PORT > 0)
+      {
+        connector.setPort(PORT);
+      }
       server.addConnector(connector);
 
       /*
@@ -150,6 +164,15 @@ public class HttpServer
       contextHandlers = new HandlerCollection(true);
       server.setHandler(contextHandlers);
       server.start();
+      Connector[] cs = server.getConnectors();
+      if (cs.length > 0)
+      {
+        if (cs[0] instanceof ServerConnector)
+        {
+          ServerConnector c = (ServerConnector) cs[0];
+          PORT = c.getPort();
+        }
+      }
       // System.out.println(String.format(
       // "HttpServer started with %d threads", server.getThreadPool()
       // .getThreads()));
@@ -297,4 +320,27 @@ public class HttpServer
               + " handler on " + handler.getUri());
     }
   }
+
+  /**
+   * This sets the "suggested" port to use. It can only be called once before
+   * starting the HttpServer instance. After the server has actually started the
+   * port is set to the actual port being used and cannot be changed.
+   * 
+   * @param port
+   * @return successful change
+   */
+  public static boolean setSuggestedPort(int port)
+  {
+    if (port < 1 || PORT > 0)
+    {
+      return false;
+    }
+    PORT = port;
+    return true;
+  }
+
+  public static int getPort()
+  {
+    return PORT;
+  }
 }
diff --git a/src/jalview/rest/API.java b/src/jalview/rest/API.java
new file mode 100644 (file)
index 0000000..aada285
--- /dev/null
@@ -0,0 +1,402 @@
+package jalview.rest;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.net.BindException;
+import java.net.MalformedURLException;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+
+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;
+import jalview.gui.SequenceFetcher;
+import jalview.util.DBRefUtils;
+
+public class API extends RestHandler
+{
+  private static final String MY_PATH = "api";
+
+  private static final String MY_NAME = "Jalview API";
+
+  private static Map<String, Status> statusMap = new HashMap<>();
+
+  private static Map<String, String> requestMap = new HashMap<>();
+
+  private static API instance = null;
+
+  public static API getInstance() throws BindException
+  {
+    synchronized (API.class)
+    {
+      if (instance == null)
+      {
+        instance = new API();
+      }
+    }
+    return instance;
+  }
+
+  private API() throws BindException
+  {
+    super();
+  }
+
+  private boolean init = false;
+
+  @Override
+  protected void init() throws BindException
+  {
+    if (init)
+      return;
+    super.init(MY_PATH);
+
+    // add endpoints here
+    addEndpoint("fetchsequence", (String s, HttpServletRequest rq,
+            HttpServletResponse rs) -> fetchSequence(s, rq, rs));
+    addEndpoint("opensequence", (String s, HttpServletRequest rq,
+            HttpServletResponse rs) -> openSequence(s, rq, rs));
+    addEndpoint("highlight", (String s, HttpServletRequest rq,
+            HttpServletResponse rs) -> highlight(s, rq, rs));
+
+    setPath(MY_PATH);
+    this.registerHandler();
+
+    init = true;
+  }
+
+  // for the "fetchsequence" endpoint
+  private void fetchSequence(String endpointName,
+          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];
+
+    // final for async later
+    final String id = getId(request, endpointName, dbName + "::" + dbId);
+    if (checkStatus(request, response, id))
+      return;
+
+    String db = DBRefUtils.getCanonicalName(dbName);
+    Desktop desktop = Desktop.instance;
+    SequenceFetcher sf = new SequenceFetcher(desktop, db, dbId);
+    CompletableFuture<Void> cf = sf.ok_actionPerformed(true);
+
+    setFuture(cf, id);
+    returnStatus(response, id);
+    changeStatus(id, Status.IN_PROGRESS);
+  }
+
+  // for the "opensequence" endpoint
+  private void openSequence(String endpointName, HttpServletRequest request,
+          HttpServletResponse response)
+  {
+    // note that endpointName should always be "opensequence"
+    String fileString = request.getParameter("file");
+    String urlString = request.getParameter("url");
+    String method = request.getMethod().toLowerCase();
+    String dataString = request.getParameter("data");
+    String body = null;
+    boolean post = method.equalsIgnoreCase("post");
+    boolean data = dataString != null;
+
+    String access = null;
+    String ref = 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;
+    }
+
+    // final for async later
+    final String id = getId(request, endpointName, access + "::" + ref);
+    if (checkStatus(request, response, id))
+      return;
+
+    CompletableFuture<Void> cf = CompletableFuture.runAsync(() -> {
+      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;
+        }
+      }
+    });
+
+    setFuture(cf, id);
+    returnStatus(response, id);
+    changeStatus(id, Status.IN_PROGRESS);
+  }
+
+  // for the "highlight" endpoint
+  private void highlight(String endpointName, HttpServletRequest request,
+          HttpServletResponse response)
+  {
+    String[] parameters = getEndpointPathParameters(request);
+
+    // check we can run highlight
+    if (parameters.length < 1)
+    {
+      returnError(request, response, "requires 1 path parameters: ranges");
+      return;
+    }
+
+    String rangesString = parameters[0];
+    String[] rangeStrings = rangesString.split(",");
+    int[][] ranges = new int[rangeStrings.length][2];
+    for (int i = 0; i < rangeStrings.length; i++)
+    {
+      String range = rangeStrings[i];
+      try
+      {
+        int hyphenpos = range.indexOf('-');
+        if (hyphenpos < 0)
+        {
+          ranges[i][0] = Integer.parseInt(range);
+          ranges[i][1] = ranges[i][0];
+        }
+        else
+        {
+          ranges[i][0] = Integer.parseInt(range.substring(0, hyphenpos));
+          ranges[i][1] = Integer.parseInt(range.substring(hyphenpos));
+        }
+      } catch (NumberFormatException nfe)
+      {
+        returnError(request, response,
+                "couldn't parse ranges component '" + range + "'");
+        return;
+      }
+    }
+
+  }
+
+  // template endpoint
+  private void templateEndpoint(String endpointName,
+          HttpServletRequest request, HttpServletResponse response)
+  {
+    // final for async later
+    final String id = getId(request, endpointName,
+            "some identifier for this endpoint request");
+    if (checkStatus(request, response, id))
+      return;
+
+    CompletableFuture<Void> cf = CompletableFuture.runAsync(() -> {
+      // do stuff
+    });
+
+    setFuture(cf, id);
+    returnStatus(response, id);
+    changeStatus(id, Status.IN_PROGRESS);
+  }
+
+  /*
+   * Shared methods below here
+   */
+
+  @Override
+  public String getPath()
+  {
+    return MY_PATH;
+  }
+
+  private void changeStatus(String id, Status status)
+  {
+    // don't change a job's status if it has finished or died
+    if (statusMap.get(id) == Status.FINISHED
+            || statusMap.get(id) == Status.ERROR)
+      return;
+    statusMap.put(id, status);
+  }
+
+  private Status getStatus(String id)
+  {
+    return statusMap.get(id);
+  }
+
+  protected void returnStatus(HttpServletResponse response, String id)
+  {
+    try
+    {
+      PrintWriter writer = response.getWriter();
+      if (id != null)
+        writer.write("id=" + id + "\n");
+      if (requestMap.get(id) != null)
+        writer.write("request=" + requestMap.get(id).toString() + "\n");
+      if (statusMap.get(id) != null)
+      {
+        if (statusMap.get(id) == Status.ERROR)
+          response.setStatus(500);
+        writer.write("status=" + statusMap.get(id).toString() + "\n");
+      }
+    } catch (IOException e)
+    {
+      Cache.debug(e);
+    }
+  }
+
+  private String getId(HttpServletRequest request, String endpointName,
+          String extra)
+  {
+    String idString = request.getParameter("id");
+    if (idString == null)
+    {
+      idString = endpointName + "::" + extra;
+    }
+    return idString;
+  }
+
+  private boolean checkStatus(HttpServletRequest request,
+          HttpServletResponse response, String id)
+  {
+    Status status = statusMap.get(id);
+    if (status == null)
+    {
+      statusMap.put(id, Status.STARTED);
+      requestMap.put(id, request.getRequestURI());
+    }
+    else
+    {
+      returnStatus(response, id);
+      return true;
+    }
+    return false;
+  }
+
+  private void setFuture(CompletableFuture<Void> cf, String id)
+  {
+    cf.whenComplete((Void, e) -> {
+      if (e != null)
+      {
+        Cache.error("API job " + id + " did not complete");
+        Cache.debug(e);
+        changeStatus(id, Status.ERROR);
+      }
+      else
+      {
+        Cache.info("API job " + id + " completed successfully");
+        changeStatus(id, Status.FINISHED);
+      }
+    });
+  }
+
+  protected static Map<String, Status> getStatusMap()
+  {
+    return statusMap;
+  }
+
+  protected static Map<String, String> getRequestMap()
+  {
+    return requestMap;
+  }
+}
index a37882f..9c103c4 100644 (file)
  */
 package jalview.rest;
 
-import jalview.httpserver.AbstractRequestHandler;
-
+import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.PrintWriter;
 import java.net.BindException;
+import java.util.HashMap;
+import java.util.Map;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
+import jalview.bin.Cache;
+import jalview.httpserver.AbstractRequestHandler;
+
 /**
  * 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
+  }
+
+  public interface EndpointI
+  {
+    public void setName(String name);
+
+    public String getName();
+
+    public void processEndpoint(HttpServletRequest request,
+            HttpServletResponse response);
+  }
+
+  public interface Endpoint
+  {
+    public void processEndpoint(String endpointName,
+            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, Endpoint> endpoints = null;
+
   /**
    * Singleton instance of this class
    */
@@ -66,9 +99,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
@@ -90,17 +123,46 @@ public class RestHandler extends AbstractRequestHandler
      * 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.getMethod() + ": " + request.getRequestURL()
-            + (queryString == null ? "" : "?" + queryString);
-    System.out.println(reply);
+    System.out.println(request.toString());
+    if (endpoints == null)
+    {
+      final String queryString = request.getQueryString();
+      final String reply = "REST not yet implemented; received "
+              + request.getMethod() + ": " + request.getRequestURL()
+              + (queryString == null ? "" : "?" + queryString);
+      System.out.println(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();
+      return;
+    }
+
+    String endpointName = getEndpointName(request);
+
+    if (!endpoints.containsKey(endpointName)
+            || endpoints.get(endpointName) == null)
+    {
+
+      response.setHeader("Cache-Control", "no-cache/no-store");
+      response.setHeader("Content-type", "text/plain");
+      response.setStatus(400);
+      PrintWriter writer = response.getWriter();
+      writer.write(missingEndpointMessage == null
+              ? "REST endpoint '" + endpointName + "' not defined"
+              : missingEndpointMessage);
+      writer.close();
+      return;
+    }
 
     response.setHeader("Cache-Control", "no-cache/no-store");
     response.setHeader("Content-type", "text/plain");
-    final PrintWriter writer = response.getWriter();
-    writer.write(reply);
-    writer.close();
+    Endpoint ep = endpoints.get(endpointName);
+    ep.processEndpoint(endpointName, request, response);
+
+    return;
   }
 
   /**
@@ -112,4 +174,100 @@ 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 boolean addEndpoint(String name, Endpoint ep)
+  {
+    if (endpoints == null)
+    {
+      endpoints = new HashMap<>();
+    }
+    endpoints.put(name, ep);
+    return true;
+  }
+
+  protected String getEndpointName(HttpServletRequest request)
+  {
+    String pathInfo = request.getPathInfo();
+    int slashpos = pathInfo.indexOf('/', 1);
+    return slashpos > 1 ? pathInfo.substring(1, slashpos)
+            : pathInfo.substring(1);
+  }
+
+  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 = getEndpointName(request);
+    Cache.error(this.MY_NAME + " error: endpoint " + endpointName
+            + " failed: '" + message + "'");
+    try
+    {
+      PrintWriter writer = response.getWriter();
+      writer.write("Endpoint " + endpointName + ": " + message);
+      writer.close();
+    } catch (IOException e)
+    {
+      Cache.debug(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)
+    {
+      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