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 index 340dee2dca..badf7685b8 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 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. @@ -25,17 +25,23 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Properties; +import java.util.Set; +import java.util.stream.Collectors; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.yaml.snakeyaml.DumperOptions; import org.yaml.snakeyaml.LoaderOptions; import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.Constructor; import org.yaml.snakeyaml.reader.UnicodeReader; +import org.yaml.snakeyaml.representer.Representer; import org.springframework.core.CollectionFactory; import org.springframework.core.io.Resource; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; /** @@ -45,6 +51,7 @@ import org.springframework.util.StringUtils; * * @author Dave Syer * @author Juergen Hoeller + * @author Sam Brannen * @since 4.1 */ public abstract class YamlProcessor { @@ -59,6 +66,8 @@ public abstract class YamlProcessor { private boolean matchDefault = true; + private Set supportedTypes = Collections.emptySet(); + /** * A map of document matchers allowing callers to selectively use only @@ -117,6 +126,27 @@ public abstract class YamlProcessor { this.resources = resources; } + /** + * Set the supported types that can be loaded from YAML documents. + *

If no supported types are configured, all types encountered in YAML + * documents will be supported. If an unsupported type is encountered, an + * {@link IllegalStateException} will be thrown when the corresponding YAML + * node is processed. + * @param supportedTypes the supported types, or an empty array to clear the + * supported types + * @since 5.1.16 + * @see #createYaml() + */ + public void setSupportedTypes(Class... supportedTypes) { + if (ObjectUtils.isEmpty(supportedTypes)) { + this.supportedTypes = Collections.emptySet(); + } + else { + Assert.noNullElements(supportedTypes, "'supportedTypes' must not contain null elements"); + this.supportedTypes = Arrays.stream(supportedTypes).map(Class::getName) + .collect(Collectors.collectingAndThen(Collectors.toSet(), Collections::unmodifiableSet)); + } + } /** * Provide an opportunity for subclasses to process the Yaml parsed from the supplied @@ -142,12 +172,22 @@ public abstract class YamlProcessor { * Create the {@link Yaml} instance to use. *

The default implementation sets the "allowDuplicateKeys" flag to {@code false}, * enabling built-in duplicate key handling in SnakeYAML 1.18+. + *

As of Spring Framework 5.1.16, if custom {@linkplain #setSupportedTypes + * supported types} have been configured, the default implementation creates + * a {@code Yaml} instance that filters out unsupported types encountered in + * YAML documents. If an unsupported type is encountered, an + * {@link IllegalStateException} will be thrown when the node is processed. * @see LoaderOptions#setAllowDuplicateKeys(boolean) */ protected Yaml createYaml() { - LoaderOptions options = new LoaderOptions(); - options.setAllowDuplicateKeys(false); - return new Yaml(options); + LoaderOptions loaderOptions = new LoaderOptions(); + loaderOptions.setAllowDuplicateKeys(false); + + if (!this.supportedTypes.isEmpty()) { + return new Yaml(new FilteringConstructor(), new Representer(), + new DumperOptions(), loaderOptions); + } + return new Yaml(loaderOptions); } private boolean process(MatchCallback callback, Yaml yaml, Resource resource) { @@ -388,4 +428,21 @@ public abstract class YamlProcessor { FIRST_FOUND } + + /** + * {@link Constructor} that supports filtering of unsupported types. + *

If an unsupported type is encountered in a YAML document, an + * {@link IllegalStateException} will be thrown from {@link #getClassForName(String)}. + * @since 5.1.16 + */ + private class FilteringConstructor extends Constructor { + + @Override + protected Class getClassForName(String name) throws ClassNotFoundException { + Assert.state(YamlProcessor.this.supportedTypes.contains(name), + () -> "Unsupported type encountered in YAML document: " + name); + return super.getClassForName(name); + } + } + } 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 index 92c266c0e6..8cc38a2729 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2020 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. @@ -16,12 +16,15 @@ package org.springframework.beans.factory.config; +import java.net.URL; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; +import org.yaml.snakeyaml.constructor.ConstructorException; import org.yaml.snakeyaml.parser.ParserException; import org.yaml.snakeyaml.scanner.ScannerException; @@ -34,6 +37,7 @@ import static org.junit.Assert.*; * * @author Dave Syer * @author Juergen Hoeller + * @author Sam Brannen */ public class YamlProcessorTests { @@ -45,7 +49,7 @@ public class YamlProcessorTests { @Test public void arrayConvertedToIndexedBeanReference() { - this.processor.setResources(new ByteArrayResource("foo: bar\nbar: [1,2,3]".getBytes())); + setYaml("foo: bar\nbar: [1,2,3]"); this.processor.process((properties, map) -> { assertEquals(4, properties.size()); assertEquals("bar", properties.get("foo")); @@ -61,13 +65,13 @@ public class YamlProcessorTests { @Test public void testStringResource() { - this.processor.setResources(new ByteArrayResource("foo # a document that is a literal".getBytes())); + setYaml("foo # a document that is a literal"); this.processor.process((properties, map) -> assertEquals("foo", map.get("document"))); } @Test public void testBadDocumentStart() { - this.processor.setResources(new ByteArrayResource("foo # a document\nbar: baz".getBytes())); + setYaml("foo # a document\nbar: baz"); this.exception.expect(ParserException.class); this.exception.expectMessage("line 2, column 1"); this.processor.process((properties, map) -> {}); @@ -75,7 +79,7 @@ public class YamlProcessorTests { @Test public void testBadResource() { - this.processor.setResources(new ByteArrayResource("foo: bar\ncd\nspam:\n foo: baz".getBytes())); + setYaml("foo: bar\ncd\nspam:\n foo: baz"); this.exception.expect(ScannerException.class); this.exception.expectMessage("line 3, column 1"); this.processor.process((properties, map) -> {}); @@ -83,7 +87,7 @@ public class YamlProcessorTests { @Test public void mapConvertedToIndexedBeanReference() { - this.processor.setResources(new ByteArrayResource("foo: bar\nbar:\n spam: bucket".getBytes())); + setYaml("foo: bar\nbar:\n spam: bucket"); this.processor.process((properties, map) -> { assertEquals("bucket", properties.get("bar.spam")); assertEquals(2, properties.size()); @@ -92,7 +96,7 @@ public class YamlProcessorTests { @Test public void integerKeyBehaves() { - this.processor.setResources(new ByteArrayResource("foo: bar\n1: bar".getBytes())); + setYaml("foo: bar\n1: bar"); this.processor.process((properties, map) -> { assertEquals("bar", properties.get("[1]")); assertEquals(2, properties.size()); @@ -101,7 +105,7 @@ public class YamlProcessorTests { @Test public void integerDeepKeyBehaves() { - this.processor.setResources(new ByteArrayResource("foo:\n 1: bar".getBytes())); + setYaml("foo:\n 1: bar"); this.processor.process((properties, map) -> { assertEquals("bar", properties.get("foo[1]")); assertEquals(1, properties.size()); @@ -111,7 +115,7 @@ public class YamlProcessorTests { @Test @SuppressWarnings("unchecked") public void flattenedMapIsSameAsPropertiesButOrdered() { - this.processor.setResources(new ByteArrayResource("foo: bar\nbar:\n spam: bucket".getBytes())); + setYaml("foo: bar\nbar:\n spam: bucket"); this.processor.process((properties, map) -> { assertEquals("bucket", properties.get("bar.spam")); assertEquals(2, properties.size()); @@ -124,4 +128,47 @@ public class YamlProcessorTests { }); } + @Test + public void customTypeSupportedByDefault() throws Exception { + URL url = new URL("https://localhost:9000/"); + setYaml("value: !!java.net.URL [\"" + url + "\"]"); + + this.processor.process((properties, map) -> { + assertEquals(1, properties.size()); + assertEquals(1, map.size()); + assertEquals(url, properties.get("value")); + assertEquals(url, map.get("value")); + }); + } + + @Test + public void customTypesSupportedDueToExplicitConfiguration() throws Exception { + this.processor.setSupportedTypes(URL.class, String.class); + + URL url = new URL("https://localhost:9000/"); + setYaml("value: !!java.net.URL [!!java.lang.String [\"" + url + "\"]]"); + + this.processor.process((properties, map) -> { + assertEquals(1, properties.size()); + assertEquals(1, map.size()); + assertEquals(url, properties.get("value")); + assertEquals(url, map.get("value")); + }); + } + + @Test + public void customTypeNotSupportedDueToExplicitConfiguration() { + this.processor.setSupportedTypes(List.class); + + setYaml("value: !!java.net.URL [\"https://localhost:9000/\"]"); + + this.exception.expect(ConstructorException.class); + this.exception.expectMessage("Unsupported type encountered in YAML document: java.net.URL"); + this.processor.process((properties, map) -> {}); + } + + private void setYaml(String yaml) { + this.processor.setResources(new ByteArrayResource(yaml.getBytes())); + } + }