e60db9abe3452ed2bf434ac6eebac579e5ae2561
[jalview.git] / src / jalview / httpserver / HttpServer.java
1 package jalview.httpserver;
2
3 import java.net.BindException;
4 import java.net.URI;
5 import java.util.Collections;
6 import java.util.HashMap;
7 import java.util.Map;
8
9 import javax.servlet.http.HttpServletRequest;
10 import javax.servlet.http.HttpServletResponse;
11
12 import org.eclipse.jetty.server.Handler;
13 import org.eclipse.jetty.server.Server;
14 import org.eclipse.jetty.server.ServerConnector;
15 import org.eclipse.jetty.server.handler.ContextHandler;
16 import org.eclipse.jetty.server.handler.HandlerCollection;
17 import org.eclipse.jetty.util.thread.QueuedThreadPool;
18
19 /**
20  * An HttpServer built on Jetty. To use it
21  * <ul>
22  * <li>call getInstance() to create and start the server</li>
23  * <li>call registerHandler to add a handler for a path (below /jalview)</li>
24  * <li>when finished, call removedHandler</li>
25  * </ul>
26  * 
27  * @author gmcarstairs
28  * @see http://eclipse.org/jetty/documentation/current/embedding-jetty.html
29  */
30 public class HttpServer
31 {
32   /*
33    * 'context root' - actually just prefixed to the path for each handler for
34    * now - see registerHandler
35    */
36   private static final String JALVIEW_PATH = "jalview";
37
38   /*
39    * Singleton instance of this server
40    */
41   private static HttpServer instance;
42
43   /*
44    * The Http server
45    */
46   private Server server;
47
48   /*
49    * Registered handlers for context paths
50    */
51   private HandlerCollection contextHandlers;
52
53   /*
54    * Lookup of ContextHandler by its wrapped handler
55    */
56   Map<Handler, ContextHandler> myHandlers = new HashMap<Handler, ContextHandler>();
57
58   /*
59    * The context root for the server
60    */
61   private URI contextRoot;
62
63   /**
64    * Returns the singleton instance of this class.
65    * 
66    * @return
67    * @throws BindException
68    */
69   public static HttpServer getInstance() throws BindException
70   {
71     synchronized (HttpServer.class)
72     {
73       if (instance == null) {
74         instance = new HttpServer();
75       }
76       return instance;
77     }
78   }
79
80   /**
81    * Private constructor to enforce use of singleton
82    * 
83    * @throws BindException
84    *           if no free port can be assigned
85    */
86   private HttpServer() throws BindException
87   {
88     startServer();
89   }
90
91   /**
92    * Start the http server
93    * 
94    * @throws BindException
95    */
96   private void startServer() throws BindException
97   {
98     try
99     {
100       /*
101        * Create a server with a small number of threads; jetty will allocate a
102        * free port
103        */
104       QueuedThreadPool tp = new QueuedThreadPool(4, 1); // max, min
105       server = new Server(tp);
106       // 2 selector threads to handle incoming connections
107       ServerConnector connector = new ServerConnector(server, 0, 2);
108       server.addConnector(connector);
109
110       /*
111        * HttpServer shuts down with Jalview process
112        */
113       server.setStopAtShutdown(true);
114
115       /*
116        * Create a mutable set of handlers (can add handlers while the server is
117        * running). Using vanilla handlers here rather than servlets
118        */
119       // TODO how to properly configure context root "/jalview"
120       contextHandlers = new HandlerCollection(true);
121       server.setHandler(contextHandlers);
122       server.start();
123       // System.out.println(String.format(
124       // "HttpServer started with %d threads", server.getThreadPool()
125       // .getThreads()));
126       contextRoot = server.getURI();
127       System.out.println("Jalview endpoint " + contextRoot);
128     } catch (Exception e)
129     {
130       System.err.println("Error trying to start HttpServer: "
131               + e.getMessage());
132       try
133       {
134         server.stop();
135       } catch (Exception e1)
136       {
137         e1.printStackTrace();
138       }
139     }
140     if (server == null)
141     {
142       throw new BindException("HttpServer failed to allocate a port");
143     }
144   }
145
146   /**
147    * Returns the URI on which we are listening
148    * 
149    * @return
150    */
151   public URI getUri()
152   {
153     return server == null ? null : server.getURI();
154   }
155
156   /**
157    * For debug - write HTTP request details to stdout
158    * 
159    * @param request
160    * @param response
161    */
162   protected void dumpRequest(HttpServletRequest request,
163           HttpServletResponse response)
164   {
165     for (String hdr : Collections.list(request.getHeaderNames()))
166     {
167       for (String val : Collections.list(request.getHeaders(hdr)))
168       {
169         System.out.println(hdr + ": " + val);
170       }
171     }
172     for (String param : Collections.list(request.getParameterNames()))
173     {
174       for (String val : request.getParameterValues(param))
175       {
176         System.out.println(param + "=" + val);
177       }
178     }
179   }
180
181   /**
182    * Stop the Http server.
183    */
184   public void stopServer()
185   {
186     if (server != null)
187     {
188       if (server.isStarted())
189       {
190         try
191         {
192           server.stop();
193         } catch (Exception e)
194         {
195           System.err.println("Error stopping Http Server on "
196                   + server.getURI() + ": " + e.getMessage());
197         }
198       }
199     }
200   }
201
202   /**
203    * Register a handler for the given path and returns its URI
204    * 
205    * @param path
206    *          a path below the context root (without leading or trailing
207    *          separator)
208    * @param handler
209    * @return
210    */
211   public String registerHandler(String path, AbstractRequestHandler handler)
212   {
213     // http://stackoverflow.com/questions/20043097/jetty-9-embedded-adding-handlers-during-runtime
214     ContextHandler ch = new ContextHandler();
215     ch.setAllowNullPathInfo(true);
216     ch.setContextPath("/" + JALVIEW_PATH + "/" + path);
217     ch.setResourceBase(".");
218     ch.setClassLoader(Thread.currentThread()
219             .getContextClassLoader());
220     ch.setHandler(handler);
221
222     /*
223      * Remember the association so we can remove it later
224      */
225     this.myHandlers.put(handler, ch);
226
227     /*
228      * A handler added to a running server must be started explicitly
229      */
230     contextHandlers.addHandler(ch);
231     try
232     {
233       ch.start();
234     } catch (Exception e)
235     {
236       System.err.println("Error starting handler for " + path + ": "
237               + e.getMessage());
238     }
239
240     return this.contextRoot + ch.getContextPath().substring(1);
241   }
242
243   /**
244    * Removes the handler from the server; more precisely, remove the
245    * ContextHandler wrapping the specified handler
246    * 
247    * @param handler
248    */
249   public void removeHandler(Handler handler)
250   {
251     /*
252      * Have to use this cached lookup table since there is no method
253      * ContextHandler.getHandler()
254      */
255     ContextHandler ch = myHandlers.get(handler);
256     if (ch != null)
257     {
258       contextHandlers.removeHandler(ch);
259       myHandlers.remove(handler);
260     }
261   }
262 }