/* * 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 java.util.stream.Collectors; 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.MappingNode; import org.yaml.snakeyaml.nodes.Node; import org.yaml.snakeyaml.nodes.NodeTuple; 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.origin.Origin; import org.springframework.boot.origin.OriginTrackedValue; import org.springframework.boot.origin.TextResourceOrigin; import org.springframework.boot.origin.TextResourceOrigin.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((properties, map) -> { result.putAll(getFlattenedMap(map)); }); return result; } /** * {@link Constructor} that tracks property origins. */ private class OriginTrackingConstructor extends StrictMapAppenderConstructor { @Override protected Object constructObject(Node node) { if (node instanceof ScalarNode) { if (!(node instanceof KeyScalarNode)) { return constructTrackedObject(node, super.constructObject(node)); } } else if (node instanceof MappingNode) { replaceMappingNodeKeys((MappingNode) node); } return super.constructObject(node); } private void replaceMappingNodeKeys(MappingNode node) { node.setValue(node.getValue().stream().map(KeyScalarNode::get) .collect(Collectors.toList())); } private Object constructTrackedObject(Node node, Object value) { Origin origin = getOrigin(node); return OriginTrackedValue.of(value, origin); } private Origin getOrigin(Node node) { Mark mark = node.getStartMark(); Location location = new Location(mark.getLine(), mark.getColumn()); return new TextResourceOrigin(OriginTrackedYamlLoader.this.resource, location); } } /** * {@link ScalarNode} that replaces the key node in a {@link NodeTuple}. */ private static class KeyScalarNode extends ScalarNode { KeyScalarNode(ScalarNode node) { super(node.getTag(), node.getValue(), node.getStartMark(), node.getEndMark(), node.getStyle()); } public static NodeTuple get(NodeTuple nodeTuple) { Node keyNode = nodeTuple.getKeyNode(); Node valueNode = nodeTuple.getValueNode(); return new NodeTuple(KeyScalarNode.get(keyNode), valueNode); } private static Node get(Node node) { if (node instanceof ScalarNode) { return new KeyScalarNode((ScalarNode) node); } return node; } } /** * {@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); } } /** * {@link SpringProfileDocumentMatcher} that deals with {@link OriginTrackedValue * OriginTrackedValues}. */ 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); } } }