Add support of YAML

This commit migrates the YAML support available in Spring Boot to
the core framework. YAML documents can be loaded either as a
properties object or as a map.

Issue: SPR-9897
This commit is contained in:
Stephane Nicoll 2014-05-08 12:02:20 +02:00
parent 7b7fe9aa17
commit 580e52372f
7 changed files with 1125 additions and 0 deletions

View File

@ -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}")
}

View File

@ -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:
*
* <pre class="code">
* foo:
* bar:
* one: two
* three: four
*
* </pre>
*
* plus (later in the list)
*
* <pre class="code">
* foo:
* bar:
* one: 2
* five: six
*
* </pre>
*
* results in an effective input of
*
* <pre class="code">
* foo:
* bar:
* one: 2
* three: four
* five: six
*
* </pre>
*
* 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<Map<String, Object>> {
private boolean singleton = true;
private Map<String, Object> singletonInstance;
/**
* Set whether a shared 'singleton' Map instance should be
* created, or rather a new Map instance on each request.
* <p>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<String, Object> 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.
* <p>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<String, Object> createProperties() {
final Map<String, Object> result = new LinkedHashMap<String, Object>();
process(new MatchCallback() {
@Override
public void process(Properties properties, Map<String, Object> map) {
merge(result, map);
}
});
return result;
}
@SuppressWarnings({"unchecked", "rawtypes"})
private void merge(Map<String, Object> output, Map<String, Object> map) {
for (Entry<String, Object> entry : map.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
Object existing = output.get(key);
if (value instanceof Map && existing instanceof Map) {
Map<String, Object> result = new LinkedHashMap<String, Object>(
(Map) existing);
merge(result, (Map) value);
output.put(key, result);
}
else {
output.put(key, value);
}
}
}
}

View File

@ -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<DocumentMatcher> 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 <code>---<code> lines, and each document is converted
* to properties before the match is made. E.g.
*
* <pre class="code">
* environment: dev
* url: http://dev.bar.com
* name: Developer Setup
* ---
* environment: prod
* url:http://foo.bar.com
* name: My Cool App
* </pre>
*
* when mapped with
* <code>documentMatchers = YamlProcessor.mapMatcher({"environment": "prod"})</code>
* would end up as
*
* <pre class="code">
* environment=prod
* url=http://foo.bar.com
* name=My Cool App
* url=http://dev.bar.com
* </pre>
* @param matchers a map of keys to value patterns (regular expressions)
*/
public void setDocumentMatchers(DocumentMatcher... matchers) {
this.documentMatchers = Collections
.unmodifiableList(new ArrayList<DocumentMatcher>(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<String, Object> asMap(Object object) {
// YAML can have numbers as keys
Map<String, Object> result = new LinkedHashMap<String, Object>();
if (!(object instanceof Map)) {
// A document can be a text literal
result.put("document", object);
return result;
}
Map<Object, Object> map = (Map<Object, Object>) object;
for (Entry<Object, Object> 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<String, Object> 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<String, Object> input,
String path) {
for (Entry<String, Object> 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<String, Object> map = (Map<String, Object>) value;
assignProperties(properties, map, key);
}
else if (value instanceof Collection) {
// Need a compound key
@SuppressWarnings("unchecked")
Collection<Object> collection = (Collection<Object>) 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<String, Object> 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
}
}

View File

@ -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
*
* <pre class="code">
* environments:
* dev:
* url: http://dev.bar.com
* name: Developer Setup
* prod:
* url: http://foo.bar.com
* name: My Cool App
* </pre>
*
* is transformed into these Properties:
*
* <pre class="code">
* 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
* </pre>
*
* Lists are represented as comma-separated values (useful for simple String
* values) and also as property keys with <code>[]</code> dereferencers, for
* example this YAML:
*
* <pre class="code">
* servers:
* - dev.bar.com
* - foo.bar.com
* </pre>
*
* becomes java Properties like this:
*
* <pre class="code">
* servers=dev.bar.com,foo.bar.com
* servers[0]=dev.bar.com
* servers[1]=foo.bar.com
* </pre>
*
* 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<Properties> {
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.
* <p>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.
* <p>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<String, Object> map) {
result.putAll(properties);
}
});
return result;
}
}

View File

@ -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<String, Object>) 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<String, Object> map = this.factory.getObject();
assertEquals(1, map.size());
assertTrue(map.containsKey("foo"));
Object object = map.get("foo");
assertTrue(object instanceof LinkedHashMap);
@SuppressWarnings("unchecked")
Map<String, Object> sub = (Map<String, Object>) object;
assertTrue(sub.containsKey("key1.key2"));
assertEquals("value", sub.get("key1.key2"));
}
}

View File

@ -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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> map) {
assertEquals("bar", properties.get("foo[1]"));
assertEquals(1, properties.size());
}
});
}
}

View File

@ -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<String, ?> map = yaml.loadAs("foo: bar\nspam:\n foo: baz", Map.class);
assertThat(map.get("foo"), equalTo((Object) "bar"));
assertThat(((Map<String, Object>) map.get("spam")).get("foo"),
equalTo((Object) "baz"));
}
}