Skip to content

Commit 008d011

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 16ab43d commit 008d011

File tree

2 files changed

+117
-13
lines changed

2 files changed

+117
-13
lines changed

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

Lines changed: 61 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(), 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,21 @@ 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+
@Override
441+
protected Class<?> getClassForName(String name) throws ClassNotFoundException {
442+
Assert.state(YamlProcessor.this.supportedTypes.contains(name),
443+
() -> "Unsupported type encountered in YAML document: " + name);
444+
return super.getClassForName(name);
445+
}
446+
}
447+
391448
}

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

Lines changed: 56 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 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,12 +16,15 @@
1616

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

19+
import java.net.URL;
1920
import java.util.LinkedHashMap;
21+
import java.util.List;
2022
import java.util.Map;
2123

2224
import org.junit.Rule;
2325
import org.junit.Test;
2426
import org.junit.rules.ExpectedException;
27+
import org.yaml.snakeyaml.constructor.ConstructorException;
2528
import org.yaml.snakeyaml.parser.ParserException;
2629
import org.yaml.snakeyaml.scanner.ScannerException;
2730

@@ -34,6 +37,7 @@
3437
*
3538
* @author Dave Syer
3639
* @author Juergen Hoeller
40+
* @author Sam Brannen
3741
*/
3842
public class YamlProcessorTests {
3943

@@ -45,7 +49,7 @@ public class YamlProcessorTests {
4549

4650
@Test
4751
public void arrayConvertedToIndexedBeanReference() {
48-
this.processor.setResources(new ByteArrayResource("foo: bar\nbar: [1,2,3]".getBytes()));
52+
setYaml("foo: bar\nbar: [1,2,3]");
4953
this.processor.process((properties, map) -> {
5054
assertEquals(4, properties.size());
5155
assertEquals("bar", properties.get("foo"));
@@ -61,29 +65,29 @@ public void arrayConvertedToIndexedBeanReference() {
6165

6266
@Test
6367
public void testStringResource() {
64-
this.processor.setResources(new ByteArrayResource("foo # a document that is a literal".getBytes()));
68+
setYaml("foo # a document that is a literal");
6569
this.processor.process((properties, map) -> assertEquals("foo", map.get("document")));
6670
}
6771

6872
@Test
6973
public void testBadDocumentStart() {
70-
this.processor.setResources(new ByteArrayResource("foo # a document\nbar: baz".getBytes()));
74+
setYaml("foo # a document\nbar: baz");
7175
this.exception.expect(ParserException.class);
7276
this.exception.expectMessage("line 2, column 1");
7377
this.processor.process((properties, map) -> {});
7478
}
7579

7680
@Test
7781
public void testBadResource() {
78-
this.processor.setResources(new ByteArrayResource("foo: bar\ncd\nspam:\n foo: baz".getBytes()));
82+
setYaml("foo: bar\ncd\nspam:\n foo: baz");
7983
this.exception.expect(ScannerException.class);
8084
this.exception.expectMessage("line 3, column 1");
8185
this.processor.process((properties, map) -> {});
8286
}
8387

8488
@Test
8589
public void mapConvertedToIndexedBeanReference() {
86-
this.processor.setResources(new ByteArrayResource("foo: bar\nbar:\n spam: bucket".getBytes()));
90+
setYaml("foo: bar\nbar:\n spam: bucket");
8791
this.processor.process((properties, map) -> {
8892
assertEquals("bucket", properties.get("bar.spam"));
8993
assertEquals(2, properties.size());
@@ -92,7 +96,7 @@ public void mapConvertedToIndexedBeanReference() {
9296

9397
@Test
9498
public void integerKeyBehaves() {
95-
this.processor.setResources(new ByteArrayResource("foo: bar\n1: bar".getBytes()));
99+
setYaml("foo: bar\n1: bar");
96100
this.processor.process((properties, map) -> {
97101
assertEquals("bar", properties.get("[1]"));
98102
assertEquals(2, properties.size());
@@ -101,7 +105,7 @@ public void integerKeyBehaves() {
101105

102106
@Test
103107
public void integerDeepKeyBehaves() {
104-
this.processor.setResources(new ByteArrayResource("foo:\n 1: bar".getBytes()));
108+
setYaml("foo:\n 1: bar");
105109
this.processor.process((properties, map) -> {
106110
assertEquals("bar", properties.get("foo[1]"));
107111
assertEquals(1, properties.size());
@@ -111,7 +115,7 @@ public void integerDeepKeyBehaves() {
111115
@Test
112116
@SuppressWarnings("unchecked")
113117
public void flattenedMapIsSameAsPropertiesButOrdered() {
114-
this.processor.setResources(new ByteArrayResource("foo: bar\nbar:\n spam: bucket".getBytes()));
118+
setYaml("foo: bar\nbar:\n spam: bucket");
115119
this.processor.process((properties, map) -> {
116120
assertEquals("bucket", properties.get("bar.spam"));
117121
assertEquals(2, properties.size());
@@ -124,4 +128,47 @@ public void flattenedMapIsSameAsPropertiesButOrdered() {
124128
});
125129
}
126130

131+
@Test
132+
public void customTypeSupportedByDefault() throws Exception {
133+
URL url = new URL("https://localhost:9000/");
134+
setYaml("value: !!java.net.URL [\"" + url + "\"]");
135+
136+
this.processor.process((properties, map) -> {
137+
assertEquals(1, properties.size());
138+
assertEquals(1, map.size());
139+
assertEquals(url, properties.get("value"));
140+
assertEquals(url, map.get("value"));
141+
});
142+
}
143+
144+
@Test
145+
public void customTypesSupportedDueToExplicitConfiguration() throws Exception {
146+
this.processor.setSupportedTypes(URL.class, String.class);
147+
148+
URL url = new URL("https://localhost:9000/");
149+
setYaml("value: !!java.net.URL [!!java.lang.String [\"" + url + "\"]]");
150+
151+
this.processor.process((properties, map) -> {
152+
assertEquals(1, properties.size());
153+
assertEquals(1, map.size());
154+
assertEquals(url, properties.get("value"));
155+
assertEquals(url, map.get("value"));
156+
});
157+
}
158+
159+
@Test
160+
public void customTypeNotSupportedDueToExplicitConfiguration() {
161+
this.processor.setSupportedTypes(List.class);
162+
163+
setYaml("value: !!java.net.URL [\"https://localhost:9000/\"]");
164+
165+
this.exception.expect(ConstructorException.class);
166+
this.exception.expectMessage("Unsupported type encountered in YAML document: java.net.URL");
167+
this.processor.process((properties, map) -> {});
168+
}
169+
170+
private void setYaml(String yaml) {
171+
this.processor.setResources(new ByteArrayResource(yaml.getBytes()));
172+
}
173+
127174
}

0 commit comments

Comments
 (0)