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/25187/head
Sam Brannen 5 years ago
parent
commit
008d011d37
  1. 65
      spring-beans/src/main/java/org/springframework/beans/factory/config/YamlProcessor.java
  2. 65
      spring-beans/src/test/java/org/springframework/beans/factory/config/YamlProcessorTests.java

65
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(), 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 { @@ -388,4 +428,21 @@ 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 {
@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);
}
}
}

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

@ -1,5 +1,5 @@ @@ -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 @@ @@ -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.*; @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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()));
}
}

Loading…
Cancel
Save