/*******************************************************************************
* Copyright (c) 2012-2017 Codenvy, S.A.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Codenvy, S.A. - initial API and implementation
*******************************************************************************/
package org.eclipse.che.inject;
import com.google.common.base.Splitter;
import com.google.common.io.Files;
import com.google.inject.AbstractModule;
import com.google.inject.Injector;
import com.google.inject.Module;
import com.google.inject.name.Names;
import com.google.inject.servlet.ServletModule;
import com.google.inject.util.Modules;
import com.google.inject.util.Providers;
import org.eclipse.che.inject.lifecycle.DestroyModule;
import org.eclipse.che.inject.lifecycle.Destroyer;
import org.eclipse.che.inject.lifecycle.InitModule;
import org.everrest.guice.servlet.EverrestGuiceContextListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import java.io.File;
import java.io.IOException;
import java.io.Reader;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.AbstractMap.SimpleEntry;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static java.lang.String.format;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;
import static org.eclipse.che.inject.lifecycle.DestroyErrorHandler.LOG_HANDLER;
/**
* CheBootstrap is entry point of Che application implemented as ServletContextListener.
* <ul>
* <li>Initializes Guice Injector</li>
* <li>Automatically binds all the subclasses of com.google.inject.Module annotated with @DynaModule</li>
* <li>Loads configuration from .properties and .xml files located in <i>/WEB-INF/classes/che</i> directory</li>
* <li>Overrides it with external configuration located in directory pointed by <i>CHE_LOCAL_CONF_DIR</i> env variable (if any)</li>
* <li>Binds all environment variables (visible as prefixed with "env.") and system properties (visible as prefixed with "sys.")</li>
* <li>Thanks to Everrest integration injects all the properly annotated (see Everrest docs) REST Resources. Providers and ExceptionMappers
* and inject necessary dependencies</li>
* </ul>
* <p/>
* Configuration properties are bound as a {@code @Named}. For example:
* Following entry in the .property file:
* {@code myProp=value}
* may be injected into constructor (other options are valid too of course) as following:
* <pre>
* @Inject
* public MyClass(@Named("myProp") String my) {
* }
* </pre>
* <p/>
* It's possible to use system properties or environment variables in .properties files.
* <pre>
* my_app.input_dir=${root_data}/input/
* my_app.output_dir=${root_data}/output/
* </pre>
* NOTE: System property always takes preference on environment variable with the same name.
* <p/>
* <table>
* <tr><th>Value</th><th>System property</th><th>Environment variable</th><th>Result</th></tr>
* <tr><td>${root_data}/input/</td><td>/home/andrew/temp</td><td> </td><td>/home/andrew/temp/input/</td></tr>
* <tr><td>${root_data}/input/</td><td> </td><td>/usr/local</td><td>/usr/local/input/</td></tr>
* <tr><td>${root_data}/input/</td><td>/home/andrew/temp</td><td>/usr/local</td><td>/home/andrew/temp/input/</td></tr>
* <tr><td>${root_data}/input/</td><td> </td><td> </td><td>${root_data}/input/</td></tr>
* </table>
*
* @author gazarenkov
* @author andrew00x
* @author Florent Benoit
*/
public class CheBootstrap extends EverrestGuiceContextListener {
private static final Logger LOG = LoggerFactory.getLogger(CheBootstrap.class);
/** Environment variable that is used to override some Che settings properties. */
public static final String CHE_LOCAL_CONF_DIR = "CHE_LOCAL_CONF_DIR";
public static final String PROPERTIES_ALIASES_CONFIG_FILE = "che_aliases.properties";
/** Path to the internal folder that is expected in WEB-INF/classes */
private static final String WEB_INF_RESOURCES = "che";
/** Backward compliant path to the internal folder that is expected in WEB-INF/classes */
private static final String COMPLIANT_WEB_INF_RESOURCES = "codenvy";
private static final String NULL = "NULL";
private final List<Module> modules = new ArrayList<>();
@Override
public void contextDestroyed(ServletContextEvent sce) {
final ServletContext ctx = sce.getServletContext();
final Injector injector = getInjector(ctx);
if (injector != null) {
injector.getInstance(Destroyer.class).destroy();
}
super.contextDestroyed(sce);
}
@Override
protected List<Module> getModules() {
// based on logic that getServletModule() is called BEFORE getModules() in the EverrestGuiceContextListener
modules.add(new InitModule(PostConstruct.class));
modules.add(new DestroyModule(PreDestroy.class, LOG_HANDLER));
modules.add(new URIConverter());
modules.add(new URLConverter());
modules.add(new FileConverter());
modules.add(new PathConverter());
modules.add(new StringArrayConverter());
modules.add(new PairConverter());
modules.add(new PairArrayConverter());
modules.addAll(ModuleScanner.findModules());
Map<String, Set<String>> aliases = readConfigurationAliases();
Module firstConfigurationPermutation = Modules.override(new WebInfConfiguration(aliases)).with(new ExtConfiguration(aliases));
Module secondConfigurationPermutation = Modules.override(firstConfigurationPermutation)
.with(new CheSystemPropertiesConfigurationModule(aliases));
Module lastConfigurationPermutation = Modules.override(secondConfigurationPermutation)
.with(new CheEnvironmentVariablesConfigurationModule(aliases));
modules.add(lastConfigurationPermutation);
return modules;
}
private Map<String, Set<String>> readConfigurationAliases() {
URL aliasesResource = getClass().getClassLoader().getResource(PROPERTIES_ALIASES_CONFIG_FILE);
Map<String, Set<String>> aliases = new HashMap<>();
if (aliasesResource != null) {
Properties properties = new Properties();
File aliasesFile = new File(aliasesResource.getFile());
try (Reader reader = Files.newReader(aliasesFile, Charset.forName("UTF-8"))) {
properties.load(reader);
} catch (IOException e) {
throw new IllegalStateException(format("Unable to read configuration aliases from file %s", aliasesFile), e);
}
for (Map.Entry<Object, Object> entry : properties.entrySet()) {
String value = (String)entry.getValue();
aliases.put((String)entry.getKey(),
Splitter.on(',').splitToList(value).stream().map(String::trim).collect(toSet()));
}
}
return aliases;
}
/** see http://google-guice.googlecode.com/git/javadoc/com/google/inject/servlet/ServletModule.html */
@Override
protected ServletModule getServletModule() {
// Servlets and other web components may be configured with custom Modules.
return null;
}
/** ConfigurationModule binding configuration located in <i>/WEB-INF/classes/che</i> directory */
static class WebInfConfiguration extends AbstractConfigurationModule {
WebInfConfiguration(Map<String, Set<String>> aliases) {
super(aliases);
}
protected void configure() {
URL compliantWebInfConf = getClass().getClassLoader().getResource(COMPLIANT_WEB_INF_RESOURCES);
if (compliantWebInfConf != null) {
bindConf(new File(compliantWebInfConf.getFile()));
}
URL webInfConf = getClass().getClassLoader().getResource(WEB_INF_RESOURCES);
if (webInfConf != null) {
bindConf(new File(webInfConf.getFile()));
}
}
}
/**
* ConfigurationModule binding environment variables, system properties and configuration in directory pointed by
* <i>CHE_LOCAL_CONF_DIR</i> Env variable.
*/
static class ExtConfiguration extends AbstractConfigurationModule {
ExtConfiguration(Map<String, Set<String>> aliases) {
super(aliases);
}
@Override
protected void configure() {
bindProperties("env.", System.getenv());
bindProperties("sys.", System.getProperties());
String extConfig = System.getenv(CHE_LOCAL_CONF_DIR);
if (extConfig != null) {
bindConf(new File(extConfig));
}
}
}
static class CheSystemPropertiesConfigurationModule extends AbstractConfigurationModule {
CheSystemPropertiesConfigurationModule(Map<String, Set<String>> aliases) {
super(aliases);
}
@Override
protected void configure() {
Iterable<Map.Entry<Object, Object>> cheProperties = System.getProperties().entrySet().stream()
.filter(new PropertyNamePrefixPredicate<>("che.", "codenvy."))
.collect(toList());
bindProperties(null, cheProperties);
}
}
static class CheEnvironmentVariablesConfigurationModule extends AbstractConfigurationModule {
CheEnvironmentVariablesConfigurationModule(Map<String, Set<String>> aliases) {
super(aliases);
}
@Override
protected void configure() {
Iterable<Map.Entry<String, String>> cheProperties = System.getenv().entrySet().stream()
.filter(new PropertyNamePrefixPredicate<>("CHE_", "CODENVY_"))
.map(new EnvironmentVariableToSystemPropertyFormatNameConverter())
.collect(toList());
bindProperties(null, cheProperties);
}
}
static class PropertyNamePrefixPredicate<K, V> implements Predicate<Map.Entry<K, V>> {
final String[] prefixes;
PropertyNamePrefixPredicate(String... prefix) {
this.prefixes = prefix;
}
@Override
public boolean test(Map.Entry<K, V> entry) {
for (String prefix : prefixes) {
if (((String)entry.getKey()).startsWith(prefix)) {
return true;
}
}
return false;
}
}
static class PropertyNamePrefixRemover<K, V> implements Function<Map.Entry<K, V>, Map.Entry<String, V>> {
final int prefixLength;
PropertyNamePrefixRemover(int prefixLength) {
this.prefixLength = prefixLength;
}
@Override
public Map.Entry<String, V> apply(Map.Entry<K, V> entry) {
return new SimpleEntry<>(((String)entry.getKey()).substring(prefixLength), entry.getValue());
}
}
static class EnvironmentVariableToSystemPropertyFormatNameConverter
implements Function<Map.Entry<String, String>, Map.Entry<String, String>> {
@Override
public Map.Entry<String, String> apply(Map.Entry<String, String> entry) {
String name = entry.getKey();
name = name.toLowerCase();
// replace single underscore with dot and double underscores with single underscore
// at first replace double underscores with equal sign which is forbidden in env variable name
// then replace single underscores
// then recover underscore from equal sign
name = name.replace("__", "=");
name = name.replace('_', '.');
name = name.replace("=", "_");
return new SimpleEntry<>(name, entry.getValue());
}
}
private static final Pattern PROPERTIES_PLACE_HOLDER_PATTERN = Pattern.compile("\\$\\{[^\\}^\\$\\{]+\\}");
static abstract class AbstractConfigurationModule extends AbstractModule {
final Map<String, Set<String>> aliases;
AbstractConfigurationModule(Map<String, Set<String>> aliases) {
this.aliases = aliases;
}
protected void bindConf(File confDir) {
final File[] files = confDir.listFiles();
if (files != null) {
for (File file : files) {
if (!file.isDirectory()) {
if ("properties".equals(Files.getFileExtension(file.getName()))) {
Properties properties = new Properties();
try (Reader reader = Files.newReader(file, Charset.forName("UTF-8"))) {
properties.load(reader);
} catch (IOException e) {
throw new IllegalStateException(format("Unable to read configuration file %s", file), e);
}
bindProperties(properties);
}
}
}
}
}
protected void bindProperties(Properties properties) {
bindProperties(null, properties.entrySet());
}
protected void bindProperties(String prefix, Properties properties) {
bindProperties(prefix, properties.entrySet());
}
protected void bindProperties(String prefix, Map<String, String> properties) {
bindProperties(prefix, properties.entrySet(), true);
}
protected <K, V> void bindProperties(String prefix, Iterable<Map.Entry<K, V>> properties) {
bindProperties(prefix, properties, false);
}
protected <K, V> void bindProperties(String prefix, Iterable<Map.Entry<K, V>> properties, boolean skipUnresolved) {
StringBuilder buf = null;
for (Map.Entry<K, V> e : properties) {
String name = (String)e.getKey();
String value = (String)e.getValue();
if (NULL.equals(value)) {
bindProperty(prefix, name, null);
} else {
final Matcher matcher = PROPERTIES_PLACE_HOLDER_PATTERN.matcher(value);
if (matcher.find()) {
int start = 0;
if (buf == null) {
buf = new StringBuilder();
} else {
buf.setLength(0);
}
do {
buf.append(value.substring(start, matcher.start()));
final String placeholder = value.substring(matcher.start(), matcher.end());
final String placeholderName = removePlaceholderFormatting(placeholder);
String resolvedPlaceholder = resolvePlaceholder(placeholderName);
if (resolvedPlaceholder != null) {
buf.append(resolvedPlaceholder);
} else if (skipUnresolved) {
buf.append(placeholder);
LOG.warn("Placeholder {} cannot be resolved neither from environment variable nor from system property," +
"leaving as is.", placeholderName);
} else {
throw new ConfigurationException(format("Property %s is not found as system property or " +
"environment variable.", placeholderName));
}
start = matcher.end();
} while (matcher.find());
buf.append(value.substring(start));
value = buf.toString();
}
bindProperty(prefix, name, value);
}
}
}
private void bindProperty(String prefix, String name, String value) {
String key = prefix == null ? name : (prefix + name);
Set<String> aliasesForName = aliases.get(name);
if (value == null) {
bind(String.class).annotatedWith(Names.named(key)).toProvider(Providers.<String>of(null));
if (aliasesForName != null) {
for (String alias : aliasesForName) {
bind(String.class).annotatedWith(Names.named(prefix == null ? alias : prefix + alias))
.toProvider(Providers.<String>of(null));
}
}
} else {
bindConstant().annotatedWith(Names.named(key)).to(value);
if (aliasesForName != null) {
for (String alias : aliasesForName) {
bindConstant().annotatedWith(Names.named(prefix == null ? alias : prefix + alias)).to(value);
}
}
}
}
private String removePlaceholderFormatting(String placeholder) {
return placeholder.substring(2, placeholder.length() - 1);
}
private String resolvePlaceholder(String placeholderName) {
String resolved = System.getProperty(placeholderName);
if (resolved == null) {
resolved = System.getenv(placeholderName);
}
return resolved;
}
}
}