JAL-3851 merged to develop 2022-03-22
authorBen Soares <b.soares@dundee.ac.uk>
Tue, 22 Mar 2022 18:42:30 +0000 (18:42 +0000)
committerBen Soares <b.soares@dundee.ac.uk>
Tue, 22 Mar 2022 18:42:30 +0000 (18:42 +0000)
18 files changed:
doc/api_specification.md [new file with mode: 0644]
src/jalview/bin/Jalview.java
src/jalview/gui/AlignFrame.java
src/jalview/gui/Desktop.java
src/jalview/gui/SequenceFetcher.java
src/jalview/httpserver/AbstractRequestHandler.java
src/jalview/httpserver/HttpServer.java
src/jalview/rest/API.java [new file with mode: 0644]
src/jalview/rest/AbstractEndpoint.java [new file with mode: 0644]
src/jalview/rest/AbstractEndpointAsync.java [new file with mode: 0644]
src/jalview/rest/FetchSequencesEndpoint.java [new file with mode: 0644]
src/jalview/rest/GetCrossReferencesEndpoint.java [new file with mode: 0644]
src/jalview/rest/HighlightSequenceEndpoint.java [new file with mode: 0644]
src/jalview/rest/InputAlignmentEndpoint.java [new file with mode: 0644]
src/jalview/rest/RestHandler.java
src/jalview/rest/SelectSequencesEndpoint.java [new file with mode: 0644]
utils/eclipse/eclipse_run.sh [new file with mode: 0755]
wget_commands.sh [new file with mode: 0644]

