Browse Source

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
pull/25758/head
Sam Brannen 6 years ago
parent
commit
768257567d
  1. 70
      spring-beans/src/main/java/org/springframework/beans/factory/config/YamlProcessor.java
  2. 78
      spring-beans/src/test/java/org/springframework/beans/factory/config/YamlProcessorTests.java

70
spring-beans/src/main/java/org/springframework/beans/factory/config/YamlProcessor.java

@ -1,5 +1,5 @@ @@ -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; @@ -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; @@ -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 { @@ -59,6 +66,8 @@ public abstract class YamlProcessor {
private boolean matchDefault = true;
private Set<String> supportedTypes = Collections.emptySet();
/**
* A map of document matchers allowing callers to selectively use only
@ -117,6 +126,27 @@ public abstract class YamlProcessor { @@ -117,6 +126,27 @@ public abstract class YamlProcessor {
this.resources = resources;
}
/**
* Set the supported types that can be loaded from YAML documents.
* <p>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 { @@ -142,12 +172,22 @@ public abstract class YamlProcessor {
* Create the {@link Yaml} instance to use.
* <p>The default implementation sets the "allowDuplicateKeys" flag to {@code false},
* enabling built-in duplicate key handling in SnakeYAML 1.18+.
* <p>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 { @@ -388,4 +428,26 @@ public abstract class YamlProcessor {
FIRST_FOUND
}
/**
* {@link Constructor} that supports filtering of unsupported types.
* <p>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);
}
}
}

78
spring-beans/src/test/java/org/springframework/beans/factory/config/YamlProcessorTests.java

@ -1,5 +1,5 @@ @@ -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 @@ @@ -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; @@ -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; @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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<String, Object> flattenedMap = processor.getFlattenedMap(map);
assertThat(flattenedMap).isInstanceOf(LinkedHashMap.class);
@ -134,4 +137,43 @@ public class YamlProcessorTests { @@ -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()));
}
}

Loading…
Cancel
Save