    001/*
    002 * Licensed to the Apache Software Foundation (ASF) under one or more
    003 * contributor license agreements.  See the NOTICE file distributed with
    004 * this work for additional information regarding copyright ownership.
    005 * The ASF licenses this file to You under the Apache License, Version 2.0
    006 * (the "License"); you may not use this file except in compliance with
    007 * the License.  You may obtain a copy of the License at
    008 *
    009 *     https://www.apache.org/licenses/LICENSE-2.0
    010 *
    011 * Unless required by applicable law or agreed to in writing, software
    012 * distributed under the License is distributed on an "AS IS" BASIS,
    013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    014 * See the License for the specific language governing permissions and
    015 * limitations under the License.
    016 */
    017
    018package org.apache.commons.configuration2;
    019
    020import java.util.ArrayList;
    021import java.util.Collection;
    022import java.util.Collections;
    023import java.util.HashMap;
    024import java.util.HashSet;
    025import java.util.List;
    026import java.util.Map;
    027import java.util.Set;
    028import java.util.stream.Collectors;
    029
    030import org.apache.commons.configuration2.ex.ConfigurationException;
    031import org.apache.commons.configuration2.io.ConfigurationLogger;
    032import org.apache.commons.configuration2.tree.ImmutableNode;
    033import org.apache.commons.lang3.StringUtils;
    034
    035/**
    036 * <p>
    037 * A base class for configuration implementations based on YAML structures.
    038 * </p>
    039 * <p>
    040 * This base class offers functionality related to YAML-like data structures based on maps. Such a map has strings as
    041 * keys and arbitrary objects as values. The class offers methods to transform such a map into a hierarchy of
    042 * {@link ImmutableNode} objects and vice versa.
    043 * </p>
    044 *
    045 * @since 2.2
    046 */
    047public class AbstractYAMLBasedConfiguration extends BaseHierarchicalConfiguration {
    048
    049    /**
    050     * Adds a key value pair to a map, taking list structures into account. If a key is added which is already present in
    051     * the map, this method ensures that a list is created.
    052     *
    053     * @param map the map
    054     * @param key the key
    055     * @param value the value
    056     */
    057    private static void addEntry(final Map<String, Object> map, final String key, final Object value) {
    058        final Object oldValue = map.get(key);
    059        if (oldValue == null) {
    060            map.put(key, value);
    061        } else if (oldValue instanceof Collection) {
    062            // safe case because the collection was created by ourselves
    063            @SuppressWarnings("unchecked")
    064            final Collection<Object> values = (Collection<Object>) oldValue;
    065            values.add(value);
    066        } else {
    067            final Collection<Object> values = new ArrayList<>();
    068            values.add(oldValue);
    069            values.add(value);
    070            map.put(key, values);
    071        }
    072    }
    073
    074    /**
    075     * Creates a part of the hierarchical nodes structure of the resulting configuration. The passed in element is converted into one or multiple configuration
    076     * nodes. (If list structures are involved, multiple nodes are returned.)
    077     *
    078     * @param key     the key of the new node(s).
    079     * @param elem    the element to be processed.
    080     * @param visited the set of visited objects.
    081     * @return a list with configuration nodes representing the element
    082     */
    083    private static List<ImmutableNode> constructHierarchy(final String key, final Object elem, final Set<Object> visited) {
    084        if (elem instanceof Map) {
    085            return isVisisted(elem, visited) ? Collections.emptyList() : parseMap((Map<String, Object>) elem, key, visited);
    086        }
    087        if (elem instanceof Collection) {
    088            return isVisisted(elem, visited) ? Collections.emptyList() : parseCollection((Collection<Object>) elem, key, visited);
    089        }
    090        return Collections.singletonList(new ImmutableNode.Builder().name(key).value(elem).create());
    091    }
    092
    093    private static boolean isVisisted(final Object elem, final Set<Object> visited) {
    094        return !visited.add(System.identityHashCode(elem));
    095    }
    096
    097    /**
    098     * Parses a collection structure. The elements of the collection are processed recursively.
    099     *
    100     * @param col     the collection to be processed.
    101     * @param key     the key under which this collection is to be stored.
    102     * @param visited the set of visited objects.
    103     * @return a node representing this collection.
    104     */
    105    private static List<ImmutableNode> parseCollection(final Collection<Object> col, final String key, final Set<Object> visited) {
    106        return col.stream().flatMap(elem -> constructHierarchy(key, elem, visited).stream()).collect(Collectors.toList());
    107    }
    108
    109    /**
    110     * Parses a map structure. The single keys of the map are processed recursively.
    111     *
    112     * @param map     the map to be processed.
    113     * @param key     the key under which this map is to be stored.
    114     * @param visited the set of visited objects.
    115     * @return a node representing this map
    116     */
    117    private static List<ImmutableNode> parseMap(final Map<String, Object> map, final String key, final Set<Object> visited) {
    118        final ImmutableNode.Builder subtree = new ImmutableNode.Builder().name(key);
    119        map.forEach((k, v) -> constructHierarchy(k, v, visited).forEach(subtree::addChild));
    120        return Collections.singletonList(subtree.create());
    121    }
    122
    123    /**
    124     * Internal helper method to wrap an exception in a {@code ConfigurationException}.
    125     *
    126     * @param e the exception to be wrapped
    127     * @throws ConfigurationException the resulting exception
    128     */
    129    static void rethrowException(final Exception e) throws ConfigurationException {
    130        if (e instanceof ClassCastException) {
    131            throw new ConfigurationException("Error parsing", e);
    132        }
    133        throw new ConfigurationException("Unable to load the configuration", e);
    134    }
    135
    136    /**
    137     * Creates a new instance of {@code AbstractYAMLBasedConfiguration}.
    138     */
    139    protected AbstractYAMLBasedConfiguration() {
    140        initLogger(new ConfigurationLogger(getClass()));
    141    }
    142
    143    /**
    144     * Creates a new instance of {@code AbstractYAMLBasedConfiguration} as a copy of the specified configuration.
    145     *
    146     * @param c the configuration to be copied
    147     */
    148    protected AbstractYAMLBasedConfiguration(final HierarchicalConfiguration<ImmutableNode> c) {
    149        super(c);
    150        initLogger(new ConfigurationLogger(getClass()));
    151    }
    152
    153    /**
    154     * Constructs a YAML map, i.e. String -&gt; Object from a given configuration node.
    155     *
    156     * @param node The configuration node to create a map from.
    157     * @return A Map that contains the configuration node information.
    158     */
    159    protected Map<String, Object> constructMap(final ImmutableNode node) {
    160        final Map<String, Object> map = new HashMap<>(node.getChildren().size());
    161        node.forEach(cNode -> addEntry(map, cNode.getNodeName(), cNode.getChildren().isEmpty() ? cNode.getValue() : constructMap(cNode)));
    162        return map;
    163    }
    164
    165    /**
    166     * Loads this configuration from the content of the specified map. The data in the map is transformed into a hierarchy
    167     * of {@link ImmutableNode} objects.
    168     *
    169     * @param map the map to be processed
    170     */
    171    protected void load(final Map<String, Object> map) {
    172        final List<ImmutableNode> roots = constructHierarchy(StringUtils.EMPTY, map, new HashSet<>());
    173        if (!roots.isEmpty()) {
    174            getNodeModel().setRootNode(roots.get(0));
    175        }
    176    }
    177}