diff --git a/doc/api_specification.md b/doc/api_specification.md
new file mode 100644 (file)
index 0000000..8ec1baa
--- /dev/null
@@ -0,0 +1,40 @@
+# Jalview Rest(-like) API
+
+## Launch Jalview with API active on specified port
+
+`java -jar jalview_all.jar -nosplash -nowebservicediscovery -startapi -serverport 2021`
+(-nosplash and -nowebservicediscovery just to save time/output)
+
+## Open a MSA window with existing data
+
+`*POST* http://localhost:2021/jalview/api/inputalignment`
+with body of HTTP request being content of file (automatic parsing by Jalview)
+
+`*GET* http://localhost:2021/jalview/api/inputalignment?data=<data>`
+with <data> being content of file (limited size)
+
+`*GET* http://localhost:2021/jalview/api/inputalignment?file=<fileURI>`
+with <fileURI> being a file
+
+`*GET* http://localhost:2021/jalview/api/inputalignment?url=<URL>`
+with <URL> being a URL to a file
+
+## Open a MSA window with online resource fetch
+
+`http://localhost:2021/jalview/api/fetchsequences/<resource name>/<sequence ids>`
+<resource name> is one of "ensemble", "pdb", "ensembl-tr", "ensembl-gn", ["uniprotkb/swiss-prot", "uniprotkb/trembl", ""uniprot/sptrembl", "uniprot/swissprot" not yet implemented]
+<sequence ids> ids for fetch
+
+### optional parameters
+`id=<your specified id for this request>`
+
+### Returns (as key=value pairs in the body of the response)
+```
+id=<the given id for this request>
+status=<STARTED|IN_PROGRESS|FINISHED|ERROR|NOT_RUN>
+
+
+
+
+
+
index fc4c821..2499cb8 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;
@@ -56,6 +57,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;
@@ -68,6 +70,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;
@@ -490,6 +493,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);
+        Console.info("Set suggested server port to " + port);
+      } catch (NumberFormatException e)
+      {
+        Console.warn(
+                "server_port '" + sPort + "' not parseable as Integer");
+      }
+    }
+    // Start a TestListener
+    if (aparser.contains("startapi"))
+    {
+      try
+      {
+        API api = API.getInstance();
+        Console.info(api.getName() + " started at "
+                + HttpServer.getInstance().getUri().toString());
+      } catch (BindException e)
+      {
+        Console.warn("Could not open a genomeapi", e);
+      }
+    }
+
     // Move any new getdown-launcher-new.jar into place over old
     // getdown-launcher.jar
     String appdirString = System.getProperty("getdownappdir");
index e24cbea..168f6c5 100644 (file)
@@ -57,9 +57,12 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Deque;
 import java.util.Enumeration;
+import java.util.HashMap;
 import java.util.Hashtable;
 import java.util.List;
+import java.util.Map;
 import java.util.Vector;
+import java.util.concurrent.CompletableFuture;
 
 import javax.swing.ButtonGroup;
 import javax.swing.JCheckBoxMenuItem;
@@ -4465,6 +4468,22 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
     return showp;
   }
 
+  public List<String> getProducts()
+  {
+    SequenceI[] seqs = viewport.getAlignment().getSequencesArray();
+    AlignmentI dataset = viewport.getAlignment().getDataset();
+
+    boolean dna = viewport.getAlignment().isNucleotide();
+
+    if (seqs == null || seqs.length == 0)
+    {
+      // nothing to see here.
+      return null;
+    }
+
+    return new CrossRef(seqs, dataset).findXrefSourcesForSequences(dna);
+  }
+
   /**
    * Finds and displays cross-references for the selected sequences (protein
    * products for nucleotide sequences, dna coding sequences for peptides).
@@ -4476,12 +4495,37 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
    * @param source
    *          the database to show cross-references for
    */
+  /*
   protected void showProductsFor(final SequenceI[] sel, final boolean _odna,
           final String source)
   {
     new Thread(CrossRefAction.getHandlerFor(sel, _odna, source, this))
             .start();
   }
+  */
+  protected void showProductsFor(final SequenceI[] sel, final boolean _odna,
+          final String source)
+  {
+    showProductsFor(sel, _odna, source, false, null);
+  }
+
+  public CompletableFuture<Void> showProductsFor(final SequenceI[] sel,
+          final boolean _odna, final String source, boolean returnFuture,
+          String id)
+  {
+    CompletableFuture<Void> cf = CompletableFuture
+            .runAsync(() -> runCrossRefActionAndCacheAlignFrame(sel, _odna,
+                    source, returnFuture, id));
+    return returnFuture ? cf : null;
+  }
+
+  private void runCrossRefActionAndCacheAlignFrame(SequenceI[] sel,
+          boolean _odna, String source, boolean cacheAlignFrame, String id)
+  {
+    final AlignFrame af = this;
+    CrossRefAction.getHandlerFor(sel, _odna, source, af).run();
+    af.cacheAlignFrameFromRestId(id);
+  }
 
   /**
    * Construct and display a new frame containing the translation of this
@@ -5419,6 +5463,7 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
               }
             }
           }
+
         });
       }
     }).start();
@@ -5905,6 +5950,27 @@ public class AlignFrame extends GAlignFrame implements DropTargetListener,
   {
     return lastFeatureSettingsBounds;
   }
+
+  /*
+   * Caching hashmaps for jalview.rest.API
+   */
+  private static Map<String, AlignFrame> alignFrameMap = null;
+
+  public static AlignFrame getAlignFrameFromRestId(String id)
+  {
+    if (id == null || alignFrameMap == null)
+      return null;
+    return alignFrameMap.get(id);
+  }
+
+  public void cacheAlignFrameFromRestId(String id)
+  {
+    if (id == null)
+      return;
+    if (alignFrameMap == null)
+      alignFrameMap = new HashMap<>();
+    alignFrameMap.put(id, this);
+  }
 }
 
 class PrintThread extends Thread
index aeb0fac..b829b5f 100644 (file)
@@ -20,8 +20,6 @@
  */
 package jalview.gui;
 
-import java.util.Locale;
-
 import java.awt.BorderLayout;
 import java.awt.Color;
 import java.awt.Dimension;
@@ -64,6 +62,7 @@ import java.util.HashMap;
 import java.util.Hashtable;
 import java.util.List;
 import java.util.ListIterator;
+import java.util.Locale;
 import java.util.Vector;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
@@ -482,6 +481,8 @@ public class Desktop extends jalview.jbgui.GDesktop
     showMemusage.setSelected(selmemusage);
     desktop.setBackground(Color.white);
 
+    this.setIconImages(ChannelProperties.getIconList());
+
     getContentPane().setLayout(new BorderLayout());
     // alternate config - have scrollbars - see notes in JAL-153
     // JScrollPane sp = new JScrollPane();
@@ -1016,7 +1017,7 @@ public class Desktop extends jalview.jbgui.GDesktop
           frame.setIcon(false);
         } catch (java.beans.PropertyVetoException ex)
         {
-
+          // System.err.println(ex.toString());
         }
       }
     });
@@ -1149,7 +1150,7 @@ public class Desktop extends jalview.jbgui.GDesktop
                   : protocols.get(i);
           FileFormatI format = null;
 
-          if (fileName.endsWith(".jar"))
+          if (fileName.toLowerCase(Locale.ROOT).endsWith(".jar"))
           {
             format = FileFormat.Jalview;
 
@@ -1202,31 +1203,37 @@ public class Desktop extends jalview.jbgui.GDesktop
         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
    * 
@@ -1289,51 +1296,13 @@ public class Desktop extends jalview.jbgui.GDesktop
                 : ((JComboBox<String>) history).getEditor().getItem()
                         .toString().trim());
 
-        if (url.toLowerCase(Locale.ROOT).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);
         }
       }
     };
@@ -1346,6 +1315,52 @@ public class Desktop extends jalview.jbgui.GDesktop
                     MessageManager.getString("action.ok"));
   }
 
+  public boolean loadUrl(String url, AlignViewport viewport)
+  {
+    if (url.toLowerCase(Locale.ROOT).endsWith(".jar"))
+    {
+      if (viewport != null)
+      {
+        new FileLoader().LoadFile(viewport, url, DataSourceType.URL,
+                FileFormat.Jalview);
+      }
+      else
+      {
+        new FileLoader().LoadFile(url, DataSourceType.URL,
+                FileFormat.Jalview);
+      }
+    }
+    else
+    {
+      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
    * 
@@ -3217,6 +3232,28 @@ public class Desktop extends jalview.jbgui.GDesktop
           Transferable t) throws Exception
   {
 
+    // BH 2018 changed List<String> to List<Object> to allow for File from
+    // SwingJS
+
+    // DataFlavor[] flavors = t.getTransferDataFlavors();
+    // for (int i = 0; i < flavors.length; i++) {
+    // if (flavors[i].isFlavorJavaFileListType()) {
+    // evt.acceptDrop(DnDConstants.ACTION_COPY_OR_MOVE);
+    // List<File> list = (List<File>) t.getTransferData(flavors[i]);
+    // for (int j = 0; j < list.size(); j++) {
+    // File file = (File) list.get(j);
+    // byte[] data = getDroppedFileBytes(file);
+    // fileName.setText(file.getName() + " - " + data.length + " " +
+    // evt.getLocation());
+    // JTextArea target = (JTextArea) ((DropTarget)
+    // evt.getSource()).getComponent();
+    // target.setText(new String(data));
+    // }
+    // dtde.dropComplete(true);
+    // return;
+    // }
+    //
+
     DataFlavor uriListFlavor = new DataFlavor(
             "text/uri-list;class=java.lang.String"), urlFlavour = null;
     try
index e596fbf..7d74f82 100755 (executable)
@@ -31,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;
@@ -168,10 +169,18 @@ public class SequenceFetcher extends JPanel implements Runnable
    * @param guiIndic
    * @param selectedDb
    * @param queryString
+   * @param interactive
    */
   public SequenceFetcher(IProgressIndicator guiIndic,
           final String selectedDb, final String queryString)
   {
+    this(guiIndic, selectedDb, queryString, true);
+  }
+
+  public SequenceFetcher(IProgressIndicator guiIndic,
+          final String selectedDb, final String queryString,
+          boolean interactive)
+  {
     this.progressIndicator = guiIndic;
     getSequenceFetcherSingleton();
     this.guiWindow = progressIndicator;
@@ -181,7 +190,7 @@ public class SequenceFetcher extends JPanel implements Runnable
       alignFrame = (AlignFrame) progressIndicator;
     }
 
-    jbInit(selectedDb);
+    jbInit(selectedDb, interactive);
     textArea.setText(queryString);
 
     frame = new JInternalFrame();
@@ -198,7 +207,7 @@ public class SequenceFetcher extends JPanel implements Runnable
                     .getString("label.additional_sequence_fetcher"));
   }
 
-  private void jbInit(String selectedDb)
+  private void jbInit(String selectedDb, boolean interactive)
   {
     this.setLayout(new BorderLayout());
 
@@ -356,6 +365,18 @@ public class SequenceFetcher extends JPanel implements Runnable
     jScrollPane1.getViewport().add(textArea);
     idsPanel.add(jScrollPane1, BorderLayout.CENTER);
 
+    // En/disable or show/hide interactive elements
+    database.setEnabled(interactive);
+    exampleAccession.setVisible(interactive);
+    replacePunctuation.setVisible(interactive);
+    okBtn.setVisible(interactive);
+    exampleBtn.setVisible(interactive);
+    closeBtn.setVisible(interactive);
+    backBtn.setVisible(interactive);
+    jLabel1.setVisible(interactive);
+    clear.setVisible(interactive);
+    textArea.setEnabled(interactive);
+
     this.add(actionPanel, BorderLayout.SOUTH);
     this.add(idsPanel, BorderLayout.CENTER);
     this.add(databasePanel, BorderLayout.NORTH);
@@ -466,7 +487,7 @@ public class SequenceFetcher extends JPanel implements Runnable
    * 
    * @param e
    */
-  protected void close_actionPerformed(ActionEvent e)
+  public void close_actionPerformed(ActionEvent e)
   {
     try
     {
@@ -485,6 +506,12 @@ public class SequenceFetcher extends JPanel implements Runnable
    */
   public void ok_actionPerformed()
   {
+    ok_actionPerformed(false, null);
+  }
+
+  public CompletableFuture<Void> ok_actionPerformed(boolean returnFuture,
+          String id)
+  {
     /*
      * tidy inputs and check there is something to search for
      */
@@ -505,14 +532,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);
@@ -521,8 +548,17 @@ 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(() -> runAndCacheAlignFrame(returnFuture, id));
+
+    return returnFuture ? worker : null;
+  }
+
+  private void runAndCacheAlignFrame(boolean cacheAlignFrame, String id)
+  {
+    AlignFrame af = this.run(cacheAlignFrame);
+    if (cacheAlignFrame && id != null && af != null)
+      af.cacheAlignFrameFromRestId(id);
   }
 
   private void resetDialog()
@@ -537,6 +573,11 @@ public class SequenceFetcher extends JPanel implements Runnable
   @Override
   public void run()
   {
+    run(false);
+  }
+
+  public AlignFrame run(boolean returnAlignFrame)
+  {
     boolean addToLast = false;
     List<String> aresultq = new ArrayList<>();
     List<String> presultTitle = new ArrayList<>();
@@ -676,9 +717,10 @@ public class SequenceFetcher extends JPanel implements Runnable
                             : MessageManager.getString("status.processing"),
                     Thread.currentThread().hashCode());
     // process results
+    AlignFrame af = null;
     while (presult.size() > 0)
     {
-      parseResult(presult.remove(0), presultTitle.remove(0), null,
+      af = parseResult(presult.remove(0), presultTitle.remove(0), null,
               preferredFeatureColours);
     }
     // only remove visual delay after we finished parsing.
@@ -706,6 +748,7 @@ public class SequenceFetcher extends JPanel implements Runnable
       showErrorMessage(sb.toString());
     }
     resetDialog();
+    return returnAlignFrame ? af : null;
   }
 
   /**
@@ -882,20 +925,21 @@ public class SequenceFetcher extends JPanel implements Runnable
    * @param preferredFeatureColours
    * @return the alignment
    */
-  public AlignmentI parseResult(AlignmentI al, String title,
+  public AlignFrame parseResult(AlignmentI al, String title,
           FileFormatI currentFileFormat,
           FeatureSettingsModelI preferredFeatureColours)
   {
 
+    AlignFrame af = alignFrame;
     if (al != null && al.getHeight() > 0)
     {
       if (title == null)
       {
         title = getDefaultRetrievalTitle();
       }
-      if (alignFrame == null)
+      if (af == null)
       {
-        AlignFrame af = new AlignFrame(al, AlignFrame.DEFAULT_WIDTH,
+        af = new AlignFrame(al, AlignFrame.DEFAULT_WIDTH,
                 AlignFrame.DEFAULT_HEIGHT);
         if (currentFileFormat != null)
         {
@@ -935,10 +979,10 @@ public class SequenceFetcher extends JPanel implements Runnable
       }
       else
       {
-        alignFrame.viewport.addAlignment(al, title);
+        af.viewport.addAlignment(al, title);
       }
     }
-    return al;
+    return af;
   }
 
   void showErrorMessage(final String error)
index ece2df0..f8c5441 100644 (file)
@@ -31,6 +31,8 @@ import javax.servlet.http.HttpServletResponse;
 import org.eclipse.jetty.server.Request;
 import org.eclipse.jetty.server.handler.AbstractHandler;
 
+import jalview.bin.Console;
+
 /**
  * 
  * @author gmcarstairs
@@ -67,8 +69,8 @@ public abstract class AbstractRequestHandler extends AbstractHandler
       /*
        * Set server error status on response
        */
-      System.err.println("Exception handling request "
-              + request.getRequestURI() + " : " + t.getMessage());
+      Console.error("Exception handling request " + request.getRequestURI(),
+              t);
       if (response.isCommitted())
       {
         /*
index a18d38d..057c714 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,38 @@ public class HttpServer
               + " handler on " + handler.getUri());
     }
   }
+
+  /**
+   * Gets the ContextHandler attached to this handler. Useful for obtaining the
+   * full path used to access a given handler.
+   * 
+   * @param handler
+   */
+  public ContextHandler getContextHandler(AbstractRequestHandler handler)
+  {
+    return myHandlers.get(handler);
+  }
+
+  /**
+   * 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..93e7622
--- /dev/null
@@ -0,0 +1,84 @@
+package jalview.rest;
+
+import java.net.BindException;
+import java.util.HashMap;
+import java.util.Map;
+
+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 Map<String, Object> objectMap = 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(new FetchSequencesEndpoint(this));
+    addEndpoint(new InputAlignmentEndpoint(this));
+    addEndpoint(new HighlightSequenceEndpoint(this));
+    addEndpoint(new SelectSequencesEndpoint(this));
+    addEndpoint(new GetCrossReferencesEndpoint(this));
+
+    setPath(MY_PATH);
+    this.registerHandler();
+
+    init = true;
+  }
+
+  /*
+   * Shared methods below here
+   */
+
+  @Override
+  public String getPath()
+  {
+    return MY_PATH;
+  }
+
+  protected static Map<String, Status> getStatusMap()
+  {
+    return statusMap;
+  }
+
+  protected static Map<String, String> getRequestMap()
+  {
+    return requestMap;
+  }
+
+  protected static Map<String, Object> getObjectMap()
+  {
+    return objectMap;
+  }
+}
diff --git a/src/jalview/rest/AbstractEndpoint.java b/src/jalview/rest/AbstractEndpoint.java
new file mode 100644 (file)
index 0000000..deb4683
--- /dev/null
@@ -0,0 +1,219 @@
+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.Console;
+import jalview.gui.AlignFrame;
+import jalview.gui.Desktop;
+import jalview.rest.RestHandler.EndpointI;
+
+public abstract class AbstractEndpoint implements EndpointI
+{
+  private final String path;
+
+  private API api;
+
+  private final String name;
+
+  private final String parameters;
+
+  private final String description;
+
+  public AbstractEndpoint(API api, String path, String name,
+          String parameters, String description)
+  {
+    this.api = api;
+    this.path = path;
+    this.name = name;
+    this.parameters = parameters;
+    this.description = description;
+  }
+
+  public String getPath()
+  {
+    return this.path;
+  }
+
+  protected API getAPI()
+  {
+    return this.api;
+  }
+
+  public String getName()
+  {
+    return this.name;
+  }
+
+  public String getParameters()
+  {
+    return this.parameters;
+  }
+
+  public String getDescription()
+  {
+    return this.description;
+  }
+
+  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)
+  {
+    String okString = request.getParameter("ok");
+    boolean ok = (okString != null && okString.equalsIgnoreCase("true"));
+    /*
+     * Annoyingly jetty is not adding content to anything other than a few
+     * 20x status codes. Possibly it is closing the PrintWriter.
+     * Find a fix for this!
+     ****************************************************/
+    response.setStatus(ok ? HttpServletResponse.SC_OK : //
+    // HttpServletResponse.SC_BAD_REQUEST //
+            HttpServletResponse.SC_PARTIAL_CONTENT //
+    );
+
+    String endpointName = getPath();
+    Console.error(getAPI().getName() + " error: endpoint " + endpointName
+            + " failed: '" + message + "'");
+    try
+    {
+      PrintWriter writer = response.getWriter();
+      writer.write("message=Endpoint " + endpointName + ": " + message);
+    } catch (IOException e)
+    {
+      Console.info("Exception writing to REST response", 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();
+  }
+
+  protected boolean checkParameters(HttpServletRequest request,
+          HttpServletResponse response, int i)
+  {
+    String[] parameters = getEndpointPathParameters(request);
+
+    // check we can run fetchsequence
+    if (parameters.length < i)
+    {
+      returnError(request, response,
+              "requires parameters:" + getParameters() + "\n" + getName()
+                      + ": " + getDescription());
+      return false;
+    }
+    return true;
+  }
+
+  public int[][] parseIntRanges(String rangesString)
+  {
+    if (rangesString.equals("*"))
+    {
+      return new int[][] { { -1 }, { -1 } };
+    }
+    String[] rangeStrings = rangesString.split(",");
+    int[][] ranges = new int[2][rangeStrings.length];
+    for (int i = 0; i < rangeStrings.length; i++)
+    {
+      String range = rangeStrings[i];
+      try
+      {
+        int hyphenpos = range.indexOf('-');
+        if (hyphenpos < 0)
+        {
+          ranges[0][i] = Integer.parseInt(range);
+          ranges[1][i] = ranges[0][i];
+        }
+        else
+        {
+          ranges[0][i] = Integer.parseInt(range.substring(0, hyphenpos));
+          ranges[1][i] = Integer.parseInt(range.substring(hyphenpos + 1));
+        }
+      } catch (NumberFormatException nfe)
+      {
+        return null;
+      }
+    }
+    return ranges;
+  }
+
+  /*
+   * Get all AlignFrames or just one if requested to work on a specific window (fromId query string parameter)
+   */
+  protected AlignFrame[] getAlignFrames(HttpServletRequest request,
+          boolean all)
+  {
+    return getAlignFrames(request, "fromId", all);
+  }
+
+  protected AlignFrame[] getAlignFrames(HttpServletRequest request,
+          String idParam, boolean all)
+  {
+    String fromIdString = request.getParameter(idParam);
+
+    if (fromIdString != null)
+    {
+      AlignFrame af = AlignFrame.getAlignFrameFromRestId(fromIdString);
+      return af == null ? null : new AlignFrame[] { af };
+    }
+    else if (all)
+    {
+      return Desktop.getAlignFrames();
+    }
+    else
+    {
+      return null;
+    }
+  }
+
+  protected AlignFrame getAlignFrameFromId(HttpServletRequest request)
+  {
+    return getAlignFrameFromId(request, "fromId");
+  }
+
+  protected AlignFrame getAlignFrameFromId(HttpServletRequest request,
+          String idParam)
+  {
+    AlignFrame[] afs = getAlignFrames(request, idParam, false);
+    return (afs == null || afs.length < 1 || afs[0] == null) ? null
+            : afs[0];
+  }
+
+  protected boolean deleteFromCache()
+  {
+    return false;
+  }
+
+}
diff --git a/src/jalview/rest/AbstractEndpointAsync.java b/src/jalview/rest/AbstractEndpointAsync.java
new file mode 100644 (file)
index 0000000..cb1ff34
--- /dev/null
@@ -0,0 +1,281 @@
+package jalview.rest;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import jalview.bin.Console;
+import jalview.rest.RestHandler.Status;
+
+public abstract class AbstractEndpointAsync extends AbstractEndpoint
+{
+  public AbstractEndpointAsync(API api, String path, String name,
+          String parameters, String description)
+  {
+    super(api, path, name, parameters, description);
+  }
+
+  protected String idExtension = null;
+
+  protected String id = null;
+
+  protected CompletableFuture<Void> cf = null;
+
+  protected Map<String, CompletableFuture<Void>> cfMap = new HashMap<>();
+
+  protected Map<String, Object> objectsPassedToProcessAsync = new HashMap<>();
+
+  private Status tempStatus = null;
+
+  protected void setCompletableFuture(CompletableFuture<Void> cf)
+  {
+    this.cf = cf;
+    if (getId() != null)
+      cfMap.put(getId(), cf);
+  }
+
+  protected CompletableFuture<Void> getCompletableFuture()
+  {
+    if (cf == null && getId() != null && cfMap.get(getId()) != null)
+      cf = cfMap.get(getId());
+    return this.cf;
+  }
+
+  protected void setId(String id)
+  {
+    this.id = id;
+  }
+
+  protected void setIdExtension(String idExtension)
+  {
+    setId(getPath() + "::" + 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 processAsync(HttpServletRequest request,
+          HttpServletResponse response, final Map<String, Object> finalMap);
+
+  protected void finalise(HttpServletRequest request,
+          HttpServletResponse response)
+  {
+    // can be Overridden
+  }
+
+  @Override
+  public void processEndpoint(HttpServletRequest request,
+          HttpServletResponse response)
+  {
+    tempStatus = null;
+    // subclass method
+    initialise(request, response);
+
+    if (checkStatus(request, response, Status.STARTED))
+    {
+      String alreadyFinishedString = null;
+      if (getStatus() == Status.FINISHED)
+      {
+        alreadyFinishedString = finishedResponseString(request, response);
+      }
+      returnStatus(request, response, alreadyFinishedString);
+      return;
+    }
+
+    if (getCompletableFuture() == null)
+    {
+      final Map<String, Object> finalObjectMap = objectsPassedToProcessAsync;
+      setCompletableFuture(CompletableFuture.runAsync(() -> {
+        // subclass method
+        try
+        {
+          this.processAsync(request, response, finalObjectMap);
+        } catch (ClassCastException e)
+        {
+          Console.info("Something went wrong with async endpoint execution"
+                  + getName(), e);
+        }
+      }));
+    }
+    addWhenCompleteCompletableFuture();
+
+    // subclass method
+    finalise(request, response);
+
+    returnStatus(response);
+    changeStatus(Status.IN_PROGRESS);
+  }
+
+  protected void atEnd()
+  {
+  }
+
+  protected String finishedResponseString(HttpServletRequest request,
+          HttpServletResponse response)
+  {
+    return null;
+  }
+
+  /*
+   * Shared methods below here
+   */
+
+  protected String setId(HttpServletRequest request, String extension)
+  {
+    String idString = request.getParameter("id");
+    if (idString == null)
+    {
+      setIdExtension(extension);
+    }
+    else
+    {
+      setId(idString);
+    }
+    return getId();
+  }
+
+  protected void changeStatus(Status status)
+  {
+    String id = getId();
+    // don't change a job's status if it has finished or died
+    if (getStatus() == Status.FINISHED || getStatus() == Status.ERROR)
+      return;
+    tempStatus = status;
+    if (status != Status.NOT_RUN)
+      API.getStatusMap().put(id, status);
+  }
+
+  protected Status getStatus()
+  {
+    Status status = API.getStatusMap().get(getId());
+    return status == null ? tempStatus : status;
+  }
+
+  protected void returnStatus(HttpServletResponse response)
+  {
+    returnStatus(null, response, null);
+  }
+
+  protected void returnStatus(HttpServletRequest request,
+          HttpServletResponse response, String message)
+  {
+    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 (getStatus() != null)
+      {
+        switch (getStatus())
+        {
+        case STARTED:
+          response.setStatus(HttpServletResponse.SC_ACCEPTED);
+          break;
+        case IN_PROGRESS:
+          response.setStatus(HttpServletResponse.SC_ACCEPTED);
+          break;
+        case FINISHED:
+          response.setStatus(HttpServletResponse.SC_CREATED);
+          break;
+        case ERROR:
+          response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
+                  message);
+          break;
+        }
+        writer.write("status=" + getStatus().toString() + "\n");
+      }
+      if (message != null)
+      {
+        writer.write(message);
+      }
+    } catch (IOException e)
+    {
+      Console.debug("Exception writing to REST response", e);
+    }
+  }
+
+  protected boolean checkStatus(HttpServletRequest request,
+          HttpServletResponse response)
+  {
+    return checkStatus(request, response, null);
+  }
+
+  protected boolean checkStatus(HttpServletRequest request,
+          HttpServletResponse response, Status set)
+  {
+    String id = getId();
+    Status status = getStatus();
+    if (status == null)
+    {
+      if (set != null)
+        changeStatus(set);
+      API.getRequestMap().put(id, request.getRequestURI());
+      return false;
+    }
+    else
+    {
+      return true;
+    }
+  }
+
+  protected void addWhenCompleteCompletableFuture()
+  {
+    String id = getId();
+    cf.whenComplete((Void, e) -> {
+      if (e != null)
+      {
+        Console.error("Endpoint job " + id + " did not complete", e);
+        changeStatus(Status.ERROR);
+      }
+      else
+      {
+        Console.info("Endpoint job " + id + " completed successfully");
+        changeStatus(Status.FINISHED);
+        atEnd();
+      }
+    });
+  }
+
+  @Override
+  protected void returnError(HttpServletRequest request,
+          HttpServletResponse response, String message)
+  {
+    changeStatus(Status.NOT_RUN);
+    super.returnError(request, response, message);
+  }
+
+  @Override
+  protected boolean deleteFromCache()
+  {
+    return false;
+  }
+}
diff --git a/src/jalview/rest/FetchSequencesEndpoint.java b/src/jalview/rest/FetchSequencesEndpoint.java
new file mode 100644 (file)
index 0000000..7fa95e2
--- /dev/null
@@ -0,0 +1,99 @@
+package jalview.rest;
+
+import java.util.List;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import jalview.api.AlignmentViewPanel;
+import jalview.bin.Console;
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.SequenceI;
+import jalview.gui.AlignFrame;
+import jalview.gui.Desktop;
+import jalview.gui.SequenceFetcher;
+import jalview.util.DBRefUtils;
+
+public class FetchSequencesEndpoint extends AbstractEndpointAsync
+{
+  public FetchSequencesEndpoint(API api)
+  {
+    super(api, path, name, parameters, description);
+  }
+
+  private static final String path = "fetchsequences";
+
+  private static final String name = "Fetch Sequences";
+
+  private static final String parameters = "<resource name>/<sequence ids>";
+
+  private static final String description = "Fetch sequences from online resource";
+
+  private SequenceFetcher sf;
+
+  @Override
+  protected void initialise(HttpServletRequest request,
+          HttpServletResponse response)
+  {
+    if (!checkParameters(request, response, 2))
+    {
+      return;
+    }
+    String[] parameters = getEndpointPathParameters(request);
+
+    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;
+    sf = new SequenceFetcher(desktop, db, dbId, false);
+    setCompletableFuture(sf.ok_actionPerformed(true, getId()));
+  }
+
+  protected void processAsync(HttpServletRequest request,
+          HttpServletResponse response, Map<String, Object> map)
+  {
+    // all the work being done by the SequenceFetcher!
+    Console.warn("THIS SHOULD NOT BE RUN");
+  }
+
+  @Override
+  protected void atEnd()
+  {
+    sf.close_actionPerformed(null);
+  }
+
+  @Override
+  protected String finishedResponseString(HttpServletRequest request,
+          HttpServletResponse response)
+  {
+    AlignFrame af = getAlignFrameFromId(request, "id");
+    if (af == null)
+      return null;
+    List<AlignmentViewPanel> aps = (List<AlignmentViewPanel>) af
+            .getAlignPanels();
+    StringBuilder sb = new StringBuilder();
+    for (AlignmentViewPanel ap : aps)
+    {
+      AlignmentI al = ap.getAlignment();
+      if (al == null)
+        continue;
+      List<SequenceI> seqs = (List<SequenceI>) al.getSequences();
+      for (SequenceI seq : seqs)
+      {
+        if (sb.length() > 0)
+          sb.append(",");
+        sb.append(seq.getName());
+      }
+    }
+    sb.insert(0, "sequences=");
+    sb.append("\n");
+    return sb.toString();
+  }
+
+}
diff --git a/src/jalview/rest/GetCrossReferencesEndpoint.java b/src/jalview/rest/GetCrossReferencesEndpoint.java
new file mode 100644 (file)
index 0000000..611c2a3
--- /dev/null
@@ -0,0 +1,141 @@
+package jalview.rest;
+
+import java.util.List;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import jalview.api.AlignmentViewPanel;
+import jalview.bin.Console;
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.SequenceI;
+import jalview.gui.AlignFrame;
+
+public class GetCrossReferencesEndpoint extends AbstractEndpointAsync
+{
+  public GetCrossReferencesEndpoint(API api)
+  {
+    super(api, path, name, parameters, description);
+  }
+
+  private static final String path = "getcrossreferences";
+
+  private static final String name = "Get Cross References";
+
+  private static final String parameters = "<resource name>/<sequence ids>?fromId=<id>";
+
+  private static final String description = "Get Cross References for alignment <id>";
+
+  private String resourceName;
+
+  private AlignFrame af;
+
+  private SequenceI[] seqs;
+
+  @Override
+  protected void initialise(HttpServletRequest request,
+          HttpServletResponse response)
+  {
+    if (!checkParameters(request, response, 1))
+    {
+      return;
+    }
+    String[] parameters = getEndpointPathParameters(request);
+
+    resourceName = parameters[0];
+
+    String fromId = request.getParameter("fromId");
+    setId(request, resourceName + "::" + fromId);
+
+    if (checkStatus(request, response))
+    {
+      return;
+    }
+
+    af = getAlignFrameFromId(request);
+    if (af == null)
+    {
+      returnError(request, response, "Could not find results");
+      return;
+    }
+
+    if (af.canShowProducts())
+    {
+      List<String> products = af.getProducts();
+      if (products == null)
+      {
+        returnError(request, response,
+                "no cross reference products available");
+        return;
+      }
+      if (!products.contains(resourceName))
+      {
+        StringBuilder sb = new StringBuilder();
+        sb.append("cross reference product '").append(resourceName)
+                .append("' not available: available products are\n");
+        sb.append("products=");
+        boolean first = true;
+        for (String p : products)
+        {
+          if (!first)
+          {
+            sb.append(",");
+            first = false;
+          }
+          sb.append(p);
+        }
+        sb.append("\n");
+        returnError(request, response, sb.toString());
+        return;
+      }
+    }
+
+    seqs = af.getViewport().getAlignment().getSequencesArray();
+    if (seqs == null || seqs.length == 0)
+    {
+      // nothing to do
+      returnError(request, response, "no sequences selected");
+      return;
+    }
+
+    final boolean dna = af.getViewport().getAlignment().isNucleotide();
+    setCompletableFuture(
+            af.showProductsFor(seqs, dna, resourceName, true, getId()));
+  }
+
+  protected void processAsync(HttpServletRequest request,
+          HttpServletResponse response, Map<String, Object> map)
+  {
+    // all the work being done by the AlignFrame!
+    Console.warn("THIS SHOULD NOT BE RUN");
+  }
+
+  protected String finishedResponseString(HttpServletRequest request,
+          HttpServletResponse response)
+  {
+    AlignFrame af = AlignFrame.getAlignFrameFromRestId(getId());
+    if (af == null)
+    {
+      return null;
+    }
+    List<AlignmentViewPanel> aps = (List<AlignmentViewPanel>) af
+            .getAlignPanels();
+    StringBuilder sb = new StringBuilder();
+    for (AlignmentViewPanel ap : aps)
+    {
+      AlignmentI al = ap.getAlignment();
+      List<SequenceI> seqs = (List<SequenceI>) al.getSequences();
+      for (SequenceI seq : seqs)
+      {
+        if (sb.length() > 0)
+          sb.append(",");
+        sb.append(seq.getName());
+      }
+    }
+    sb.insert(0, "sequences=");
+    sb.append("\n");
+    return sb.toString();
+  }
+
+}
diff --git a/src/jalview/rest/HighlightSequenceEndpoint.java b/src/jalview/rest/HighlightSequenceEndpoint.java
new file mode 100644 (file)
index 0000000..f228473
--- /dev/null
@@ -0,0 +1,91 @@
+package jalview.rest;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import jalview.api.AlignmentViewPanel;
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.SequenceI;
+import jalview.gui.AlignFrame;
+import jalview.structure.StructureSelectionManager;
+
+public class HighlightSequenceEndpoint extends AbstractEndpoint
+{
+  public HighlightSequenceEndpoint(API api)
+  {
+    super(api, path, name, parameters, description);
+  }
+
+  protected static final String path = "highlightsequence";
+
+  private static final String name = "Highlight sequence position";
+
+  private static final String parameters = "<sequence names>/<position>";
+
+  private static final String description = "Highlight the specified sequences at the specified position";
+
+  public void processEndpoint(HttpServletRequest request,
+          HttpServletResponse response)
+  {
+    if (!checkParameters(request, response, 2))
+    {
+      return;
+    }
+    String[] parameters = getEndpointPathParameters(request);
+
+    String posString = parameters[1];
+    int pos = -1;
+    try
+    {
+      pos = Integer.parseInt(posString);
+    } catch (NumberFormatException e)
+    {
+      returnError(request, response,
+              "Could not parse postition integer " + posString);
+    }
+
+    String sequenceNames = parameters[0];
+
+    Map<SequenceI, StructureSelectionManager> ssmMap = new HashMap<>();
+    AlignFrame[] alignFrames = getAlignFrames(request, true);
+    if (alignFrames == null)
+    {
+      returnError(request, response, "could not find results");
+      return;
+    }
+    for (int i = 0; i < alignFrames.length; i++)
+    {
+      AlignFrame af = alignFrames[i];
+      List<AlignmentViewPanel> aps = (List<AlignmentViewPanel>) af
+              .getAlignPanels();
+      for (AlignmentViewPanel ap : aps)
+      {
+        StructureSelectionManager ssm = ap.getStructureSelectionManager();
+        AlignmentI al = ap.getAlignment();
+        List<SequenceI> seqs = (List<SequenceI>) al.getSequences();
+        for (SequenceI seq : seqs)
+        {
+          if (sequenceNames.equals(seq.getName()))
+          {
+            ssmMap.put(seq, ssm);
+          }
+        }
+      }
+    }
+    // highlight
+    for (SequenceI seq : ssmMap.keySet())
+    {
+      StructureSelectionManager ssm = ssmMap.get(seq);
+      if (ssm == null)
+      {
+        continue;
+      }
+      ssm.mouseOverSequence(seq, pos, -1, null);
+    }
+
+  }
+}
\ No newline at end of file
diff --git a/src/jalview/rest/InputAlignmentEndpoint.java b/src/jalview/rest/InputAlignmentEndpoint.java
new file mode 100644 (file)
index 0000000..095fd24
--- /dev/null
@@ -0,0 +1,184 @@
+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 java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.xml.bind.DatatypeConverter;
+
+import jalview.bin.Console;
+import jalview.gui.CutAndPasteTransfer;
+import jalview.gui.Desktop;
+
+public class InputAlignmentEndpoint extends AbstractEndpointAsync
+{
+  public InputAlignmentEndpoint(API api)
+  {
+    super(api, path, name, parameters, description);
+  }
+
+  protected static final String path = "inputalignment";
+
+  private static final String name = "Input Alignment";
+
+  private static final String parameters = "POST <body> | GET ?[data=<data>|file=<fileURI>|url=<URL>]";
+
+  private static final String description = "Input an alignment from POST request body, GET request 'data' parameter, local file in 'file' parameter, url in 'url' parameter";
+
+  private String fileString;
+
+  private String urlString;
+
+  private String method;
+
+  private String data;
+
+  private String body;
+
+  private boolean isPost;
+
+  private boolean hasData;
+
+  private String access = null;
+
+  private String ref = null;
+
+  @Override
+  protected void initialise(HttpServletRequest request,
+          HttpServletResponse response)
+  {
+    fileString = request.getParameter("file");
+    urlString = request.getParameter("url");
+    method = request.getMethod().toLowerCase();
+    data = request.getParameter("data");
+    body = null;
+    isPost = method.equalsIgnoreCase("post");
+    hasData = data != null;
+
+    if (isPost)
+    {
+      access = "post";
+      try
+      {
+        body = getRequestBody(request);
+      } catch (IOException e)
+      {
+        returnError(request, response, "could not read POST body");
+        Console.debug("Could not read POST body", e);
+        return;
+      }
+      // for ref see md5 later
+    }
+    else if (hasData)
+    {
+      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
+    String content;
+    if (isPost || hasData)
+    {
+      content = isPost ? body : data;
+      objectsPassedToProcessAsync.put("content", content); // needed as "final
+                                                           // String" in process
+      try
+      {
+        MessageDigest md5 = MessageDigest.getInstance("MD5");
+        md5.update(content.getBytes());
+        byte[] digest = md5.digest();
+        ref = DatatypeConverter.printBase64Binary(digest).toLowerCase();
+      } catch (NoSuchAlgorithmException e)
+      {
+        Console.debug("Could not find MD5 algorithm", e);
+      }
+    }
+    else
+    {
+      content = null;
+    }
+    objectsPassedToProcessAsync.put("content", content);
+
+    setId(request, access + "::" + ref);
+  }
+
+  protected void process(HttpServletRequest request,
+          HttpServletResponse response)
+  {
+    processAsync(request, response, null);
+  }
+
+  protected void processAsync(HttpServletRequest request,
+          HttpServletResponse response, final Map<String, Object> finalMap)
+  {
+    String content = (String) finalMap.get("content");
+    if (isPost || hasData)
+    {
+      // 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 + "'");
+        Console.debug("Could not resolve file '" + fileString + "'", 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;
+      }
+    }
+  }
+}
index a37882f..caec830 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 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, EndpointI> endpoints = null;
+
+  protected Map<String, EndpointI> getEndpoints()
+  {
+    return endpoints;
+  }
+
   /**
    * Singleton instance of this class
    */
@@ -66,9 +106,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 +130,53 @@ 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);
+      return;
+    }
+
+    String endpointName = getRequestedEndpointName(request);
+
+    if (!endpoints.containsKey(endpointName)
+            || endpoints.get(endpointName) == null)
+    {
+
+      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 : endpoints.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");
-    final PrintWriter writer = response.getWriter();
-    writer.write(reply);
-    writer.close();
+    EndpointI ep = endpoints.get(endpointName);
+    ep.processEndpoint(request, response);
+
+    return;
   }
 
   /**
@@ -112,4 +188,102 @@ 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(EndpointI ep)
+  {
+    if (endpoints == null)
+    {
+      endpoints = new HashMap<>();
+    }
+    AbstractEndpoint e = (AbstractEndpoint) ep;
+    endpoints.put(ep.getPath(), ep);
+    return true;
+  }
+
+  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 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(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+    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)
+          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/SelectSequencesEndpoint.java b/src/jalview/rest/SelectSequencesEndpoint.java
new file mode 100644 (file)
index 0000000..7ca5d23
--- /dev/null
@@ -0,0 +1,103 @@
+package jalview.rest;
+
+import java.util.Arrays;
+import java.util.List;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import jalview.api.AlignViewportI;
+import jalview.api.AlignmentViewPanel;
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.SequenceGroup;
+import jalview.datamodel.SequenceI;
+import jalview.gui.AlignFrame;
+
+public class SelectSequencesEndpoint extends AbstractEndpoint
+{
+  public SelectSequencesEndpoint(API api)
+  {
+    super(api, path, name, parameters, description);
+  }
+
+  protected static final String path = "selectsequences";
+
+  private static final String name = "Select sequence(s) positions";
+
+  private static final String parameters = "<sequence names>/<range>";
+
+  private static final String description = "Select the specified sequence(s) with the specified range";
+
+  public void processEndpoint(HttpServletRequest request,
+          HttpServletResponse response)
+  {
+    if (!checkParameters(request, response, 2))
+    {
+      return;
+    }
+    String[] parameters = getEndpointPathParameters(request);
+
+    String rangesString = parameters[1];
+    int[][] ranges = parseIntRanges(rangesString);
+    if (ranges == null || ranges.length < 2 || ranges[0].length < 1)
+    {
+      returnError(request, response,
+              "couldn't parse range '" + rangesString + "'");
+      return;
+    }
+    if (ranges[0].length > 1)
+    {
+      returnError(request, response,
+              "only provide 1 range '" + rangesString + "'");
+      return;
+    }
+    int start = ranges[0][0];
+    int end = ranges[1][0];
+
+    String sequenceNamesString = parameters[0];
+    List<String> sequenceNames = Arrays
+            .asList(sequenceNamesString.split(","));
+
+    AlignFrame[] alignFrames = getAlignFrames(request, true);
+    if (alignFrames == null)
+    {
+      returnError(request, response, "could not find results");
+      return;
+    }
+    for (int i = 0; i < alignFrames.length; i++)
+    {
+      AlignFrame af = alignFrames[i];
+      List<AlignmentViewPanel> aps = (List<AlignmentViewPanel>) af
+              .getAlignPanels();
+      for (AlignmentViewPanel ap : aps)
+      {
+        AlignViewportI avp = ap.getAlignViewport();
+        AlignmentI al = ap.getAlignment();
+        List<SequenceI> seqs = (List<SequenceI>) al.getSequences();
+        SequenceGroup stretchGroup = new SequenceGroup();
+        for (SequenceI seq : seqs)
+        {
+          if (sequenceNames.contains(seq.getName())
+                  || (sequenceNamesString.equals("*")
+                          && alignFrames.length == 1))
+          {
+            stretchGroup.addSequence(seq, false);
+          }
+        }
+        if (start == -1 && end == -1) // "*" as range
+        {
+          stretchGroup.setStartRes(al.getStartRes());
+          stretchGroup.setEndRes(al.getEndRes());
+        }
+        else
+        {
+          stretchGroup.setStartRes(start);
+          stretchGroup.setEndRes(end);
+        }
+        avp.setSelectionGroup(stretchGroup);
+        ap.paintAlignment(false, false);
+        avp.sendSelection();
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/utils/eclipse/eclipse_run.sh b/utils/eclipse/eclipse_run.sh
new file mode 100755 (executable)
index 0000000..820c2ad
--- /dev/null
@@ -0,0 +1,3 @@
+#!/usr/bin/env bash
+
+java -cp "bin/main:j11lib/*:resources" jalview.bin.Launcher ${@}
diff --git a/wget_commands.sh b/wget_commands.sh
new file mode 100644 (file)
index 0000000..58fd7c0
--- /dev/null
@@ -0,0 +1,10 @@
+wget -q -S -O - 'http://localhost:2021/jalview/api/fetchsequences/ensembl/ENSG00000139618?id=myBRCA2'
+wget -q -S -O - 'http://localhost:2021/jalview/api/getcrossreferences/UNIPROT?fromId=myBRCA2'
+wget -q -S -O - "http://localhost:2021/jalview/api/highlightsequence/ENST00000544455/30000?fromId=myBRCA2"
+N=22946; while [ $N -lt 23002 ]; do wget -q -S -O - "http://localhost:2021/jalview/api/highlightsequence/ENST00000544455/${N}?fromId=myBRCA2"; N=$((N+1)); sleep 0.2; done
+wget -q -S -O - 'http://localhost:2021/jalview/api/selectsequences/*/*?fromId=myBRCA2'
+wget -q -S -O - 'http://localhost:2021/jalview/api/selectsequences/*/22946-23001?fromId=myBRCA2'
+wget -q -S -O - 'http://localhost:2021/jalview/api/selectsequences/ENSG00000139618,ENST00000544455,ENST00000530893,ENST00000380152,ENST00000680887,ENST00000614259,ENST00000665585,ENST00000528762/22946-23001?fromId=myBRCA2'
+wget -q -S -O - 'http://localhost:2021/jalview/api/selectsequences/ENSG00000139618,ENST00000544455,ENST00000380152,ENST00000680887,ENST00000614259,ENST00000528762/22946-23001?fromId=myBRCA2'
+### open 3D structure 6hqu and repeat highlight
+