Check for duplicate keys in YAML map nodes

Snake YAML allows for duplicate keys in map nodes. See
https://code.google.com/p/snakeyaml/issues/detail?id=199

This commit uses a dedicated Constructor extension that explicitly
checks for such duplicate keys.

Issue: SPR-12318
This commit is contained in:
Stephane Nicoll 2014-10-14 15:58:35 +02:00
parent 3056301015
commit 859e1e8003
4 changed files with 100 additions and 50 deletions

View File

@ -17,6 +17,7 @@
package org.springframework.beans.factory.config; package org.springframework.beans.factory.config;
import java.io.IOException; import java.io.IOException;
import java.util.AbstractMap;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
@ -25,10 +26,14 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.Properties; import java.util.Properties;
import java.util.Set;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import org.yaml.snakeyaml.Yaml; import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.Constructor;
import org.yaml.snakeyaml.nodes.MappingNode;
import org.yaml.snakeyaml.parser.ParserException;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.util.Assert; import org.springframework.util.Assert;
@ -126,7 +131,7 @@ public abstract class YamlProcessor {
* @param callback a callback to delegate to once matching documents are found * @param callback a callback to delegate to once matching documents are found
*/ */
protected void process(MatchCallback callback) { protected void process(MatchCallback callback) {
Yaml yaml = new Yaml(); Yaml yaml = new Yaml(new StrictMapAppenderConstructor());
for (Resource resource : this.resources) { for (Resource resource : this.resources) {
boolean found = process(callback, yaml, resource); boolean found = process(callback, yaml, resource);
if (this.resolutionMethod == ResolutionMethod.FIRST_FOUND && found) { if (this.resolutionMethod == ResolutionMethod.FIRST_FOUND && found) {
@ -351,4 +356,44 @@ public abstract class YamlProcessor {
FIRST_FOUND FIRST_FOUND
} }
/**
* A specialized {@link Constructor} that checks for duplicate keys.
*/
private static class StrictMapAppenderConstructor extends Constructor {
public StrictMapAppenderConstructor() {
super();
}
@Override
protected Map<Object, Object> constructMapping(MappingNode node) {
try {
return super.constructMapping(node);
} catch (IllegalStateException e) {
throw new ParserException("while parsing MappingNode",
node.getStartMark(), e.getMessage(), node.getEndMark());
}
}
@Override
protected Map<Object, Object> createDefaultMap() {
final Map<Object, Object> delegate = super.createDefaultMap();
return new AbstractMap<Object, Object>() {
@Override
public Object put(Object key, Object value) {
if (delegate.containsKey(key)) {
throw new IllegalStateException("duplicate key: " + key);
}
return delegate.put(key, value);
}
@Override
public Set<Entry<Object, Object>> entrySet() {
return delegate.entrySet();
}
};
}
}
} }

View File

@ -24,11 +24,11 @@ import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
import org.junit.Test; import org.junit.Test;
import org.yaml.snakeyaml.parser.ParserException;
import org.springframework.core.io.AbstractResource; import org.springframework.core.io.AbstractResource;
import org.springframework.core.io.ByteArrayResource; import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
/** /**
* Tests for {@link YamlMapFactoryBean}. * Tests for {@link YamlMapFactoryBean}.
@ -76,7 +76,7 @@ public class YamlMapFactoryBeanTests {
@Test @Test
public void testFirstFound() throws Exception { public void testFirstFound() throws Exception {
this.factory.setResolutionMethod(YamlProcessor.ResolutionMethod.FIRST_FOUND); this.factory.setResolutionMethod(YamlProcessor.ResolutionMethod.FIRST_FOUND);
this.factory.setResources(new Resource[] {new AbstractResource() { this.factory.setResources(new AbstractResource() {
@Override @Override
public String getDescription() { public String getDescription() {
return "non-existent"; return "non-existent";
@ -86,7 +86,7 @@ public class YamlMapFactoryBeanTests {
public InputStream getInputStream() throws IOException { public InputStream getInputStream() throws IOException {
throw new IOException("planned"); throw new IOException("planned");
} }
}, new ByteArrayResource("foo:\n spam: bar".getBytes())}); }, new ByteArrayResource("foo:\n spam: bar".getBytes()));
assertEquals(1, this.factory.getObject().size()); assertEquals(1, this.factory.getObject().size());
} }
@ -105,4 +105,11 @@ public class YamlMapFactoryBeanTests {
assertEquals("value", sub.get("key1.key2")); assertEquals("value", sub.get("key1.key2"));
} }
@Test(expected = ParserException.class)
public void testDuplicateKey() throws Exception {
this.factory.setResources(new ByteArrayResource[] {new ByteArrayResource(
"mymap:\n foo: bar\nmymap:\n bar: foo".getBytes())});
this.factory.getObject().get("mymap");
}
} }

View File

@ -28,7 +28,6 @@ import org.yaml.snakeyaml.parser.ParserException;
import org.yaml.snakeyaml.scanner.ScannerException; import org.yaml.snakeyaml.scanner.ScannerException;
import org.springframework.core.io.ByteArrayResource; import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.Resource;
/** /**
* Tests for {@link YamlProcessor}. * Tests for {@link YamlProcessor}.
@ -45,8 +44,8 @@ public class YamlProcessorTests {
@Test @Test
public void arrayConvertedToIndexedBeanReference() { public void arrayConvertedToIndexedBeanReference() {
this.processor.setResources(new Resource[] {new ByteArrayResource( this.processor.setResources(new ByteArrayResource(
"foo: bar\nbar: [1,2,3]".getBytes())}); "foo: bar\nbar: [1,2,3]".getBytes()));
this.processor.process(new MatchCallback() { this.processor.process(new MatchCallback() {
@Override @Override
public void process(Properties properties, Map<String, Object> map) { public void process(Properties properties, Map<String, Object> map) {
@ -60,8 +59,8 @@ public class YamlProcessorTests {
@Test @Test
public void testStringResource() throws Exception { public void testStringResource() throws Exception {
this.processor.setResources(new Resource[] {new ByteArrayResource( this.processor.setResources(new ByteArrayResource(
"foo # a document that is a literal".getBytes())}); "foo # a document that is a literal".getBytes()));
this.processor.process(new MatchCallback() { this.processor.process(new MatchCallback() {
@Override @Override
public void process(Properties properties, Map<String, Object> map) { public void process(Properties properties, Map<String, Object> map) {
@ -72,8 +71,8 @@ public class YamlProcessorTests {
@Test @Test
public void testBadDocumentStart() throws Exception { public void testBadDocumentStart() throws Exception {
this.processor.setResources(new Resource[] {new ByteArrayResource( this.processor.setResources(new ByteArrayResource(
"foo # a document\nbar: baz".getBytes())}); "foo # a document\nbar: baz".getBytes()));
this.exception.expect(ParserException.class); this.exception.expect(ParserException.class);
this.exception.expectMessage("line 2, column 1"); this.exception.expectMessage("line 2, column 1");
this.processor.process(new MatchCallback() { this.processor.process(new MatchCallback() {
@ -85,8 +84,8 @@ public class YamlProcessorTests {
@Test @Test
public void testBadResource() throws Exception { public void testBadResource() throws Exception {
this.processor.setResources(new Resource[] {new ByteArrayResource( this.processor.setResources(new ByteArrayResource(
"foo: bar\ncd\nspam:\n foo: baz".getBytes())}); "foo: bar\ncd\nspam:\n foo: baz".getBytes()));
this.exception.expect(ScannerException.class); this.exception.expect(ScannerException.class);
this.exception.expectMessage("line 3, column 1"); this.exception.expectMessage("line 3, column 1");
this.processor.process(new MatchCallback() { this.processor.process(new MatchCallback() {
@ -98,8 +97,8 @@ public class YamlProcessorTests {
@Test @Test
public void mapConvertedToIndexedBeanReference() { public void mapConvertedToIndexedBeanReference() {
this.processor.setResources(new Resource[] {new ByteArrayResource( this.processor.setResources(new ByteArrayResource(
"foo: bar\nbar:\n spam: bucket".getBytes())}); "foo: bar\nbar:\n spam: bucket".getBytes()));
this.processor.process(new MatchCallback() { this.processor.process(new MatchCallback() {
@Override @Override
public void process(Properties properties, Map<String, Object> map) { public void process(Properties properties, Map<String, Object> map) {
@ -112,8 +111,8 @@ public class YamlProcessorTests {
@Test @Test
public void integerKeyBehaves() { public void integerKeyBehaves() {
this.processor.setResources(new Resource[] {new ByteArrayResource( this.processor.setResources(new ByteArrayResource(
"foo: bar\n1: bar".getBytes())}); "foo: bar\n1: bar".getBytes()));
this.processor.process(new MatchCallback() { this.processor.process(new MatchCallback() {
@Override @Override
public void process(Properties properties, Map<String, Object> map) { public void process(Properties properties, Map<String, Object> map) {
@ -125,8 +124,8 @@ public class YamlProcessorTests {
@Test @Test
public void integerDeepKeyBehaves() { public void integerDeepKeyBehaves() {
this.processor.setResources(new Resource[] {new ByteArrayResource( this.processor.setResources(new ByteArrayResource(
"foo:\n 1: bar".getBytes())}); "foo:\n 1: bar".getBytes()));
this.processor.process(new MatchCallback() { this.processor.process(new MatchCallback() {
@Override @Override

View File

@ -28,11 +28,11 @@ import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.rules.ExpectedException; import org.junit.rules.ExpectedException;
import org.yaml.snakeyaml.Yaml; import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.parser.ParserException;
import org.yaml.snakeyaml.scanner.ScannerException; import org.yaml.snakeyaml.scanner.ScannerException;
import org.springframework.core.io.ByteArrayResource; import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
/** /**
* Tests for {@link YamlPropertiesFactoryBean}. * Tests for {@link YamlPropertiesFactoryBean}.
@ -47,8 +47,8 @@ public class YamlPropertiesFactoryBeanTests {
@Test @Test
public void testLoadResource() throws Exception { public void testLoadResource() throws Exception {
YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean(); YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
factory.setResources(new Resource[] {new ByteArrayResource( factory.setResources(new ByteArrayResource(
"foo: bar\nspam:\n foo: baz".getBytes())}); "foo: bar\nspam:\n foo: baz".getBytes()));
Properties properties = factory.getObject(); Properties properties = factory.getObject();
assertThat(properties.getProperty("foo"), equalTo("bar")); assertThat(properties.getProperty("foo"), equalTo("bar"));
assertThat(properties.getProperty("spam.foo"), equalTo("baz")); assertThat(properties.getProperty("spam.foo"), equalTo("baz"));
@ -57,8 +57,8 @@ public class YamlPropertiesFactoryBeanTests {
@Test @Test
public void testBadResource() throws Exception { public void testBadResource() throws Exception {
YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean(); YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
factory.setResources(new Resource[] {new ByteArrayResource( factory.setResources(new ByteArrayResource(
"foo: bar\ncd\nspam:\n foo: baz".getBytes())}); "foo: bar\ncd\nspam:\n foo: baz".getBytes()));
this.exception.expect(ScannerException.class); this.exception.expect(ScannerException.class);
this.exception.expectMessage("line 3, column 1"); this.exception.expectMessage("line 3, column 1");
factory.getObject(); factory.getObject();
@ -67,9 +67,9 @@ public class YamlPropertiesFactoryBeanTests {
@Test @Test
public void testLoadResourcesWithOverride() throws Exception { public void testLoadResourcesWithOverride() throws Exception {
YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean(); YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
factory.setResources(new Resource[] { factory.setResources(
new ByteArrayResource("foo: bar\nspam:\n foo: baz".getBytes()), new ByteArrayResource("foo: bar\nspam:\n foo: baz".getBytes()),
new ByteArrayResource("foo:\n bar: spam".getBytes())}); new ByteArrayResource("foo:\n bar: spam".getBytes()));
Properties properties = factory.getObject(); Properties properties = factory.getObject();
assertThat(properties.getProperty("foo"), equalTo("bar")); assertThat(properties.getProperty("foo"), equalTo("bar"));
assertThat(properties.getProperty("spam.foo"), equalTo("baz")); assertThat(properties.getProperty("spam.foo"), equalTo("baz"));
@ -77,21 +77,20 @@ public class YamlPropertiesFactoryBeanTests {
} }
@Test @Test
@Ignore("We can't fail on duplicate keys because the Map is created by the YAML library")
public void testLoadResourcesWithInternalOverride() throws Exception { public void testLoadResourcesWithInternalOverride() throws Exception {
YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean(); YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
factory.setResources(new Resource[] {new ByteArrayResource( factory.setResources(new ByteArrayResource(
"foo: bar\nspam:\n foo: baz\nfoo: bucket".getBytes())}); "foo: bar\nspam:\n foo: baz\nfoo: bucket".getBytes()));
Properties properties = factory.getObject(); exception.expect(ParserException.class);
assertThat(properties.getProperty("foo"), equalTo("bar")); factory.getObject();
} }
@Test @Test
@Ignore("We can't fail on duplicate keys because the Map is created by the YAML library") @Ignore("We can't fail on duplicate keys because the Map is created by the YAML library")
public void testLoadResourcesWithNestedInternalOverride() throws Exception { public void testLoadResourcesWithNestedInternalOverride() throws Exception {
YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean(); YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
factory.setResources(new Resource[] {new ByteArrayResource( factory.setResources(new ByteArrayResource(
"foo:\n bar: spam\n foo: baz\nbreak: it\nfoo: bucket".getBytes())}); "foo:\n bar: spam\n foo: baz\nbreak: it\nfoo: bucket".getBytes()));
Properties properties = factory.getObject(); Properties properties = factory.getObject();
assertThat(properties.getProperty("foo.bar"), equalTo("spam")); assertThat(properties.getProperty("foo.bar"), equalTo("spam"));
} }
@ -99,8 +98,8 @@ public class YamlPropertiesFactoryBeanTests {
@Test @Test
public void testLoadResourceWithMultipleDocuments() throws Exception { public void testLoadResourceWithMultipleDocuments() throws Exception {
YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean(); YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
factory.setResources(new Resource[] {new ByteArrayResource( factory.setResources(new ByteArrayResource(
"foo: bar\nspam: baz\n---\nfoo: bag".getBytes())}); "foo: bar\nspam: baz\n---\nfoo: bag".getBytes()));
Properties properties = factory.getObject(); Properties properties = factory.getObject();
assertThat(properties.getProperty("foo"), equalTo("bag")); assertThat(properties.getProperty("foo"), equalTo("bag"));
assertThat(properties.getProperty("spam"), equalTo("baz")); assertThat(properties.getProperty("spam"), equalTo("baz"));
@ -109,8 +108,8 @@ public class YamlPropertiesFactoryBeanTests {
@Test @Test
public void testLoadResourceWithSelectedDocuments() throws Exception { public void testLoadResourceWithSelectedDocuments() throws Exception {
YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean(); YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
factory.setResources(new Resource[] {new ByteArrayResource( factory.setResources(new ByteArrayResource(
"foo: bar\nspam: baz\n---\nfoo: bag\nspam: bad".getBytes())}); "foo: bar\nspam: baz\n---\nfoo: bag\nspam: bad".getBytes()));
factory.setDocumentMatchers(new DocumentMatcher() { factory.setDocumentMatchers(new DocumentMatcher() {
@Override @Override
public MatchStatus matches(Properties properties) { public MatchStatus matches(Properties properties) {
@ -127,8 +126,8 @@ public class YamlPropertiesFactoryBeanTests {
public void testLoadResourceWithDefaultMatch() throws Exception { public void testLoadResourceWithDefaultMatch() throws Exception {
YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean(); YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
factory.setMatchDefault(true); factory.setMatchDefault(true);
factory.setResources(new Resource[] {new ByteArrayResource( factory.setResources(new ByteArrayResource(
"one: two\n---\nfoo: bar\nspam: baz\n---\nfoo: bag\nspam: bad".getBytes())}); "one: two\n---\nfoo: bar\nspam: baz\n---\nfoo: bag\nspam: bad".getBytes()));
factory.setDocumentMatchers(new DocumentMatcher() { factory.setDocumentMatchers(new DocumentMatcher() {
@Override @Override
public MatchStatus matches(Properties properties) { public MatchStatus matches(Properties properties) {
@ -149,8 +148,8 @@ public class YamlPropertiesFactoryBeanTests {
public void testLoadResourceWithoutDefaultMatch() throws Exception { public void testLoadResourceWithoutDefaultMatch() throws Exception {
YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean(); YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
factory.setMatchDefault(false); factory.setMatchDefault(false);
factory.setResources(new Resource[] {new ByteArrayResource( factory.setResources(new ByteArrayResource(
"one: two\n---\nfoo: bar\nspam: baz\n---\nfoo: bag\nspam: bad".getBytes())}); "one: two\n---\nfoo: bar\nspam: baz\n---\nfoo: bag\nspam: bad".getBytes()));
factory.setDocumentMatchers(new DocumentMatcher() { factory.setDocumentMatchers(new DocumentMatcher() {
@Override @Override
public MatchStatus matches(Properties properties) { public MatchStatus matches(Properties properties) {
@ -171,8 +170,8 @@ public class YamlPropertiesFactoryBeanTests {
public void testLoadResourceWithDefaultMatchSkippingMissedMatch() throws Exception { public void testLoadResourceWithDefaultMatchSkippingMissedMatch() throws Exception {
YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean(); YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
factory.setMatchDefault(true); factory.setMatchDefault(true);
factory.setResources(new Resource[] {new ByteArrayResource( factory.setResources(new ByteArrayResource(
"one: two\n---\nfoo: bag\nspam: bad\n---\nfoo: bar\nspam: baz".getBytes())}); "one: two\n---\nfoo: bag\nspam: bad\n---\nfoo: bar\nspam: baz".getBytes()));
factory.setDocumentMatchers(new DocumentMatcher() { factory.setDocumentMatchers(new DocumentMatcher() {
@Override @Override
public MatchStatus matches(Properties properties) { public MatchStatus matches(Properties properties) {
@ -193,7 +192,7 @@ public class YamlPropertiesFactoryBeanTests {
public void testLoadNonExistentResource() throws Exception { public void testLoadNonExistentResource() throws Exception {
YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean(); YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
factory.setResolutionMethod(ResolutionMethod.OVERRIDE_AND_IGNORE); factory.setResolutionMethod(ResolutionMethod.OVERRIDE_AND_IGNORE);
factory.setResources(new Resource[] {new ClassPathResource("no-such-file.yml")}); factory.setResources(new ClassPathResource("no-such-file.yml"));
Properties properties = factory.getObject(); Properties properties = factory.getObject();
assertThat(properties.size(), equalTo(0)); assertThat(properties.size(), equalTo(0));
} }
@ -201,8 +200,8 @@ public class YamlPropertiesFactoryBeanTests {
@Test @Test
public void testLoadNull() throws Exception { public void testLoadNull() throws Exception {
YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean(); YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
factory.setResources(new Resource[] {new ByteArrayResource("foo: bar\nspam:" factory.setResources(new ByteArrayResource("foo: bar\nspam:"
.getBytes())}); .getBytes()));
Properties properties = factory.getObject(); Properties properties = factory.getObject();
assertThat(properties.getProperty("foo"), equalTo("bar")); assertThat(properties.getProperty("foo"), equalTo("bar"));
assertThat(properties.getProperty("spam"), equalTo("")); assertThat(properties.getProperty("spam"), equalTo(""));
@ -211,8 +210,8 @@ public class YamlPropertiesFactoryBeanTests {
@Test @Test
public void testLoadArrayOfString() throws Exception { public void testLoadArrayOfString() throws Exception {
YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean(); YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
factory.setResources(new Resource[] {new ByteArrayResource("foo:\n- bar\n- baz" factory.setResources(new ByteArrayResource("foo:\n- bar\n- baz"
.getBytes())}); .getBytes()));
Properties properties = factory.getObject(); Properties properties = factory.getObject();
assertThat(properties.getProperty("foo[0]"), equalTo("bar")); assertThat(properties.getProperty("foo[0]"), equalTo("bar"));
assertThat(properties.getProperty("foo[1]"), equalTo("baz")); assertThat(properties.getProperty("foo[1]"), equalTo("baz"));
@ -221,10 +220,10 @@ public class YamlPropertiesFactoryBeanTests {
@Test @Test
public void testLoadArrayOfObject() throws Exception { public void testLoadArrayOfObject() throws Exception {
YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean(); YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
factory.setResources(new Resource[] {new ByteArrayResource( factory.setResources(new ByteArrayResource(
"foo:\n- bar:\n spam: crap\n- baz\n- one: two\n three: four" "foo:\n- bar:\n spam: crap\n- baz\n- one: two\n three: four"
.getBytes() .getBytes()
)}); ));
Properties properties = factory.getObject(); Properties properties = factory.getObject();
assertThat(properties.getProperty("foo[0].bar.spam"), equalTo("crap")); assertThat(properties.getProperty("foo[0].bar.spam"), equalTo("crap"));
assertThat(properties.getProperty("foo[1]"), equalTo("baz")); assertThat(properties.getProperty("foo[1]"), equalTo("baz"));