com.proofpoint.bootstrap.Bootstrap Maven / Gradle / Ivy
/*
* Copyright 2010 Proofpoint, 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 com.proofpoint.bootstrap;
import com.google.common.base.Function;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableList.Builder;
import com.google.common.collect.ImmutableMap;
import com.google.inject.Binder;
import com.google.inject.ConfigurationException;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.Module;
import com.google.inject.Stage;
import com.google.inject.spi.Message;
import com.proofpoint.configuration.ConfigurationAwareModule;
import com.proofpoint.configuration.ConfigurationDefaultingModule;
import com.proofpoint.configuration.ConfigurationFactory;
import com.proofpoint.configuration.ConfigurationFactoryBuilder;
import com.proofpoint.configuration.ConfigurationInspector;
import com.proofpoint.configuration.ConfigurationInspector.ConfigAttribute;
import com.proofpoint.configuration.ConfigurationInspector.ConfigRecord;
import com.proofpoint.configuration.ConfigurationModule;
import com.proofpoint.configuration.ConfigurationValidator;
import com.proofpoint.configuration.PropertiesBuilder;
import com.proofpoint.configuration.ValidationErrorModule;
import com.proofpoint.configuration.WarningsMonitor;
import com.proofpoint.log.Logger;
import com.proofpoint.log.Logging;
import com.proofpoint.log.LoggingConfiguration;
import com.proofpoint.node.ApplicationNameModule;
import com.proofpoint.node.NodeInfo;
import java.io.File;
import java.io.FileFilter;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicBoolean;
import static com.google.common.base.Preconditions.checkState;
import static java.util.Objects.requireNonNull;
import static java.util.Objects.requireNonNullElse;
/**
* Entry point for an application built using the platform codebase.
*
* This class will:
*
* - load, validate and bind configurations
* - initialize logging
* - set up lifecycle management
* - create an Guice injector
*
*
* An application is started with an invocation such as:
*
* try {
* Injector injector = bootstrapApplication("nameOfApplication")
* .withModules(
* new NodeModule(),
* new DiscoveryModule(),
* new HttpServerModule(),
* new JsonModule(),
* explicitJaxrsModule(),
* new MBeanModule(),
* new JmxModule(),
* new JmxHttpModule(),
* new LogJmxModule(),
* new ReportingModule(),
* new ReportingClientModule(),
* new MainModule()
* )
* .withApplicationDefaults(ImmutableMap.<String, String>builder()
* .put("http-server.http.enabled", "false")
* .put("http-server.https.enabled", "true")
* .put("http-server.https.port", "8443")
* .build()
* )
* .initialize();
*
* injector.getInstance(Announcer.class).start();
* }
* catch (Throwable e) {
* log.error(e);
* System.exit(1);
* }
*
*
* The configuration is read from a file specified by the "config" system property.
*
*
* A unit test would start an application instance with an invocation such as:
*
* @BeforeMethod
* public void setup()
* throws Exception
* {
* Injector injector = bootstrapTest()
* .withModules(
* new TestingNodeModule(),
* new TestingHttpServerModule(),
* new JsonModule(),
* explicitJaxrsModule(),
* new ReportingModule(),
* new TestingMBeanModule(),
* new MainModule()
* )
* .setRequiredConfigurationProperties(properties)
* .initialize();
*
* lifeCycleManager = injector.getInstance(LifeCycleManager.class);
* server = injector.getInstance(TestingHttpServer.class);
* }
*
* @AfterMethod(alwaysRun = true)
* public void teardown()
* throws Exception
* {
* if (lifeCycleManager != null) {
* lifeCycleManager.stop();
* }
* }
*
*/
public class Bootstrap
{
private final Logger log = Logger.get("Bootstrap");
private final Logging logging;
private final List modules;
private Map requiredConfigurationProperties = null;
private Map applicationDefaults = null;
private boolean quiet = false;
private boolean requireExplicitBindings = true;
private boolean initialized = false;
/**
* Start building an object for starting an application.
*
* @param applicationName the lowercase hyphen-separated name of the application
* @return an intermediate object for initializing the application
*/
public static BootstrapBeforeModules bootstrapApplication(String applicationName)
{
return new StaticBootstrapBeforeModules(applicationName);
}
/**
* Start building an object for starting an application whose name is dependent upon configuration.
*
* @param configClass the configuration class needed to determine the application name
* @param applicationNameFunction the {@link Function} to map from the configuration class
* to the lowercase hyphen-separated name of the application
* @return an intermediate object for initializing the application
*/
public static BootstrapBeforeModules bootstrapApplication(Class configClass, Function applicationNameFunction)
{
return new DynamicBootstrapBeforeModules<>(configClass, applicationNameFunction);
}
/**
* Start building an object for starting an application for a unit test.
*
* Suppresses logging initializing, reading of a configuration file, and verbose logging.
*
* @return an intermediate object for initializing the test application
*/
public static UnitTestBootstrapBeforeModules bootstrapTest()
{
return new UnitTestBootstrapBeforeModules();
}
private Bootstrap(Module applicationNameModule, Iterable extends Module> modules, boolean initializeLogging)
{
if (initializeLogging) {
logging = Logging.initialize();
Thread.setDefaultUncaughtExceptionHandler((t, e) -> log.error(e, "Uncaught exception in thread %s", t.getName()));
}
else {
logging = null;
}
this.modules = ImmutableList.builder()
.add(requireNonNull(applicationNameModule, "applicationNameModule is null"))
.add(new LifeCycleModule())
.addAll(modules)
.build();
}
/**
* Set a configuration property for use by the application's configuration. The property
* must be consumed by configuration. Suppresses reading a configuration file.
* Intended for use in unit tests.
*
* @deprecated Use {@link #bootstrapTest()} to bootstrap a unit test.
* @param key the name of the configuration property
* @param value the value of the configuration property
* @return the object, for chaining method calls.
*/
@Deprecated
public Bootstrap setRequiredConfigurationProperty(String key, String value)
{
if (this.requiredConfigurationProperties == null) {
this.requiredConfigurationProperties = new TreeMap<>();
}
this.requiredConfigurationProperties.put(key, value);
return this;
}
/**
* Set configuration properties for use by the application's configuration.
* All specified properties must be consumed by configuration. Suppresses
* reading a configuration file. Intended for use in unit tests.
*
* @deprecated Use {@link #bootstrapTest()} to bootstrap a unit test.
* @param requiredConfigurationProperties the configuration properties
* @return the object, for chaining method calls.
*/
@Deprecated
public Bootstrap setRequiredConfigurationProperties(Map requiredConfigurationProperties)
{
if (this.requiredConfigurationProperties == null) {
this.requiredConfigurationProperties = new TreeMap<>();
}
this.requiredConfigurationProperties.putAll(requiredConfigurationProperties);
return this;
}
/**
* Override the configuration parameter defaults with application-specific
* values. All specified properties must be consumed by configuration,
* though the values may be overridden by the application's configuration.
*
* An application would normally use this to, as a minimum, enable HTTPS
* by default and specify the application's ports.
*
* @param applicationDefaults properties specifying the application's defaults
* @return the object, for chaining method calls.
*/
public Bootstrap withApplicationDefaults(Map applicationDefaults)
{
checkState(this.applicationDefaults == null, "applicationDefaults already specified");
this.applicationDefaults = requireNonNull(applicationDefaults, "applicationDefaults is null");
return this;
}
/**
* Suppress some logging, such as that of the configuration. Intended for
* use in unit tests.
*
* @return the object, for chaining method calls.
*/
public Bootstrap quiet()
{
this.quiet = true;
return this;
}
/**
* Set the policy on Guice implicit bindings.
*
* @param requireExplicitBindings true if bindings must be listed in a Module in
* order to be injected. Default true.
* @return the object, for chaining method calls.
*/
@SuppressWarnings("unused")
public Bootstrap requireExplicitBindings(boolean requireExplicitBindings)
{
this.requireExplicitBindings = requireExplicitBindings;
return this;
}
/**
* Initialize the application and start its lifecycle.
*
* @return the application's Guice injector
* @throws Exception
*/
public Injector initialize()
throws Exception
{
checkState(!initialized, "Already initialized");
initialized = true;
Map moduleDefaults = new HashMap<>();
Map moduleDefaultSource = new HashMap<>();
List moduleDefaultErrors = new ArrayList<>();
for (Module module : modules) {
if (module instanceof ConfigurationDefaultingModule) {
ConfigurationDefaultingModule configurationDefaultingModule = (ConfigurationDefaultingModule) module;
Map defaults = configurationDefaultingModule.getConfigurationDefaults();
for (Entry entry : defaults.entrySet()) {
ConfigurationDefaultingModule oldModule = moduleDefaultSource.put(entry.getKey(), configurationDefaultingModule);
if (oldModule != null) {
moduleDefaultErrors.add(
new Message(module, "Configuration default for \"" + entry.getKey() + "\" set by both " + oldModule.toString() + " and " + module.toString()));
}
moduleDefaults.put(entry.getKey(), entry.getValue());
}
}
}
// initialize configuration
ConfigurationFactoryBuilder builder = new ConfigurationFactoryBuilder();
if (!moduleDefaults.isEmpty()) {
builder = builder.withModuleDefaults(moduleDefaults, moduleDefaultSource);
}
if (applicationDefaults != null) {
builder = builder.withApplicationDefaults(applicationDefaults);
}
if (requiredConfigurationProperties == null) {
String configPropertiesPath = System.getProperty("config");
if (configPropertiesPath == null && new File("etc/config/config.json").exists()) {
log.info("Loading configuration from etc/config/config.json");
builder = builder.withJsonFile("etc/config/config.json");
File[] jsonConfigFiles = new File("etc").listFiles((FileFilter) file -> {
if (!file.isDirectory() || "config".equals(file.getName()) || file.getName().startsWith(".")) {
return false;
}
File configJsonPath = file.toPath().resolve("config.json").toFile();
return configJsonPath.exists();
});
if (jsonConfigFiles == null) {
throw new RuntimeException("Could not list directories under etc");
}
for (File jsonConfigFile : jsonConfigFiles) {
builder = builder.withJsonFile(jsonConfigFile.toPath().resolve("config.json").toString());
}
}
else {
configPropertiesPath = requireNonNullElse(configPropertiesPath, "etc/config.properties");
log.info("Loading configuration from %s", configPropertiesPath);
builder = builder.withFile(configPropertiesPath);
String secretsConfigPath = System.getProperty("secrets-config");
if (secretsConfigPath == null && new File("etc/secrets.properties").exists()) {
secretsConfigPath = "etc/secrets.properties";
}
if (secretsConfigPath != null) {
builder = builder.withFile(secretsConfigPath);
}
}
builder = builder.withSystemProperties();
}
else {
builder = builder.withRequiredProperties(requiredConfigurationProperties);
}
WarningLoggingMonitor warningsMonitor = new WarningLoggingMonitor();
builder = builder.withWarningsMonitor(warningsMonitor);
ConfigurationFactory configurationFactory = builder.build();
if (logging != null) {
// initialize logging
log.info("Initializing logging");
LoggingConfiguration configuration = configurationFactory.build(LoggingConfiguration.class);
logging.configure(configuration);
if (configuration.getLevelsFile() == null) {
if (new File("etc/config/log.json").exists()) {
logging.setLevels(new PropertiesBuilder().withJsonFile("etc/config/log.json").throwOnError().getProperties());
}
else if (new File("etc/log.properties").exists()) {
logging.setLevels(new File("etc/log.properties"));
}
}
}
warningsMonitor.loggingInitialized();
// initialize configuration factory
modules.stream()
.filter(ConfigurationAwareModule.class::isInstance)
.map(ConfigurationAwareModule.class::cast)
.forEach(module -> module.setConfigurationFactory(configurationFactory));
// Validate configuration
ConfigurationValidator configurationValidator = new ConfigurationValidator(configurationFactory);
List messages = configurationValidator.validate(modules);
// Log effective configuration
if (!quiet) {
logConfiguration(configurationFactory);
}
// system modules
Builder moduleList = ImmutableList.builder();
moduleList.add(new ConfigurationModule(configurationFactory));
if (!moduleDefaultErrors.isEmpty()) {
moduleList.add(new ValidationErrorModule(moduleDefaultErrors));
}
if (!messages.isEmpty()) {
moduleList.add(new ValidationErrorModule(messages));
}
moduleList.add(binder -> binder.bind(WarningsMonitor.class).toInstance(warningsMonitor));
moduleList.add(binder -> binder.bindConstant().annotatedWith(QuietMode.class).to(quiet));
// disable broken Guice "features"
moduleList.add(Binder::disableCircularProxies);
if (requireExplicitBindings) {
moduleList.add(Binder::requireExplicitBindings);
}
moduleList.addAll(modules);
// create the injector
final Injector injector = Guice.createInjector(Stage.PRODUCTION, moduleList.build());
if (!quiet) {
try {
NodeInfo nodeInfo = injector.getInstance(NodeInfo.class);
log.info("Node ID %s", nodeInfo.getNodeId());
}
catch (ConfigurationException ignored) {
}
}
// Create the life-cycle manager
LifeCycleManager lifeCycleManager = injector.getInstance(LifeCycleManager.class);
// Start services
if (lifeCycleManager.size() > 0) {
lifeCycleManager.start();
}
return injector;
}
private void logConfiguration(ConfigurationFactory configurationFactory)
{
ColumnPrinter columnPrinter = makePrinterForConfiguration(configurationFactory);
try (PrintWriter out = new PrintWriter(new LoggingWriter(log))) {
columnPrinter.print(out);
}
}
private static ColumnPrinter makePrinterForConfiguration(ConfigurationFactory configurationFactory)
{
ConfigurationInspector configurationInspector = new ConfigurationInspector();
ColumnPrinter columnPrinter = new ColumnPrinter(
"PROPERTY", "DEFAULT", "RUNTIME", "DESCRIPTION");
Set attributes = new TreeSet<>((o1, o2) -> o1.getPropertyName().compareTo(o2.getPropertyName()));
for (ConfigRecord> record : configurationInspector.inspect(configurationFactory)) {
attributes.addAll(record.getAttributes());
}
for (ConfigAttribute attribute : attributes) {
columnPrinter.addValues(
attribute.getPropertyName(),
attribute.getDefaultValue(),
attribute.getCurrentValue(),
attribute.getDescription());
}
return columnPrinter;
}
public abstract static class BootstrapBeforeModules
{
private BootstrapBeforeModules()
{
}
boolean initializeLogging = true;
/**
* Suppresses initialization of the logging subsystem. Intended for
* use by unit tests.
*
* @return the object, for chaining method calls.
*/
public BootstrapBeforeModules doNotInitializeLogging()
{
this.initializeLogging = false;
return this;
}
/**
* Specify the application's Guice Modules
*
* @param modules the application's Modules
* @return the object, for chaining method calls.
*/
public Bootstrap withModules(Module... modules)
{
return withModules(ImmutableList.copyOf(modules));
}
/**
* Specify the application's Guice Modules
*
* @param modules the application's Modules
* @return the object, for chaining method calls.
*/
public abstract Bootstrap withModules(Iterable extends Module> modules);
}
private static class StaticBootstrapBeforeModules extends BootstrapBeforeModules
{
private final String applicationName;
private StaticBootstrapBeforeModules(String applicationName)
{
this.applicationName = requireNonNull(applicationName, "applicationName is null");
}
@Override
public Bootstrap withModules(Iterable extends Module> modules)
{
return new Bootstrap(new ApplicationNameModule(applicationName), modules, initializeLogging);
}
}
private static class DynamicBootstrapBeforeModules extends BootstrapBeforeModules
{
private final Class configClass;
private final Function applicationNameFunction;
private DynamicBootstrapBeforeModules(Class configClass, Function applicationNameFunction)
{
this.configClass = requireNonNull(configClass, "configClass is null");
this.applicationNameFunction = requireNonNull(applicationNameFunction, "applicationNameFunction is null");
}
@Override
public Bootstrap withModules(Iterable extends Module> modules)
{
return new Bootstrap(new DynamicApplicationNameModule<>(configClass, applicationNameFunction), modules, initializeLogging);
}
}
public static class UnitTestBootstrapBeforeModules
{
private UnitTestBootstrapBeforeModules()
{
}
/**
* Specify the application's Guice Modules
*
* @param modules the application's Modules
* @return the object, for chaining method calls.
*/
public UnitTestBootstrap withModules(Iterable extends Module> modules)
{
return new UnitTestBootstrap(modules);
}
/**
* Specify the application's Guice Modules
*
* @param modules the application's Modules
* @return the object, for chaining method calls.
*/
public UnitTestBootstrap withModules(Module... modules)
{
return withModules(ImmutableList.copyOf(modules));
}
}
public static class UnitTestBootstrap
{
private Bootstrap bootstrap;
@SuppressWarnings("deprecation")
private UnitTestBootstrap(Iterable extends Module> modules)
{
bootstrap = new Bootstrap(new ApplicationNameModule("test-application"), modules, false)
.quiet()
.setRequiredConfigurationProperties(ImmutableMap.of()); // Suppress reading configuration file
}
/**
* Set a configuration property for use by the application's configuration. The property
* must be consumed by configuration.
*
* @param key the name of the configuration property
* @param value the value of the configuration property
* @return the object, for chaining method calls.
*/
@SuppressWarnings("deprecation")
public UnitTestBootstrap setRequiredConfigurationProperty(String key, String value)
{
bootstrap = bootstrap.setRequiredConfigurationProperty(key, value);
return this;
}
/**
* Set configuration properties for use by the application's configuration.
* All specified properties must be consumed by configuration.
*
* @param requiredConfigurationProperties the configuration properties
* @return the object, for chaining method calls.
*/
@SuppressWarnings("deprecation")
public UnitTestBootstrap setRequiredConfigurationProperties(Map requiredConfigurationProperties)
{
bootstrap = bootstrap.setRequiredConfigurationProperties(requiredConfigurationProperties);
return this;
}
/**
* Override the configuration parameter defaults with application-specific
* values. All specified properties must be consumed by configuration,
* though the values may be overridden by the application's configuration.
*
* An application would normally use this to, as a minimum, enable HTTPS
* by default and specify the application's ports.
*
* @param applicationDefaults properties specifying the application's defaults
* @return the object, for chaining method calls.
*/
public UnitTestBootstrap withApplicationDefaults(Map applicationDefaults)
{
bootstrap = bootstrap.withApplicationDefaults(applicationDefaults);
return this;
}
/**
* Set whether properties in configuration files must be consumed by
* configuration.
*
* @param requireExplicitBindings true if properties in configuration
* files must be consumed. Default true.
* @return the object, for chaining method calls.
*/
public UnitTestBootstrap requireExplicitBindings(boolean requireExplicitBindings)
{
bootstrap = bootstrap.requireExplicitBindings(requireExplicitBindings);
return this;
}
/**
* Initialize the application and start its lifecycle.
*
* @return the application's Guice injector
*/
public Injector initialize()
throws Exception
{
return bootstrap.initialize();
}
}
private class WarningLoggingMonitor
implements WarningsMonitor
{
private final AtomicBoolean loggingInitialized = new AtomicBoolean();
private final List warnings = new ArrayList<>();
@Override
public void onWarning(String message)
{
if (loggingInitialized.get()) {
log.warn(message);
}
else {
warnings.add(message);
}
}
public void loggingInitialized()
{
loggingInitialized.set(true);
for (String warning : warnings) {
onWarning(warning);
}
warnings.clear();
}
}
}