From 768257567d75b3f0142770d0a81bffdd33f062f1 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Fri, 29 May 2020 13:32:52 +0200 Subject: [PATCH] Make use of custom types configurable in YamlProcessor Prior to this commit, there was no easy way to restrict what types could be loaded from a YAML document in subclasses of YamlProcessor such as YamlPropertiesFactoryBean and YamlMapFactoryBean. This commit introduces a setSupportedTypes(Class...) method in YamlProcessor in order to address this. If no supported types are configured, all types encountered in YAML documents will be supported. If an unsupported type is encountered, an IllegalStateException will be thrown when the corresponding YAML node is processed. Closes gh-25152 --- .../beans/factory/config/YamlProcessor.java | 70 ++++++++++++++++- .../factory/config/YamlProcessorTests.java | 78 ++++++++++++++----- 2 files changed, 126 insertions(+), 22 deletions(-) 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())); + } + }