From 4ce6b64dce73e030ce6ce2f8cc73e69023025e0d Mon Sep 17 00:00:00 2001 From: Dave Syer Date: Fri, 12 Jul 2013 11:29:17 +0100 Subject: [PATCH] [bs-175] Generic dispatcher features for SpringApplication * Move Spring.main into SpringApplication.main * User can bind command line or application.properties into SpringApplication * User can provide sources dynamically with --spring.main.sources (a CSV list of class names, package names or XML resource locations) * One side effect was to make DocumentMatchers stateless [#52830829] --- .../autoconfigure/main/Spring.java | 98 --------- .../autoconfigure/AdhocTestSuite.java | 2 +- .../autoconfigure/main/SimpleMainTests.java | 66 ------ .../bootstrap/BeanDefinitionLoader.java | 30 ++- .../bootstrap/SpringApplication.java | 191 +++++++---------- .../SpringApplicationInitializer.java | 5 +- .../config/DefaultProfileDocumentMatcher.java | 2 +- .../PropertiesPropertySourceLoader.java | 8 +- .../config/PropertySourceLoader.java | 3 +- .../config/SpringProfileDocumentMatcher.java | 25 ++- .../config/YamlPropertySourceLoader.java | 33 ++- ...nfigFileApplicationContextInitializer.java | 197 +++++++++++++++--- .../LoggingApplicationContextInitializer.java | 2 +- .../ProfileSettingDocumentMatcher.java | 60 ------ ...urationPropertiesBindingPostProcessor.java | 5 +- .../bootstrap/logging/logback/logback.xml | 4 +- .../bootstrap/SimpleMainTests.java | 104 +++++++++ .../bootstrap/SpringApplicationTests.java | 16 +- .../bind/RelaxedDataBinderTests.java | 72 ++++++- .../bootstrap/sampleconfig/package-info.java | 21 ++ .../src/test/resources/dispatcher.properties | 1 + 21 files changed, 531 insertions(+), 414 deletions(-) delete mode 100644 spring-autoconfigure/src/main/java/org/springframework/autoconfigure/main/Spring.java delete mode 100644 spring-autoconfigure/src/test/java/org/springframework/autoconfigure/main/SimpleMainTests.java delete mode 100644 spring-bootstrap/src/main/java/org/springframework/bootstrap/context/initializer/ProfileSettingDocumentMatcher.java create mode 100644 spring-bootstrap/src/test/java/org/springframework/bootstrap/SimpleMainTests.java create mode 100644 spring-bootstrap/src/test/java/org/springframework/bootstrap/sampleconfig/package-info.java create mode 100644 spring-bootstrap/src/test/resources/dispatcher.properties diff --git a/spring-autoconfigure/src/main/java/org/springframework/autoconfigure/main/Spring.java b/spring-autoconfigure/src/main/java/org/springframework/autoconfigure/main/Spring.java deleted file mode 100644 index 554e7a985c4d..000000000000 --- a/spring-autoconfigure/src/main/java/org/springframework/autoconfigure/main/Spring.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright 2012-2013 the original author or authors. - * - * 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 org.springframework.autoconfigure.main; - -import java.util.ArrayList; -import java.util.List; - -import org.apache.commons.logging.LogFactory; -import org.springframework.autoconfigure.EnableAutoConfiguration; -import org.springframework.bootstrap.SpringApplication; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.SpringVersion; -import org.springframework.util.ClassUtils; - -/** - * Very simple main class that can be used to launch an application from sources (class, - * package or XML). Useful for demos and testing, perhaps less for production use (where - * the {@link SpringApplication} run methods are often more convenient). - * - * @author Dave Syer - */ -@Configuration -@EnableAutoConfiguration -@ComponentScan -public abstract class Spring { - - // FIXME can we delete this? is it used? does it belong here - - private static ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider( - true); - - private static ApplicationContext context; - - /** - * @return the context if there is one - */ - public static ApplicationContext getApplicationContext() { - return context; - } - - /** - * A basic main that can be used to launch an application. - * - * @param args command line arguments - * @see SpringApplication#run(Object[], String[]) - * @see SpringApplication#run(Object, String...) - */ - public static void main(String[] args) throws Exception { - - List strings = new ArrayList(); - List sources = new ArrayList(); - - for (String arg : args) { - if (ClassUtils.isPresent(arg, null)) { - sources.add(ClassUtils.forName(arg, null)); - } - else if (arg.endsWith(".xml")) { - sources.add(arg); - } - else if (!scanner.findCandidateComponents(arg).isEmpty()) { - sources.add(arg); - } - else { - strings.add(arg); - } - } - - if (sources.isEmpty()) { - sources.add(Spring.class); - } - - context = SpringApplication.run(sources.toArray(new Object[sources.size()]), - strings.toArray(new String[strings.size()])); - - LogFactory.getLog(Spring.class).info( - "Running Spring " + SpringVersion.getVersion() + " with sources: " - + sources); - - } - -} diff --git a/spring-autoconfigure/src/test/java/org/springframework/autoconfigure/AdhocTestSuite.java b/spring-autoconfigure/src/test/java/org/springframework/autoconfigure/AdhocTestSuite.java index 5b91abf4722c..4c0d7661c533 100644 --- a/spring-autoconfigure/src/test/java/org/springframework/autoconfigure/AdhocTestSuite.java +++ b/spring-autoconfigure/src/test/java/org/springframework/autoconfigure/AdhocTestSuite.java @@ -20,7 +20,7 @@ import org.junit.runner.RunWith; import org.junit.runners.Suite; import org.junit.runners.Suite.SuiteClasses; -import org.springframework.autoconfigure.main.SimpleMainTests; +import org.springframework.bootstrap.SimpleMainTests; import org.springframework.bootstrap.context.embedded.jetty.JettyEmbeddedServletContainerFactoryTests; /** diff --git a/spring-autoconfigure/src/test/java/org/springframework/autoconfigure/main/SimpleMainTests.java b/spring-autoconfigure/src/test/java/org/springframework/autoconfigure/main/SimpleMainTests.java deleted file mode 100644 index 2d0adf6225a9..000000000000 --- a/spring-autoconfigure/src/test/java/org/springframework/autoconfigure/main/SimpleMainTests.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 2012-2013 the original author or authors. - * - * 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 org.springframework.autoconfigure.main; - -import org.junit.After; -import org.junit.Test; -import org.springframework.autoconfigure.main.Spring; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.util.ClassUtils; - -import static org.junit.Assert.assertNotNull; - -/** - * Tests for {@link Spring}. - * - * @author Dave Syer - */ -public class SimpleMainTests { - - private ApplicationContext context; - - @After - public void close() { - if (this.context instanceof ConfigurableApplicationContext) { - ((ConfigurableApplicationContext) this.context).close(); - } - } - - @Test - public void emptyApplicationContext() throws Exception { - Spring.main(new String[0]); - this.context = Spring.getApplicationContext(); - assertNotNull(this.context); - } - - @Test - public void basePackageScan() throws Exception { - Spring.main(new String[] { ClassUtils.getPackageName(Spring.class) }); - this.context = Spring.getApplicationContext(); - assertNotNull(this.context); - } - - @Test - public void xmlContext() throws Exception { - Spring.main(new String[] { Spring.class.getName(), - "org/springframework/bootstrap/sample-beans.xml" }); - this.context = Spring.getApplicationContext(); - assertNotNull(this.context); - } - -} diff --git a/spring-bootstrap/src/main/java/org/springframework/bootstrap/BeanDefinitionLoader.java b/spring-bootstrap/src/main/java/org/springframework/bootstrap/BeanDefinitionLoader.java index 0853caac9b03..16f4a9529f58 100644 --- a/spring-bootstrap/src/main/java/org/springframework/bootstrap/BeanDefinitionLoader.java +++ b/spring-bootstrap/src/main/java/org/springframework/bootstrap/BeanDefinitionLoader.java @@ -29,10 +29,14 @@ import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.core.io.support.ResourcePatternResolver; import org.springframework.core.type.filter.AbstractTypeHierarchyTraversingFilter; import org.springframework.core.type.filter.TypeFilter; import org.springframework.stereotype.Component; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; /** * Loads bean definitions from underlying sources, including XML and JavaConfig. Acts as a @@ -162,13 +166,37 @@ private int load(CharSequence source) { if (loadedResource != null && loadedResource.exists()) { return load(loadedResource); } - Package packageResource = Package.getPackage(source.toString()); + Package packageResource = findPackage(source); if (packageResource != null) { return load(packageResource); } throw new IllegalArgumentException("Invalid source '" + source + "'"); } + private Package findPackage(CharSequence source) { + Package pkg = Package.getPackage(source.toString()); + if (pkg != null) { + return pkg; + } + try { + // Attempt to find a class in this package + ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver( + getClass().getClassLoader()); + Resource[] resources = resolver.getResources(ClassUtils + .convertClassNameToResourcePath(source.toString()) + "/*.class"); + for (Resource resource : resources) { + String className = StringUtils.stripFilenameExtension(resource + .getFilename()); + load(Class.forName(source.toString() + "." + className)); + break; + } + } + catch (Exception ex) { + // swallow exception and continue + } + return Package.getPackage(source.toString()); + } + private boolean isComponent(Class type) { // This has to be a bit of a guess. The only way to be sure that this type is // eligible is to make a bean definition out of it and try to instantiate it. diff --git a/spring-bootstrap/src/main/java/org/springframework/bootstrap/SpringApplication.java b/spring-bootstrap/src/main/java/org/springframework/bootstrap/SpringApplication.java index 6c7f66d28683..ba4c4ecd1cc4 100644 --- a/spring-bootstrap/src/main/java/org/springframework/bootstrap/SpringApplication.java +++ b/spring-bootstrap/src/main/java/org/springframework/bootstrap/SpringApplication.java @@ -19,9 +19,9 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; -import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; -import java.util.Map; +import java.util.Set; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -36,20 +36,19 @@ import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.AnnotationConfigUtils; import org.springframework.context.annotation.ClassPathBeanDefinitionScanner; +import org.springframework.context.annotation.Configuration; import org.springframework.context.support.AbstractApplicationContext; import org.springframework.context.support.GenericApplicationContext; import org.springframework.core.GenericTypeResolver; import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.core.env.CommandLinePropertySource; import org.springframework.core.env.ConfigurableEnvironment; -import org.springframework.core.env.Environment; -import org.springframework.core.env.MapPropertySource; -import org.springframework.core.env.PropertySource; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.support.SpringFactoriesLoader; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; import org.springframework.web.context.ConfigurableWebApplicationContext; /** @@ -133,7 +132,7 @@ public class SpringApplication { private final Log log = LogFactory.getLog(getClass()); - private Object[] sources; + private Set sources = new LinkedHashSet(); private Class mainApplicationClass; @@ -169,8 +168,7 @@ public class SpringApplication { * @see #SpringApplication(ResourceLoader, Object...) */ public SpringApplication(Object... sources) { - Assert.notEmpty(sources, "Sources must not be empty"); - this.sources = sources; + addSources(sources); initialize(); } @@ -185,12 +183,20 @@ public SpringApplication(Object... sources) { * @see #SpringApplication(ResourceLoader, Object...) */ public SpringApplication(ResourceLoader resourceLoader, Object... sources) { - Assert.notEmpty(sources, "Sources must not be empty"); this.resourceLoader = resourceLoader; - this.sources = sources; + addSources(sources); initialize(); } + private void addSources(Object[] sources) { + if (sources == null) { + return; + } + for (Object source : sources) { + this.sources.add(source); + } + } + private void initialize() { this.webEnvironment = deduceWebEnvironment(); this.initializers = new ArrayList>(); @@ -228,6 +234,22 @@ private Class deduceMainApplicationClass() { return null; } + /** + * A basic main that can be used to launch an application. + * + * @param args command line arguments + * @see SpringApplication#run(Object[], String[]) + * @see SpringApplication#run(Object, String...) + */ + public static void main(String[] args) throws Exception { + SpringApplication.run(new Object[0], args); + } + + @Configuration + protected static class EmptyConfiguration { + + } + /** * Run the Spring application, creating and refreshing a new * {@link ApplicationContext}. @@ -235,29 +257,30 @@ private Class deduceMainApplicationClass() { * @return a running {@link ApplicationContext} */ public ApplicationContext run(String... args) { - applySpringApplicationInitializers(); + applySpringApplicationInitializers(args); + Assert.notEmpty(this.sources, "Sources must not be empty"); if (this.showBanner) { printBanner(); } ApplicationContext context = createApplicationContext(); postProcessApplicationContext(context); - addPropertySources(context, args); if (context instanceof ConfigurableApplicationContext) { applyInitializers((ConfigurableApplicationContext) context); } if (this.logStartupInfo) { logStartupInfo(); } - load(context, this.sources); + load(context, this.sources.toArray(new Object[this.sources.size()])); refresh(context); runCommandLineRunners(context, args); return context; } - private void applySpringApplicationInitializers() { + private void applySpringApplicationInitializers(String[] args) { + args = StringUtils.mergeStringArrays(this.defaultCommandLineArgs, args); for (ApplicationContextInitializer initializer : this.initializers) { if (initializer instanceof SpringApplicationInitializer) { - ((SpringApplicationInitializer) initializer).initialize(this); + ((SpringApplicationInitializer) initializer).initialize(this, args); } } } @@ -289,6 +312,7 @@ protected void applyInitializers(ConfigurableApplicationContext context) { protected void logStartupInfo() { new StartupInfoLogger(this.mainApplicationClass).log(getApplicationLog()); + getApplicationLog().info("Sources: " + this.sources); } /** @@ -358,88 +382,6 @@ protected void postProcessApplicationContext(ApplicationContext context) { } } - /** - * Add any {@link PropertySource}s to the application context environment. - * @param context the application context - * @param args run arguments - */ - protected void addPropertySources(ApplicationContext context, String[] args) { - Environment environment = context.getEnvironment(); - if (environment instanceof ConfigurableEnvironment) { - ConfigurableEnvironment configurable = (ConfigurableEnvironment) environment; - if (this.addCommandLineProperties) { - // Don't use SimpleCommandLinePropertySource (SPR-10579) - PropertySource propertySource = new MapPropertySource( - "commandLineArgs", mergeCommandLineArgs( - this.defaultCommandLineArgs, args)); - configurable.getPropertySources().addFirst(propertySource); - } - } - } - - /** - * Merge two sets of command lines, the defaults and the ones passed in at run time. - * - * @param defaults the default values - * @param args the ones passed in at runtime - * @return a new command line - */ - protected Map mergeCommandLineArgs(String[] defaults, String[] args) { - - if (defaults == null) { - defaults = new String[0]; - } - - List nonopts = new ArrayList(); - Map options = new LinkedHashMap(); - - for (String arg : defaults) { - if (isOptionArg(arg)) { - addOptionArg(options, arg); - } - else { - nonopts.add(arg); - } - } - for (String arg : args) { - if (isOptionArg(arg)) { - addOptionArg(options, arg); - } - else if (!nonopts.contains(arg)) { - nonopts.add(arg); - } - } - - for (String key : nonopts) { - options.put(key, ""); - } - - return options; - - } - - private boolean isOptionArg(String arg) { - return arg.startsWith("--"); - } - - private void addOptionArg(Map map, String arg) { - String optionText = arg.substring(2, arg.length()); - String optionName; - String optionValue = ""; - if (optionText.contains("=")) { - optionName = optionText.substring(0, optionText.indexOf('=')); - optionValue = optionText.substring(optionText.indexOf('=') + 1, - optionText.length()); - } - else { - optionName = optionText; - } - if (optionName.isEmpty()) { - throw new IllegalArgumentException("Invalid argument syntax: " + arg); - } - map.put(optionName, optionValue); - } - /** * Load beans into the application context. * @param context the context to load beans into @@ -552,6 +494,13 @@ public void setAddCommandLineProperties(boolean addCommandLineProperties) { this.addCommandLineProperties = addCommandLineProperties; } + /** + * @return the addCommandLineProperties + */ + public boolean isAddCommandLineProperties() { + return this.addCommandLineProperties; + } + /** * Set some default command line arguments which can be overridden by those passed * into the run methods. @@ -571,12 +520,45 @@ public void setBeanNameGenerator(BeanNameGenerator beanNameGenerator) { /** * Sets the underlying environment that should be used when loading. + * * @param environment the environment */ public void setEnvironment(ConfigurableEnvironment environment) { this.environment = environment; } + /** + * The environment that will be used to create the application context (can be null in + * which case a default will be provided). + * + * @return the environment + */ + public ConfigurableEnvironment getEnvironment() { + return this.environment; + } + + /** + * The sources that will be used to create an ApplicationContext if this application + * {@link #run(String...)} is called. + * + * @return the sources + */ + public Set getSources() { + return this.sources; + } + + /** + * The sources that will be used to create an ApplicationContext. A valid source is + * one of: a class, class name, package, package name, or an XML resource location. + * Can also be set using contructors and static convenience methods (e.g. + * {@link #run(Object[], String[])}). + * + * @param sources the sources to set + */ + public void setSources(Set sources) { + this.sources = sources; + } + /** * Sets the {@link ResourceLoader} that should be used when loading resources. * @param resourceLoader the resource loader @@ -660,21 +642,6 @@ public static ApplicationContext run(Object[] sources, String[] args) { return new SpringApplication(sources).run(args); } - /** - * Static helper that can be used to run a {@link SpringApplication} from a script - * using the specified sources with default settings. This method is useful when - * calling this calls from a script environment that will not have a single main - * application class. - * @param sources the sources to load - * @param args the application arguments (usually passed from a Java main method) - * @return the running {@link ApplicationContext} - */ - public static ApplicationContext runFromScript(Object[] sources, String[] args) { - SpringApplication application = new SpringApplication(sources); - application.setMainApplicationClass(null); - return application.run(args); - } - /** * Static helper that can be used to exit a {@link SpringApplication} and obtain a * code indicating success (0) or otherwise. Does not throw exceptions but should diff --git a/spring-bootstrap/src/main/java/org/springframework/bootstrap/SpringApplicationInitializer.java b/spring-bootstrap/src/main/java/org/springframework/bootstrap/SpringApplicationInitializer.java index 3e7d16a9c8e1..a16f87707310 100644 --- a/spring-bootstrap/src/main/java/org/springframework/bootstrap/SpringApplicationInitializer.java +++ b/spring-bootstrap/src/main/java/org/springframework/bootstrap/SpringApplicationInitializer.java @@ -25,9 +25,10 @@ public interface SpringApplicationInitializer { /** - * Initialize the applcation + * Initialize the application * @param springApplication the spring application. + * @param args the args provided on command line by caller */ - void initialize(SpringApplication springApplication); + void initialize(SpringApplication springApplication, String[] args); } diff --git a/spring-bootstrap/src/main/java/org/springframework/bootstrap/config/DefaultProfileDocumentMatcher.java b/spring-bootstrap/src/main/java/org/springframework/bootstrap/config/DefaultProfileDocumentMatcher.java index ce22f2a531c7..d8dcadd7a569 100644 --- a/spring-bootstrap/src/main/java/org/springframework/bootstrap/config/DefaultProfileDocumentMatcher.java +++ b/spring-bootstrap/src/main/java/org/springframework/bootstrap/config/DefaultProfileDocumentMatcher.java @@ -27,7 +27,7 @@ * * @author Dave Syer */ -public final class DefaultProfileDocumentMatcher implements DocumentMatcher { +public class DefaultProfileDocumentMatcher implements DocumentMatcher { @Override public MatchStatus matches(Properties properties) { diff --git a/spring-bootstrap/src/main/java/org/springframework/bootstrap/config/PropertiesPropertySourceLoader.java b/spring-bootstrap/src/main/java/org/springframework/bootstrap/config/PropertiesPropertySourceLoader.java index 6bd6a8a5c76c..3cc3de93a20b 100644 --- a/spring-bootstrap/src/main/java/org/springframework/bootstrap/config/PropertiesPropertySourceLoader.java +++ b/spring-bootstrap/src/main/java/org/springframework/bootstrap/config/PropertiesPropertySourceLoader.java @@ -19,7 +19,6 @@ import java.io.IOException; import java.util.Properties; -import org.springframework.core.env.Environment; import org.springframework.core.env.PropertiesPropertySource; import org.springframework.core.env.PropertySource; import org.springframework.core.io.Resource; @@ -38,9 +37,9 @@ public boolean supports(Resource resource) { } @Override - public PropertySource load(Resource resource, Environment environment) { + public PropertySource load(Resource resource) { try { - Properties properties = loadProperties(resource, environment); + Properties properties = loadProperties(resource); return new PropertiesPropertySource(resource.getDescription(), properties); } catch (IOException ex) { @@ -49,8 +48,7 @@ public PropertySource load(Resource resource, Environment environment) { } } - protected Properties loadProperties(Resource resource, Environment environment) - throws IOException { + protected Properties loadProperties(Resource resource) throws IOException { return PropertiesLoaderUtils.loadProperties(resource); } } \ No newline at end of file diff --git a/spring-bootstrap/src/main/java/org/springframework/bootstrap/config/PropertySourceLoader.java b/spring-bootstrap/src/main/java/org/springframework/bootstrap/config/PropertySourceLoader.java index 6bf17f9742aa..54944b38b766 100644 --- a/spring-bootstrap/src/main/java/org/springframework/bootstrap/config/PropertySourceLoader.java +++ b/spring-bootstrap/src/main/java/org/springframework/bootstrap/config/PropertySourceLoader.java @@ -16,7 +16,6 @@ package org.springframework.bootstrap.config; -import org.springframework.core.env.Environment; import org.springframework.core.env.PropertySource; import org.springframework.core.io.Resource; @@ -37,6 +36,6 @@ public interface PropertySourceLoader { * Load the resource into a property source. * @return a property source */ - PropertySource load(Resource resource, Environment environment); + PropertySource load(Resource resource); } \ No newline at end of file diff --git a/spring-bootstrap/src/main/java/org/springframework/bootstrap/config/SpringProfileDocumentMatcher.java b/spring-bootstrap/src/main/java/org/springframework/bootstrap/config/SpringProfileDocumentMatcher.java index 0b1573f792c2..ac4f077aba76 100644 --- a/spring-bootstrap/src/main/java/org/springframework/bootstrap/config/SpringProfileDocumentMatcher.java +++ b/spring-bootstrap/src/main/java/org/springframework/bootstrap/config/SpringProfileDocumentMatcher.java @@ -16,15 +16,18 @@ package org.springframework.bootstrap.config; +import java.util.Arrays; +import java.util.LinkedHashSet; import java.util.Properties; import org.springframework.bootstrap.config.YamlProcessor.DocumentMatcher; import org.springframework.bootstrap.config.YamlProcessor.MatchStatus; import org.springframework.core.env.Environment; -import org.springframework.util.Assert; /** - * {@link DocumentMatcher} backed by {@link Environment#getActiveProfiles()}. + * {@link DocumentMatcher} backed by {@link Environment#getActiveProfiles()}. A YAML + * document matches if it contains an element "spring.profiles" (a comma-separated list) + * and one of the profiles is in the active list. * * @author Dave Syer */ @@ -32,20 +35,20 @@ public class SpringProfileDocumentMatcher implements DocumentMatcher { private static final String[] DEFAULT_PROFILES = new String[] { "default" }; - private final Environment environment; + private String[] activeProfiles = new String[0]; - /** - * Create a new {@link SpringProfileDocumentMatcher} instance. - * @param environment the environment - */ - public SpringProfileDocumentMatcher(Environment environment) { - Assert.notNull(environment, "Environment must not be null"); - this.environment = environment; + public void addActiveProfiles(String... profiles) { + LinkedHashSet set = new LinkedHashSet( + Arrays.asList(this.activeProfiles)); + for (String profile : profiles) { + set.add(profile); + } + this.activeProfiles = set.toArray(new String[set.size()]); } @Override public MatchStatus matches(Properties properties) { - String[] profiles = this.environment.getActiveProfiles(); + String[] profiles = this.activeProfiles; if (profiles.length == 0) { profiles = DEFAULT_PROFILES; } diff --git a/spring-bootstrap/src/main/java/org/springframework/bootstrap/config/YamlPropertySourceLoader.java b/spring-bootstrap/src/main/java/org/springframework/bootstrap/config/YamlPropertySourceLoader.java index 4cc07d9fd26f..37274c2db5e5 100644 --- a/spring-bootstrap/src/main/java/org/springframework/bootstrap/config/YamlPropertySourceLoader.java +++ b/spring-bootstrap/src/main/java/org/springframework/bootstrap/config/YamlPropertySourceLoader.java @@ -20,11 +20,13 @@ import java.util.Arrays; import java.util.List; import java.util.Properties; +import java.util.Set; import org.springframework.bootstrap.config.YamlProcessor.DocumentMatcher; -import org.springframework.core.env.Environment; +import org.springframework.bootstrap.config.YamlProcessor.MatchStatus; import org.springframework.core.env.PropertySource; import org.springframework.core.io.Resource; +import org.springframework.util.StringUtils; /** * Strategy to load '.yml' files into a {@link PropertySource}. @@ -49,8 +51,7 @@ public boolean supports(Resource resource) { } @Override - protected Properties loadProperties(final Resource resource, - final Environment environment) throws IOException { + protected Properties loadProperties(final Resource resource) throws IOException { YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean(); if (this.matchers != null && !this.matchers.isEmpty()) { factory.setMatchDefault(false); @@ -74,13 +75,31 @@ public static YamlPropertySourceLoader matchAllLoader() { * which have an explicit "spring.profiles.active" value in the current active * profiles. * + * @param activeProfiles the active profiles to match independent of file contents + * * @return a property source loader */ public static YamlPropertySourceLoader springProfileAwareLoader( - Environment environment) { - return new YamlPropertySourceLoader( - new SpringProfileDocumentMatcher(environment), - new DefaultProfileDocumentMatcher()); + String[] activeProfiles) { + final SpringProfileDocumentMatcher matcher = new SpringProfileDocumentMatcher(); + for (String profile : activeProfiles) { + matcher.addActiveProfiles(profile); + } + return new YamlPropertySourceLoader(matcher, new DefaultProfileDocumentMatcher() { + @Override + public MatchStatus matches(Properties properties) { + MatchStatus result = super.matches(properties); + if (result == MatchStatus.FOUND) { + Set profiles = StringUtils.commaDelimitedListToSet(properties + .getProperty("spring.profiles.active", "")); + for (String profile : profiles) { + // allow document with no profile to set the active one + matcher.addActiveProfiles(profile); + } + } + return result; + } + }); } } \ No newline at end of file diff --git a/spring-bootstrap/src/main/java/org/springframework/bootstrap/context/initializer/ConfigFileApplicationContextInitializer.java b/spring-bootstrap/src/main/java/org/springframework/bootstrap/context/initializer/ConfigFileApplicationContextInitializer.java index d01363ce4373..49015ab05ac6 100644 --- a/spring-bootstrap/src/main/java/org/springframework/bootstrap/context/initializer/ConfigFileApplicationContextInitializer.java +++ b/spring-bootstrap/src/main/java/org/springframework/bootstrap/context/initializer/ConfigFileApplicationContextInitializer.java @@ -17,20 +17,35 @@ package org.springframework.bootstrap.context.initializer; import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; +import java.util.Set; -import org.springframework.bootstrap.config.SpringProfileDocumentMatcher; +import org.springframework.beans.PropertyValues; +import org.springframework.bootstrap.SpringApplication; +import org.springframework.bootstrap.SpringApplicationInitializer; +import org.springframework.bootstrap.bind.PropertySourcesPropertyValues; +import org.springframework.bootstrap.bind.RelaxedDataBinder; import org.springframework.bootstrap.config.PropertiesPropertySourceLoader; import org.springframework.bootstrap.config.PropertySourceLoader; import org.springframework.bootstrap.config.YamlPropertySourceLoader; import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.core.Ordered; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.env.CommandLinePropertySource; import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.MapPropertySource; import org.springframework.core.env.MutablePropertySources; import org.springframework.core.env.PropertySource; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; /** @@ -44,6 +59,7 @@ *
  • classpath:config/
  • *
  • file:./config/:
  • * + * *

    * Alternative locations and names can be specified using * {@link #setSearchLocations(String[])} and {@link #setName(String)}. @@ -62,7 +78,8 @@ * @author Phillip Webb */ public class ConfigFileApplicationContextInitializer implements - ApplicationContextInitializer, Ordered { + ApplicationContextInitializer, + SpringApplicationInitializer, Ordered { private static final String LOCATION_VARIABLE = "${spring.config.location}"; @@ -73,24 +90,76 @@ public class ConfigFileApplicationContextInitializer implements private int order = Integer.MIN_VALUE + 10; + private Map> cached = new HashMap>(); + + private ConversionService conversionService = new DefaultConversionService(); + + /** + * Creates a property source from the command line, loads additional external + * configuration, and then binds it to the application. This makes it possible to set + * things dynamically, like the sources ("spring.main.sources" - a CSV list) the flag + * to indicate a web environment ("spring.main.web_environment=true") or the flag to + * switch off the banner ("spring.main.show_banner=false"). + */ + @Override + public void initialize(SpringApplication application, String[] args) { + + if (application.isAddCommandLineProperties()) { + this.cached.put(CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME, + new MapPropertySource( + CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME, + extractCommandLineArgs(args))); + } + + ConfigurableEnvironment environment = application.getEnvironment(); + if (environment == null) { + environment = new StandardEnvironment(); + } + load(environment, new DefaultResourceLoader()); + + // Set bean properties from command line args parameters. + PropertyValues pvs = new PropertySourcesPropertyValues( + environment.getPropertySources()); + RelaxedDataBinder binder = new RelaxedDataBinder(application, "spring.main"); + binder.setConversionService(this.conversionService); + binder.bind(pvs); + + } + @Override public void initialize(ConfigurableApplicationContext applicationContext) { + load(applicationContext.getEnvironment(), applicationContext); + } + + private void load(ConfigurableEnvironment environment, ResourceLoader resourceLoader) { + List candidates = getCandidateLocations(); + if (this.cached + .containsKey(CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME)) { + environment + .getPropertySources() + .addFirst( + this.cached + .get(CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME)); + } + // Initial load allows profiles to be activated for (String candidate : candidates) { - load(applicationContext, candidate, null); + load(environment, resourceLoader, candidate, null); } // Second load for specific profiles - for (String profile : applicationContext.getEnvironment().getActiveProfiles()) { + for (String profile : environment.getActiveProfiles()) { for (String candidate : candidates) { - load(applicationContext, candidate, profile); + load(environment, resourceLoader, candidate, profile); } } + } private List getCandidateLocations() { + List candidates = new ArrayList(); for (String searchLocation : this.searchLocations) { for (String extension : new String[] { ".properties", ".yml" }) { @@ -100,38 +169,118 @@ private List getCandidateLocations() { } candidates.add(LOCATION_VARIABLE); return candidates; + } - private void load(ConfigurableApplicationContext applicationContext, String location, - String profile) { + private void load(ConfigurableEnvironment environment, ResourceLoader resourceLoader, + String location, String profile) { - ConfigurableEnvironment environment = applicationContext.getEnvironment(); location = environment.resolvePlaceholders(location); String suffix = "." + StringUtils.getFilenameExtension(location); + if (StringUtils.hasLength(profile)) { location = location.replace(suffix, "-" + profile + suffix); } - PropertySourceLoader[] loaders = { - new PropertiesPropertySourceLoader(), - new YamlPropertySourceLoader(new SpringProfileDocumentMatcher(environment), - new ProfileSettingDocumentMatcher(environment)) }; + + List loaders = new ArrayList(); + loaders.add(new PropertiesPropertySourceLoader()); + if (ClassUtils.isPresent("org.yaml.snakeyaml.Yaml", null)) { + loaders.add(YamlPropertySourceLoader.springProfileAwareLoader(environment + .getActiveProfiles())); + } + + Resource resource = resourceLoader.getResource(location); + PropertySource propertySource = getPropertySource(resource, loaders); + if (propertySource == null) { + return; + } + if (propertySource.containsProperty("spring.profiles.active")) { + Set profiles = StringUtils.commaDelimitedListToSet(propertySource + .getProperty("spring.profiles.active").toString()); + for (String active : profiles) { + // allow document with no profile to set the active one + environment.addActiveProfile(active); + } + + } + MutablePropertySources propertySources = environment.getPropertySources(); + if (propertySources + .contains(CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME)) { + propertySources.addAfter( + CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME, + propertySource); + + } + else { + propertySources.addFirst(propertySource); + } + + } + + private PropertySource getPropertySource(Resource resource, + List loaders) { + String key = resource.getDescription(); + if (this.cached.containsKey(key)) { + return this.cached.get(key); + } for (PropertySourceLoader loader : loaders) { - Resource resource = applicationContext.getResource(location); if (resource != null && resource.exists() && loader.supports(resource)) { - PropertySource propertySource = loader.load(resource, environment); - MutablePropertySources propertySources = environment.getPropertySources(); - if (propertySources - .contains(CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME)) { - propertySources.addAfter( - CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME, - propertySource); + PropertySource propertySource = loader.load(resource); + this.cached.put(key, propertySource); + return propertySource; + } + } + return null; + } - } else { - propertySources.addFirst(propertySource); - } - return; + /** + * Merge two sets of command lines, the defaults and the ones passed in at run time. + * + * @param args the ones passed in at runtime + * @return a new command line + */ + protected Map extractCommandLineArgs(String[] args) { + + List nonopts = new ArrayList(); + Map options = new LinkedHashMap(); + + for (String arg : args) { + if (isOptionArg(arg)) { + addOptionArg(options, arg); } + else if (!nonopts.contains(arg)) { + nonopts.add(arg); + } + } + + for (String key : nonopts) { + options.put(key, ""); + } + + return options; + + } + + private boolean isOptionArg(String arg) { + return arg.startsWith("--"); + } + + private void addOptionArg(Map map, String arg) { + String optionText = arg.substring(2, arg.length()); + String optionName; + String optionValue = ""; + if (optionText.contains("=")) { + optionName = optionText.substring(0, optionText.indexOf('=')); + optionValue = optionText.substring(optionText.indexOf('=') + 1, + optionText.length()); + } + else { + optionName = optionText; + } + if (optionName.isEmpty()) { + throw new IllegalArgumentException("Invalid argument syntax: " + arg); } + map.put(optionName, optionValue); } public void setOrder(int order) { diff --git a/spring-bootstrap/src/main/java/org/springframework/bootstrap/context/initializer/LoggingApplicationContextInitializer.java b/spring-bootstrap/src/main/java/org/springframework/bootstrap/context/initializer/LoggingApplicationContextInitializer.java index 73ad5448f7dd..3ce7b7939604 100644 --- a/spring-bootstrap/src/main/java/org/springframework/bootstrap/context/initializer/LoggingApplicationContextInitializer.java +++ b/spring-bootstrap/src/main/java/org/springframework/bootstrap/context/initializer/LoggingApplicationContextInitializer.java @@ -81,7 +81,7 @@ public class LoggingApplicationContextInitializer implements private int order = Integer.MIN_VALUE + 11; @Override - public void initialize(SpringApplication springApplication) { + public void initialize(SpringApplication springApplication, String[] args) { LoggingSystem.get(springApplication.getClass().getClassLoader()) .beforeInitialize(); } diff --git a/spring-bootstrap/src/main/java/org/springframework/bootstrap/context/initializer/ProfileSettingDocumentMatcher.java b/spring-bootstrap/src/main/java/org/springframework/bootstrap/context/initializer/ProfileSettingDocumentMatcher.java deleted file mode 100644 index 8d371c3e35c7..000000000000 --- a/spring-bootstrap/src/main/java/org/springframework/bootstrap/context/initializer/ProfileSettingDocumentMatcher.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2012-2013 the original author or authors. - * - * 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 org.springframework.bootstrap.context.initializer; - -import java.util.Properties; -import java.util.Set; - -import org.springframework.bootstrap.config.YamlProcessor.DocumentMatcher; -import org.springframework.bootstrap.config.YamlProcessor.MatchStatus; -import org.springframework.core.env.ConfigurableEnvironment; -import org.springframework.core.env.Environment; -import org.springframework.util.StringUtils; - -/** - * A {@link DocumentMatcher} that sets the active profile if it finds a document with a - * key spring.profiles.active. - * - * @author Dave Syer - * - */ -public class ProfileSettingDocumentMatcher implements DocumentMatcher { - - private final Environment environment; - - public ProfileSettingDocumentMatcher(Environment environment) { - this.environment = environment; - } - - @Override - public MatchStatus matches(Properties properties) { - if (!properties.containsKey("spring.profiles")) { - Set profiles = StringUtils.commaDelimitedListToSet(properties - .getProperty("spring.profiles.active", "")); - if (this.environment instanceof ConfigurableEnvironment) { - ConfigurableEnvironment configurable = (ConfigurableEnvironment) this.environment; - for (String profile : profiles) { - // allow document with no profile to set the active one - configurable.addActiveProfile(profile); - } - } - // matches default profile - return MatchStatus.FOUND; - } else { - return MatchStatus.NOT_FOUND; - } - } -} \ No newline at end of file diff --git a/spring-bootstrap/src/main/java/org/springframework/bootstrap/context/properties/ConfigurationPropertiesBindingPostProcessor.java b/spring-bootstrap/src/main/java/org/springframework/bootstrap/context/properties/ConfigurationPropertiesBindingPostProcessor.java index acd4b76a7942..321c1b4cd521 100644 --- a/spring-bootstrap/src/main/java/org/springframework/bootstrap/context/properties/ConfigurationPropertiesBindingPostProcessor.java +++ b/spring-bootstrap/src/main/java/org/springframework/bootstrap/context/properties/ConfigurationPropertiesBindingPostProcessor.java @@ -157,15 +157,14 @@ private void postProcessAfterInitialization(Object bean, String beanName, private PropertySources loadPropertySources(String[] path) { MutablePropertySources propertySources = new MutablePropertySources(); PropertySourceLoader[] loaders = { new PropertiesPropertySourceLoader(), - YamlPropertySourceLoader.springProfileAwareLoader(this.environment) }; + YamlPropertySourceLoader.springProfileAwareLoader(environment.getActiveProfiles()) }; for (String location : path) { location = this.environment.resolvePlaceholders(location); Resource resource = this.resourceLoader.getResource(location); if (resource != null && resource.exists()) { for (PropertySourceLoader loader : loaders) { if (loader.supports(resource)) { - PropertySource propertySource = loader.load(resource, - this.environment); + PropertySource propertySource = loader.load(resource); propertySources.addFirst(propertySource); } } diff --git a/spring-bootstrap/src/main/resources/org/springframework/bootstrap/logging/logback/logback.xml b/spring-bootstrap/src/main/resources/org/springframework/bootstrap/logging/logback/logback.xml index f2a196cf40a3..62277412bd6d 100644 --- a/spring-bootstrap/src/main/resources/org/springframework/bootstrap/logging/logback/logback.xml +++ b/spring-bootstrap/src/main/resources/org/springframework/bootstrap/logging/logback/logback.xml @@ -1,8 +1,8 @@ - - + + diff --git a/spring-bootstrap/src/test/java/org/springframework/bootstrap/SimpleMainTests.java b/spring-bootstrap/src/test/java/org/springframework/bootstrap/SimpleMainTests.java new file mode 100644 index 000000000000..e963fba402c3 --- /dev/null +++ b/spring-bootstrap/src/test/java/org/springframework/bootstrap/SimpleMainTests.java @@ -0,0 +1,104 @@ +/* + * Copyright 2012-2013 the original author or authors. + * + * 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 org.springframework.bootstrap; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.context.annotation.Configuration; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +import static org.junit.Assert.assertTrue; + +/** + * Tests for {@link SpringApplication} main method. + * + * @author Dave Syer + */ +@Configuration +public class SimpleMainTests { + + private PrintStream savedOutput; + private ByteArrayOutputStream output; + + @Before + public void open() { + this.savedOutput = System.out; + this.output = new ByteArrayOutputStream(); + System.setOut(new PrintStream(this.output)); + } + + @After + public void after() { + System.setOut(this.savedOutput); + System.out.println(getOutput()); + } + + @Test(expected = IllegalArgumentException.class) + public void emptyApplicationContext() throws Exception { + SpringApplication.main(getArgs()); + assertTrue(getOutput().contains("Pre-instantiating singletons")); + } + + @Test + public void basePackageScan() throws Exception { + SpringApplication.main(getArgs(ClassUtils.getPackageName(getClass()) + + ".sampleconfig")); + assertTrue(getOutput().contains("Pre-instantiating singletons")); + } + + @Test + public void configClassContext() throws Exception { + SpringApplication.main(getArgs(getClass().getName())); + assertTrue(getOutput().contains("Pre-instantiating singletons")); + } + + @Test + public void xmlContext() throws Exception { + SpringApplication.main(getArgs("org/springframework/bootstrap/sample-beans.xml")); + assertTrue(getOutput().contains("Pre-instantiating singletons")); + } + + @Test + public void mixedContext() throws Exception { + SpringApplication.main(getArgs(getClass().getName(), + "org/springframework/bootstrap/sample-beans.xml")); + assertTrue(getOutput().contains("Pre-instantiating singletons")); + } + + private String[] getArgs(String... args) { + List list = new ArrayList(Arrays.asList( + "--spring.main.webEnvironment=false", "--spring.main.showBanner=false")); + if (args.length > 0) { + list.add("--spring.main.sources=" + + StringUtils.arrayToCommaDelimitedString(args)); + } + return list.toArray(new String[list.size()]); + } + + private String getOutput() { + return this.output.toString(); + } + +} diff --git a/spring-bootstrap/src/test/java/org/springframework/bootstrap/SpringApplicationTests.java b/spring-bootstrap/src/test/java/org/springframework/bootstrap/SpringApplicationTests.java index 4c37f56beecb..1af3a5cb19ca 100644 --- a/spring-bootstrap/src/test/java/org/springframework/bootstrap/SpringApplicationTests.java +++ b/spring-bootstrap/src/test/java/org/springframework/bootstrap/SpringApplicationTests.java @@ -26,10 +26,6 @@ import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.BeanNameGenerator; import org.springframework.beans.factory.support.DefaultBeanNameGenerator; -import org.springframework.bootstrap.BeanDefinitionLoader; -import org.springframework.bootstrap.CommandLineRunner; -import org.springframework.bootstrap.ExitCodeGenerator; -import org.springframework.bootstrap.SpringApplication; import org.springframework.bootstrap.context.embedded.AnnotationConfigEmbeddedWebApplicationContext; import org.springframework.bootstrap.context.embedded.jetty.JettyEmbeddedServletContainerFactory; import org.springframework.context.ApplicationContext; @@ -94,14 +90,14 @@ public void close() { public void sourcesMustNotBeNull() throws Exception { this.thrown.expect(IllegalArgumentException.class); this.thrown.expectMessage("Sources must not be empty"); - new SpringApplication((Object[]) null); + new SpringApplication((Object[]) null).run(); } @Test public void sourcesMustNotBeEmpty() throws Exception { this.thrown.expect(IllegalArgumentException.class); this.thrown.expectMessage("Sources must not be empty"); - new SpringApplication(); + new SpringApplication().run(); } @Test @@ -270,7 +266,7 @@ public void loadSources() throws Exception { application.setWebEnvironment(false); application.setUseMockLoader(true); application.run(); - assertThat(application.getSources(), equalTo(sources)); + assertThat(application.getSources().toArray(), equalTo(sources)); } @Test @@ -341,8 +337,6 @@ private static class TestSpringApplication extends SpringApplication { private boolean useMockLoader; - private Object[] sources; - public TestSpringApplication(Object... sources) { super(sources); } @@ -358,7 +352,6 @@ public void setUseMockLoader(boolean useMockLoader) { @Override protected BeanDefinitionLoader createBeanDefinitionLoader( BeanDefinitionRegistry registry, Object[] sources) { - this.sources = sources; if (this.useMockLoader) { this.loader = mock(BeanDefinitionLoader.class); } @@ -372,9 +365,6 @@ public BeanDefinitionLoader getLoader() { return this.loader; } - public Object[] getSources() { - return this.sources; - } } @Configuration diff --git a/spring-bootstrap/src/test/java/org/springframework/bootstrap/bind/RelaxedDataBinderTests.java b/spring-bootstrap/src/test/java/org/springframework/bootstrap/bind/RelaxedDataBinderTests.java index 9beeedc9f888..3aede5154c57 100644 --- a/spring-bootstrap/src/test/java/org/springframework/bootstrap/bind/RelaxedDataBinderTests.java +++ b/spring-bootstrap/src/test/java/org/springframework/bootstrap/bind/RelaxedDataBinderTests.java @@ -20,11 +20,15 @@ import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.Target; +import java.util.ArrayList; +import java.util.Collection; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Properties; +import java.util.Set; import javax.validation.Constraint; import javax.validation.ConstraintValidator; @@ -36,7 +40,7 @@ import org.junit.Test; import org.junit.rules.ExpectedException; import org.springframework.beans.MutablePropertyValues; -import org.springframework.bootstrap.bind.RelaxedDataBinder; +import org.springframework.beans.NotWritablePropertyException; import org.springframework.context.support.StaticMessageSource; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.support.DefaultConversionService; @@ -119,20 +123,50 @@ public void testBindNested() throws Exception { @Test public void testBindNestedList() throws Exception { TargetWithNestedList target = new TargetWithNestedList(); - bind(target, "nested: bar,foo"); - bind(target, "nested[0]: bar"); - bind(target, "nested[1]: foo"); + bind(target, "nested[0]: bar\nnested[1]: foo"); assertEquals("[bar, foo]", target.getNested().toString()); } @Test - public void testBindNestedListCommaDelimitedONly() throws Exception { + public void testBindNestedListCommaDelimitedOnly() throws Exception { TargetWithNestedList target = new TargetWithNestedList(); this.conversionService = new DefaultConversionService(); bind(target, "nested: bar,foo"); assertEquals("[bar, foo]", target.getNested().toString()); } + @Test + public void testBindNestedSetCommaDelimitedOnly() throws Exception { + TargetWithNestedSet target = new TargetWithNestedSet(); + this.conversionService = new DefaultConversionService(); + bind(target, "nested: bar,foo"); + assertEquals("[bar, foo]", target.getNested().toString()); + } + + @Test(expected = NotWritablePropertyException.class) + public void testBindNestedReadOnlyListCommaSeparated() throws Exception { + TargetWithReadOnlyNestedList target = new TargetWithReadOnlyNestedList(); + this.conversionService = new DefaultConversionService(); + bind(target, "nested: bar,foo"); + assertEquals("[bar, foo]", target.getNested().toString()); + } + + @Test + public void testBindNestedReadOnlyListIndexed() throws Exception { + TargetWithReadOnlyNestedList target = new TargetWithReadOnlyNestedList(); + this.conversionService = new DefaultConversionService(); + bind(target, "nested[0]: bar\nnested[1]:foo"); + assertEquals("[bar, foo]", target.getNested().toString()); + } + + @Test + public void testBindNestedReadOnlyCollectionIndexed() throws Exception { + TargetWithReadOnlyNestedCollection target = new TargetWithReadOnlyNestedCollection(); + this.conversionService = new DefaultConversionService(); + bind(target, "nested[0]: bar\nnested[1]:foo"); + assertEquals("[bar, foo]", target.getNested().toString()); + } + @Test public void testBindNestedMap() throws Exception { TargetWithNestedMap target = new TargetWithNestedMap(); @@ -306,6 +340,34 @@ public void setNested(List nested) { } } + public static class TargetWithReadOnlyNestedList { + private List nested = new ArrayList(); + + public List getNested() { + return this.nested; + } + } + + public static class TargetWithReadOnlyNestedCollection { + private Collection nested = new ArrayList(); + + public Collection getNested() { + return this.nested; + } + } + + public static class TargetWithNestedSet { + private Set nested = new LinkedHashSet(); + + public Set getNested() { + return this.nested; + } + + public void setNested(Set nested) { + this.nested = nested; + } + } + public static class TargetWithNestedObject { private VanillaTarget nested; diff --git a/spring-bootstrap/src/test/java/org/springframework/bootstrap/sampleconfig/package-info.java b/spring-bootstrap/src/test/java/org/springframework/bootstrap/sampleconfig/package-info.java new file mode 100644 index 000000000000..04a1f01bbaa1 --- /dev/null +++ b/spring-bootstrap/src/test/java/org/springframework/bootstrap/sampleconfig/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * 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. + */ + +/** + * Sample config for tests + */ +package org.springframework.bootstrap.sampleconfig; + diff --git a/spring-bootstrap/src/test/resources/dispatcher.properties b/spring-bootstrap/src/test/resources/dispatcher.properties new file mode 100644 index 000000000000..e030745edabe --- /dev/null +++ b/spring-bootstrap/src/test/resources/dispatcher.properties @@ -0,0 +1 @@ +main.sources: org.springframework.bootstrap.main.DispatcherMainTests \ No newline at end of file