/* * 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.brooklyn.rest; import static com.google.common.base.Preconditions.checkNotNull; import java.io.File; import java.io.FilenameFilter; import java.io.IOException; import java.net.InetSocketAddress; import java.util.EnumSet; import java.util.List; import javax.servlet.DispatcherType; import javax.servlet.Filter; import org.apache.brooklyn.api.mgmt.ManagementContext; import org.apache.brooklyn.camp.brooklyn.BrooklynCampPlatformLauncherAbstract; import org.apache.brooklyn.camp.brooklyn.BrooklynCampPlatformLauncherNoServer; import org.apache.brooklyn.core.internal.BrooklynProperties; import org.apache.brooklyn.core.mgmt.internal.LocalManagementContext; import org.apache.brooklyn.core.mgmt.internal.ManagementContextInternal; import org.apache.brooklyn.core.server.BrooklynServerConfig; import org.apache.brooklyn.core.server.BrooklynServiceAttributes; import org.apache.brooklyn.rest.filter.BrooklynPropertiesSecurityFilter; import org.apache.brooklyn.rest.filter.HaMasterCheckFilter; import org.apache.brooklyn.rest.filter.LoggingFilter; import org.apache.brooklyn.rest.filter.NoCacheFilter; import org.apache.brooklyn.rest.filter.RequestTaggingFilter; import org.apache.brooklyn.rest.filter.SwaggerFilter; import org.apache.brooklyn.rest.security.provider.AnyoneSecurityProvider; import org.apache.brooklyn.rest.security.provider.SecurityProvider; import org.apache.brooklyn.rest.util.ManagementContextProvider; import org.apache.brooklyn.rest.util.OsgiCompat; import org.apache.brooklyn.rest.util.ServerStoppingShutdownHandler; import org.apache.brooklyn.rest.util.ShutdownHandlerProvider; import org.apache.brooklyn.util.exceptions.Exceptions; import org.apache.brooklyn.util.guava.Maybe; import org.apache.brooklyn.util.net.Networking; import org.apache.brooklyn.util.text.WildcardGlobs; import org.eclipse.jetty.server.NetworkConnector; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.servlet.FilterHolder; import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletHolder; import org.eclipse.jetty.webapp.WebAppContext; import org.reflections.util.ClasspathHelper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.annotations.Beta; import com.google.common.base.Charsets; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.google.common.io.Files; import com.sun.jersey.api.core.DefaultResourceConfig; import com.sun.jersey.api.core.ResourceConfig; import com.sun.jersey.spi.container.servlet.ServletContainer; /** Convenience and demo for launching programmatically. Also used for automated tests. * <p> * BrooklynLauncher has a more full-featured CLI way to start, * but if you want more control you can: * <li> take the WAR this project builds (REST API) -- NB probably want the unshaded one (containing all deps) * <li> take the WAR from the brooklyn-jsgui project (brooklyn-ui repo) _and_ this WAR and combine them * (this one should run as a filter on the others, _not_ as a ResourceCollection where they fight over who's got root) * <li> programmatically install things, following the examples herein; * in particular {@link #installAsServletFilter(ServletContextHandler)} is quite handy! * <p> * You can also just run this class. In most installs it just works, assuming your IDE or maven-fu gives you the classpath. * Add more apps and entities on the classpath and they'll show up in the catalog. **/ public class BrooklynRestApiLauncher { private static final Logger log = LoggerFactory.getLogger(BrooklynRestApiLauncher.class); final static int FAVOURITE_PORT = 8081; public static final String SCANNING_CATALOG_BOM_URL = "classpath://brooklyn/scanning.catalog.bom"; enum StartMode { FILTER, SERVLET, /** web-xml is not fully supported */ @Beta WEB_XML } public static final List<Class<? extends Filter>> DEFAULT_FILTERS = ImmutableList.of( RequestTaggingFilter.class, BrooklynPropertiesSecurityFilter.class, LoggingFilter.class, HaMasterCheckFilter.class, SwaggerFilter.class); private boolean forceUseOfDefaultCatalogWithJavaClassPath = false; private Class<? extends SecurityProvider> securityProvider; private List<Class<? extends Filter>> filters = DEFAULT_FILTERS; private StartMode mode = StartMode.FILTER; private ManagementContext mgmt; private ContextHandler customContext; private boolean deployJsgui = true; private boolean disableHighAvailability = true; private ServerStoppingShutdownHandler shutdownListener; protected BrooklynRestApiLauncher() {} public BrooklynRestApiLauncher managementContext(ManagementContext mgmt) { this.mgmt = mgmt; return this; } public BrooklynRestApiLauncher forceUseOfDefaultCatalogWithJavaClassPath(boolean forceUseOfDefaultCatalogWithJavaClassPath) { this.forceUseOfDefaultCatalogWithJavaClassPath = forceUseOfDefaultCatalogWithJavaClassPath; return this; } public BrooklynRestApiLauncher securityProvider(Class<? extends SecurityProvider> securityProvider) { this.securityProvider = securityProvider; return this; } /** * Runs the server with the given set of filters. * Overrides any previously supplied set (or {@link #DEFAULT_FILTERS} which is used by default). */ public BrooklynRestApiLauncher filters(@SuppressWarnings("unchecked") Class<? extends Filter>... filters) { this.filters = Lists.newArrayList(filters); return this; } public BrooklynRestApiLauncher mode(StartMode mode) { this.mode = checkNotNull(mode, "mode"); return this; } /** Overrides start mode to use an explicit context */ public BrooklynRestApiLauncher customContext(ContextHandler customContext) { this.customContext = checkNotNull(customContext, "customContext"); return this; } public BrooklynRestApiLauncher withJsgui() { this.deployJsgui = true; return this; } public BrooklynRestApiLauncher withoutJsgui() { this.deployJsgui = false; return this; } public BrooklynRestApiLauncher disableHighAvailability(boolean value) { this.disableHighAvailability = value; return this; } public Server start() { if (this.mgmt == null) { mgmt = new LocalManagementContext(); } BrooklynCampPlatformLauncherAbstract platform = new BrooklynCampPlatformLauncherNoServer() .useManagementContext(mgmt) .launch(); ((LocalManagementContext)mgmt).noteStartupComplete(); log.debug("started "+platform); ContextHandler context; String summary; if (customContext == null) { switch (mode) { case SERVLET: context = servletContextHandler(mgmt); summary = "programmatic Jersey ServletContainer servlet"; break; case WEB_XML: context = webXmlContextHandler(mgmt); summary = "from WAR at " + ((WebAppContext) context).getWar(); break; case FILTER: default: context = filterContextHandler(mgmt); summary = "programmatic Jersey ServletContainer filter on webapp at " + ((WebAppContext) context).getWar(); break; } } else { context = customContext; summary = (context instanceof WebAppContext) ? "from WAR at " + ((WebAppContext) context).getWar() : "from custom context"; } if (securityProvider != null) { ((BrooklynProperties) mgmt.getConfig()).put( BrooklynWebConfig.SECURITY_PROVIDER_CLASSNAME, securityProvider.getName()); } if (forceUseOfDefaultCatalogWithJavaClassPath) { // sets URLs for a surefire ((BrooklynProperties) mgmt.getConfig()).put(BrooklynServerConfig.BROOKLYN_CATALOG_URL, SCANNING_CATALOG_BOM_URL); ((LocalManagementContext) mgmt).setBaseClassPathForScanning(ClasspathHelper.forJavaClassPath()); } else { // don't use any catalog.xml which is set ((BrooklynProperties) mgmt.getConfig()).put(BrooklynServerConfig.BROOKLYN_CATALOG_URL, ManagementContextInternal.EMPTY_CATALOG_URL); } Server server = startServer(mgmt, context, summary, disableHighAvailability); if (shutdownListener!=null) { // not available in some modes, eg webapp shutdownListener.setServer(server); } return server; } private ContextHandler filterContextHandler(ManagementContext mgmt) { WebAppContext context = new WebAppContext(); context.setAttribute(BrooklynServiceAttributes.BROOKLYN_MANAGEMENT_CONTEXT, mgmt); context.setContextPath("/"); // here we run with the JS GUI, for convenience, if we can find it, else set up an empty dir // TODO pretty sure there is an option to monitor this dir and load changes to static content // NOTE: When running Brooklyn from an IDE (i.e. by launching BrooklynJavascriptGuiLauncher.main()) // you will need to ensure that the working directory is set to the brooklyn-ui repo folder. For IntelliJ, // set the 'Working directory' of the Run/Debug Configuration to $MODULE_DIR$/brooklyn-server/launcher. // For Eclipse, use the default option of ${workspace_loc:brooklyn-launcher}. // If the working directory is not set correctly, Brooklyn will be unable to find the jsgui .war // file and the 'gui not available' message will be shown. context.setWar(this.deployJsgui && findJsguiWebappInSource().isPresent() ? findJsguiWebappInSource().get() : createTempWebDirWithIndexHtml("Brooklyn REST API <p> (gui not available)")); installAsServletFilter(context, this.filters); return context; } private ContextHandler servletContextHandler(ManagementContext managementContext) { ResourceConfig config = new DefaultResourceConfig(); for (Object r: BrooklynRestApi.getAllResources()) config.getSingletons().add(r); addShutdownListener(config, mgmt); ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS); context.setAttribute(BrooklynServiceAttributes.BROOKLYN_MANAGEMENT_CONTEXT, managementContext); ServletHolder servletHolder = new ServletHolder(new ServletContainer(config)); context.addServlet(servletHolder, "/*"); context.setContextPath("/"); installBrooklynFilters(context, this.filters); return context; } /** NB: not fully supported; use one of the other {@link StartMode}s */ private ContextHandler webXmlContextHandler(ManagementContext mgmt) { // TODO add security to web.xml WebAppContext context; if (findMatchingFile("src/main/webapp")!=null) { // running in source mode; need to use special classpath context = new WebAppContext("src/main/webapp", "/"); context.setExtraClasspath("./target/classes"); } else if (findRestApiWar()!=null) { context = new WebAppContext(findRestApiWar(), "/"); } else { throw new IllegalStateException("Cannot find WAR for REST API. Expected in target/*.war, Maven repo, or in source directories."); } context.setAttribute(BrooklynServiceAttributes.BROOKLYN_MANAGEMENT_CONTEXT, mgmt); // TODO shutdown hook return context; } /** starts a server, on all NICs if security is configured, * otherwise (no security) only on loopback interface * @deprecated since 0.9.0 becoming private */ @Deprecated public static Server startServer(ManagementContext mgmt, ContextHandler context, String summary, boolean disableHighAvailability) { // TODO this repeats code in BrooklynLauncher / WebServer. should merge the two paths. boolean secure = mgmt != null && !BrooklynWebConfig.hasNoSecurityOptions(mgmt.getConfig()); if (secure) { log.debug("Detected security configured, launching server on all network interfaces"); } else { log.debug("Detected no security configured, launching server on loopback (localhost) network interface only"); if (mgmt!=null) { log.debug("Detected no security configured, running on loopback; disabling authentication"); ((BrooklynProperties)mgmt.getConfig()).put(BrooklynWebConfig.SECURITY_PROVIDER_CLASSNAME, AnyoneSecurityProvider.class.getName()); } } if (mgmt != null && disableHighAvailability) mgmt.getHighAvailabilityManager().disabled(); InetSocketAddress bindLocation = new InetSocketAddress( secure ? Networking.ANY_NIC : Networking.LOOPBACK, Networking.nextAvailablePort(FAVOURITE_PORT)); return startServer(context, summary, bindLocation); } /** @deprecated since 0.9.0 becoming private */ @Deprecated public static Server startServer(ContextHandler context, String summary, InetSocketAddress bindLocation) { Server server = new Server(bindLocation); server.setHandler(context); try { server.start(); } catch (Exception e) { throw Exceptions.propagate(e); } log.info("Brooklyn REST server started ("+summary+") on"); log.info(" http://localhost:"+((NetworkConnector)server.getConnectors()[0]).getLocalPort()+"/"); return server; } public static BrooklynRestApiLauncher launcher() { return new BrooklynRestApiLauncher(); } public static void main(String[] args) throws Exception { startRestResourcesViaFilter(); log.info("Press Ctrl-C to quit."); } public static Server startRestResourcesViaFilter() { return new BrooklynRestApiLauncher() .mode(StartMode.FILTER) .start(); } public static Server startRestResourcesViaServlet() throws Exception { return new BrooklynRestApiLauncher() .mode(StartMode.SERVLET) .start(); } public static Server startRestResourcesViaWebXml() throws Exception { return new BrooklynRestApiLauncher() .mode(StartMode.WEB_XML) .start(); } public void installAsServletFilter(ServletContextHandler context) { installAsServletFilter(context, DEFAULT_FILTERS); } private void installAsServletFilter(ServletContextHandler context, List<Class<? extends Filter>> filters) { installBrooklynFilters(context, filters); // now set up the REST servlet resources ResourceConfig config = new DefaultResourceConfig(); // load all our REST API modules, JSON, and Swagger for (Object r: BrooklynRestApi.getAllResources()) config.getSingletons().add(r); // disable caching for dynamic content config.getProperties().put(ResourceConfig.PROPERTY_CONTAINER_RESPONSE_FILTERS, NoCacheFilter.class.getName()); // Checks if appropriate request given HA status config.getProperties().put(ResourceConfig.PROPERTY_RESOURCE_FILTER_FACTORIES, org.apache.brooklyn.rest.filter.HaHotCheckResourceFilter.class.getName()); // configure to match empty path, or any thing which looks like a file path with /assets/ and extension html, css, js, or png // and treat that as static content config.getProperties().put(ServletContainer.PROPERTY_WEB_PAGE_CONTENT_REGEX, "(/?|[^?]*/assets/[^?]+\\.[A-Za-z0-9_]+)"); // and anything which is not matched as a servlet also falls through (but more expensive than a regex check?) config.getFeatures().put(ServletContainer.FEATURE_FILTER_FORWARD_ON_404, true); // finally create this as a _filter_ which falls through to a web app or something (optionally) FilterHolder filterHolder = new FilterHolder(new ServletContainer(config)); context.addFilter(filterHolder, "/*", EnumSet.allOf(DispatcherType.class)); ManagementContext mgmt = OsgiCompat.getManagementContext(context); config.getSingletons().add(new ManagementContextProvider(mgmt)); addShutdownListener(config, mgmt); } protected synchronized void addShutdownListener(ResourceConfig config, ManagementContext mgmt) { if (shutdownListener!=null) throw new IllegalStateException("Can only retrieve one shutdown listener"); shutdownListener = new ServerStoppingShutdownHandler(mgmt); config.getSingletons().add(new ShutdownHandlerProvider(shutdownListener)); } private static void installBrooklynFilters(ServletContextHandler context, List<Class<? extends Filter>> filters) { for (Class<? extends Filter> filter : filters) { context.addFilter(filter, "/*", EnumSet.allOf(DispatcherType.class)); } } /** * Starts the server on all nics (even if security not enabled). * @deprecated since 0.6.0; use {@link #launcher()} and set a custom context */ @Deprecated public static Server startServer(ContextHandler context, String summary) { return BrooklynRestApiLauncher.startServer(context, summary, new InetSocketAddress(Networking.ANY_NIC, Networking.nextAvailablePort(FAVOURITE_PORT))); } /** look for the JS GUI webapp in common source places, returning path to it if found, or null. * assumes `brooklyn-ui` is checked out as a sibling to `brooklyn-server`, and both are 2, 3, 1, or 0 * levels above the CWD. */ @Beta public static Maybe<String> findJsguiWebappInSource() { // normally up 2 levels to where brooklyn-* folders are, then into ui // (but in rest projects it might be 3 up, and in some IDEs we might run from parent dirs.) // TODO could also look in maven repo ? return findFirstMatchingFile( "../../brooklyn-ui/src/main/webapp", "../../../brooklyn-ui/src/main/webapp", "../brooklyn-ui/src/main/webapp", "./brooklyn-ui/src/main/webapp", "../../brooklyn-ui/target/*.war", "../../..brooklyn-ui/target/*.war", "../brooklyn-ui/target/*.war", "./brooklyn-ui/target/*.war"); } /** look for the REST WAR file in common places, returning path to it if found, or null */ private static String findRestApiWar() { // don't look at src/main/webapp here -- because classes won't be there! // could also look in maven repo ? // TODO looks like this stopped working at runtime a long time ago; // only needed for WEB_XML mode, and not used, but should remove or check? // (probably will be superseded by CXF/OSGi work however) return findMatchingFile("../rest/target/*.war").orNull(); } /** as {@link #findMatchingFile(String)} but finding the first */ public static Maybe<String> findFirstMatchingFile(String ...filenames) { for (String f: filenames) { Maybe<String> result = findMatchingFile(f); if (result.isPresent()) return result; } return Maybe.absent(); } /** returns the supplied filename if it exists (absolute or relative to the current directory); * supports globs in the filename portion only, in which case it returns the _newest_ matching file. * <p> * otherwise returns null */ @Beta // public because used in dependent test projects public static Maybe<String> findMatchingFile(String filename) { final File f = new File(filename); if (f.exists()) return Maybe.of(filename); File dir = f.getParentFile(); File result = null; if (dir.exists()) { File[] matchingFiles = dir.listFiles(new FilenameFilter() { @Override public boolean accept(File dir, String name) { return WildcardGlobs.isGlobMatched(f.getName(), name); } }); for (File mf: matchingFiles) { if (result==null || mf.lastModified() > result.lastModified()) result = mf; } } if (result==null) return Maybe.absent(); return Maybe.of(result.getAbsolutePath()); } /** create a directory with a simple index.html so we have some content being served up */ private static String createTempWebDirWithIndexHtml(String indexHtmlContent) { File dir = Files.createTempDir(); dir.deleteOnExit(); try { Files.write(indexHtmlContent, new File(dir, "index.html"), Charsets.UTF_8); } catch (IOException e) { Exceptions.propagate(e); } return dir.getAbsolutePath(); } }