com.wl4g.infra.common.yaml.map.YamlProcessor Maven / Gradle / Ivy
/*
* Copyright 2002-2021 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 com.wl4g.infra.common.yaml.map;
import java.io.IOException;
import java.io.Reader;
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 javax.annotation.Nullable;
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 com.wl4g.infra.common.lang.StringUtils2;
import com.wl4g.infra.common.resource.StreamResource;
import lombok.Setter;
/**
* Base class for YAML factories.
*
* @since Based on modifiy of
* {@link com.wl4g.infra.common.yaml.map.springframework.beans.factory.config.YamlProcessor}
*/
@Setter
class YamlProcessor {
private final Log logger = LogFactory.getLog(getClass());
private Constructor constructor = new Constructor();
/**
* 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}.
*/
private ResolutionMethod resolutionMethod = ResolutionMethod.OVERRIDE;
/**
* Set locations of YAML {@link StreamResource resources} to be loaded.
*
* @see ResolutionMethod
*/
private StreamResource[] resources = new StreamResource[0];
/**
* 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
*
*/
private List documentMatchers = Collections.emptyList();
/**
* Flag indicating that a document for which all the
* {@link #setDocumentMatchers(DocumentMatcher...) document matchers}
* abstain will nevertheless match. Default is {@code true}.
*/
private boolean matchDefault = true;
/**
* 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 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 (StreamResource 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() {
return new Yaml(constructor, new Representer(), new DumperOptions());
}
private boolean process(MatchCallback callback, Yaml yaml, StreamResource 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(StreamResource 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