/*
* Copyright 2013 Netflix, Inc.
*
* Licensed 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 netflix.adminresources;
import com.google.inject.AbstractModule;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.Module;
import com.google.inject.Singleton;
import com.google.inject.Stage;
import com.netflix.governator.guice.LifecycleInjector;
import com.netflix.governator.lifecycle.LifecycleManager;
import org.mortbay.jetty.Connector;
import org.mortbay.jetty.Handler;
import org.mortbay.jetty.Server;
import org.mortbay.jetty.handler.HandlerCollection;
import org.mortbay.jetty.handler.ResourceHandler;
import org.mortbay.jetty.servlet.Context;
import org.mortbay.jetty.servlet.DefaultServlet;
import org.mortbay.jetty.servlet.FilterHolder;
import org.mortbay.jetty.servlet.ServletHolder;
import org.mortbay.jetty.servlet.SessionHandler;
import org.mortbay.resource.Resource;
import org.mortbay.thread.QueuedThreadPool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.servlet.Filter;
import netflix.admin.AdminConfigImpl;
import netflix.admin.AdminContainerConfig;
/**
* This class starts an embedded jetty server, listening at port specified by property
* {@link netflix.admin.AdminContainerConfig#listenPort()} and defaulting to
* {@link netflix.admin.AdminContainerConfig}. <br>
* <p/>
* The embedded server uses jersey so any jersey resources available in packages
* specified via properties {@link netflix.admin.AdminContainerConfig#jerseyResourcePkgList()}will be scanned and initialized. <br>
* <p/>
* Karyon admin starts in an embedded container to have a "always available" endpoint for any application. This helps
* in a homogeneous admin view for all applications. <br>
* <p/>
* <h3>Available resources</h3>
* <p/>
* The following resources are available by default:
* <p/>
* </ul>
*/
@Singleton
public class AdminResourcesContainer {
private static final Logger logger = LoggerFactory.getLogger(AdminResourcesContainer.class);
/**
* @deprecated here for backwards compatibility. Use {@link AdminConfigImpl#CONTAINER_LISTEN_PORT}.
*/
@Deprecated
public static final String CONTAINER_LISTEN_PORT = AdminConfigImpl.CONTAINER_LISTEN_PORT;
private Server server;
@Inject(optional = true)
private Injector appInjector;
@Inject(optional = true)
private AdminContainerConfig adminContainerConfig;
@Inject(optional = true)
private AdminPageRegistry adminPageRegistry;
private AtomicBoolean alreadyInited = new AtomicBoolean(false);
private int serverPort; // actual server listen port (apart from what's in Config)
/**
* Starts the container and hence the embedded jetty server.
*
* @throws Exception if there is an issue while starting the server
*/
@PostConstruct
public void init() throws Exception {
try {
if (alreadyInited.compareAndSet(false, true)) {
initAdminContainerConfigIfNeeded();
initAdminRegistryIfNeeded();
if (! adminContainerConfig.shouldEnable()) {
return;
}
if (adminContainerConfig.shouldScanClassPathForPluginDiscovery()) {
adminPageRegistry.registerAdminPagesWithClasspathScan();
}
Injector adminResourceInjector;
if (shouldShareResourcesWithParentInjector()) {
adminResourceInjector = appInjector.createChildInjector(buildAdminPluginsGuiceModules());
} else {
adminResourceInjector = LifecycleInjector
.builder()
.inStage(Stage.DEVELOPMENT)
.usingBasePackages("com.netflix.explorers")
.withModules(buildAdminPluginsGuiceModules())
.build()
.createInjector();
adminResourceInjector.getInstance(LifecycleManager.class).start();
}
server = new Server(adminContainerConfig.listenPort());
final List<Filter> additionaFilters = adminContainerConfig.additionalFilters();
// redirect filter based on configurable RedirectRules
final Context rootHandler = new Context();
rootHandler.setContextPath("/");
rootHandler.addFilter(new FilterHolder(adminResourceInjector.getInstance(RedirectFilter.class)), "/*", Handler.DEFAULT);
rootHandler.addServlet(new ServletHolder(new DefaultServlet()), "/*");
// admin page template resources
AdminResourcesFilter arfTemplatesResources = adminResourceInjector.getInstance(AdminResourcesFilter.class);
arfTemplatesResources.setPackages(adminContainerConfig.jerseyViewableResourcePkgList());
logger.info("Admin templates context : {}", adminContainerConfig.templateResourceContext());
final Context adminTemplatesResHandler = new Context();
adminTemplatesResHandler.setContextPath(adminContainerConfig.templateResourceContext());
adminTemplatesResHandler.setSessionHandler(new SessionHandler());
adminTemplatesResHandler.addFilter(LoggingFilter.class, "/*", Handler.DEFAULT);
adminTemplatesResHandler.addFilter(new FilterHolder(adminResourceInjector.getInstance(RedirectFilter.class)), "/*", Handler.DEFAULT);
applyAdditionalFilters(adminTemplatesResHandler, additionaFilters);
adminTemplatesResHandler.addFilter(new FilterHolder(arfTemplatesResources), "/*", Handler.DEFAULT);
adminTemplatesResHandler.addServlet(new ServletHolder(new DefaultServlet()), "/*");
// admin page data resources
final String jerseyPkgListForAjaxResources = appendCoreJerseyPackages(adminPageRegistry.buildJerseyResourcePkgListForAdminPages());
AdminResourcesFilter arfDataResources = adminResourceInjector.getInstance(AdminResourcesFilter.class);
arfDataResources.setPackages(jerseyPkgListForAjaxResources);
logger.info("Admin resources context : {}", adminContainerConfig.ajaxDataResourceContext());
final Context adminDataResHandler = new Context();
adminDataResHandler.setContextPath(adminContainerConfig.ajaxDataResourceContext());
adminDataResHandler.addFilter(LoggingFilter.class, "/*", Handler.DEFAULT);
adminDataResHandler.addFilter(new FilterHolder(adminResourceInjector.getInstance(RedirectFilter.class)), "/*", Handler.DEFAULT);
applyAdditionalFilters(adminDataResHandler, additionaFilters);
adminDataResHandler.addFilter(new FilterHolder(arfDataResources), "/*", Handler.DEFAULT);
adminDataResHandler.addServlet(new ServletHolder(new DefaultServlet()), "/*");
QueuedThreadPool threadPool = new QueuedThreadPool();
threadPool.setDaemon(true);
server.setThreadPool(threadPool);
ResourceHandler resource_handler = new ResourceHandler() {
@Override
public Resource getResource(String path) throws MalformedURLException {
Resource resource = Resource.newClassPathResource(path);
if (resource == null || !resource.exists()) {
resource = Resource.newClassPathResource("META-INF/resources" + path);
}
if (resource != null && resource.isDirectory()) {
return null;
}
return resource;
}
};
resource_handler.setResourceBase("/");
HandlerCollection handlers = new HandlerCollection();
handlers.setHandlers(new Handler[]{resource_handler, adminTemplatesResHandler, adminDataResHandler, rootHandler});
server.setHandler(handlers);
server.start();
final Connector connector = server.getConnectors()[0];
serverPort = connector.getLocalPort();
logger.info("jetty started on port {}", serverPort);
}
} catch (Exception e) {
logger.error("Exception in building AdminResourcesContainer ", e);
}
}
private void initAdminContainerConfigIfNeeded() {
if (adminContainerConfig == null) {
adminContainerConfig = new AdminConfigImpl();
}
}
private void initAdminRegistryIfNeeded() {
if (adminPageRegistry == null) {
adminPageRegistry = new AdminPageRegistry();
}
}
public int getServerPort() {
return serverPort;
}
public AdminPageRegistry getAdminPageRegistry() {
return adminPageRegistry;
}
private Module getAdditionalBindings() {
return new AbstractModule() {
@Override
protected void configure() {
bind(AdminResourcesFilter.class);
if (! shouldShareResourcesWithParentInjector()) {
bind(AdminPageRegistry.class).toInstance(adminPageRegistry);
bind(AdminContainerConfig.class).toInstance(adminContainerConfig);
}
}
};
}
private String appendCoreJerseyPackages(String jerseyResourcePkgListForAdminPages) {
String pkgPath = adminContainerConfig.jerseyResourcePkgList();
if (jerseyResourcePkgListForAdminPages != null && !jerseyResourcePkgListForAdminPages.isEmpty()) {
pkgPath += ";" + jerseyResourcePkgListForAdminPages;
}
return pkgPath;
}
private List<Module> buildAdminPluginsGuiceModules() {
List<Module> guiceModules = new ArrayList<>();
if (adminPageRegistry != null) {
final Collection<AdminPageInfo> allPages = adminPageRegistry.getAllPages();
for (AdminPageInfo adminPlugin : allPages) {
logger.info("Adding admin page {}: jersey={} modules{}",
adminPlugin.getName(),
adminPlugin.getJerseyResourcePackageList(),
adminPlugin.getGuiceModules());
final List<Module> guiceModuleList = adminPlugin.getGuiceModules();
if (guiceModuleList != null && !guiceModuleList.isEmpty()) {
guiceModules.addAll(adminPlugin.getGuiceModules());
}
}
}
guiceModules.add(getAdditionalBindings());
return guiceModules;
}
private boolean shouldShareResourcesWithParentInjector() {
return appInjector != null && ! adminContainerConfig.shouldIsolateResources();
}
private void applyAdditionalFilters(final Context contextHandler, List<Filter> additionalFilters) {
if (additionalFilters != null && !additionalFilters.isEmpty()) {
for(Filter f : additionalFilters) {
contextHandler.addFilter(new FilterHolder(f), "/*", Handler.DEFAULT);
}
}
}
@PreDestroy
public void shutdown() {
try {
if (server != null) {
server.stop();
}
} catch (Throwable t) {
logger.warn("Error while shutting down Admin resources server", t);
} finally {
alreadyInited.set(false);
}
}
}