/** * 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 org.apache.hadoop.yarn.webapp; import static com.google.common.base.Preconditions.checkNotNull; import java.io.IOException; import java.net.ConnectException; import java.net.URL; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import javax.servlet.http.HttpServlet; import org.apache.commons.lang.StringUtils; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.http.HttpServer; import org.apache.hadoop.yarn.security.AdminACLsManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.inject.AbstractModule; import com.google.inject.Guice; import com.google.inject.Injector; import com.google.inject.servlet.GuiceFilter; /** * Helpers to create an embedded webapp. * * <h4>Quick start:</h4> * <pre> * WebApp wa = WebApps.$for(myApp).start();</pre> * Starts a webapp with default routes binds to 0.0.0.0 (all network interfaces) * on an ephemeral port, which can be obtained with:<pre> * int port = wa.port();</pre> * <h4>With more options:</h4> * <pre> * WebApp wa = WebApps.$for(myApp).at(address, port). * with(configuration). * start(new WebApp() { * @Override public void setup() { * route("/foo/action", FooController.class); * route("/foo/:id", FooController.class, "show"); * } * });</pre> */ public class WebApps { static final Logger LOG = LoggerFactory.getLogger(WebApps.class); public static class Builder<T> { static class ServletStruct { public Class<? extends HttpServlet> clazz; public String name; public String spec; } final String name; final String wsName; final Class<T> api; final T application; String bindAddress = "0.0.0.0"; int port = 0; boolean findPort = false; Configuration conf; boolean devMode = false; private final HashSet<ServletStruct> servlets = new HashSet<ServletStruct>(); private final HashMap<String, Object> attributes = new HashMap<String, Object>(); Builder(String name, Class<T> api, T application, String wsName) { this.name = name; this.api = api; this.application = application; this.wsName = wsName; } Builder(String name, Class<T> api, T application) { this(name, api, application, null); } public Builder<T> at(String bindAddress) { String[] parts = StringUtils.split(bindAddress, ':'); if (parts.length == 2) { return at(parts[0], Integer.parseInt(parts[1]), true); } return at(bindAddress, 0, true); } public Builder<T> at(int port) { return at("0.0.0.0", port, false); } public Builder<T> at(String address, int port, boolean findPort) { this.bindAddress = checkNotNull(address, "bind address"); this.port = port; this.findPort = findPort; return this; } public Builder<T> withAttribute(String key, Object value) { attributes.put(key, value); return this; } public Builder<T> withServlet(String name, String pathSpec, Class<? extends HttpServlet> servlet) { ServletStruct struct = new ServletStruct(); struct.clazz = servlet; struct.name = name; struct.spec = pathSpec; servlets.add(struct); return this; } public Builder<T> with(Configuration conf) { this.conf = conf; return this; } public Builder<T> inDevMode() { devMode = true; return this; } public WebApp start(WebApp webapp) { if (webapp == null) { webapp = new WebApp() { @Override public void setup() { // Defaults should be fine in usual cases } }; } webapp.setName(name); webapp.setWebServices(wsName); String basePath = "/" + name; webapp.setRedirectPath(basePath); if (basePath.equals("/")) { webapp.addServePathSpec("/*"); } else { webapp.addServePathSpec(basePath); webapp.addServePathSpec(basePath + "/*"); } if (wsName != null && !wsName.equals(basePath)) { if (wsName.equals("/")) { webapp.addServePathSpec("/*"); } else { webapp.addServePathSpec("/" + wsName); webapp.addServePathSpec("/" + wsName + "/*"); } } if (conf == null) { conf = new Configuration(); } try { if (application != null) { webapp.setHostClass(application.getClass()); } else { String cls = inferHostClass(); LOG.debug("setting webapp host class to {}", cls); webapp.setHostClass(Class.forName(cls)); } if (devMode) { if (port > 0) { try { new URL("http://localhost:"+ port +"/__stop").getContent(); LOG.info("stopping existing webapp instance"); Thread.sleep(100); } catch (ConnectException e) { LOG.info("no existing webapp instance found: {}", e.toString()); } catch (Exception e) { // should not be fatal LOG.warn("error stopping existing instance: {}", e.toString()); } } else { LOG.error("dev mode does NOT work with ephemeral port!"); System.exit(1); } } HttpServer server = new HttpServer(name, bindAddress, port, findPort, conf, new AdminACLsManager(conf).getAdminAcl(), null, webapp.getServePathSpecs()); for(ServletStruct struct: servlets) { server.addServlet(struct.name, struct.spec, struct.clazz); } for(Map.Entry<String, Object> entry : attributes.entrySet()) { server.setAttribute(entry.getKey(), entry.getValue()); } server.addGlobalFilter("guice", GuiceFilter.class.getName(), null); webapp.setConf(conf); webapp.setHttpServer(server); server.start(); LOG.info("Web app /"+ name +" started at "+ server.getPort()); } catch (ClassNotFoundException e) { throw new WebAppException("Error starting http server", e); } catch (IOException e) { throw new WebAppException("Error starting http server", e); } Injector injector = Guice.createInjector(webapp, new AbstractModule() { @Override protected void configure() { if (api != null) { bind(api).toInstance(application); } } }); LOG.info("Registered webapp guice modules"); // save a guice filter instance for webapp stop (mostly for unit tests) webapp.setGuiceFilter(injector.getInstance(GuiceFilter.class)); if (devMode) { injector.getInstance(Dispatcher.class).setDevMode(devMode); LOG.info("in dev mode!"); } return webapp; } public WebApp start() { return start(null); } private String inferHostClass() { String thisClass = this.getClass().getName(); Throwable t = new Throwable(); for (StackTraceElement e : t.getStackTrace()) { if (e.getClassName().equals(thisClass)) continue; return e.getClassName(); } LOG.warn("could not infer host class from", t); return thisClass; } } /** * Create a new webapp builder. * @see WebApps for a complete example * @param <T> application (holding the embedded webapp) type * @param prefix of the webapp * @param api the api class for the application * @param app the application instance * @param wsPrefix the prefix for the webservice api for this app * @return a webapp builder */ public static <T> Builder<T> $for(String prefix, Class<T> api, T app, String wsPrefix) { return new Builder<T>(prefix, api, app, wsPrefix); } /** * Create a new webapp builder. * @see WebApps for a complete example * @param <T> application (holding the embedded webapp) type * @param prefix of the webapp * @param api the api class for the application * @param app the application instance * @return a webapp builder */ public static <T> Builder<T> $for(String prefix, Class<T> api, T app) { return new Builder<T>(prefix, api, app); } // Short cut mostly for tests/demos @SuppressWarnings("unchecked") public static <T> Builder<T> $for(String prefix, T app) { return $for(prefix, (Class<T>)app.getClass(), app); } // Ditto public static <T> Builder<T> $for(T app) { return $for("", app); } public static <T> Builder<T> $for(String prefix) { return $for(prefix, null, null); } }