JAL-3851 merged to develop 2022-03-22
[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 java.net.BindException;
24 import java.net.URI;
25 import java.util.Collections;
26 import java.util.HashMap;
27 import java.util.Map;
28
29 import javax.servlet.http.HttpServletRequest;
30 import javax.servlet.http.HttpServletResponse;
31
32 import org.eclipse.jetty.server.Connector;
33 import org.eclipse.jetty.server.Handler;
34 import org.eclipse.jetty.server.Server;
35 import org.eclipse.jetty.server.ServerConnector;
36 import org.eclipse.jetty.server.handler.ContextHandler;
37 import org.eclipse.jetty.server.handler.HandlerCollection;
38 import org.eclipse.jetty.util.thread.QueuedThreadPool;
39
40 import jalview.rest.RestHandler;
41
42 /**
43  * An HttpServer built on Jetty. To use it
44  * <ul>
45  * <li>call getInstance() to create and start the server</li>
46  * <li>call registerHandler to add a handler for a path (below /jalview)</li>
47  * <li>when finished, call removedHandler</li>
48  * </ul>
49  * 
50  * @author gmcarstairs
51  * @see http://eclipse.org/jetty/documentation/current/embedding-jetty.html
52  */
53 public class HttpServer
54 {
55   /*
56    * 'context root' - actually just prefixed to the path for each handler for
57    * now - see registerHandler
58    */
59   private static final String JALVIEW_PATH = "jalview";
60
61   /*
62    * Singleton instance of this server
63    */
64   private static HttpServer instance;
65
66   /*
67    * The Http server
68    */
69   private Server server;
70
71   /*
72    * Registered handlers for context paths
73    */
74   private HandlerCollection contextHandlers;
75
76   /*
77    * Lookup of ContextHandler by its wrapped handler
78    */
79   Map<Handler, ContextHandler> myHandlers = new HashMap<Handler, ContextHandler>();
80
81   /*
82    * The context root for the server
83    */
84   private URI contextRoot;
85
86   /*
87    * The port of the server.  This can be set before starting the instance
88    * as a suggested port to use (it is not guaranteed).
89    * The value will be set to the actual port being used after the instance
90    * is started.
91    */
92   private static int PORT = 0;
93
94   /**
95    * Returns the singleton instance of this class.
96    * 
97    * @return
98    * @throws BindException
99    */
100   public static HttpServer getInstance() throws BindException
101   {
102     synchronized (HttpServer.class)
103     {
104       if (instance == null)
105       {
106         instance = new HttpServer();
107       }
108       return instance;
109     }
110   }
111
112   /**
113    * Private constructor to enforce use of singleton
114    * 
115    * @throws BindException
116    *           if no free port can be assigned
117    */
118   private HttpServer() throws BindException
119   {
120     startServer();
121
122     /*
123      * Provides a REST server by default; add more programmatically as required
124      */
125     registerHandler(RestHandler.getInstance());
126   }
127
128   /**
129    * Start the http server
130    * 
131    * @throws BindException
132    */
133   private void startServer() throws BindException
134   {
135     try
136     {
137       /*
138        * Create a server with a small number of threads;
139        * If PORT has been set then jetty will try and use this, otherwise
140        * jetty will allocate a free port
141        */
142       QueuedThreadPool tp = new QueuedThreadPool(4, 1); // max, min
143       server = new Server(tp);
144       // 2 selector threads to handle incoming connections
145       ServerConnector connector = new ServerConnector(server, 0, 2);
146       // restrict to localhost
147       connector.setHost("localhost");
148       if (PORT > 0)
149       {
150         connector.setPort(PORT);
151       }
152       server.addConnector(connector);
153
154       /*
155        * HttpServer shuts down with Jalview process
156        */
157       server.setStopAtShutdown(true);
158
159       /*
160        * Create a mutable set of handlers (can add handlers while the server is
161        * running). Using vanilla handlers here rather than servlets
162        */
163       // TODO how to properly configure context root "/jalview"
164       contextHandlers = new HandlerCollection(true);
165       server.setHandler(contextHandlers);
166       server.start();
167       Connector[] cs = server.getConnectors();
168       if (cs.length > 0)
169       {
170         if (cs[0] instanceof ServerConnector)
171         {
172           ServerConnector c = (ServerConnector) cs[0];
173           PORT = c.getPort();
174         }
175       }
176       // System.out.println(String.format(
177       // "HttpServer started with %d threads", server.getThreadPool()
178       // .getThreads()));
179       contextRoot = server.getURI();
180     } catch (Exception e)
181     {
182       System.err.println(
183               "Error trying to start HttpServer: " + e.getMessage());
184       try
185       {
186         server.stop();
187       } catch (Exception e1)
188       {
189         e1.printStackTrace();
190       }
191     }
192     if (server == null)
193     {
194       throw new BindException("HttpServer failed to allocate a port");
195     }
196   }
197
198   /**
199    * Returns the URI on which we are listening
200    * 
201    * @return
202    */
203   public URI getUri()
204   {
205     return server == null ? null : server.getURI();
206   }
207
208   /**
209    * For debug - write HTTP request details to stdout
210    * 
211    * @param request
212    * @param response
213    */
214   protected void dumpRequest(HttpServletRequest request,
215           HttpServletResponse response)
216   {
217     for (String hdr : Collections.list(request.getHeaderNames()))
218     {
219       for (String val : Collections.list(request.getHeaders(hdr)))
220       {
221         System.out.println(hdr + ": " + val);
222       }
223     }
224     for (String param : Collections.list(request.getParameterNames()))
225     {
226       for (String val : request.getParameterValues(param))
227       {
228         System.out.println(param + "=" + val);
229       }
230     }
231   }
232
233   /**
234    * Stop the Http server.
235    */
236   public void stopServer()
237   {
238     if (server != null)
239     {
240       if (server.isStarted())
241       {
242         try
243         {
244           server.stop();
245         } catch (Exception e)
246         {
247           System.err.println("Error stopping Http Server on "
248                   + server.getURI() + ": " + e.getMessage());
249         }
250       }
251     }
252   }
253
254   /**
255    * Register a handler for the given path and set its URI
256    * 
257    * @param handler
258    * @return
259    * @throws IllegalStateException
260    *           if handler path has not been set
261    */
262   public void registerHandler(AbstractRequestHandler handler)
263   {
264     String path = handler.getPath();
265     if (path == null)
266     {
267       throw new IllegalStateException(
268               "Must set handler path before registering handler");
269     }
270
271     // http://stackoverflow.com/questions/20043097/jetty-9-embedded-adding-handlers-during-runtime
272     ContextHandler ch = new ContextHandler();
273     ch.setAllowNullPathInfo(true);
274     ch.setContextPath("/" + JALVIEW_PATH + "/" + path);
275     ch.setResourceBase(".");
276     ch.setClassLoader(Thread.currentThread().getContextClassLoader());
277     ch.setHandler(handler);
278
279     /*
280      * Remember the association so we can remove it later
281      */
282     this.myHandlers.put(handler, ch);
283
284     /*
285      * A handler added to a running server must be started explicitly
286      */
287     contextHandlers.addHandler(ch);
288     try
289     {
290       ch.start();
291     } catch (Exception e)
292     {
293       System.err.println(
294               "Error starting handler for " + path + ": " + e.getMessage());
295     }
296
297     handler.setUri(this.contextRoot + ch.getContextPath().substring(1));
298     System.out.println("Jalview " + handler.getName()
299             + " handler started on " + handler.getUri());
300   }
301
302   /**
303    * Removes the handler from the server; more precisely, remove the
304    * ContextHandler wrapping the specified handler
305    * 
306    * @param handler
307    */
308   public void removeHandler(AbstractRequestHandler handler)
309   {
310     /*
311      * Have to use this cached lookup table since there is no method
312      * ContextHandler.getHandler()
313      */
314     ContextHandler ch = myHandlers.get(handler);
315     if (ch != null)
316     {
317       contextHandlers.removeHandler(ch);
318       myHandlers.remove(handler);
319       System.out.println("Stopped Jalview " + handler.getName()
320               + " handler on " + handler.getUri());
321     }
322   }
323
324   /**
325    * Gets the ContextHandler attached to this handler. Useful for obtaining the
326    * full path used to access a given handler.
327    * 
328    * @param handler
329    */
330   public ContextHandler getContextHandler(AbstractRequestHandler handler)
331   {
332     return myHandlers.get(handler);
333   }
334
335   /**
336    * This sets the "suggested" port to use. It can only be called once before
337    * starting the HttpServer instance. After the server has actually started the
338    * port is set to the actual port being used and cannot be changed.
339    * 
340    * @param port
341    * @return successful change
342    */
343   public static boolean setSuggestedPort(int port)
344   {
345     if (port < 1 || PORT > 0)
346     {
347       return false;
348     }
349     PORT = port;
350     return true;
351   }
352
353   public static int getPort()
354   {
355     return PORT;
356   }
357 }