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}