Add OriginTrackedYamlLoader
Update the YAML parser so that origin information can be tracked. Line and column numbers are now available for each loaded property value. Fixes gh-8142pull/8526/head
parent
7d793fd123
commit
484c72cd19
@ -0,0 +1,153 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2017 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.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.env;
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Properties;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import org.yaml.snakeyaml.DumperOptions;
|
||||||
|
import org.yaml.snakeyaml.Yaml;
|
||||||
|
import org.yaml.snakeyaml.constructor.BaseConstructor;
|
||||||
|
import org.yaml.snakeyaml.constructor.Constructor;
|
||||||
|
import org.yaml.snakeyaml.error.Mark;
|
||||||
|
import org.yaml.snakeyaml.nodes.Node;
|
||||||
|
import org.yaml.snakeyaml.nodes.ScalarNode;
|
||||||
|
import org.yaml.snakeyaml.nodes.Tag;
|
||||||
|
import org.yaml.snakeyaml.representer.Representer;
|
||||||
|
import org.yaml.snakeyaml.resolver.Resolver;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.config.YamlProcessor;
|
||||||
|
import org.springframework.boot.env.TextResourcePropertyOrigin.Location;
|
||||||
|
import org.springframework.boot.yaml.SpringProfileDocumentMatcher;
|
||||||
|
import org.springframework.core.io.Resource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class to load {@code .yml} files into a map of {@code String} ->
|
||||||
|
* {@link OriginTrackedValue}.
|
||||||
|
*
|
||||||
|
* @author Madhura Bhave
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
class OriginTrackedYamlLoader extends YamlProcessor {
|
||||||
|
|
||||||
|
private final Resource resource;
|
||||||
|
|
||||||
|
OriginTrackedYamlLoader(Resource resource, String profile) {
|
||||||
|
this.resource = resource;
|
||||||
|
if (profile == null) {
|
||||||
|
setMatchDefault(true);
|
||||||
|
setDocumentMatchers(new OriginTrackedSpringProfileDocumentMatcher());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
setMatchDefault(false);
|
||||||
|
setDocumentMatchers(new OriginTrackedSpringProfileDocumentMatcher(profile));
|
||||||
|
}
|
||||||
|
setResources(resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Yaml createYaml() {
|
||||||
|
BaseConstructor constructor = new OriginTrackingConstructor();
|
||||||
|
Representer representer = new Representer();
|
||||||
|
DumperOptions dumperOptions = new DumperOptions();
|
||||||
|
LimitedResolver resolver = new LimitedResolver();
|
||||||
|
return new Yaml(constructor, representer, dumperOptions, resolver);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> load() {
|
||||||
|
final Map<String, Object> result = new LinkedHashMap<String, Object>();
|
||||||
|
process(new MatchCallback() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void process(Properties properties, Map<String, Object> map) {
|
||||||
|
result.putAll(getFlattenedMap(map));
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link Constructor}.
|
||||||
|
*/
|
||||||
|
private class OriginTrackingConstructor extends StrictMapAppenderConstructor {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Object constructObject(Node node) {
|
||||||
|
if (node instanceof ScalarNode) {
|
||||||
|
return constructTrackedObject(node, super.constructObject(node));
|
||||||
|
}
|
||||||
|
return super.constructObject(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Object constructTrackedObject(Node node, Object value) {
|
||||||
|
PropertyOrigin origin = getOrigin(node);
|
||||||
|
return OriginTrackedValue.of(value, origin);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PropertyOrigin getOrigin(Node node) {
|
||||||
|
Mark mark = node.getStartMark();
|
||||||
|
Location location = new Location(mark.getLine(), mark.getColumn());
|
||||||
|
return new TextResourcePropertyOrigin(OriginTrackedYamlLoader.this.resource,
|
||||||
|
location);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link Resolver} that limits {@link Tag#TIMESTAMP} tags.
|
||||||
|
*/
|
||||||
|
private static class LimitedResolver extends Resolver {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addImplicitResolver(Tag tag, Pattern regexp, String first) {
|
||||||
|
if (tag == Tag.TIMESTAMP) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
super.addImplicitResolver(tag, regexp, first);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class OriginTrackedSpringProfileDocumentMatcher
|
||||||
|
extends SpringProfileDocumentMatcher {
|
||||||
|
|
||||||
|
OriginTrackedSpringProfileDocumentMatcher(String... profiles) {
|
||||||
|
super(profiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected List<String> extractSpringProfiles(Properties properties) {
|
||||||
|
Properties springProperties = new Properties();
|
||||||
|
for (Map.Entry<Object, Object> entry : properties.entrySet()) {
|
||||||
|
if (String.valueOf(entry.getKey()).startsWith("spring.")) {
|
||||||
|
Object value = entry.getValue();
|
||||||
|
if (value instanceof OriginTrackedValue) {
|
||||||
|
value = ((OriginTrackedValue) value).getValue();
|
||||||
|
}
|
||||||
|
springProperties.put(entry.getKey(), value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.extractSpringProfiles(springProperties);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,110 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2017 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.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.env;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import org.springframework.core.io.ClassPathResource;
|
||||||
|
import org.springframework.core.io.Resource;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link OriginTrackedYamlLoader}.
|
||||||
|
*
|
||||||
|
* @author Madhura Bhave
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
public class OriginTrackedYamlLoaderTests {
|
||||||
|
|
||||||
|
private OriginTrackedYamlLoader loader;
|
||||||
|
|
||||||
|
private Map<String, Object> result;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setUp() throws Exception {
|
||||||
|
Resource resource = new ClassPathResource("test-yaml.yml", getClass());
|
||||||
|
this.loader = new OriginTrackedYamlLoader(resource, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void processSimpleKey() throws Exception {
|
||||||
|
OriginTrackedValue value = getValue("name");
|
||||||
|
assertThat(value.toString()).isEqualTo("Martin D'vloper");
|
||||||
|
assertThat(getLocation(value)).isEqualTo("3:7");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void processMap() throws Exception {
|
||||||
|
OriginTrackedValue perl = getValue("languages.perl");
|
||||||
|
OriginTrackedValue python = getValue("languages.python");
|
||||||
|
OriginTrackedValue pascal = getValue("languages.pascal");
|
||||||
|
assertThat(perl.toString()).isEqualTo("Elite");
|
||||||
|
assertThat(getLocation(perl)).isEqualTo("13:11");
|
||||||
|
assertThat(python.toString()).isEqualTo("Elite");
|
||||||
|
assertThat(getLocation(python)).isEqualTo("14:13");
|
||||||
|
assertThat(pascal.toString()).isEqualTo("Lame");
|
||||||
|
assertThat(getLocation(pascal)).isEqualTo("15:13");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void processCollection() throws Exception {
|
||||||
|
OriginTrackedValue apple = getValue("foods[0]");
|
||||||
|
OriginTrackedValue orange = getValue("foods[1]");
|
||||||
|
OriginTrackedValue strawberry = getValue("foods[2]");
|
||||||
|
OriginTrackedValue mango = getValue("foods[3]");
|
||||||
|
assertThat(apple.toString()).isEqualTo("Apple");
|
||||||
|
assertThat(getLocation(apple)).isEqualTo("8:7");
|
||||||
|
assertThat(orange.toString()).isEqualTo("Orange");
|
||||||
|
assertThat(getLocation(orange)).isEqualTo("9:7");
|
||||||
|
assertThat(strawberry.toString()).isEqualTo("Strawberry");
|
||||||
|
assertThat(getLocation(strawberry)).isEqualTo("10:7");
|
||||||
|
assertThat(mango.toString()).isEqualTo("Mango");
|
||||||
|
assertThat(getLocation(mango)).isEqualTo("11:7");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void processMultiline() throws Exception {
|
||||||
|
OriginTrackedValue education = getValue("education");
|
||||||
|
assertThat(education.toString())
|
||||||
|
.isEqualTo("4 GCSEs\n3 A-Levels\nBSc in the Internet of Things\n");
|
||||||
|
assertThat(getLocation(education)).isEqualTo("16:12");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void processWithActiveProfile() throws Exception {
|
||||||
|
Resource resource = new ClassPathResource("test-yaml.yml", getClass());
|
||||||
|
this.loader = new OriginTrackedYamlLoader(resource, "development");
|
||||||
|
Map<String, Object> result = this.loader.load();
|
||||||
|
assertThat(result.get("name").toString()).isEqualTo("Test Name");
|
||||||
|
}
|
||||||
|
|
||||||
|
private OriginTrackedValue getValue(String name) {
|
||||||
|
if (this.result == null) {
|
||||||
|
this.result = this.loader.load();
|
||||||
|
}
|
||||||
|
return (OriginTrackedValue) this.result.get(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getLocation(OriginTrackedValue value) {
|
||||||
|
return ((TextResourcePropertyOrigin) value.getOrigin()).getLocation().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
# http://docs.ansible.com/ansible/YAMLSyntax.html
|
||||||
|
|
||||||
|
name: Martin D'vloper
|
||||||
|
job: Developer
|
||||||
|
skill: Elite
|
||||||
|
employed: True
|
||||||
|
foods:
|
||||||
|
- Apple
|
||||||
|
- Orange
|
||||||
|
- Strawberry
|
||||||
|
- Mango
|
||||||
|
languages:
|
||||||
|
perl: Elite
|
||||||
|
python: Elite
|
||||||
|
pascal: Lame
|
||||||
|
education: |
|
||||||
|
4 GCSEs
|
||||||
|
3 A-Levels
|
||||||
|
BSc in the Internet of Things
|
||||||
|
---
|
||||||
|
|
||||||
|
spring:
|
||||||
|
profiles: development
|
||||||
|
name: Test Name
|
||||||
|
|
||||||
|
---
|
||||||
|
|
Loading…
Reference in New Issue