Skip to content

Commit

Permalink
Make use of custom types configurable in YamlProcessor
Browse files Browse the repository at this point in the history
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
  • Loading branch information
sbrannen committed May 29, 2020
1 parent 3201671 commit 7682575
Show file tree
Hide file tree
Showing 2 changed files with 126 additions and 22 deletions.
@@ -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.
Expand All @@ -25,17 +25,23 @@
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;

/**
Expand All @@ -45,6 +51,7 @@
*
* @author Dave Syer
* @author Juergen Hoeller
* @author Sam Brannen
* @since 4.1
*/
public abstract class YamlProcessor {
Expand All @@ -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
Expand Down Expand Up @@ -117,6 +126,27 @@ public void setResources(Resource... resources) {
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
Expand All @@ -142,12 +172,22 @@ protected void process(MatchCallback callback) {
* 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) {
Expand Down Expand Up @@ -388,4 +428,26 @@ public enum ResolutionMethod {
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);
}
}

}
@@ -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.
Expand All @@ -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;

Expand All @@ -29,6 +31,7 @@
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}.
Expand All @@ -37,14 +40,14 @@
* @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");
Expand All @@ -59,48 +62,48 @@ public void arrayConvertedToIndexedBeanReference() {
}

@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);
});
}

@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);
});
}

@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);
Expand All @@ -109,8 +112,8 @@ public void integerDeepKeyBehaves() {

@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);
Expand All @@ -134,4 +137,43 @@ public void flattenedMapIsSameAsPropertiesButOrdered() {
});
}

@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()));
}

}

0 comments on commit 7682575

Please sign in to comment.