JAL-2546 JAL-2490 efficient lookup of features, correct handling of
[jalview.git] / src / jalview / httpserver / HttpServer.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
4  * 
5  * This file is part of Jalview.
6  * 
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License 
9  * as published by the Free Software Foundation, either version 3
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 package jalview.httpserver;
22
23 import jalview.rest.RestHandler;
24
25 import java.net.BindException;
26 import java.net.URI;
27 import java.util.Collections;
28 import java.util.HashMap;
29 import java.util.Map;
30
31 import javax.servlet.http.HttpServletRequest;
32 import javax.servlet.http.HttpServletResponse;
33
34 import org.eclipse.jetty.server.Handler;
35 import org.eclipse.jetty.server.Server;
36 import org.eclipse.jetty.server.ServerConnector;
37 import org.eclipse.jetty.server.handler.ContextHandler;
38 import org.eclipse.jetty.server.handler.HandlerCollection;
39 import org.eclipse.jetty.util.thread.QueuedThreadPool;
40
41 /**
42  * An HttpServer built on Jetty. To use it
43  * <ul>
44  * <li>call getInstance() to create and start the server</li>
45  * <li>call registerHandler to add a handler for a path (below /jalview)</li>
46  * <li>when finished, call removedHandler</li>
47  * </ul>
48  * 
49  * @author gmcarstairs
50  * @see http://eclipse.org/jetty/documentation/current/embedding-jetty.html
51  */
52 public class HttpServer
53 {
54   /*
55    * 'context root' - actually just prefixed to the path for each handler for
56    * now - see registerHandler
57    */
58   private static final String JALVIEW_PATH = "jalview";
59
60   /*
61    * Singleton instance of this server
62    */
63   private static HttpServer instance;
64
65   /*
66    * The Http server
67    */
68   private Server server;
69
70   /*
71    * Registered handlers for context paths
72    */
73   private HandlerCollection contextHandlers;
74
75   /*
76    * Lookup of ContextHandler by its wrapped handler
77    */
78   Map<Handler, ContextHandler> myHandlers = new HashMap<Handler, ContextHandler>();
79
80   /*
81    * The context root for the server
82    */
83   private URI contextRoot;
84
85   /**
86    * Returns the singleton instance of this class.
87    * 
88    * @return
89    * @throws BindException
90    */
91   public static HttpServer getInstance() throws BindException
92   {
93     synchronized (HttpServer.class)
94     {
95       if (instance == null)
96       {
97         instance = new HttpServer();
98       }
99       return instance;
100     }
101   }
102
103   /**
104    * Private constructor to enforce use of singleton
105    * 
106    * @throws BindException
107    *           if no free port can be assigned
108    */
109   private HttpServer() throws BindException
110   {
111     startServer();
112
113     /*
114      * Provides a REST server by default; add more programmatically as required
115      */
116     registerHandler(RestHandler.getInstance());
117   }
118
119   /**
120    * Start the http server
121    * 
122    * @throws BindException
123    */
124   private void startServer() throws BindException
125   {
126     try
127     {
128       /*
129        * Create a server with a small number of threads; jetty will allocate a
130        * free port
131        */
132       QueuedThreadPool tp = new QueuedThreadPool(4, 1); // max, min
133       server = new Server(tp);
134       // 2 selector threads to handle incoming connections
135       ServerConnector connector = new ServerConnector(server, 0, 2);
136       // restrict to localhost
137       connector.setHost("localhost");
138       server.addConnector(connector);
139
140       /*
141        * HttpServer shuts down with Jalview process
142        */
143       server.setStopAtShutdown(true);
144
145       /*
146        * Create a mutable set of handlers (can add handlers while the server is
147        * running). Using vanilla handlers here rather than servlets
148        */
149       // TODO how to properly configure context root "/jalview"
150       contextHandlers = new HandlerCollection(true);
151       server.setHandler(contextHandlers);
152       server.start();
153       // System.out.println(String.format(
154       // "HttpServer started with %d threads", server.getThreadPool()
155       // .getThreads()));
156       contextRoot = server.getURI();
157     } catch (Exception e)
158     {
159       System.err.println("Error trying to start HttpServer: "
160               + e.getMessage());
161       try
162       {
163         server.stop();
164       } catch (Exception e1)
165       {
166         e1.printStackTrace();
167       }
168     }
169     if (server == null)
170     {
171       throw new BindException("HttpServer failed to allocate a port");
172     }
173   }
174
175   /**
176    * Returns the URI on which we are listening
177    * 
178    * @return
179    */
180   public URI getUri()
181   {
182     return server == null ? null : server.getURI();
183   }
184
185   /**
186    * For debug - write HTTP request details to stdout
187    * 
188    * @param request
189    * @param response
190    */
191   protected void dumpRequest(HttpServletRequest request,
192           HttpServletResponse response)
193   {
194     for (String hdr : Collections.list(request.getHeaderNames()))
195     {
196       for (String val : Collections.list(request.getHeaders(hdr)))
197       {
198         System.out.println(hdr + ": " + val);
199       }
200     }
201     for (String param : Collections.list(request.getParameterNames()))
202     {
203       for (String val : request.getParameterValues(param))
204       {
205         System.out.println(param + "=" + val);
206       }
207     }
208   }
209
210   /**
211    * Stop the Http server.
212    */
213   public void stopServer()
214   {
215     if (server != null)
216     {
217       if (server.isStarted())
218       {
219         try
220         {
221           server.stop();
222         } catch (Exception e)
223         {
224           System.err.println("Error stopping Http Server on "
225                   + server.getURI() + ": " + e.getMessage());
226         }
227       }
228     }
229   }
230
231   /**
232    * Register a handler for the given path and set its URI
233    * 
234    * @param handler
235    * @return
236    * @throws IllegalStateException
237    *           if handler path has not been set
238    */
239   public void registerHandler(AbstractRequestHandler handler)
240   {
241     String path = handler.getPath();
242     if (path == null)
243     {
244       throw new IllegalStateException(
245               "Must set handler path before registering handler");
246     }
247
248     // http://stackoverflow.com/questions/20043097/jetty-9-embedded-adding-handlers-during-runtime
249     ContextHandler ch = new ContextHandler();
250     ch.setAllowNullPathInfo(true);
251     ch.setContextPath("/" + JALVIEW_PATH + "/" + path);
252     ch.setResourceBase(".");
253     ch.setClassLoader(Thread.currentThread().getContextClassLoader());
254     ch.setHandler(handler);
255
256     /*
257      * Remember the association so we can remove it later
258      */
259     this.myHandlers.put(handler, ch);
260
261     /*
262      * A handler added to a running server must be started explicitly
263      */
264     contextHandlers.addHandler(ch);
265     try
266     {
267       ch.start();
268     } catch (Exception e)
269     {
270       System.err.println("Error starting handler for " + path + ": "
271               + e.getMessage());
272     }
273
274     handler.setUri(this.contextRoot + ch.getContextPath().substring(1));
275     System.out.println("Jalview " + handler.getName()
276             + " handler started on " + handler.getUri());
277   }
278
279   /**
280    * Removes the handler from the server; more precisely, remove the
281    * ContextHandler wrapping the specified handler
282    * 
283    * @param handler
284    */
285   public void removeHandler(AbstractRequestHandler handler)
286   {
287     /*
288      * Have to use this cached lookup table since there is no method
289      * ContextHandler.getHandler()
290      */
291     ContextHandler ch = myHandlers.get(handler);
292     if (ch != null)
293     {
294       contextHandlers.removeHandler(ch);
295       myHandlers.remove(handler);
296       System.out.println("Stopped Jalview " + handler.getName()
297               + " handler on " + handler.getUri());
298     }
299   }
300 }