Skip to content

Commit 898b970

Browse files
committed
Switch to Yaml 1.2 with snakeyaml-engine
This PR replace snakeyaml with snakeyaml-engine in order to support YAML 1.2 Version 1.2 solves among others Norway problem https://hitchdev.com/strictyaml/why/implicit-typing-removed, mentioned in spring-projects/spring-boot#17113 The following compares versions 1.1 and 1.2: https://yaml.readthedocs.io/en/latest/pyyaml.html I drop off 'supportedTypes' support. The feature was add in spring-projects/spring-framework#25152. I asked question for purpose, but I didn't get answer. I decide to drop because: 1. Supported types are not directly provided by snakeyaml-engine 2. Clients - for example Spring Boot OriginTrackedYamlLoader - still may modify yaml setup 3. Supported types are not documented 4. I think that supported types is wrong idea. Parsing YAML should be simple and conversion may be implemented in application code.
1 parent f40a391 commit 898b970

File tree

6 files changed

+30
-121
lines changed

6 files changed

+30
-121
lines changed

build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ configure(allprojects) { project ->
8484
exclude group: "stax", name: "stax-api"
8585
}
8686
dependency "org.ogce:xpp3:1.1.6"
87-
dependency "org.yaml:snakeyaml:1.30"
87+
dependency "org.snakeyaml:snakeyaml-engine:2.3"
8888

8989
dependency "com.h2database:h2:2.1.210"
9090
dependency "com.github.ben-manes.caffeine:caffeine:3.0.6"

spring-beans/spring-beans.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ apply plugin: "kotlin"
55
dependencies {
66
api(project(":spring-core"))
77
optional("jakarta.inject:jakarta.inject-api")
8-
optional("org.yaml:snakeyaml")
8+
optional("org.snakeyaml:snakeyaml-engine")
99
optional("org.apache.groovy:groovy-xml")
1010
optional("org.jetbrains.kotlin:kotlin-reflect")
1111
optional("org.jetbrains.kotlin:kotlin-stdlib")
@@ -14,4 +14,4 @@ dependencies {
1414
testImplementation("jakarta.annotation:jakarta.annotation-api")
1515
testFixturesApi("org.junit.jupiter:junit-jupiter-api")
1616
testFixturesImplementation("org.assertj:assertj-core")
17-
}
17+
}

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

Lines changed: 12 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -17,37 +17,29 @@
1717
package org.springframework.beans.factory.config;
1818

1919
import java.io.IOException;
20-
import java.io.Reader;
20+
import java.io.InputStream;
2121
import java.util.Arrays;
2222
import java.util.Collection;
2323
import java.util.Collections;
2424
import java.util.LinkedHashMap;
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;
30-
3128
import org.apache.commons.logging.Log;
3229
import org.apache.commons.logging.LogFactory;
33-
import org.yaml.snakeyaml.DumperOptions;
34-
import org.yaml.snakeyaml.LoaderOptions;
35-
import org.yaml.snakeyaml.Yaml;
36-
import org.yaml.snakeyaml.constructor.Constructor;
37-
import org.yaml.snakeyaml.reader.UnicodeReader;
38-
import org.yaml.snakeyaml.representer.Representer;
30+
import org.snakeyaml.engine.v2.api.Load;
31+
import org.snakeyaml.engine.v2.api.LoadSettings;
3932

4033
import org.springframework.core.CollectionFactory;
4134
import org.springframework.core.io.Resource;
4235
import org.springframework.lang.Nullable;
4336
import org.springframework.util.Assert;
44-
import org.springframework.util.ObjectUtils;
4537
import org.springframework.util.StringUtils;
4638

