JAL-1725 first version of Jetty server / chimera selection listener
[jalview.git] / src / jalview / httpserver / HttpServer.java
diff --git a/src/jalview/httpserver/HttpServer.java b/src/jalview/httpserver/HttpServer.java
new file mode 100644 (file)
index 0000000..d39ada1
--- /dev/null
@@ -0,0 +1,245 @@
+package jalview.httpserver;
+
+import java.net.BindException;
+import java.net.URI;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.server.Handler;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.handler.ContextHandler;
+import org.eclipse.jetty.server.handler.HandlerCollection;
+import org.eclipse.jetty.util.thread.QueuedThreadPool;
+
+/**
+ * An HttpServer built on Jetty
+ * 
+ * @author gmcarstairs
+ * @see http://eclipse.org/jetty/documentation/current/embedding-jetty.html
+ */
+public class HttpServer
+{
+  private static final String JALVIEW_PATH = "/jalview";
+
+  /*
+   * Singleton instance of this server
+   */
+  private static HttpServer instance;
+
+  /*
+   * The Http server
+   */
+  private Server server;
+
+  /*
+   * Registered handlers for context paths
+   */
+  private HandlerCollection contextHandlers;
+
+  /*
+   * Lookup of ContextHandler by its wrapped handler
+   */
+  Map<Handler, ContextHandler> myHandlers = new HashMap<Handler, ContextHandler>();
+
+  /*
+   * The context root for the server
+   */
+  private URI contextRoot;
+
+  /**
+   * Returns the singleton instance of this class.
+   * 
+   * @return
+   * @throws BindException
+   */
+  public static HttpServer getInstance() throws BindException
+  {
+    synchronized (HttpServer.class)
+    {
+      if (instance == null) {
+        instance = new HttpServer();
+      }
+      return instance;
+    }
+  }
+
+  /**
+   * Private constructor to enforce use of singleton
+   * 
+   * @throws BindException
+   *           if no free port can be assigned
+   */
+  private HttpServer() throws BindException
+  {
+    startServer();
+  }
+
+  /**
+   * Start the http server with a default thread pool size of 1
+   * 
+   * @throws BindException
+   */
+  private void startServer() throws BindException
+  {
+    try
+    {
+      // problem: how to both limit thread pool size and
+      // pick a random free port - two alternative constructors
+      QueuedThreadPool tp = new QueuedThreadPool(1, 1);
+      // server = new Server(tp); // ? fails with URI null
+      server = new Server(0); // pick a free port number
+
+      /*
+       * HttpServer shuts down with Jalview process
+       */
+      server.setStopAtShutdown(true);
+
+      /*
+       * Create a mutable set of handlers (can add handlers while the server is
+       * running)
+       */
+      // TODO how to combine this with context root "/jalview"
+      contextHandlers = new HandlerCollection(true);
+      server.setHandler(contextHandlers);
+
+      server.start();
+      contextRoot = server.getURI();
+      System.out.println("Jalview endpoint " + contextRoot);
+    } catch (Exception e)
+    {
+      System.err.println("Error trying to start HttpServer: "
+              + e.getMessage());
+      try
+      {
+        server.stop();
+      } catch (Exception e1)
+      {
+        e1.printStackTrace();
+      }
+    }
+    if (server == null)
+    {
+      throw new BindException("HttpServer failed to allocate a port");
+    }
+  }
+
+  /**
+   * Returns the URI on which we are listening
+   * 
+   * @return
+   */
+  public URI getUri()
+  {
+    return server == null ? null : server.getURI();
+  }
+
+  /**
+   * For debug - write HTTP request details to stdout
+   * 
+   * @param request
+   * @param response
+   */
+  protected void dumpRequest(HttpServletRequest request,
+          HttpServletResponse response)
+  {
+    for (String hdr : Collections.list(request.getHeaderNames()))
+    {
+      for (String val : Collections.list(request.getHeaders(hdr)))
+      {
+        System.out.println(hdr + ": " + val);
+      }
+    }
+    for (String param : Collections.list(request.getParameterNames()))
+    {
+      for (String val : request.getParameterValues(param))
+      {
+        System.out.println(param + "=" + val);
+      }
+    }
+  }
+
+  /**
+   * Stop the Http server.
+   */
+  public void stopServer()
+  {
+    if (server != null)
+    {
+      if (server.isStarted())
+      {
+        try
+        {
+          server.stop();
+        } catch (Exception e)
+        {
+          System.err.println("Error stopping Http Server on "
+                  + server.getURI() + ": " + e.getMessage());
+        }
+      }
+    }
+  }
+
+  /**
+   * Register a handler for the given path and returns its URI
+   * 
+   * @param path
+   *          a path below the context root (without leading or trailing
+   *          separator)
+   * @param handler
+   * @return
+   */
+  public String registerHandler(String path, AbstractRequestHandler handler)
+  {
+    // http://stackoverflow.com/questions/20043097/jetty-9-embedded-adding-handlers-during-runtime
+    ContextHandler ch = new ContextHandler();
+    ch.setContextPath("/" + path);
+    ch.setResourceBase(".");
+    ch.setClassLoader(Thread.currentThread()
+            .getContextClassLoader());
+    ch.setHandler(handler);
+
+    /*
+     * Remember the association so we can remove it later
+     */
+    this.myHandlers.put(handler, ch);
+
+    /*
+     * A handler added to a running server must be started explicitly
+     */
+    contextHandlers.addHandler(ch);
+    try
+    {
+      ch.start();
+    } catch (Exception e)
+    {
+      System.err.println("Error starting handler for " + path + ": "
+              + e.getMessage());
+    }
+
+    return this.contextRoot + ch.getContextPath().substring(1);
+  }
+
+  /**
+   * Removes the handler from the server; more precisely, remove the
+   * ContextHandler wrapping the specified handler
+   * 
+   * @param handler
+   */
+  public void removeHandler(Handler handler)
+  {
+    /*
+     * Have to use this cached lookup table since there is no method
+     * ContextHandler.getHandler()
+     */
+    ContextHandler ch = myHandlers.get(handler);
+    if (ch != null)
+    {
+      contextHandlers.removeHandler(ch);
+      myHandlers.remove(handler);
+    }
+  }
+}