All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.wl4g.infra.common.yaml.map.YamlProcessor Maven / Gradle / Ivy

The newest version!
/*
 * 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 map = (Map) object; map.forEach((key, value) -> { if (value instanceof Map) { value = asMap(value); } if (key instanceof CharSequence) { result.put(key.toString(), value); } else { // It has to be a map key in this case result.put("[" + key.toString() + "]", value); } }); return result; } private boolean process(Map map, MatchCallback callback) { Properties properties = CollectionFactory.createStringAdaptingProperties(); properties.putAll(getFlattenedMap(map)); if (this.documentMatchers.isEmpty()) { if (logger.isDebugEnabled()) { logger.debug("Merging document (no matchers set): " + map); } callback.process(properties, map); return true; } MatchStatus result = MatchStatus.ABSTAIN; for (DocumentMatcher matcher : this.documentMatchers) { MatchStatus match = matcher.matches(properties); result = MatchStatus.getMostSpecific(match, result); if (match == MatchStatus.FOUND) { if (logger.isDebugEnabled()) { logger.debug("Matched document with document matcher: " + properties); } callback.process(properties, map); return true; } } if (result == MatchStatus.ABSTAIN && this.matchDefault) { if (logger.isDebugEnabled()) { logger.debug("Matched document with default matcher: " + map); } callback.process(properties, map); return true; } if (logger.isDebugEnabled()) { logger.debug("Unmatched document: " + map); } return false; } /** * Return a flattened version of the given map, recursively following any * nested Map or Collection values. Entries from the resulting map retain * the same order as the source. When called with the Map from a * {@link MatchCallback} the result will contain the same values as the * {@link MatchCallback} Properties. * * @param source * the source map * @return a flattened map * @since 4.1.3 */ protected final Map getFlattenedMap(Map source) { Map result = new LinkedHashMap<>(); buildFlattenedMap(result, source, null); return result; } private void buildFlattenedMap(Map result, Map source, @Nullable String path) { source.forEach((key, value) -> { if (StringUtils2.hasLength(path)) { if (key.startsWith("[")) { key = path + key; } else { key = path + '.' + key; } } if (value instanceof String) { result.put(key, value); } else if (value instanceof Map) { // Need a compound key @SuppressWarnings("unchecked") Map map = (Map) value; buildFlattenedMap(result, map, key); } else if (value instanceof Collection) { // Need a compound key @SuppressWarnings("unchecked") Collection collection = (Collection) value; if (collection.isEmpty()) { result.put(key, ""); } else { int count = 0; for (Object object : collection) { buildFlattenedMap(result, Collections.singletonMap("[" + (count++) + "]", object), key); } } } else { result.put(key, (value != null ? value : "")); } }); } /** * Callback interface used to process the YAML parsing results. */ @FunctionalInterface public interface MatchCallback { /** * Process the given representation of the parsing results. * * @param properties * the properties to process (as a flattened representation * with indexed keys in case of a collection or map) * @param map * the result map (preserving the original value structure in * the YAML document) */ void process(Properties properties, Map map); } /** * Strategy interface used to test if properties match. */ @FunctionalInterface public interface DocumentMatcher { /** * Test if the given properties match. * * @param properties * the properties to test * @return the status of the match */ MatchStatus matches(Properties properties); } /** * Status returned from * {@link DocumentMatcher#matches(java.util.Properties)}. */ public enum MatchStatus { /** * A match was found. */ FOUND, /** * No match was found. */ NOT_FOUND, /** * The matcher should not be considered. */ ABSTAIN; /** * Compare two {@link MatchStatus} items, returning the most specific * status. */ public static MatchStatus getMostSpecific(MatchStatus a, MatchStatus b) { return (a.ordinal() < b.ordinal() ? a : b); } } /** * Method to use for resolving resources. */ public enum ResolutionMethod { /** * Replace values from earlier in the list. */ OVERRIDE, /** * Replace values from earlier in the list, ignoring any failures. */ OVERRIDE_AND_IGNORE, /** * Take the first resource in the list that exists and use just that. */ FIRST_FOUND } }