Skip to content

Commit 7682575

Browse files
committed
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
1 parent 3201671 commit 7682575

File tree

2 files changed

+126
-22
lines changed

2 files changed

+126
-22
lines changed

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

Lines changed: 66 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -25,17 +25,23 @@
2525
import java.util.List;
2626
import java.util.Map;
2727
import java.util.Properties;
28+
import java.util.Set;
29+
import java.util.stream.Collectors;
2830

2931
import org.apache.commons.logging.Log;
3032
import org.apache.commons.logging.LogFactory;
33+
import org.yaml.snakeyaml.DumperOptions;
3134
import org.yaml.snakeyaml.LoaderOptions;
3235
import org.yaml.snakeyaml.Yaml;
36+
import org.yaml.snakeyaml.constructor.Constructor;
3337
import org.yaml.snakeyaml.reader.UnicodeReader;
38+
import org.yaml.snakeyaml.representer.Representer;
3439

3540
import org.springframework.core.CollectionFactory;
3641
import org.springframework.core.io.Resource;
3742
import org.springframework.lang.Nullable;
3843
import org.springframework.util.Assert;
44+
import org.springframework.util.ObjectUtils;
3945
import org.springframework.util.StringUtils;
4046

4147
/**
@@ -45,6 +51,7 @@
4551
*
4652
* @author Dave Syer
4753
* @author Juergen Hoeller
54+
* @author Sam Brannen
4855
* @since 4.1
4956
*/
5057
public abstract class YamlProcessor {
@@ -59,6 +66,8 @@ public abstract class YamlProcessor {
5966

6067
private boolean matchDefault = true;
6168

69+
private Set<String> supportedTypes = Collections.emptySet();
70+
6271

6372
/**
6473
* A map of document matchers allowing callers to selectively use only
@@ -117,6 +126,27 @@ public void setResources(Resource... resources) {
117126
this.resources = resources;
118127
}
119128

129+
/**
130+
* Set the supported types that can be loaded from YAML documents.
131+
* <p>If no supported types are configured, all types encountered in YAML
132+
* documents will be supported. If an unsupported type is encountered, an
133+
* {@link IllegalStateException} will be thrown when the corresponding YAML
134+
* node is processed.
135+
* @param supportedTypes the supported types, or an empty array to clear the
136+
* supported types
137+
* @since 5.1.16
138+
* @see #createYaml()
139+
*/
140+
public void setSupportedTypes(Class<?>... supportedTypes) {
141+
if (ObjectUtils.isEmpty(supportedTypes)) {
142+
this.supportedTypes = Collections.emptySet();
143+
}
144+
else {
145+
Assert.noNullElements(supportedTypes, "'supportedTypes' must not contain null elements");
146+
this.supportedTypes = Arrays.stream(supportedTypes).map(Class::getName)
147+
.collect(Collectors.collectingAndThen(Collectors.toSet(), Collections::unmodifiableSet));
148+
}
149+
}
120150

121151
/**
122152
* Provide an opportunity for subclasses to process the Yaml parsed from the supplied
@@ -142,12 +172,22 @@ protected void process(MatchCallback callback) {
142172
* Create the {@link Yaml} instance to use.
143173
* <p>The default implementation sets the "allowDuplicateKeys" flag to {@code false},
144174
* enabling built-in duplicate key handling in SnakeYAML 1.18+.
175+
* <p>As of Spring Framework 5.1.16, if custom {@linkplain #setSupportedTypes
176+
* supported types} have been configured, the default implementation creates
177+
* a {@code Yaml} instance that filters out unsupported types encountered in
178+
* YAML documents. If an unsupported type is encountered, an
179+
* {@link IllegalStateException} will be thrown when the node is processed.
145180
* @see LoaderOptions#setAllowDuplicateKeys(boolean)
146181
*/
147182
protected Yaml createYaml() {
148-
LoaderOptions options = new LoaderOptions();
149-
options.setAllowDuplicateKeys(false);
150-
return new Yaml(options);
183+
LoaderOptions loaderOptions = new LoaderOptions();
184+
loaderOptions.setAllowDuplicateKeys(false);
185+
186+
if (!this.supportedTypes.isEmpty()) {
187+
return new Yaml(new FilteringConstructor(loaderOptions), new Representer(),
188+
new DumperOptions(), loaderOptions);
189+
}
190+
return new Yaml(loaderOptions);
151191
}
152192

153193
private boolean process(MatchCallback callback, Yaml yaml, Resource resource) {
@@ -388,4 +428,26 @@ public enum ResolutionMethod {
388428
FIRST_FOUND
389429
}
390430

431+
432+
/**
433+
* {@link Constructor} that supports filtering of unsupported types.
434+
* <p>If an unsupported type is encountered in a YAML document, an
435+
* {@link IllegalStateException} will be thrown from {@link #getClassForName(String)}.
436+
* @since 5.1.16
437+
*/
438+
private class FilteringConstructor extends Constructor {
439+
440+
FilteringConstructor(LoaderOptions loaderOptions) {
441+
super(loaderOptions);
442+
}
443+
444+
445+
@Override
446+
protected Class<?> getClassForName(String name) throws ClassNotFoundException {
447+
Assert.state(YamlProcessor.this.supportedTypes.contains(name),
448+
() -> "Unsupported type encountered in YAML document: " + name);
449+
return super.getClassForName(name);
450+
}
451+
}
452+
391453
}

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

Lines changed: 60 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,11 +16,13 @@
1616

1717
package org.springframework.beans.factory.config;
1818

19+
import java.net.URL;
1920
import java.util.LinkedHashMap;
2021
import java.util.List;
2122
import java.util.Map;
2223

2324
import org.junit.jupiter.api.Test;
25+
import org.yaml.snakeyaml.constructor.ConstructorException;
2426
import org.yaml.snakeyaml.parser.ParserException;
2527
import org.yaml.snakeyaml.scanner.ScannerException;
2628

@@ -29,6 +31,7 @@
2931
import static java.util.stream.Collectors.toList;
3032
import static org.assertj.core.api.Assertions.assertThat;
3133
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
34+
import static org.assertj.core.api.Assertions.entry;
3235

3336
/**
3437
* Tests for {@link YamlProcessor}.
@@ -37,14 +40,14 @@
3740
* @author Juergen Hoeller
3841
* @author Sam Brannen
3942
*/
40-
public class YamlProcessorTests {
43+
class YamlProcessorTests {
4144

4245
private final YamlProcessor processor = new YamlProcessor() {};
4346

4447

4548
@Test
46-
public void arrayConvertedToIndexedBeanReference() {
47-
this.processor.setResources(new ByteArrayResource("foo: bar\nbar: [1,2,3]".getBytes()));
49+
void arrayConvertedToIndexedBeanReference() {
50+
setYaml("foo: bar\nbar: [1,2,3]");
4851
this.processor.process((properties, map) -> {
4952
assertThat(properties.size()).isEqualTo(4);
5053
assertThat(properties.get("foo")).isEqualTo("bar");
@@ -59,48 +62,48 @@ public void arrayConvertedToIndexedBeanReference() {
5962
}
6063

6164
@Test
62-
public void stringResource() {
63-
this.processor.setResources(new ByteArrayResource("foo # a document that is a literal".getBytes()));
65+
void stringResource() {
66+
setYaml("foo # a document that is a literal");
6467
this.processor.process((properties, map) -> assertThat(map.get("document")).isEqualTo("foo"));
6568
}
6669

6770
@Test
68-
public void badDocumentStart() {
69-
this.processor.setResources(new ByteArrayResource("foo # a document\nbar: baz".getBytes()));
71+
void badDocumentStart() {
72+
setYaml("foo # a document\nbar: baz");
7073
assertThatExceptionOfType(ParserException.class)
7174
.isThrownBy(() -> this.processor.process((properties, map) -> {}))
7275
.withMessageContaining("line 2, column 1");
7376
}
7477

7578
@Test
76-
public void badResource() {
77-
this.processor.setResources(new ByteArrayResource("foo: bar\ncd\nspam:\n foo: baz".getBytes()));
79+
void badResource() {
80+
setYaml("foo: bar\ncd\nspam:\n foo: baz");
7881
assertThatExceptionOfType(ScannerException.class)
7982
.isThrownBy(() -> this.processor.process((properties, map) -> {}))
8083
.withMessageContaining("line 3, column 1");
8184
}
8285

8386
@Test
84-
public void mapConvertedToIndexedBeanReference() {
85-
this.processor.setResources(new ByteArrayResource("foo: bar\nbar:\n spam: bucket".getBytes()));
87+
void mapConvertedToIndexedBeanReference() {
88+
setYaml("foo: bar\nbar:\n spam: bucket");
8689
this.processor.process((properties, map) -> {
8790
assertThat(properties.get("bar.spam")).isEqualTo("bucket");
8891
assertThat(properties).hasSize(2);
8992
});
9093
}
9194

9295
@Test
93-
public void integerKeyBehaves() {
94-
this.processor.setResources(new ByteArrayResource("foo: bar\n1: bar".getBytes()));
96+
void integerKeyBehaves() {
97+
setYaml("foo: bar\n1: bar");
9598
this.processor.process((properties, map) -> {
9699
assertThat(properties.get("[1]")).isEqualTo("bar");
97100
assertThat(properties).hasSize(2);
98101
});
99102
}
100103

101104
@Test
102-
public void integerDeepKeyBehaves() {
103-
this.processor.setResources(new ByteArrayResource("foo:\n 1: bar".getBytes()));
105+
void integerDeepKeyBehaves() {
106+
setYaml("foo:\n 1: bar");
104107
this.processor.process((properties, map) -> {
105108
assertThat(properties.get("foo[1]")).isEqualTo("bar");
106109
assertThat(properties).hasSize(1);
@@ -109,8 +112,8 @@ public void integerDeepKeyBehaves() {
109112

110113
@Test
111114
@SuppressWarnings("unchecked")
112-
public void flattenedMapIsSameAsPropertiesButOrdered() {
113-
this.processor.setResources(new ByteArrayResource("cat: dog\nfoo: bar\nbar:\n spam: bucket".getBytes()));
115+
void flattenedMapIsSameAsPropertiesButOrdered() {
116+
setYaml("cat: dog\nfoo: bar\nbar:\n spam: bucket");
114117
this.processor.process((properties, map) -> {
115118
Map<String, Object> flattenedMap = processor.getFlattenedMap(map);
116119
assertThat(flattenedMap).isInstanceOf(LinkedHashMap.class);
@@ -134,4 +137,43 @@ public void flattenedMapIsSameAsPropertiesButOrdered() {
134137
});
135138
}
136139

140+
@Test
141+
void customTypeSupportedByDefault() throws Exception {
142+
URL url = new URL("https://localhost:9000/");
143+
setYaml("value: !!java.net.URL [\"" + url + "\"]");
144+
145+
this.processor.process((properties, map) -> {
146+
assertThat(properties).containsExactly(entry("value", url));
147+
assertThat(map).containsExactly(entry("value", url));
148+
});
149+
}
150+
151+
@Test
152+
void customTypesSupportedDueToExplicitConfiguration() throws Exception {
153+
this.processor.setSupportedTypes(URL.class, String.class);
154+
155+
URL url = new URL("https://localhost:9000/");
156+
setYaml("value: !!java.net.URL [!!java.lang.String [\"" + url + "\"]]");
157+
158+
this.processor.process((properties, map) -> {
159+
assertThat(properties).containsExactly(entry("value", url));
160+
assertThat(map).containsExactly(entry("value", url));
161+
});
162+
}
163+
164+
@Test
165+
void customTypeNotSupportedDueToExplicitConfiguration() {
166+
this.processor.setSupportedTypes(List.class);
167+
168+
setYaml("value: !!java.net.URL [\"https://localhost:9000/\"]");
169+
170+
assertThatExceptionOfType(ConstructorException.class)
171+
.isThrownBy(() -> this.processor.process((properties, map) -> {}))
172+
.withMessageContaining("Unsupported type encountered in YAML document: java.net.URL");
173+
}
174+
175+
private void setYaml(String yaml) {
176+
this.processor.setResources(new ByteArrayResource(yaml.getBytes()));
177+
}
178+
137179
}

0 commit comments

Comments
 (0)