4739
/**
4840
* Base class for YAML factories.
4941
*
50-
* <p>Requires SnakeYAML 1.18 or higher, as of Spring Framework 5.0.6.
42+
* <p>Requires SnakeYAML engine 2.3 or higher
5143
*
5244
* @author Dave Syer
5345
* @author Juergen Hoeller
@@ -67,9 +59,6 @@ public abstract class YamlProcessor {
6759

6860
private boolean matchDefault = true;
6961

70-
private Set<String> supportedTypes = Collections.emptySet();
71-
72-
7362
/**
7463
* A map of document matchers allowing callers to selectively use only
7564
* some of the documents in a YAML resource. In YAML documents are
@@ -127,29 +116,6 @@ public void setResources(Resource... resources) {
127116
this.resources = resources;
128117
}
129118

130-
/**
131-
* Set the supported types that can be loaded from YAML documents.
132-
* <p>If no supported types are configured, only Java standard classes
133-
* (as defined in {@link org.yaml.snakeyaml.constructor.SafeConstructor})
134-
* encountered in YAML documents will be supported.
135-
* If an unsupported type is encountered, an {@link IllegalStateException}
136-
* will be thrown when the corresponding YAML node is processed.
137-
* @param supportedTypes the supported types, or an empty array to clear the
138-
* supported types
139-
* @since 5.1.16
140-
* @see #createYaml()
141-
*/
142-
public void setSupportedTypes(Class<?>... supportedTypes) {
143-
if (ObjectUtils.isEmpty(supportedTypes)) {
144-
this.supportedTypes = Collections.emptySet();
145-
}
146-
else {
147-
Assert.noNullElements(supportedTypes, "'supportedTypes' must not contain null elements");
148-
this.supportedTypes = Arrays.stream(supportedTypes).map(Class::getName)
149-
.collect(Collectors.toUnmodifiableSet());
150-
}
151-
}
152-
153119
/**
154120
* Provide an opportunity for subclasses to process the Yaml parsed from the supplied
155121
* resources. Each resource is parsed in turn and the documents inside checked against
@@ -161,7 +127,7 @@ public void setSupportedTypes(Class<?>... supportedTypes) {
161127
* @see #createYaml()
162128
*/
163129
protected void process(MatchCallback callback) {
164-
Yaml yaml = createYaml();
130+
Load yaml = createYaml();
165131
for (Resource resource : this.resources) {
166132
boolean found = process(callback, yaml, resource);
167133
if (this.resolutionMethod == ResolutionMethod.FIRST_FOUND && found) {
@@ -171,31 +137,21 @@ protected void process(MatchCallback callback) {
171137
}
172138

173139
/**
174-
* Create the {@link Yaml} instance to use.
175-
* <p>The default implementation sets the "allowDuplicateKeys" flag to {@code false},
176-
* enabling built-in duplicate key handling in SnakeYAML 1.18+.
177-
* <p>As of Spring Framework 5.1.16, if custom {@linkplain #setSupportedTypes
178-
* supported types} have been configured, the default implementation creates
179-
* a {@code Yaml} instance that filters out unsupported types encountered in
180-
* YAML documents. If an unsupported type is encountered, an
181-
* {@link IllegalStateException} will be thrown when the node is processed.
182-
* @see LoaderOptions#setAllowDuplicateKeys(boolean)
140+
* Create the {@link Load} instance to use.
183141
*/
184-
protected Yaml createYaml() {
185-
LoaderOptions loaderOptions = new LoaderOptions();
186-
loaderOptions.setAllowDuplicateKeys(false);
187-
return new Yaml(new FilteringConstructor(loaderOptions), new Representer(),
188-
new DumperOptions(), loaderOptions);
142+
protected Load createYaml() {
143+
LoadSettings settings = LoadSettings.builder().build();
144+
return new Load(settings);
189145
}
190146

191-
private boolean process(MatchCallback callback, Yaml yaml, Resource resource) {
147+
private boolean process(MatchCallback callback, Load yaml, Resource resource) {
192148
int count = 0;
193149
try {
194150
if (logger.isDebugEnabled()) {
195151
logger.debug("Loading from YAML: " + resource);
196152
}
197-
try (Reader reader = new UnicodeReader(resource.getInputStream())) {
198-
for (Object object : yaml.loadAll(reader)) {
153+
try (InputStream is = resource.getInputStream()) {
154+
for (Object object : yaml.loadAllFromInputStream(is)) {
199155
if (object != null && process(asMap(object), callback)) {
200156
count++;
201157
if (this.resolutionMethod == ResolutionMethod.FIRST_FOUND) {
@@ -428,24 +384,4 @@ public enum ResolutionMethod {
428384
FIRST_FOUND
429385
}
430386

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}.
436-
*/
437-
private class FilteringConstructor extends Constructor {
438-
439-
FilteringConstructor(LoaderOptions loaderOptions) {
440-
super(loaderOptions);
441-
}
442-
443-
@Override
444-
protected Class<?> getClassForName(String name) throws ClassNotFoundException {
445-
Assert.state(YamlProcessor.this.supportedTypes.contains(name),
446-
() -> "Unsupported type encountered in YAML document: " + name);
447-
return super.getClassForName(name);
448-
}
449-
}
450-
451387
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
import java.util.Map;
2323

2424
import org.junit.jupiter.api.Test;
25-
import org.yaml.snakeyaml.constructor.DuplicateKeyException;
25+
import org.snakeyaml.engine.v2.exceptions.DuplicateKeyException;
2626

2727
import org.springframework.core.io.AbstractResource;
2828
import org.springframework.core.io.ByteArrayResource;

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

Lines changed: 11 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@
2424
import java.util.Set;
2525

2626
import org.junit.jupiter.api.Test;
27-
import org.yaml.snakeyaml.constructor.ConstructorException;
28-
import org.yaml.snakeyaml.parser.ParserException;
29-
import org.yaml.snakeyaml.scanner.ScannerException;
27+
import org.snakeyaml.engine.v2.exceptions.ConstructorException;
28+
import org.snakeyaml.engine.v2.exceptions.ParserException;
29+
import org.snakeyaml.engine.v2.exceptions.ScannerException;
3030

3131
import org.springframework.core.io.ByteArrayResource;
3232

@@ -70,6 +70,13 @@ void stringResource() {
7070
this.processor.process((properties, map) -> assertThat(map.get("document")).isEqualTo("foo"));
7171
}
7272

73+
@Test
74+
void norwayProblem() {
75+
setYaml("foo: no");
76+
this.processor.process((properties, map) ->
77+
assertThat(map.get("foo")).as("In YAML 1.1 it would be: false").isEqualTo("no"));
78+
}
79+
7380
@Test
7481
void badDocumentStart() {
7582
setYaml("foo # a document\nbar: baz");
@@ -158,31 +165,7 @@ void customTypeNotSupportedByDefault() throws Exception {
158165
setYaml("value: !!java.net.URL [\"" + url + "\"]");
159166
assertThatExceptionOfType(ConstructorException.class)
160167
.isThrownBy(() -> this.processor.process((properties, map) -> {}))
161-
.withMessageContaining("Unsupported type encountered in YAML document: java.net.URL");
162-
}
163-
164-
@Test
165-
void customTypesSupportedDueToExplicitConfiguration() throws Exception {
166-
this.processor.setSupportedTypes(URL.class, String.class);
167-
168-
URL url = new URL("https://localhost:9000/");
169-
setYaml("value: !!java.net.URL [!!java.lang.String [\"" + url + "\"]]");
170-
171-
this.processor.process((properties, map) -> {
172-
assertThat(properties).containsExactly(entry("value", url));
173-
assertThat(map).containsExactly(entry("value", url));
174-
});
175-
}
176-
177-
@Test
178-
void customTypeNotSupportedDueToExplicitConfiguration() {
179-
this.processor.setSupportedTypes(List.class);
180-
181-
setYaml("value: !!java.net.URL [\"https://localhost:9000/\"]");
182-
183-
assertThatExceptionOfType(ConstructorException.class)
184-
.isThrownBy(() -> this.processor.process((properties, map) -> {}))
185-
.withMessageContaining("Unsupported type encountered in YAML document: java.net.URL");
168+
.withMessageContaining("java.net.URL");
186169
}
187170

188171
private void setYaml(String yaml) {

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

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@
2020
import java.util.Properties;
2121

2222
import org.junit.jupiter.api.Test;
23-
import org.yaml.snakeyaml.Yaml;
24-
import org.yaml.snakeyaml.constructor.DuplicateKeyException;
25-
import org.yaml.snakeyaml.scanner.ScannerException;
23+
24+
import org.snakeyaml.engine.v2.exceptions.DuplicateKeyException;
25+
import org.snakeyaml.engine.v2.exceptions.ScannerException;
2626

2727
import org.springframework.beans.factory.config.YamlProcessor.MatchStatus;
2828
import org.springframework.beans.factory.config.YamlProcessor.ResolutionMethod;
@@ -225,14 +225,4 @@ void loadArrayOfObject() {
225225
assertThat(properties.getProperty("foo[2].three")).isEqualTo("four");
226226
assertThat(properties.get("foo")).isNull();
227227
}
228-
229-
@Test
230-
@SuppressWarnings("unchecked")
231-
void yaml() {
232-
Yaml yaml = new Yaml();
233-
Map<String, ?> map = yaml.loadAs("foo: bar\nspam:\n foo: baz", Map.class);
234-
assertThat(map.get("foo")).isEqualTo("bar");
235-
assertThat(((Map<String, Object>) map.get("spam")).get("foo")).isEqualTo("baz");
236-
}
237-
238228
}

0 commit comments

Comments
 (0)