/*
* Copyright 2016 Function1. All Rights Reserved.
*
* 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 tools.gsf.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.Set;
import static tools.gsf.config.ReflectionUtils.readConfigurationResource;
/**
* ServletContextListener that loads and configures the FactoryProducer for this
* application.
* <p>
* It is configured to auto-load into the servlet context by way of the
* {@code @WebListener} annotation. Overriding of this class is allowed, and overridden
* classes that are annotated with {@code @WebListener} will take precedence over this one.
* <p>
* By default, this class looks for the FactoryProducer class in a servletContext init
* parameter called {@link #GSF_FACTORY_PRODUCER}. The class should have a public zero-arg
* constructor.
* <p>
* If no servlet context init parameter is found, this loader will search the classPath for
* a resource called {@link #CONFIG_FILE}. This file can contain blank lines and comments
* (denoted by #) but must contain only one active line - a class name of a class implementing
* the FactoryProducer interface. If more than one class name is found, an exception
* will be thrown.
* <p>
* If neither of the above are found, {@link DefaultFactoryProducer} will be instantiated
* as the factory producer.
* <p>
* When the servlet context is initialized, the factory producer that is created is registered
* the servlet context using the parameter {@link #GSF_FACTORY_PRODUCER}, and removed when the
* servlet context is destroyed.
*
* @author Tony Field
* @since 2016-08-05
*/
@WebListener
public class ServletContextLoader implements ServletContextListener {
private static final Logger LOG = LoggerFactory.getLogger(ServletContextLoader.class);
/**
* Name of the servlet context init parameter containing the factory producer to be booted
*/
public static final String GSF_FACTORY_PRODUCER = "gsf-factory-producer";
/**
* Name of config file where the factory producer class is configured.
*/
public static final String CONFIG_FILE = "META-INF/gsf-factory-producer";
@Override
public void contextInitialized(ServletContextEvent servletContextEvent) {
final ServletContext context = servletContextEvent.getServletContext();
if (context.getMajorVersion() == 3 && context.getMinorVersion() < 0) {
throw new IllegalStateException("Servlet Container is configured for version less than 3.0. " +
"This ServletContextListener does not support 2.x and earlier as the load order of " +
"Listeners is not guaranteed.");
}
FactoryProducer factoryProducer = null;
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
if (factoryProducer == null) {
factoryProducer = configureFromInitParam(context, classLoader);
if (factoryProducer != null) {
LOG.info("FactoryProducer configured from init param: " + factoryProducer.getClass().getName());
}
}
if (factoryProducer == null) {
factoryProducer = configureFromServiceLocator(classLoader);
if (factoryProducer != null) {
LOG.info("FactoryProducer configured from service locator: " + factoryProducer.getClass().getName());
}
}
if (factoryProducer == null) {
factoryProducer = new DefaultFactoryProducer();
LOG.info("FactoryProducer defaulting to: " + factoryProducer.getClass().getName());
}
context.setAttribute(GSF_FACTORY_PRODUCER, factoryProducer);
}
@Override
public void contextDestroyed(ServletContextEvent servletContextEvent) {
servletContextEvent.getServletContext().removeAttribute(GSF_FACTORY_PRODUCER);
LOG.info("FactoryProducer un-registered from servlet context.");
}
private FactoryProducer configureFromInitParam(ServletContext servletContext, ClassLoader classLoader) {
String factoryProducerClassName = servletContext.getInitParameter(GSF_FACTORY_PRODUCER);
return instantiateFactoryProducer(factoryProducerClassName, classLoader);
}
private FactoryProducer configureFromServiceLocator(ClassLoader classLoader) {
Set<String> classes = readConfigurationResource(classLoader, CONFIG_FILE);
switch (classes.size()) {
case 0: {
// not found
return null;
}
case 1: {
// found it!
String fpClass = classes.iterator().next();
return instantiateFactoryProducer(fpClass, classLoader);
}
default: {
// too many
throw new IllegalStateException("Too many lines in the " + CONFIG_FILE +
" configuration file. Only one FactoryProducer class name is allowed.");
}
}
}
private static FactoryProducer instantiateFactoryProducer(String className, ClassLoader classLoader) {
if (className == null) {
return null;
}
try {
@SuppressWarnings("unchecked")
Class<FactoryProducer> cls = (Class<FactoryProducer>) classLoader.loadClass(className);
Constructor<FactoryProducer> constructor = cls.getConstructor();
return constructor.newInstance();
} catch (ClassNotFoundException e) {
throw new IllegalArgumentException("Could not find FactoryProducer class: " + className, e);
} catch (NoSuchMethodException e) {
throw new IllegalStateException("Could not locate zero-arg constructor for FactoryProducer in class: " + className, e);
} catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
throw new IllegalStateException("Could not invoke constructor for FactoryProducer class: " + className, e);
}
}
}