Java tutorial
/** * Copyright 2011 The Apache Software Foundation * * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.cloudera.util; import java.io.File; import java.io.IOException; import java.io.OutputStream; import java.io.PrintWriter; import java.net.BindException; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.hadoop.util.ReflectionUtils; import org.mortbay.jetty.Connector; import org.mortbay.jetty.Server; import org.mortbay.jetty.handler.ContextHandlerCollection; import org.mortbay.jetty.nio.SelectChannelConnector; import org.mortbay.jetty.servlet.Context; import org.mortbay.jetty.servlet.DefaultServlet; import org.mortbay.jetty.servlet.ServletHolder; import org.mortbay.jetty.webapp.WebAppContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * <p> * An embedded Jetty HTTP server. It defers addition of contexts/handlers to * a callback so that the we can provide a method that increments ports until * a valid port is found. This is mostly a thin wrapper around Jetty's * {@link Server} class and behaves as Jetty does. * * Here is an example usage: * <pre> * InternalHttpServer server = new InternalHttpServer(); * * // applicationHome/webapps/ will be scanned for war files and directories. * server.setWebappDir(new File(applicationHome, "webapps")); * server.setPort(8080); * server.setBindAddress("0.0.0.0"); * server.setContextCreator(new ContextCreator() { * @Override * public void addContexts(ContextHandlerCollection handlers) { * handlers.addHandler(InternalHttpServer.createLogAppContext()); * handlers.addHandler(InternalHttpServer.createStackSevletContext( * StackServlet.class, "/stacks", "/*", "stacks")); * String webAppRoot = FlumeConfiguration.get().getNodeWebappRoot(); * InternalHttpServer.addHandlersFromPaths(handlers, * new File(webAppRoot)); * } * }); * * server.start(); * * // at some later time... * server.stop(); * </pre> */ public class InternalHttpServer { private static final Logger logger = LoggerFactory.getLogger(InternalHttpServer.class); private Server server; private int port; private int boundPort = -1; private String bindAddress; private ContextHandlerCollection handlers; private ContextCreator contextCreator = null; public InternalHttpServer() { port = 0; bindAddress = "0.0.0.0"; handlers = new ContextHandlerCollection(); } public void initialize() { if (server == null) { server = new Server(); Connector connector = new SelectChannelConnector(); connector.setPort(port); connector.setHost(bindAddress); server.addConnector(connector); if (contextCreator != null) { contextCreator.addContexts(handlers); } server.setHandler(handlers); } } /** * <p> * Start a configured HTTP server. Users should have already injected all the * necessary configuration parameters at this point. It is not considered safe * to call start() more than once and will lead to undefined behavior. * </p> * <p> * The configured webappDir is not scanned for applications until start() is * called. * </p> * * @throws BindException * @throws InternalHttpServerException */ public void start() throws BindException { initialize(); logger.info("Starting internal HTTP server"); try { server.start(); boundPort = server.getConnectors()[0].getLocalPort(); logger.info("Server started on port " + boundPort); } catch (BindException be) { throw be; } catch (Exception e) { logger.warn("Caught exception during HTTP server start.", e); throw new InternalHttpServerException("Unable to start HTTP server", e); } } /** * Stop the running HTTP server. If {@link #start()} has never been called, * this is a no-op. This call blocks until the internal Jetty instance is * stopped (to the extent the Jetty {@link Server#stop()} call blocks). * * @throws InternalHttpServerException */ public void stop() { if (server == null) { return; } logger.info("Stopping internal HTTP server"); try { server.stop(); } catch (Exception e) { logger.warn("Caught exception during HTTP server stop.", e); throw new InternalHttpServerException("Unable to stop HTTP server", e); } } @Override public String toString() { return "{ bindAddress:" + bindAddress + " port:" + port + " boundPort:" + boundPort + " server:" + server + " }"; } public Server getServer() { return server; } public void setServer(Server server) { this.server = server; } public int getPort() { return port; } public int getBoundPort() { return boundPort; } public void setPort(int port) { this.port = port; } public String getBindAddress() { return bindAddress; } public void setBindAddress(String bindAddress) { this.bindAddress = bindAddress; } public static class InternalHttpServerException extends RuntimeException { private static final long serialVersionUID = -4936285404574873547L; public InternalHttpServerException() { super(); } public InternalHttpServerException(String msg) { super(msg); } public InternalHttpServerException(String msg, Throwable t) { super(msg, t); } public InternalHttpServerException(Throwable t) { super(t); } } public void setHandlers(ContextHandlerCollection ctx) { if (ctx == null) { logger.warn("Attempting to add null webapp context"); return; } handlers = ctx; } public ContextHandlerCollection getHandlers() { return handlers; } protected void addHandler(Context ctx) { if (ctx == null) { logger.warn("Attempting to add null webapp context"); return; } handlers.addHandler(ctx); } public void setContextCreator(ContextCreator cc) { this.contextCreator = cc; } /** * The jetty server cannot properly reload contexts if it attempts to bind to * a port and fails. To support automatically going finding a new port, we * thus need to parameterize the creation and addition of context. This class * provides a call back that gets a instance of the server's * ContextHandlerCollection, and gives clients the opportunity to populate it. */ public abstract static class ContextCreator { public abstract void addContexts(ContextHandlerCollection handlers); } public static WebAppContext createWarContext(File path) { logger.debug("checking {}", path); String name; if (path.isFile()) { // if not a war file reject int idx = path.getName().indexOf(".war"); if (idx < 0) { return null; } // drop the .war suffix name = path.getName().substring(0, idx); } else { // is a dir name = path.getName(); } // WebAppContext is for loading war files. logger.debug("creating context {} -> {}", name, path); WebAppContext handler = new WebAppContext(path.getPath(), "/" + name); handler.setParentLoaderPriority(true); return handler; } /** * This method adds support for both normal and exploded war file deployment. * <p> * This scannings the specified webapp directory for applications * Both traditional and exploded war formats are supported in the webapp * directory. In the case of exploded directories, the directory name is used * as the context. For war files, everything from the first instance of ".war" * to the end of the file name (inclusive) is stripped and the remainder is * used for the context name. * </p> * <p> * Name examples: * </p> * <table> * <tr> * <td>Name</td> * <td>Type</td> * <td>Context</td> * </tr> * <tr> * <td>app.war</td> * <td>file</td> * <td>app</td> * </tr> * <tr> * <td>app</td> * <td>dir</td> * <td>app</td> * </tr> * <tr> * <td>app.war</td> * <td>dir</td> * <td>app.war</td> * </tr> * <tr> * <td>app.war.war</td> * <td>file</td> * <td>app</td> * </tr> * <tr> * <td> * </table> * <p> * Example usage: * </p> */ public static void addHandlersFromPaths(ContextHandlerCollection handlers, File webappDir) { logger.debug("Registering webapps in {}", webappDir); if (webappDir.isDirectory()) { for (File entry : webappDir.listFiles()) { Context ctx = createWarContext(entry); if (ctx != null) { handlers.addHandler(ctx); } } } else { Context ctx = createWarContext(webappDir); if (ctx != null) { handlers.addHandler(ctx); } } } /** * This creates file listing servlet context that is used to point to the log * directory of the daemon via the web interface. * * @return */ public static Context createLogAppContext() { Context ctx = new Context(); // logs applet String logDir = System.getProperty("flume.log.dir"); if (logDir != null) { ctx.setContextPath("/logs"); ctx.setResourceBase(logDir); ctx.addServlet(DefaultServlet.class, "/*"); ctx.setDisplayName("logs"); } return ctx; } /** * A very simple servlet to serve up a text representation of the current * stack traces. It both returns the stacks to the caller and logs them. * Currently the stack traces are done sequentially rather than exactly the * same data. */ public static class StackServlet extends HttpServlet { private static final Log LOG = LogFactory.getLog(InternalHttpServer.class.getName()); private static final long serialVersionUID = -6284183679759467039L; @Override public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { OutputStream outStream = response.getOutputStream(); ReflectionUtils.printThreadInfo(new PrintWriter(outStream), ""); outStream.close(); ReflectionUtils.logThreadInfo(LOG, "jsp requested", 1); } } /** * This creates a stack dumping servlet that can be used to debug a running * daemon via the web interface. * * @param sltClz * @param contextPath * @param pathSpec * @param name * @return */ public static Context createStackSevletContext() { Context ctx = new Context(); ServletHolder holder = new ServletHolder(StackServlet.class); ctx.setContextPath("/stacks"); ctx.addServlet(holder, "/*"); ctx.setDisplayName("stacks"); return ctx; } /** * If successful returns the port the http server successfully bound to. If it * failed, returns -1 */ public static InternalHttpServer startFindPortHttpServer(ContextCreator cc, String bindAddress, int nodePort) { do { try { return startHttpServer(cc, bindAddress, nodePort); } catch (BindException be) { logger.error( "Unable to start webserver on " + bindAddress + ":" + nodePort + ". Trying next port..."); nodePort++; } } while (true); } /** * Single attempt to create an http server for the node. * * @param bindAddress * @param nodePort * @return instance of a started http server or null if failed. * @throws BindException */ public static InternalHttpServer startHttpServer(ContextCreator cc, String bindAddress, int nodePort) throws BindException { InternalHttpServer http = null; try { http = new InternalHttpServer(); http.setBindAddress(bindAddress); http.setPort(nodePort); http.setContextCreator(cc); http.start(); return http; } catch (BindException be) { http.stop(); http = null; throw be; } catch (Throwable t) { logger.error("Unexpected exception/error thrown! " + t.getMessage(), t); // if any exception happens bail out and cleanup. http.stop(); return null; } } }