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