diff --git a/build.gradle b/build.gradle index 0f5b97822a..64399ae741 100644 --- a/build.gradle +++ b/build.gradle @@ -26,6 +26,7 @@ configure(allprojects) { project -> ext.jodaVersion = "2.3" ext.junitVersion = "4.11" ext.slf4jVersion = "1.7.7" + ext.snakeYamlVersion = "1.13" ext.tiles2Version = "2.2.2" ext.tiles3Version = "3.0.3" ext.tomcatVersion = "8.0.5" @@ -321,6 +322,7 @@ project("spring-beans") { compile(files(project(":spring-core").cglibRepackJar)) optional("javax.inject:javax.inject:1") optional("javax.el:javax.el-api:2.2.4") + optional("org.yaml:snakeyaml:${snakeYamlVersion}") testCompile("log4j:log4j:1.2.17") testCompile("org.apache.tomcat.embed:tomcat-embed-core:${tomcatVersion}") } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/YamlMapFactoryBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/YamlMapFactoryBean.java new file mode 100644 index 0000000000..8efe50a12d --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/YamlMapFactoryBean.java @@ -0,0 +1,143 @@ +/* + * Copyright 2002-2014 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.beans.factory.config; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Properties; + +import org.springframework.beans.factory.FactoryBean; + +/** + * Factory for Map that reads from a YAML source. YAML is a nice human-readable + * format for configuration, and it has some useful hierarchical properties. It's + * more or less a superset of JSON, so it has a lot of similar features. If + * multiple resources are provided the later ones will override entries in the + * earlier ones hierarchically - that is all entries with the same nested key of + * type Map at any depth are merged. For example: + * + *
+ * foo:
+ *   bar:
+ *    one: two
+ * three: four
+ *
+ * 
+ * + * plus (later in the list) + * + *
+ * foo:
+ *   bar:
+ *    one: 2
+ * five: six
+ *
+ * 
+ * + * results in an effective input of + * + *
+ * foo:
+ *   bar:
+ *    one: 2
+ * three: four
+ * five: six
+ *
+ * 
+ * + * Note that the value of "foo" in the first document is not simply replaced + * with the value in the second, but its nested values are merged. + * + * @author Dave Syer + * @since 4.1 + */ +public class YamlMapFactoryBean extends YamlProcessor implements + FactoryBean> { + + private boolean singleton = true; + + private Map singletonInstance; + + + /** + * Set whether a shared 'singleton' Map instance should be + * created, or rather a new Map instance on each request. + *

Default is "true" (a shared singleton). + */ + public final void setSingleton(boolean singleton) { + this.singleton = singleton; + } + + @Override + public final boolean isSingleton() { + return this.singleton; + } + + + @Override + public Map getObject() { + if (!this.singleton || this.singletonInstance == null) { + this.singletonInstance = createProperties(); + } + return this.singletonInstance; + } + + @Override + public Class getObjectType() { + return Map.class; + } + + /** + * Template method that subclasses may override to construct the object + * returned by this factory. The default implementation returns the + * merged Map instance. + *

Invoked lazily the first time {@link #getObject()} is invoked in + * case of a shared singleton; else, on each {@link #getObject()} call. + * @return the object returned by this factory + * @see #process(java.util.Map, MatchCallback) + */ + protected Map createProperties() { + final Map result = new LinkedHashMap(); + process(new MatchCallback() { + @Override + public void process(Properties properties, Map map) { + merge(result, map); + } + }); + return result; + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private void merge(Map output, Map map) { + for (Entry entry : map.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + Object existing = output.get(key); + if (value instanceof Map && existing instanceof Map) { + Map result = new LinkedHashMap( + (Map) existing); + merge(result, (Map) value); + output.put(key, result); + } + else { + output.put(key, value); + } + } + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/YamlProcessor.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/YamlProcessor.java new file mode 100644 index 0000000000..c936981da5 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/YamlProcessor.java @@ -0,0 +1,356 @@ +/* + * Copyright 2002-2014 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.beans.factory.config; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Properties; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.yaml.snakeyaml.Yaml; + +import org.springframework.core.io.Resource; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Base class for Yaml factories. + * + * @author Dave Syer + * @since 4.1 + */ +public abstract class YamlProcessor { + + private final Log logger = LogFactory.getLog(getClass()); + + private ResolutionMethod resolutionMethod = ResolutionMethod.OVERRIDE; + + private Resource[] resources = new Resource[0]; + + private List documentMatchers = Collections.emptyList(); + + private boolean matchDefault = true; + + /** + * A map of document matchers allowing callers to selectively use only + * some of the documents in a YAML resource. In YAML documents are + * separated by --- lines, and each document is converted + * to properties before the match is made. E.g. + * + *

+	 * environment: dev
+	 * url: http://dev.bar.com
+	 * name: Developer Setup
+	 * ---
+	 * environment: prod
+	 * url:http://foo.bar.com
+	 * name: My Cool App
+	 * 
+ * + * when mapped with + * documentMatchers = YamlProcessor.mapMatcher({"environment": "prod"}) + * would end up as + * + *
+	 * environment=prod
+	 * url=http://foo.bar.com
+	 * name=My Cool App
+	 * url=http://dev.bar.com
+	 * 
+ * @param matchers a map of keys to value patterns (regular expressions) + */ + public void setDocumentMatchers(DocumentMatcher... matchers) { + this.documentMatchers = Collections + .unmodifiableList(new ArrayList(Arrays.asList(matchers))); + } + + /** + * Flag indicating that a document for which all the + * {@link #setDocumentMatchers(DocumentMatcher...) document matchers} abstain will + * nevertheless match. + * @param matchDefault the flag to set (default true) + */ + public void setMatchDefault(boolean matchDefault) { + this.matchDefault = matchDefault; + } + + /** + * Method to use for resolving resources. Each resource will be converted to a Map, so + * this property is used to decide which map entries to keep in the final output from + * this factory. + * @param resolutionMethod the resolution method to set (defaults to + * {@link ResolutionMethod#OVERRIDE}). + */ + public void setResolutionMethod(ResolutionMethod resolutionMethod) { + Assert.notNull(resolutionMethod, "ResolutionMethod must not be null"); + this.resolutionMethod = resolutionMethod; + } + + /** + * Set locations of YAML {@link Resource resources} to be loaded. + * @see ResolutionMethod + */ + public void setResources(Resource[] resources) { + this.resources = (resources == null ? null : resources.clone()); + } + + /** + * Provide an opportunity for subclasses to process the Yaml parsed from the supplied + * resources. Each resource is parsed in turn and the documents inside checked against + * the {@link #setDocumentMatchers(DocumentMatcher...) matchers}. If a document + * matches it is passed into the callback, along with its representation as + * Properties. Depending on the {@link #setResolutionMethod(ResolutionMethod)} not all + * of the documents will be parsed. + * @param callback a callback to delegate to once matching documents are found + */ + protected void process(MatchCallback callback) { + Yaml yaml = new Yaml(); + for (Resource resource : this.resources) { + boolean found = process(callback, yaml, resource); + if (this.resolutionMethod == ResolutionMethod.FIRST_FOUND && found) { + return; + } + } + } + + private boolean process(MatchCallback callback, Yaml yaml, Resource resource) { + int count = 0; + try { + if (this.logger.isDebugEnabled()) { + this.logger.debug("Loading from YAML: " + resource); + } + for (Object object : yaml.loadAll(resource.getInputStream())) { + if (object != null && process(asMap(object), callback)) { + count++; + if (this.resolutionMethod == ResolutionMethod.FIRST_FOUND) { + break; + } + } + } + if (this.logger.isDebugEnabled()) { + this.logger.debug("Loaded " + count + " document" + + (count > 1 ? "s" : "") + " from YAML resource: " + resource); + } + } + catch (IOException ex) { + handleProcessError(resource, ex); + } + return count > 0; + } + + private void handleProcessError(Resource resource, IOException ex) { + if (this.resolutionMethod != ResolutionMethod.FIRST_FOUND + && this.resolutionMethod != ResolutionMethod.OVERRIDE_AND_IGNORE) { + throw new IllegalStateException(ex); + } + if (this.logger.isWarnEnabled()) { + this.logger.warn("Could not load map from " + resource + ": " + + ex.getMessage()); + } + } + + @SuppressWarnings("unchecked") + private Map asMap(Object object) { + // YAML can have numbers as keys + Map result = new LinkedHashMap(); + if (!(object instanceof Map)) { + // A document can be a text literal + result.put("document", object); + return result; + } + + Map map = (Map) object; + for (Entry entry : map.entrySet()) { + Object value = entry.getValue(); + if (value instanceof Map) { + value = asMap(value); + } + Object key = entry.getKey(); + if (key instanceof CharSequence) { + result.put(key.toString(), value); + } + else { + // It has to be a map key in this case + result.put("[" + key.toString() + "]", value); + } + } + return result; + } + + private boolean process(Map map, MatchCallback callback) { + + Properties properties = new Properties(); + assignProperties(properties, map, null); + + if (this.documentMatchers.isEmpty()) { + if (this.logger.isDebugEnabled()) { + this.logger.debug("Merging document (no matchers set)" + map); + } + callback.process(properties, map); + return true; + } + + MatchStatus result = MatchStatus.ABSTAIN; + for (DocumentMatcher matcher : this.documentMatchers) { + MatchStatus match = matcher.matches(properties); + result = MatchStatus.getMostSpecific(match, result); + if (match == MatchStatus.FOUND) { + if (this.logger.isDebugEnabled()) { + this.logger.debug("Matched document with document matcher: " + + properties); + } + callback.process(properties, map); + return true; + } + } + + if (result == MatchStatus.ABSTAIN && this.matchDefault) { + if (this.logger.isDebugEnabled()) { + this.logger.debug("Matched document with default matcher: " + map); + } + callback.process(properties, map); + return true; + } + + this.logger.debug("Unmatched document"); + return false; + } + + private void assignProperties(Properties properties, Map input, + String path) { + for (Entry entry : input.entrySet()) { + String key = entry.getKey(); + if (StringUtils.hasText(path)) { + if (key.startsWith("[")) { + key = path + key; + } + else { + key = path + "." + key; + } + } + Object value = entry.getValue(); + if (value instanceof String) { + properties.put(key, value); + } + else if (value instanceof Map) { + // Need a compound key + @SuppressWarnings("unchecked") + Map map = (Map) value; + assignProperties(properties, map, key); + } + else if (value instanceof Collection) { + // Need a compound key + @SuppressWarnings("unchecked") + Collection collection = (Collection) value; + int count = 0; + for (Object object : collection) { + assignProperties(properties, + Collections.singletonMap("[" + (count++) + "]", object), key); + } + } + else { + properties.put(key, value == null ? "" : value); + } + } + } + + /** + * Callback interface used to process properties in a resulting map. + */ + public interface MatchCallback { + + /** + * Process the properties. + * @param properties the properties to process + * @param map a mutable result map + */ + void process(Properties properties, Map map); + + } + + /** + * Strategy interface used to test if properties match. + */ + public interface DocumentMatcher { + + /** + * Test if the given properties match. + * @param properties the properties to test + * @return the status of the match. + */ + MatchStatus matches(Properties properties); + + } + + /** + * Status returned from {@link DocumentMatcher#matches(java.util.Properties)} + */ + public static enum MatchStatus { + + /** + * A match was found. + */ + FOUND, + + /** + * No match was found. + */ + NOT_FOUND, + + /** + * The matcher should not be considered. + */ + ABSTAIN; + + /** + * Compare two {@link MatchStatus} items, returning the most specific status. + */ + public static MatchStatus getMostSpecific(MatchStatus a, MatchStatus b) { + return a.ordinal() < b.ordinal() ? a : b; + } + } + + /** + * Method to use for resolving resources. + */ + public static enum ResolutionMethod { + + /** + * Replace values from earlier in the list. + */ + OVERRIDE, + + /** + * Replace values from earlier in the list, ignoring any failures. + */ + OVERRIDE_AND_IGNORE, + + /** + * Take the first resource in the list that exists and use just that. + */ + FIRST_FOUND + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/YamlPropertiesFactoryBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/YamlPropertiesFactoryBean.java new file mode 100644 index 0000000000..699b26acda --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/YamlPropertiesFactoryBean.java @@ -0,0 +1,132 @@ +/* + * Copyright 2002-2014 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.beans.factory.config; + +import java.util.Map; +import java.util.Properties; + +import org.springframework.beans.factory.FactoryBean; + +/** + * Factory for Java Properties that reads from a YAML source. YAML is a nice + * human-readable format for configuration, and it has some useful hierarchical + * properties. It's more or less a superset of JSON, so it has a lot of similar + * features. The Properties created by this factory have nested paths for + * hierarchical objects, so for instance this YAML + * + *
+ * environments:
+ *   dev:
+ *     url: http://dev.bar.com
+ *     name: Developer Setup
+ *   prod:
+ *     url: http://foo.bar.com
+ *     name: My Cool App
+ * 
+ * + * is transformed into these Properties: + * + *
+ * environments.dev.url=http://dev.bar.com
+ * environments.dev.name=Developer Setup
+ * environments.prod.url=http://foo.bar.com
+ * environments.prod.name=My Cool App
+ * 
+ * + * Lists are represented as comma-separated values (useful for simple String + * values) and also as property keys with [] dereferencers, for + * example this YAML: + * + *
+ * servers:
+ * - dev.bar.com
+ * - foo.bar.com
+ * 
+ * + * becomes java Properties like this: + * + *
+ * servers=dev.bar.com,foo.bar.com
+ * servers[0]=dev.bar.com
+ * servers[1]=foo.bar.com
+ * 
+ * + * Can create a singleton or a new object on each request. Default is + * a singleton. + * + * @author Dave Syer + * @author Stephane Nicoll + * @since 4.1 + */ +public class YamlPropertiesFactoryBean extends YamlProcessor implements + FactoryBean { + + private boolean singleton = true; + + private Properties singletonInstance; + + + /** + * Set whether a shared 'singleton' Properties instance should be + * created, or rather a new Properties instance on each request. + *

Default is "true" (a shared singleton). + */ + public final void setSingleton(boolean singleton) { + this.singleton = singleton; + } + + @Override + public final boolean isSingleton() { + return this.singleton; + } + + + @Override + public final Properties getObject() { + if (!this.singleton || this.singletonInstance == null) { + this.singletonInstance = createProperties(); + } + return this.singletonInstance; + } + + @Override + public Class getObjectType() { + return Properties.class; + } + + + /** + * Template method that subclasses may override to construct the object + * returned by this factory. The default implementation returns a + * properties with the content of all resources. + *

Invoked lazily the first time {@link #getObject()} is invoked in + * case of a shared singleton; else, on each {@link #getObject()} call. + * @return the object returned by this factory + * @see #process(MatchCallback) () + */ + protected Properties createProperties() { + final Properties result = new Properties(); + process(new MatchCallback() { + @Override + public void process(Properties properties, Map map) { + result.putAll(properties); + } + }); + return result; + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/config/YamlMapFactoryBeanTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/config/YamlMapFactoryBeanTests.java new file mode 100644 index 0000000000..a69762fd78 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/config/YamlMapFactoryBeanTests.java @@ -0,0 +1,108 @@ +/* + * Copyright 2002-2014 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.beans.factory.config; + +import static org.junit.Assert.*; + +import java.io.IOException; +import java.io.InputStream; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.junit.Test; + +import org.springframework.core.io.AbstractResource; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; + +/** + * Tests for {@link YamlMapFactoryBean}. + * + * @author Dave Syer + */ +public class YamlMapFactoryBeanTests { + + private final YamlMapFactoryBean factory = new YamlMapFactoryBean(); + + @Test + public void testSetIgnoreResourceNotFound() throws Exception { + this.factory + .setResolutionMethod(YamlMapFactoryBean.ResolutionMethod.OVERRIDE_AND_IGNORE); + this.factory.setResources(new FileSystemResource[] {new FileSystemResource( + "non-exsitent-file.yml")}); + assertEquals(0, this.factory.getObject().size()); + } + + @Test(expected = IllegalStateException.class) + public void testSetBarfOnResourceNotFound() throws Exception { + this.factory.setResources(new FileSystemResource[] {new FileSystemResource( + "non-exsitent-file.yml")}); + assertEquals(0, this.factory.getObject().size()); + } + + @Test + public void testGetObject() throws Exception { + this.factory.setResources(new ByteArrayResource[] {new ByteArrayResource( + "foo: bar".getBytes())}); + assertEquals(1, this.factory.getObject().size()); + } + + @SuppressWarnings("unchecked") + @Test + public void testOverrideAndremoveDefaults() throws Exception { + this.factory.setResources(new ByteArrayResource[] { + new ByteArrayResource("foo:\n bar: spam".getBytes()), + new ByteArrayResource("foo:\n spam: bar".getBytes())}); + assertEquals(1, this.factory.getObject().size()); + assertEquals(2, + ((Map) this.factory.getObject().get("foo")).size()); + } + + @Test + public void testFirstFound() throws Exception { + this.factory.setResolutionMethod(YamlProcessor.ResolutionMethod.FIRST_FOUND); + this.factory.setResources(new Resource[] {new AbstractResource() { + @Override + public String getDescription() { + return "non-existent"; + } + + @Override + public InputStream getInputStream() throws IOException { + throw new IOException("planned"); + } + }, new ByteArrayResource("foo:\n spam: bar".getBytes())}); + assertEquals(1, this.factory.getObject().size()); + } + + @Test + public void testMapWithPeriodsInKey() throws Exception { + this.factory.setResources(new ByteArrayResource[] {new ByteArrayResource( + "foo:\n ? key1.key2\n : value".getBytes())}); + Map map = this.factory.getObject(); + assertEquals(1, map.size()); + assertTrue(map.containsKey("foo")); + Object object = map.get("foo"); + assertTrue(object instanceof LinkedHashMap); + @SuppressWarnings("unchecked") + Map sub = (Map) object; + assertTrue(sub.containsKey("key1.key2")); + assertEquals("value", sub.get("key1.key2")); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/config/YamlProcessorTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/config/YamlProcessorTests.java new file mode 100644 index 0000000000..fca1edfdfc --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/config/YamlProcessorTests.java @@ -0,0 +1,139 @@ +/* + * Copyright 2002-2014 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.beans.factory.config; + +import static org.junit.Assert.*; +import static org.springframework.beans.factory.config.YamlProcessor.*; + +import java.util.Map; +import java.util.Properties; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.yaml.snakeyaml.parser.ParserException; +import org.yaml.snakeyaml.scanner.ScannerException; + +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.Resource; + +/** + * Tests for {@link YamlProcessor}. + * + * @author Dave Syer + */ +public class YamlProcessorTests { + + private final YamlProcessor processor = new YamlProcessor() { + }; + + @Rule + public ExpectedException exception = ExpectedException.none(); + + @Test + public void arrayConvertedToIndexedBeanReference() { + this.processor.setResources(new Resource[] {new ByteArrayResource( + "foo: bar\nbar: [1,2,3]".getBytes())}); + this.processor.process(new MatchCallback() { + @Override + public void process(Properties properties, Map map) { + assertEquals(1, properties.get("bar[0]")); + assertEquals(2, properties.get("bar[1]")); + assertEquals(3, properties.get("bar[2]")); + assertEquals(4, properties.size()); + } + }); + } + + @Test + public void testStringResource() throws Exception { + this.processor.setResources(new Resource[] {new ByteArrayResource( + "foo # a document that is a literal".getBytes())}); + this.processor.process(new MatchCallback() { + @Override + public void process(Properties properties, Map map) { + assertEquals("foo", map.get("document")); + } + }); + } + + @Test + public void testBadDocumentStart() throws Exception { + this.processor.setResources(new Resource[] {new ByteArrayResource( + "foo # a document\nbar: baz".getBytes())}); + this.exception.expect(ParserException.class); + this.exception.expectMessage("line 2, column 1"); + this.processor.process(new MatchCallback() { + @Override + public void process(Properties properties, Map map) { + } + }); + } + + @Test + public void testBadResource() throws Exception { + this.processor.setResources(new Resource[] {new ByteArrayResource( + "foo: bar\ncd\nspam:\n foo: baz".getBytes())}); + this.exception.expect(ScannerException.class); + this.exception.expectMessage("line 3, column 1"); + this.processor.process(new MatchCallback() { + @Override + public void process(Properties properties, Map map) { + } + }); + } + + @Test + public void mapConvertedToIndexedBeanReference() { + this.processor.setResources(new Resource[] {new ByteArrayResource( + "foo: bar\nbar:\n spam: bucket".getBytes())}); + this.processor.process(new MatchCallback() { + @Override + public void process(Properties properties, Map map) { + // System.err.println(properties); + assertEquals("bucket", properties.get("bar.spam")); + assertEquals(2, properties.size()); + } + }); + } + + @Test + public void integerKeyBehaves() { + this.processor.setResources(new Resource[] {new ByteArrayResource( + "foo: bar\n1: bar".getBytes())}); + this.processor.process(new MatchCallback() { + @Override + public void process(Properties properties, Map map) { + assertEquals("bar", properties.get("[1]")); + assertEquals(2, properties.size()); + } + }); + } + + @Test + public void integerDeepKeyBehaves() { + this.processor.setResources(new Resource[] {new ByteArrayResource( + "foo:\n 1: bar".getBytes())}); + this.processor.process(new MatchCallback() { + + @Override + public void process(Properties properties, Map map) { + assertEquals("bar", properties.get("foo[1]")); + assertEquals(1, properties.size()); + } + }); + } +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/config/YamlPropertiesFactoryBeanTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/config/YamlPropertiesFactoryBeanTests.java new file mode 100644 index 0000000000..3c2c711b64 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/config/YamlPropertiesFactoryBeanTests.java @@ -0,0 +1,245 @@ +/* + * Copyright 2002-2014 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.beans.factory.config; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; +import static org.springframework.beans.factory.config.YamlProcessor.*; + +import java.util.Map; +import java.util.Properties; + +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.scanner.ScannerException; + +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; + +/** + * Tests for {@link YamlPropertiesFactoryBean}. + * + * @author Dave Syer + */ +public class YamlPropertiesFactoryBeanTests { + + @Rule + public ExpectedException exception = ExpectedException.none(); + + @Test + public void testLoadResource() throws Exception { + YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean(); + factory.setResources(new Resource[] {new ByteArrayResource( + "foo: bar\nspam:\n foo: baz".getBytes())}); + Properties properties = factory.getObject(); + assertThat(properties.getProperty("foo"), equalTo("bar")); + assertThat(properties.getProperty("spam.foo"), equalTo("baz")); + } + + @Test + public void testBadResource() throws Exception { + YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean(); + factory.setResources(new Resource[] {new ByteArrayResource( + "foo: bar\ncd\nspam:\n foo: baz".getBytes())}); + this.exception.expect(ScannerException.class); + this.exception.expectMessage("line 3, column 1"); + factory.getObject(); + } + + @Test + public void testLoadResourcesWithOverride() throws Exception { + YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean(); + factory.setResources(new Resource[] { + new ByteArrayResource("foo: bar\nspam:\n foo: baz".getBytes()), + new ByteArrayResource("foo:\n bar: spam".getBytes())}); + Properties properties = factory.getObject(); + assertThat(properties.getProperty("foo"), equalTo("bar")); + assertThat(properties.getProperty("spam.foo"), equalTo("baz")); + assertThat(properties.getProperty("foo.bar"), equalTo("spam")); + } + + @Test + @Ignore("We can't fail on duplicate keys because the Map is created by the YAML library") + public void testLoadResourcesWithInternalOverride() throws Exception { + YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean(); + factory.setResources(new Resource[] {new ByteArrayResource( + "foo: bar\nspam:\n foo: baz\nfoo: bucket".getBytes())}); + Properties properties = factory.getObject(); + assertThat(properties.getProperty("foo"), equalTo("bar")); + } + + @Test + @Ignore("We can't fail on duplicate keys because the Map is created by the YAML library") + public void testLoadResourcesWithNestedInternalOverride() throws Exception { + YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean(); + factory.setResources(new Resource[] {new ByteArrayResource( + "foo:\n bar: spam\n foo: baz\nbreak: it\nfoo: bucket".getBytes())}); + Properties properties = factory.getObject(); + assertThat(properties.getProperty("foo.bar"), equalTo("spam")); + } + + @Test + public void testLoadResourceWithMultipleDocuments() throws Exception { + YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean(); + factory.setResources(new Resource[] {new ByteArrayResource( + "foo: bar\nspam: baz\n---\nfoo: bag".getBytes())}); + Properties properties = factory.getObject(); + assertThat(properties.getProperty("foo"), equalTo("bag")); + assertThat(properties.getProperty("spam"), equalTo("baz")); + } + + @Test + public void testLoadResourceWithSelectedDocuments() throws Exception { + YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean(); + factory.setResources(new Resource[] {new ByteArrayResource( + "foo: bar\nspam: baz\n---\nfoo: bag\nspam: bad".getBytes())}); + factory.setDocumentMatchers(new DocumentMatcher() { + @Override + public MatchStatus matches(Properties properties) { + return "bag".equals(properties.getProperty("foo")) ? MatchStatus.FOUND + : MatchStatus.NOT_FOUND; + } + }); + Properties properties = factory.getObject(); + assertThat(properties.getProperty("foo"), equalTo("bag")); + assertThat(properties.getProperty("spam"), equalTo("bad")); + } + + @Test + public void testLoadResourceWithDefaultMatch() throws Exception { + YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean(); + factory.setMatchDefault(true); + factory.setResources(new Resource[] {new ByteArrayResource( + "one: two\n---\nfoo: bar\nspam: baz\n---\nfoo: bag\nspam: bad".getBytes())}); + factory.setDocumentMatchers(new DocumentMatcher() { + @Override + public MatchStatus matches(Properties properties) { + if (!properties.containsKey("foo")) { + return MatchStatus.ABSTAIN; + } + return "bag".equals(properties.getProperty("foo")) ? MatchStatus.FOUND + : MatchStatus.NOT_FOUND; + } + }); + Properties properties = factory.getObject(); + assertThat(properties.getProperty("foo"), equalTo("bag")); + assertThat(properties.getProperty("spam"), equalTo("bad")); + assertThat(properties.getProperty("one"), equalTo("two")); + } + + @Test + public void testLoadResourceWithoutDefaultMatch() throws Exception { + YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean(); + factory.setMatchDefault(false); + factory.setResources(new Resource[] {new ByteArrayResource( + "one: two\n---\nfoo: bar\nspam: baz\n---\nfoo: bag\nspam: bad".getBytes())}); + factory.setDocumentMatchers(new DocumentMatcher() { + @Override + public MatchStatus matches(Properties properties) { + if (!properties.containsKey("foo")) { + return MatchStatus.ABSTAIN; + } + return "bag".equals(properties.getProperty("foo")) ? MatchStatus.FOUND + : MatchStatus.NOT_FOUND; + } + }); + Properties properties = factory.getObject(); + assertThat(properties.getProperty("foo"), equalTo("bag")); + assertThat(properties.getProperty("spam"), equalTo("bad")); + assertThat(properties.getProperty("one"), nullValue()); + } + + @Test + public void testLoadResourceWithDefaultMatchSkippingMissedMatch() throws Exception { + YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean(); + factory.setMatchDefault(true); + factory.setResources(new Resource[] {new ByteArrayResource( + "one: two\n---\nfoo: bag\nspam: bad\n---\nfoo: bar\nspam: baz".getBytes())}); + factory.setDocumentMatchers(new DocumentMatcher() { + @Override + public MatchStatus matches(Properties properties) { + if (!properties.containsKey("foo")) { + return MatchStatus.ABSTAIN; + } + return "bag".equals(properties.getProperty("foo")) ? MatchStatus.FOUND + : MatchStatus.NOT_FOUND; + } + }); + Properties properties = factory.getObject(); + assertThat(properties.getProperty("foo"), equalTo("bag")); + assertThat(properties.getProperty("spam"), equalTo("bad")); + assertThat(properties.getProperty("one"), equalTo("two")); + } + + @Test + public void testLoadNonExistentResource() throws Exception { + YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean(); + factory.setResolutionMethod(ResolutionMethod.OVERRIDE_AND_IGNORE); + factory.setResources(new Resource[] {new ClassPathResource("no-such-file.yml")}); + Properties properties = factory.getObject(); + assertThat(properties.size(), equalTo(0)); + } + + @Test + public void testLoadNull() throws Exception { + YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean(); + factory.setResources(new Resource[] {new ByteArrayResource("foo: bar\nspam:" + .getBytes())}); + Properties properties = factory.getObject(); + assertThat(properties.getProperty("foo"), equalTo("bar")); + assertThat(properties.getProperty("spam"), equalTo("")); + } + + @Test + public void testLoadArrayOfString() throws Exception { + YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean(); + factory.setResources(new Resource[] {new ByteArrayResource("foo:\n- bar\n- baz" + .getBytes())}); + Properties properties = factory.getObject(); + assertThat(properties.getProperty("foo[0]"), equalTo("bar")); + assertThat(properties.getProperty("foo[1]"), equalTo("baz")); + } + + @Test + public void testLoadArrayOfObject() throws Exception { + YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean(); + factory.setResources(new Resource[] {new ByteArrayResource( + "foo:\n- bar:\n spam: crap\n- baz\n- one: two\n three: four" + .getBytes() + )}); + Properties properties = factory.getObject(); + assertThat(properties.getProperty("foo[0].bar.spam"), equalTo("crap")); + assertThat(properties.getProperty("foo[1]"), equalTo("baz")); + assertThat(properties.getProperty("foo[2].one"), equalTo("two")); + assertThat(properties.getProperty("foo[2].three"), equalTo("four")); + } + + @SuppressWarnings("unchecked") + @Test + public void testYaml() { + Yaml yaml = new Yaml(); + Map map = yaml.loadAs("foo: bar\nspam:\n foo: baz", Map.class); + assertThat(map.get("foo"), equalTo((Object) "bar")); + assertThat(((Map) map.get("spam")).get("foo"), + equalTo((Object) "baz")); + } + +}