package com.kryptnostic.rhizome.core;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.health.HealthCheckRegistry;
import com.codahale.metrics.servlets.AdminServlet;
import com.codahale.metrics.servlets.HealthCheckServlet;
import com.codahale.metrics.servlets.MetricsServlet;
import com.google.common.base.Charsets;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.google.common.io.Resources;
import com.hazelcast.web.SessionListener;
import com.hazelcast.web.WebFilter;
import com.kryptnostic.rhizome.configuration.ConfigurationConstants.Profiles;
import com.kryptnostic.rhizome.configuration.RhizomeConfiguration;
import com.kryptnostic.rhizome.configuration.amazon.AmazonLaunchConfiguration;
import com.kryptnostic.rhizome.configuration.jetty.JettyConfiguration;
import com.kryptnostic.rhizome.configuration.servlets.DispatcherServletConfiguration;
import com.kryptnostic.rhizome.pods.*;
import com.kryptnostic.rhizome.startup.Requirement;
import org.apache.commons.lang.StringUtils;
import org.eclipse.jetty.servlet.DefaultServlet;
import org.glassfish.jersey.servlet.ServletContainer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.context.request.RequestContextListener;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
import javax.servlet.*;
import java.io.IOException;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* Note: if using jetty, jetty creates an instance of this class with a no-arg constructor in order to call onStartup
* TODO: break out WebApplicationInitializer's onStartup to a different class because of Jetty issue
*
*/
public class Rhizome implements WebApplicationInitializer {
private static final Logger logger = LoggerFactory
.getLogger( Rhizome.class );
private static final String HAZELCAST_SESSION_FILTER_NAME = "hazelcastSessionFilter";
protected static final Class<?>[] DEFAULT_SERVICE_PODS = new Class<?>[] {
ConfigurationPod.class,
MetricsPod.class,
AsyncPod.class };
protected static final Lock startupLock = new ReentrantLock();
protected static AnnotationConfigWebApplicationContext rhizomeContext = null;
protected final AnnotationConfigWebApplicationContext context;
private JettyLoam jetty;
public Rhizome() {
this( new Class<?>[] {} );
}
public Rhizome( Class<?>... pods ) {
this( JettyContainerPod.class, pods );
}
public Rhizome( Class<? extends LoamPod> loamPodClass, Class<?>... pods ) {
this( new AnnotationConfigWebApplicationContext(), loamPodClass, pods );
}
public Rhizome(
AnnotationConfigWebApplicationContext context,
Class<? extends LoamPod> loamPodClass,
Class<?>... pods ) {
this.context = context;
intercrop( pods );
if ( loamPodClass != null ) {
intercrop( loamPodClass );
}
intercrop( getDefaultServicePods() );
}
@Override
public void onStartup( ServletContext servletContext ) throws ServletException {
Preconditions.checkNotNull( rhizomeContext, "Rhizome context cannot be null for startup." );
servletContext.addListener( new SessionListener() );
/*
* We have the luxury of being able to access the RhizomeConfiguration from the rhizomeContext. This allows us
* to conditionally enabled session clustering among other things.
*/
RhizomeConfiguration configuration = rhizomeContext.getBean( RhizomeConfiguration.class );
JettyConfiguration jettyConfiguration = rhizomeContext.getBean( JettyConfiguration.class );
if ( configuration.isSessionClusteringEnabled() ) {
FilterRegistration.Dynamic addFilter = servletContext.addFilter(
HAZELCAST_SESSION_FILTER_NAME,
rhizomeContext.getBean( WebFilter.class ) );
addFilter.addMappingForUrlPatterns(
Sets.newEnumSet(
ImmutableSet.of( DispatcherType.FORWARD, DispatcherType.REQUEST, DispatcherType.REQUEST ),
DispatcherType.class ),
false,
"/*" );
addFilter.setAsyncSupported( true );
}
// Prevent jersey-spring3 from trying to initialize a spring application context.
// servletContext.setInitParameter( CONTEXT_CONFIG_LOCATION_PARAMETER_NAME , "" );
servletContext.addListener( new ContextLoaderListener( rhizomeContext ) );
servletContext.addListener( new RequestContextListener() );
// Register the health check registry.
servletContext.setAttribute(
HealthCheckServlet.HEALTH_CHECK_REGISTRY,
rhizomeContext.getBean( "getHealthCheckRegistry", HealthCheckRegistry.class ) );
servletContext.setAttribute(
MetricsServlet.METRICS_REGISTRY,
rhizomeContext.getBean( "getMetricRegistry", MetricRegistry.class ) );
/*
*
*/
ServletRegistration.Dynamic adminServlet = servletContext.addServlet( "admin", AdminServlet.class );
adminServlet.setLoadOnStartup( 1 );
adminServlet.addMapping( "/admin/*" );
adminServlet.setInitParameter( "show-jvm-metrics", "true" );
/*
* Jersey Servlet For lovers of the JAX-RS standard.
*/
ServletRegistration.Dynamic jerseyDispatcher = servletContext.addServlet(
"defaultJerseyServlet",
new ServletContainer() );
jerseyDispatcher.setInitParameter( "javax.ws.rs.Application", RhizomeApplication.class.getName() );
jerseyDispatcher.setLoadOnStartup( 1 );
jerseyDispatcher.addMapping( "/health/*" );
/*
* Atmosphere Servlet
*/
// TODO: Add support for atmosphere servlet.
/*
* Default Servlet
*/
if ( jettyConfiguration.isDefaultServletEnabled() ) {
ServletRegistration.Dynamic defaultServlet = servletContext.addServlet( "default", new DefaultServlet() );
defaultServlet.addMapping( new String[] { "/*" } );
defaultServlet.setLoadOnStartup( 1 );
defaultServlet.setAsyncSupported( true );
}
registerDispatcherServlets( servletContext );
}
private void registerDispatcherServlets( ServletContext servletContext ) {
Map<String, DispatcherServletConfiguration> dispatcherServletsConfigs = rhizomeContext.getBeansOfType(
DispatcherServletConfiguration.class,
false,
true );
for ( Entry<String, DispatcherServletConfiguration> configPair : dispatcherServletsConfigs.entrySet() ) {
DispatcherServletConfiguration configuration = configPair.getValue();
AnnotationConfigWebApplicationContext dispatchServletContext = new AnnotationConfigWebApplicationContext();
logger.info( " Registering dispatcher servlet: {}", configuration );
dispatchServletContext.setParent( rhizomeContext );
dispatchServletContext.register( configuration.getPods().toArray( new Class<?>[ 0 ] ) );
ServletRegistration.Dynamic dispatcher = servletContext.addServlet(
configuration.getServletName(),
new DispatcherServlet( dispatchServletContext ) );
Preconditions.checkNotNull( dispatcher,
"A DispatcherServlet with this name has already been registered and fully configured" );
if ( configuration.getLoadOnStartup().isPresent() ) {
dispatcher.setLoadOnStartup( configuration.getLoadOnStartup().get() );
}
dispatcher.setAsyncSupported( true );
dispatcher.addMapping( configuration.getMappings() );
}
}
public AnnotationConfigWebApplicationContext getContext() {
return context;
}
public <T> T harvest( Class<T> clazz ) {
return context.getBean( clazz );
}
public void intercrop( Class<?>... pods ) {
if ( pods != null && pods.length > 0 ) {
context.register( pods );
}
}
public void sprout( String... activeProfiles ) throws Exception {
boolean awsProfile = false;
boolean localProfile = false;
for ( String profile : activeProfiles ) {
if ( StringUtils.equals( Profiles.AWS_CONFIGURATION_PROFILE, profile ) ) {
awsProfile = true;
logger.info( "Using AWS profile for configuration." );
}
if ( StringUtils.equals( Profiles.LOCAL_CONFIGURATION_PROFILE, profile ) ) {
localProfile = true;
logger.info( "Using Local profile for configuration." );
}
context.getEnvironment().addActiveProfile( profile );
}
if ( !awsProfile && !localProfile ) {
context.getEnvironment().addActiveProfile( Profiles.LOCAL_CONFIGURATION_PROFILE );
}
/*
* This will trigger creation of Jetty, so we: 1) Lock on singleton context 2) switch in the correct singleton
* context 3) set back to null and release lock once Jetty has finished starting.
*/
try {
startupLock.lock();
Preconditions.checkState(
rhizomeContext == null,
"Rhizome context should be null before startup of startup." );
rhizomeContext = context;
context.refresh();
if ( awsProfile ) {
startJettyAws( rhizomeContext.getBean( JettyConfiguration.class ),
rhizomeContext.getBean( AmazonLaunchConfiguration.class ) );
} else {
// For now we assume just AWS as an alternate to a local profile.
startJetty( rhizomeContext.getBean( JettyConfiguration.class ) );
}
} finally {
rhizomeContext = null;
if ( context.isActive() && startupRequirementsSatisfied( context ) ) {
showBanner();
}
startupLock.unlock();
}
}
protected void startJetty( JettyConfiguration configuration ) throws Exception {
this.jetty = new JettyLoam( configuration );
jetty.start();
}
protected void startJettyAws( JettyConfiguration configuration, AmazonLaunchConfiguration awsConfig )
throws Exception {
this.jetty = new AwsJettyLoam(
Preconditions.checkNotNull( configuration, "Jetty configuration cannot be null" ),
Preconditions.checkNotNull( awsConfig, "AwsConfig cannot be null" ) );
jetty.start();
}
public void wilt() throws BeansException, Exception {
jetty.stop();
context.close();
}
// This method is not static so it can be sub-classed and overridden.
public Class<?>[] getDefaultServicePods() {
return DEFAULT_SERVICE_PODS;
}
public static void showBanner() {
try {
logger.info( "\n\n{}\n\n", Resources.toString( Resources.getResource( "banner.txt" ), Charsets.UTF_8 ) );
} catch ( IOException e ) {
logger.warn( "No startup banner found." );
}
}
public static boolean startupRequirementsSatisfied( AnnotationConfigWebApplicationContext context ) {
return context.getBeansOfType( Requirement.class )
.values()
.parallelStream()
.allMatch( Requirement::isSatisfied );
}
}