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"));
+ }
+
+}