View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *     https://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.commons.configuration2;
19  
20  import java.util.ArrayList;
21  import java.util.Collection;
22  import java.util.Collections;
23  import java.util.HashMap;
24  import java.util.HashSet;
25  import java.util.List;
26  import java.util.Map;
27  import java.util.Set;
28  import java.util.stream.Collectors;
29  
30  import org.apache.commons.configuration2.ex.ConfigurationException;
31  import org.apache.commons.configuration2.io.ConfigurationLogger;
32  import org.apache.commons.configuration2.tree.ImmutableNode;
33  import org.apache.commons.lang3.StringUtils;
34  
35  /**
36   * <p>
37   * A base class for configuration implementations based on YAML structures.
38   * </p>
39   * <p>
40   * This base class offers functionality related to YAML-like data structures based on maps. Such a map has strings as
41   * keys and arbitrary objects as values. The class offers methods to transform such a map into a hierarchy of
42   * {@link ImmutableNode} objects and vice versa.
43   * </p>
44   *
45   * @since 2.2
46   */
47  public class AbstractYAMLBasedConfiguration extends BaseHierarchicalConfiguration {
48  
49      /**
50       * Adds a key value pair to a map, taking list structures into account. If a key is added which is already present in
51       * the map, this method ensures that a list is created.
52       *
53       * @param map the map
54       * @param key the key
55       * @param value the value
56       */
57      private static void addEntry(final Map<String, Object> map, final String key, final Object value) {
58          final Object oldValue = map.get(key);
59          if (oldValue == null) {
60              map.put(key, value);
61          } else if (oldValue instanceof Collection) {
62              // safe case because the collection was created by ourselves
63              @SuppressWarnings("unchecked")
64              final Collection<Object> values = (Collection<Object>) oldValue;
65              values.add(value);
66          } else {
67              final Collection<Object> values = new ArrayList<>();
68              values.add(oldValue);
69              values.add(value);
70              map.put(key, values);
71          }
72      }
73  
74      /**
75       * Creates a part of the hierarchical nodes structure of the resulting configuration. The passed in element is converted into one or multiple configuration
76       * nodes. (If list structures are involved, multiple nodes are returned.)
77       *
78       * @param key     the key of the new node(s).
79       * @param elem    the element to be processed.
80       * @param visited the set of visited objects.
81       * @return a list with configuration nodes representing the element
82       */
83      private static List<ImmutableNode> constructHierarchy(final String key, final Object elem, final Set<Object> visited) {
84          if (elem instanceof Map) {
85              return isVisisted(elem, visited) ? Collections.emptyList() : parseMap((Map<String, Object>) elem, key, visited);
86          }
87          if (elem instanceof Collection) {
88              return isVisisted(elem, visited) ? Collections.emptyList() : parseCollection((Collection<Object>) elem, key, visited);
89          }
90          return Collections.singletonList(new ImmutableNode.Builder().name(key).value(elem).create());
91      }
92  
93      private static boolean isVisisted(final Object elem, final Set<Object> visited) {
94          return !visited.add(System.identityHashCode(elem));
95      }
96  
97      /**
98       * Parses a collection structure. The elements of the collection are processed recursively.
99       *
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 }