diff --git a/core-support/core/src/main/java/org/seedstack/seed/core/internal/CorePlugin.java b/core-support/core/src/main/java/org/seedstack/seed/core/internal/CorePlugin.java index a882be7f4..182ddf982 100644 --- a/core-support/core/src/main/java/org/seedstack/seed/core/internal/CorePlugin.java +++ b/core-support/core/src/main/java/org/seedstack/seed/core/internal/CorePlugin.java @@ -9,7 +9,6 @@ */ package org.seedstack.seed.core.internal; -import com.google.common.collect.ImmutableMap; import com.google.inject.Module; import io.nuun.kernel.api.plugin.InitState; import io.nuun.kernel.api.plugin.PluginException; @@ -18,16 +17,14 @@ import io.nuun.kernel.core.AbstractPlugin; import io.nuun.kernel.core.internal.scanner.AbstractClasspathScanner; import org.apache.commons.configuration.Configuration; -import org.apache.commons.configuration.MapConfiguration; -import org.apache.commons.configuration.PropertiesConfiguration; import org.apache.commons.lang.StringUtils; import org.reflections.vfs.Vfs; +import org.seedstack.seed.core.api.CoreErrorCode; import org.seedstack.seed.core.api.DiagnosticManager; import org.seedstack.seed.core.api.Install; import org.seedstack.seed.core.api.SeedException; import org.seedstack.seed.core.internal.scan.ClasspathScanHandler; import org.seedstack.seed.core.internal.scan.FallbackUrlType; -import org.seedstack.seed.core.api.CoreErrorCode; import org.seedstack.seed.core.spi.diagnostic.DiagnosticDomain; import org.seedstack.seed.core.spi.diagnostic.DiagnosticInfoCollector; import org.seedstack.seed.core.utils.SeedReflectionUtils; @@ -49,8 +46,6 @@ public class CorePlugin extends AbstractPlugin { public static final String SEED_PACKAGE_ROOT = "org.seedstack"; public static final String CORE_PLUGIN_PREFIX = "org.seedstack.seed.core"; public static final String DETAILS_MESSAGE = "Details of the previous error below"; - - private static final String SEED_BOOTSTRAP_PATH = "META-INF/seed-bootstrap.properties"; private static final Logger LOGGER = LoggerFactory.getLogger(CorePlugin.class); private static final DiagnosticManagerImpl DIAGNOSTIC_MANAGER = new DiagnosticManagerImpl(); @@ -102,7 +97,7 @@ public static DiagnosticManager getDiagnosticManager() { } public CorePlugin() { - bootstrapConfiguration = loadBootstrapConfiguration(); + bootstrapConfiguration = new SeedConfigLoader().bootstrapConfig(); } @Override @@ -176,28 +171,6 @@ public String pluginPackageRoot() { return packageRoots; } - private Configuration loadBootstrapConfiguration() { - MapConfiguration globalConfiguration = new MapConfiguration(new HashMap()); - - ClassLoader classLoader = SeedReflectionUtils.findMostCompleteClassLoader(); - if (classLoader == null) { - throw SeedException.createNew(CoreErrorCode.UNABLE_TO_FIND_CLASSLOADER); - } - - try { - Enumeration urls = classLoader.getResources(SEED_BOOTSTRAP_PATH); - - while (urls.hasMoreElements()) { - URL url = urls.nextElement(); - globalConfiguration.append(new PropertiesConfiguration(url)); - } - } catch (Exception e) { - throw SeedException.wrap(e, CoreErrorCode.UNEXPECTED_EXCEPTION); - } - - return new MapConfiguration(new ImmutableMap.Builder().putAll(globalConfiguration.getMap()).build()); - } - /** * Returns the configuration coming from the SEED bootstrap properties. * diff --git a/core-support/core/src/main/java/org/seedstack/seed/core/internal/SeedConfigLoader.java b/core-support/core/src/main/java/org/seedstack/seed/core/internal/SeedConfigLoader.java new file mode 100644 index 000000000..f205c65f7 --- /dev/null +++ b/core-support/core/src/main/java/org/seedstack/seed/core/internal/SeedConfigLoader.java @@ -0,0 +1,222 @@ +/** + * Copyright (c) 2013-2015 by The SeedStack authors. All rights reserved. + * + * This file is part of SeedStack, An enterprise-oriented full development stack. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.seedstack.seed.core.internal; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Sets; +import jodd.props.Props; +import org.apache.commons.configuration.Configuration; +import org.apache.commons.configuration.MapConfiguration; +import org.javatuples.Pair; +import org.seedstack.seed.core.api.CoreErrorCode; +import org.seedstack.seed.core.api.SeedException; +import org.seedstack.seed.core.internal.application.ApplicationErrorCode; +import org.seedstack.seed.core.internal.application.EnvLookup; +import org.seedstack.seed.core.utils.SeedReflectionUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nullable; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.*; + +/** + * Utility class which allows to load the application and Seed properties. + * + * @author pierre.thirouin@ext.mpsa.com (Pierre Thirouin) + */ +public class SeedConfigLoader { + + private static final String SEED_BOOTSTRAP_PROPS_PATH = "META-INF/seed-bootstrap.props"; + private static final String SEED_BOOTSTRAP_PROPERTIES_PATH = "META-INF/seed-bootstrap.properties"; + private static final Logger LOGGER = LoggerFactory.getLogger(SeedConfigLoader.class); + + // Please let the volatile modifier on bootstrapConfiguration + private static volatile Configuration bootstrapConfiguration; + + /** + * Gets the configuration needed to bootstrap a Seed application. + *

+ * This configuration is loaded from the seed-bootstrap.properties files. + * If multiple files are present the configuration is concatenated. + *

+ * + * @return the bootstrap configuration + */ + public Configuration bootstrapConfig() { + if (bootstrapConfiguration == null) { + synchronized (SeedConfigLoader.class) { + if (bootstrapConfiguration == null) { + bootstrapConfiguration = buildBootstrapConfiguration(); + } + } + } + return bootstrapConfiguration; + } + + private Configuration buildBootstrapConfiguration() { + Set resources = Sets.newHashSet(SEED_BOOTSTRAP_PROPS_PATH, SEED_BOOTSTRAP_PROPERTIES_PATH); + MapConfiguration globalConfiguration = buildConfiguration(resources, null).getValue0(); + globalConfiguration.getInterpolator().registerLookup("env", new EnvLookup()); + return new MapConfiguration(new ImmutableMap.Builder().putAll(globalConfiguration.getMap()).build()); + } + + /** + * Gets the application active profiles. + * + * @return the array of active profiles or null if none is active + */ + public String[] applicationProfiles() { + return getStringArray(System.getProperty("org.seedstack.seed.profiles")); + } + + /** + * Build the application configuration. + * + * @param configurationResources the paths to the configuration resources + * @param defaultConfiguration the default configuration registered with the SPI + * @return the final configuration + */ + public Pair buildConfiguration(Set configurationResources, @Nullable Map defaultConfiguration) { + final Props props = buildProps(); + final Props propsOverride = buildProps(); + + for (String configurationResource : configurationResources) { + try { + ClassLoader classLoader = SeedReflectionUtils.findMostCompleteClassLoader(); + if (classLoader == null) { + throw SeedException.createNew(CoreErrorCode.UNABLE_TO_FIND_CLASSLOADER); + } + Enumeration urlEnumeration = classLoader.getResources(configurationResource); + while (urlEnumeration.hasMoreElements()) { + URL url = urlEnumeration.nextElement(); + InputStream resourceAsStream = null; + + try { + resourceAsStream = url.openStream(); + + if (isOverrideResource(configurationResource)) { + LOGGER.debug("Adding {} to configuration override", url.toExternalForm()); + propsOverride.load(resourceAsStream); + } else { + LOGGER.debug("Adding {} to configuration", url.toExternalForm()); + props.load(resourceAsStream); + } + } finally { + if (resourceAsStream != null) { + try { // NOSONAR + resourceAsStream.close(); + } catch (IOException e) { + LOGGER.warn("Unable to close configuration resource " + configurationResource, e); + } + } + } + } + } catch (IOException e) { + throw SeedException.wrap(e, ApplicationErrorCode.UNABLE_TO_LOAD_CONFIGURATION_RESOURCE).put("resource", configurationResource); + } + } + + // Build configuration + return Pair.with(buildConfiguration(props, propsOverride, defaultConfiguration), props); + } + + /** + * Indicates whether the resource path represents an override file, i.e. all the files ending with + * ".override.properties" or the files ".override.props". + * + * @param configurationResource the path to test + * @return true if the path correspond to an override file, false otherwise. + */ + public boolean isOverrideResource(String configurationResource) { + return configurationResource.endsWith(".override.properties") || configurationResource.endsWith(".override.props"); + } + + private Props buildProps() { + Props newProps = new Props(); + + newProps.setSkipEmptyProps(false); + newProps.setAppendDuplicateProps(true); + + return newProps; + } + + private MapConfiguration buildConfiguration(Props props, Props propsOverride, Map defaultConfiguration) { + Map finalConfiguration = new HashMap(); + Map configurationMap = new HashMap(); + Map configurationOverrideMap = new HashMap(); + + // Extract props to maps + props.extractProps(configurationMap, applicationProfiles()); + propsOverride.extractProps(configurationOverrideMap, applicationProfiles()); + + // Put defaults to final configuration + if (defaultConfiguration != null) { + finalConfiguration.putAll(defaultConfiguration); + } + + // Put nominal to final configuration + finalConfiguration.putAll(configurationMap); + + applyPropertiesRemoval(finalConfiguration, configurationOverrideMap); + + // Put override to final configuration + finalConfiguration.putAll(configurationOverrideMap); + + // Convert final configuration to an immutable Apache Commons Configuration + return new MapConfiguration(new ImmutableMap.Builder().putAll(finalConfiguration).build()); + } + + /** + * Looks the override config for properties starting with "-". These properties will be removed from the actual config. + * + * For instance the config contains: + *
+     * key1=foo
+     * 
+ * and the override config contains: + *
+     * -key1
+     * 
+ * The property will be removed from the final configuration + * + * @param config the actual configuration + * @param overrideConfig the configuration containing the overrides + */ + private void applyPropertiesRemoval(Map config, Map overrideConfig) { + Iterator> overrideIterator = overrideConfig.entrySet().iterator(); + while (overrideIterator.hasNext()) { + String overrideKey = overrideIterator.next().getKey(); + if (overrideKey.startsWith("-")) { + config.remove(overrideKey.substring(1)); + overrideIterator.remove(); + } + } + } + + private String[] getStringArray(String value) { + if (value == null) { + return null; + } else { + String[] split = value.split(","); + for (int i = 0; i < split.length; i++) { + split[i] = split[i].trim(); + } + + if (split.length == 0) { + return null; + } else { + return split; + } + } + } +} diff --git a/core-support/core/src/main/java/org/seedstack/seed/core/internal/application/ApplicationErrorCode.java b/core-support/core/src/main/java/org/seedstack/seed/core/internal/application/ApplicationErrorCode.java index 296cf973c..d99cac741 100644 --- a/core-support/core/src/main/java/org/seedstack/seed/core/internal/application/ApplicationErrorCode.java +++ b/core-support/core/src/main/java/org/seedstack/seed/core/internal/application/ApplicationErrorCode.java @@ -16,7 +16,7 @@ * * @author adrien.lauer@mpsa.com */ -enum ApplicationErrorCode implements ErrorCode { +public enum ApplicationErrorCode implements ErrorCode { MISSING_APPLICATION_IDENTIFIER, STORAGE_PATH_IS_NOT_A_DIRECTORY, UNABLE_TO_CREATE_STORAGE_DIRECTORY, diff --git a/core-support/core/src/main/java/org/seedstack/seed/core/internal/application/ApplicationPlugin.java b/core-support/core/src/main/java/org/seedstack/seed/core/internal/application/ApplicationPlugin.java index 44e347aac..ad9e42888 100644 --- a/core-support/core/src/main/java/org/seedstack/seed/core/internal/application/ApplicationPlugin.java +++ b/core-support/core/src/main/java/org/seedstack/seed/core/internal/application/ApplicationPlugin.java @@ -9,7 +9,6 @@ */ package org.seedstack.seed.core.internal.application; -import com.google.common.collect.ImmutableMap; import com.google.common.collect.Sets; import io.nuun.kernel.api.Plugin; import io.nuun.kernel.api.plugin.InitState; @@ -20,31 +19,22 @@ import org.apache.commons.configuration.Configuration; import org.apache.commons.configuration.MapConfiguration; import org.apache.commons.lang.text.StrLookup; +import org.javatuples.Pair; import org.seedstack.seed.core.api.Application; import org.seedstack.seed.core.api.SeedException; import org.seedstack.seed.core.internal.CorePlugin; +import org.seedstack.seed.core.internal.SeedConfigLoader; import org.seedstack.seed.core.spi.configuration.ConfigurationLookup; -import org.seedstack.seed.core.utils.SeedReflectionUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.bridge.SLF4JBridgeHandler; import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.net.URL; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Enumeration; -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; -import java.util.Set; +import java.util.*; import java.util.concurrent.ConcurrentHashMap; /** - * Plugin that initialize the application identity, storage location and configuration. + * Plugin that initializes the application identity, storage location and configuration. * * @author adrien.lauer@mpsa.com */ @@ -55,9 +45,7 @@ public class ApplicationPlugin extends AbstractPlugin { static final String PROPERTIES_REGEX = ".*\\.properties"; private final Map defaultConfiguration = new ConcurrentHashMap(); - private final ClassLoader classLoader = SeedReflectionUtils.findMostCompleteClassLoader(ApplicationPlugin.class); - private final Props props = buildProps(); - private final Props propsOverride = buildProps(); + private Props props; private Application application; @@ -100,42 +88,15 @@ public InitState init(InitContext initContext) { } } - for (String configurationResource : allConfigurationResources) { - boolean isOverrideResource = configurationResource.endsWith(".override.properties") || configurationResource.endsWith(".override.props"); + SeedConfigLoader seedConfigLoader = new SeedConfigLoader(); - try { - Enumeration urlEnumeration = classLoader.getResources(configurationResource); - while (urlEnumeration.hasMoreElements()) { - URL url = urlEnumeration.nextElement(); - InputStream resourceAsStream = null; - - try { - resourceAsStream = url.openStream(); - - if (isOverrideResource) { - LOGGER.debug("Adding {} to configuration override", url.toExternalForm()); - propsOverride.load(resourceAsStream); - } else { - LOGGER.debug("Adding {} to configuration", url.toExternalForm()); - props.load(resourceAsStream); - } - } finally { - if (resourceAsStream != null) { - try { // NOSONAR - resourceAsStream.close(); - } catch (IOException e) { - LOGGER.warn("Unable to close configuration resource " + configurationResource, e); - } - } - } - } - } catch (IOException e) { - throw SeedException.wrap(e, ApplicationErrorCode.UNABLE_TO_LOAD_CONFIGURATION_RESOURCE).put("resource", configurationResource); - } - } + // Build configuration + Pair confs = seedConfigLoader.buildConfiguration(allConfigurationResources, defaultConfiguration); + MapConfiguration configuration = confs.getValue0(); + props = confs.getValue1(); // Determine configuration profile - String[] profiles = getStringArray(System.getProperty("org.seedstack.seed.profiles")); + String[] profiles = seedConfigLoader.applicationProfiles(); if (profiles == null || profiles.length == 0) { LOGGER.info("No configuration profile selected"); applicationDiagnosticCollector.setActiveProfiles(""); @@ -145,9 +106,8 @@ public InitState init(InitContext initContext) { applicationDiagnosticCollector.setActiveProfiles(activeProfiles); } - // Build configuration - MapConfiguration configuration = buildConfiguration(props, propsOverride, configurationLookups, profiles); applicationDiagnosticCollector.setConfiguration(configuration); + Configuration coreConfiguration = configuration.subset(CorePlugin.CORE_PLUGIN_PREFIX); String appId = coreConfiguration.getString("application-id"); @@ -171,25 +131,7 @@ public InitState init(InitContext initContext) { applicationDiagnosticCollector.setApplicationName(appName); applicationDiagnosticCollector.setApplicationVersion(appVersion); - String seedStorage = coreConfiguration.getString("storage"); - File seedDirectory; - if (seedStorage == null) { - seedDirectory = new File(new File(getUserHome(), ".seed"), appId); - } else { - seedDirectory = new File(seedStorage); - } - - if (!seedDirectory.exists() && !seedDirectory.mkdirs()) { - throw SeedException.createNew(ApplicationErrorCode.UNABLE_TO_CREATE_STORAGE_DIRECTORY).put("path", seedDirectory.getAbsolutePath()); - } - - if (!seedDirectory.isDirectory()) { - throw SeedException.createNew(ApplicationErrorCode.STORAGE_PATH_IS_NOT_A_DIRECTORY).put("path", seedDirectory.getAbsolutePath()); - } - - if (!seedDirectory.canWrite()) { - throw SeedException.createNew(ApplicationErrorCode.STORAGE_DIRECTORY_IS_NOT_WRITABLE).put("path", seedDirectory.getAbsolutePath()); - } + File seedDirectory = registerApplicationStorage(coreConfiguration, appId); LOGGER.debug("Application storage location is {}", seedDirectory.getAbsolutePath()); applicationDiagnosticCollector.setStorageLocation(seedDirectory.getAbsolutePath()); @@ -213,6 +155,29 @@ public InitState init(InitContext initContext) { return InitState.INITIALIZED; } + private File registerApplicationStorage(Configuration coreConfiguration, String appId) { + String seedStorage = coreConfiguration.getString("storage"); + File seedDirectory; + if (seedStorage == null) { + seedDirectory = new File(new File(getUserHome(), ".seed"), appId); + } else { + seedDirectory = new File(seedStorage); + } + + if (!seedDirectory.exists() && !seedDirectory.mkdirs()) { + throw SeedException.createNew(ApplicationErrorCode.UNABLE_TO_CREATE_STORAGE_DIRECTORY).put("path", seedDirectory.getAbsolutePath()); + } + + if (!seedDirectory.isDirectory()) { + throw SeedException.createNew(ApplicationErrorCode.STORAGE_PATH_IS_NOT_A_DIRECTORY).put("path", seedDirectory.getAbsolutePath()); + } + + if (!seedDirectory.canWrite()) { + throw SeedException.createNew(ApplicationErrorCode.STORAGE_DIRECTORY_IS_NOT_WRITABLE).put("path", seedDirectory.getAbsolutePath()); + } + return seedDirectory; + } + @Override public Collection classpathScanRequests() { return classpathScanRequestBuilder().resourcesRegex(PROPERTIES_REGEX).resourcesRegex(PROPS_REGEX).annotationType(ConfigurationLookup.class).build(); @@ -266,32 +231,6 @@ private String getUserHome() { } } - private String[] getStringArray(String value) { - if (value == null) { - return null; - } else { - String[] split = value.split(","); - for (int i = 0; i < split.length; i++) { - split[i] = split[i].trim(); - } - - if (split.length == 0) { - return null; - } else { - return split; - } - } - } - - private Props buildProps() { - Props newProps = new Props(); - - newProps.setSkipEmptyProps(false); - newProps.setAppendDuplicateProps(true); - - return newProps; - } - private StrLookup buildStrLookup(Class strLookupClass, Application application) { try { try { @@ -307,36 +246,4 @@ private StrLookup buildStrLookup(Class strLookupClass, Appl throw SeedException.wrap(e, ApplicationErrorCode.UNABLE_TO_INSTANTIATE_CONFIGURATION_LOOKUP).put("className", strLookupClass.getCanonicalName()); } } - - private MapConfiguration buildConfiguration(Props props, Props propsOverride, Map> configurationLookups, String... profiles) { - Map finalConfiguration = new HashMap(); - Map configurationMap = new HashMap(); - Map configurationOverrideMap = new HashMap(); - - // Extract props to maps - props.extractProps(configurationMap, profiles); - propsOverride.extractProps(configurationOverrideMap, profiles); - - // Put defaults to final configuration - finalConfiguration.putAll(defaultConfiguration); - - // Put nominal to final configuration - finalConfiguration.putAll(configurationMap); - - // Apply removal behavior - Iterator> overrideIterator = configurationOverrideMap.entrySet().iterator(); - while (overrideIterator.hasNext()) { - String overrideKey = overrideIterator.next().getKey(); - if (overrideKey.startsWith("-")) { - finalConfiguration.remove(overrideKey.substring(1)); - overrideIterator.remove(); - } - } - - // Put override to final configuration - finalConfiguration.putAll(configurationOverrideMap); - - // Convert final configuration to an immutable Apache Commons Configuration - return new MapConfiguration(new ImmutableMap.Builder().putAll(finalConfiguration).build()); - } } diff --git a/core-support/core/src/test/java/org/seedstack/seed/core/internal/application/ApplicationPluginTest.java b/core-support/core/src/test/java/org/seedstack/seed/core/internal/application/ApplicationPluginTest.java index 0dc673835..c1e5b3d3a 100644 --- a/core-support/core/src/test/java/org/seedstack/seed/core/internal/application/ApplicationPluginTest.java +++ b/core-support/core/src/test/java/org/seedstack/seed/core/internal/application/ApplicationPluginTest.java @@ -13,12 +13,13 @@ package org.seedstack.seed.core.internal.application; import io.nuun.kernel.api.plugin.context.InitContext; +import org.apache.commons.configuration.Configuration; import org.assertj.core.api.Assertions; import org.junit.Before; import org.junit.Test; import org.seedstack.seed.core.api.Application; -import org.seedstack.seed.core.api.Install; import org.seedstack.seed.core.internal.CorePlugin; +import org.seedstack.seed.core.internal.SeedConfigLoader; import org.seedstack.seed.core.spi.configuration.ConfigurationLookup; import java.lang.annotation.Annotation; @@ -47,6 +48,27 @@ public void setUp() throws Exception { pluginUnderTest = new ApplicationPlugin(); } + @Test + public void testBootstrapConfig() { + Configuration configuration = new SeedConfigLoader().bootstrapConfig(); + + Assertions.assertThat(configuration).isNotNull(); + Assertions.assertThat(configuration).isEqualTo(new SeedConfigLoader().bootstrapConfig()); + Assertions.assertThat(configuration.getString("package-roots")).isEqualTo("some.other.pkg"); + Assertions.assertThat(configuration.getString("test.key2")).isEqualTo("val2"); + } + + @Test + public void environment_variables_are_accessible_in_bootstrap_configuration() { + Configuration configuration = new SeedConfigLoader().bootstrapConfig(); + + String javaHome = System.getenv().get("JAVA_HOME"); + if (javaHome == null) { + Assertions.assertThat(configuration.getString("test.environmentVariable")).isEqualTo("${env:JAVA_HOME}"); + } else { + Assertions.assertThat(configuration.getString("test.environmentVariable")).isEqualTo(javaHome); + } + } @Test public void initTest() { diff --git a/core-support/core/src/test/resources/META-INF/seed-bootstrap.props b/core-support/core/src/test/resources/META-INF/seed-bootstrap.props new file mode 100644 index 000000000..fd7c25643 --- /dev/null +++ b/core-support/core/src/test/resources/META-INF/seed-bootstrap.props @@ -0,0 +1,14 @@ +# +# Copyright (c) 2013-2015 by The SeedStack authors. All rights reserved. +# +# This file is part of SeedStack, An enterprise-oriented full development stack. +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# + +[test] +key1=val1 +key2=val2 +environmentVariable=${env:JAVA_HOME} \ No newline at end of file