org.springframework.beans.factory.config.YamlProcessor Maven / Gradle / Ivy
/*
* Copyright 2002-2020 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
*
* https://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.beans.factory.config;
import java.io.IOException;
import java.io.Reader;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.stream.Collectors;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.LoaderOptions;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.Constructor;
import org.yaml.snakeyaml.reader.UnicodeReader;
import org.yaml.snakeyaml.representer.Representer;
import org.springframework.core.CollectionFactory;
import org.springframework.core.io.Resource;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* Base class for YAML factories.
*
* Requires SnakeYAML 1.18 or higher, as of Spring Framework 5.0.6.
*
* @author Dave Syer
* @author Juergen Hoeller
* @author Sam Brannen
* @since 4.1
*/
public abstract class YamlProcessor {
private final Log logger = LogFactory.getLog(getClass());
private ResolutionMethod resolutionMethod = ResolutionMethod.OVERRIDE;
private Resource[] resources = new Resource[0];
private List documentMatchers = Collections.emptyList();
private boolean matchDefault = true;
private Set supportedTypes = Collections.emptySet();
/**
* A map of document matchers allowing callers to selectively use only
* some of the documents in a YAML resource. In YAML documents are
* separated by {@code ---} lines, and each document is converted
* to properties before the match is made. E.g.
*
* environment: dev
* url: https://dev.bar.com
* name: Developer Setup
* ---
* environment: prod
* url:https://foo.bar.com
* name: My Cool App
*
* when mapped with
*
* setDocumentMatchers(properties ->
* ("prod".equals(properties.getProperty("environment")) ? MatchStatus.FOUND : MatchStatus.NOT_FOUND));
*
* would end up as
*
* environment=prod
* url=https://foo.bar.com
* name=My Cool App
*
*/
public void setDocumentMatchers(DocumentMatcher... matchers) {
this.documentMatchers = Arrays.asList(matchers);
}
/**
* Flag indicating that a document for which all the
* {@link #setDocumentMatchers(DocumentMatcher...) document matchers} abstain will
* nevertheless match. Default is {@code true}.
*/
public void setMatchDefault(boolean matchDefault) {
this.matchDefault = matchDefault;
}
/**
* Method to use for resolving resources. Each resource will be converted to a Map,
* so this property is used to decide which map entries to keep in the final output
* from this factory. Default is {@link ResolutionMethod#OVERRIDE}.
*/
public void setResolutionMethod(ResolutionMethod resolutionMethod) {
Assert.notNull(resolutionMethod, "ResolutionMethod must not be null");
this.resolutionMethod = resolutionMethod;
}
/**
* Set locations of YAML {@link Resource resources} to be loaded.
* @see ResolutionMethod
*/
public void setResources(Resource... resources) {
this.resources = resources;
}
/**
* Set the supported types that can be loaded from YAML documents.
* If no supported types are configured, all types encountered in YAML
* documents will be supported. If an unsupported type is encountered, an
* {@link IllegalStateException} will be thrown when the corresponding YAML
* node is processed.
* @param supportedTypes the supported types, or an empty array to clear the
* supported types
* @since 5.1.16
* @see #createYaml()
*/
public void setSupportedTypes(Class... supportedTypes) {
if (ObjectUtils.isEmpty(supportedTypes)) {
this.supportedTypes = Collections.emptySet();
}
else {
Assert.noNullElements(supportedTypes, "'supportedTypes' must not contain null elements");
this.supportedTypes = Arrays.stream(supportedTypes).map(Class::getName)
.collect(Collectors.collectingAndThen(Collectors.toSet(), Collections::unmodifiableSet));
}
}
/**
* Provide an opportunity for subclasses to process the Yaml parsed from the supplied
* resources. Each resource is parsed in turn and the documents inside checked against
* the {@link #setDocumentMatchers(DocumentMatcher...) matchers}. If a document
* matches it is passed into the callback, along with its representation as Properties.
* Depending on the {@link #setResolutionMethod(ResolutionMethod)} not all of the
* documents will be parsed.
* @param callback a callback to delegate to once matching documents are found
* @see #createYaml()
*/
protected void process(MatchCallback callback) {
Yaml yaml = createYaml();
for (Resource resource : this.resources) {
boolean found = process(callback, yaml, resource);
if (this.resolutionMethod == ResolutionMethod.FIRST_FOUND && found) {
return;
}
}
}
/**
* Create the {@link Yaml} instance to use.
*
The default implementation sets the "allowDuplicateKeys" flag to {@code false},
* enabling built-in duplicate key handling in SnakeYAML 1.18+.
*
As of Spring Framework 5.1.16, if custom {@linkplain #setSupportedTypes
* supported types} have been configured, the default implementation creates
* a {@code Yaml} instance that filters out unsupported types encountered in
* YAML documents. If an unsupported type is encountered, an
* {@link IllegalStateException} will be thrown when the node is processed.
* @see LoaderOptions#setAllowDuplicateKeys(boolean)
*/
protected Yaml createYaml() {
LoaderOptions loaderOptions = new LoaderOptions();
loaderOptions.setAllowDuplicateKeys(false);
if (!this.supportedTypes.isEmpty()) {
return new Yaml(new FilteringConstructor(loaderOptions), new Representer(),
new DumperOptions(), loaderOptions);
}
return new Yaml(loaderOptions);
}
private boolean process(MatchCallback callback, Yaml yaml, Resource resource) {
int count = 0;
try {
if (logger.isDebugEnabled()) {
logger.debug("Loading from YAML: " + resource);
}
try (Reader reader = new UnicodeReader(resource.getInputStream())) {
for (Object object : yaml.loadAll(reader)) {
if (object != null && process(asMap(object), callback)) {
count++;
if (this.resolutionMethod == ResolutionMethod.FIRST_FOUND) {
break;
}
}
}
if (logger.isDebugEnabled()) {
logger.debug("Loaded " + count + " document" + (count > 1 ? "s" : "") +
" from YAML resource: " + resource);
}
}
}
catch (IOException ex) {
handleProcessError(resource, ex);
}
return (count > 0);
}
private void handleProcessError(Resource resource, IOException ex) {
if (this.resolutionMethod != ResolutionMethod.FIRST_FOUND &&
this.resolutionMethod != ResolutionMethod.OVERRIDE_AND_IGNORE) {
throw new IllegalStateException(ex);
}
if (logger.isWarnEnabled()) {
logger.warn("Could not load map from " + resource + ": " + ex.getMessage());
}
}
@SuppressWarnings("unchecked")
private Map asMap(Object object) {
// YAML can have numbers as keys
Map result = new LinkedHashMap<>();
if (!(object instanceof Map)) {
// A document can be a text literal
result.put("document", object);
return result;
}
Map