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 340dee2dcac..1b4e2bdeeaa 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(loaderOptions), new Representer(), + new DumperOptions(), loaderOptions); + } + return new Yaml(loaderOptions); } private boolean process(MatchCallback callback, Yaml yaml, Resource resource) { @@ -388,4 +428,26 @@ 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 { + + FilteringConstructor(LoaderOptions loaderOptions) { + super(loaderOptions); + } + + + @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 0bacf216258..60fbd272ce2 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-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. @@ -16,11 +16,13 @@ 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.jupiter.api.Test; +import org.yaml.snakeyaml.constructor.ConstructorException; import org.yaml.snakeyaml.parser.ParserException; import org.yaml.snakeyaml.scanner.ScannerException; @@ -29,6 +31,7 @@ import org.springframework.core.io.ByteArrayResource; import static java.util.stream.Collectors.toList; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.entry; /** * Tests for {@link YamlProcessor}. @@ -37,14 +40,14 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; * @author Juergen Hoeller * @author Sam Brannen */ -public class YamlProcessorTests { +class YamlProcessorTests { private final YamlProcessor processor = new YamlProcessor() {}; @Test - public void arrayConvertedToIndexedBeanReference() { - this.processor.setResources(new ByteArrayResource("foo: bar\nbar: [1,2,3]".getBytes())); + void arrayConvertedToIndexedBeanReference() { + setYaml("foo: bar\nbar: [1,2,3]"); this.processor.process((properties, map) -> { assertThat(properties.size()).isEqualTo(4); assertThat(properties.get("foo")).isEqualTo("bar"); @@ -59,30 +62,30 @@ public class YamlProcessorTests { } @Test - public void stringResource() { - this.processor.setResources(new ByteArrayResource("foo # a document that is a literal".getBytes())); + void stringResource() { + setYaml("foo # a document that is a literal"); this.processor.process((properties, map) -> assertThat(map.get("document")).isEqualTo("foo")); } @Test - public void badDocumentStart() { - this.processor.setResources(new ByteArrayResource("foo # a document\nbar: baz".getBytes())); + void badDocumentStart() { + setYaml("foo # a document\nbar: baz"); assertThatExceptionOfType(ParserException.class) .isThrownBy(() -> this.processor.process((properties, map) -> {})) .withMessageContaining("line 2, column 1"); } @Test - public void badResource() { - this.processor.setResources(new ByteArrayResource("foo: bar\ncd\nspam:\n foo: baz".getBytes())); + void badResource() { + setYaml("foo: bar\ncd\nspam:\n foo: baz"); assertThatExceptionOfType(ScannerException.class) .isThrownBy(() -> this.processor.process((properties, map) -> {})) .withMessageContaining("line 3, column 1"); } @Test - public void mapConvertedToIndexedBeanReference() { - this.processor.setResources(new ByteArrayResource("foo: bar\nbar:\n spam: bucket".getBytes())); + void mapConvertedToIndexedBeanReference() { + setYaml("foo: bar\nbar:\n spam: bucket"); this.processor.process((properties, map) -> { assertThat(properties.get("bar.spam")).isEqualTo("bucket"); assertThat(properties).hasSize(2); @@ -90,8 +93,8 @@ public class YamlProcessorTests { } @Test - public void integerKeyBehaves() { - this.processor.setResources(new ByteArrayResource("foo: bar\n1: bar".getBytes())); + void integerKeyBehaves() { + setYaml("foo: bar\n1: bar"); this.processor.process((properties, map) -> { assertThat(properties.get("[1]")).isEqualTo("bar"); assertThat(properties).hasSize(2); @@ -99,8 +102,8 @@ public class YamlProcessorTests { } @Test - public void integerDeepKeyBehaves() { - this.processor.setResources(new ByteArrayResource("foo:\n 1: bar".getBytes())); + void integerDeepKeyBehaves() { + setYaml("foo:\n 1: bar"); this.processor.process((properties, map) -> { assertThat(properties.get("foo[1]")).isEqualTo("bar"); assertThat(properties).hasSize(1); @@ -109,8 +112,8 @@ public class YamlProcessorTests { @Test @SuppressWarnings("unchecked") - public void flattenedMapIsSameAsPropertiesButOrdered() { - this.processor.setResources(new ByteArrayResource("cat: dog\nfoo: bar\nbar:\n spam: bucket".getBytes())); + void flattenedMapIsSameAsPropertiesButOrdered() { + setYaml("cat: dog\nfoo: bar\nbar:\n spam: bucket"); this.processor.process((properties, map) -> { Map flattenedMap = processor.getFlattenedMap(map); assertThat(flattenedMap).isInstanceOf(LinkedHashMap.class); @@ -134,4 +137,43 @@ public class YamlProcessorTests { }); } + @Test + void customTypeSupportedByDefault() throws Exception { + URL url = new URL("https://localhost:9000/"); + setYaml("value: !!java.net.URL [\"" + url + "\"]"); + + this.processor.process((properties, map) -> { + assertThat(properties).containsExactly(entry("value", url)); + assertThat(map).containsExactly(entry("value", url)); + }); + } + + @Test + 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) -> { + assertThat(properties).containsExactly(entry("value", url)); + assertThat(map).containsExactly(entry("value", url)); + }); + } + + @Test + void customTypeNotSupportedDueToExplicitConfiguration() { + this.processor.setSupportedTypes(List.class); + + setYaml("value: !!java.net.URL [\"https://localhost:9000/\"]"); + + assertThatExceptionOfType(ConstructorException.class) + .isThrownBy(() -> this.processor.process((properties, map) -> {})) + .withMessageContaining("Unsupported type encountered in YAML document: java.net.URL"); + } + + private void setYaml(String yaml) { + this.processor.setResources(new ByteArrayResource(yaml.getBytes())); + } + }