001/*
002 * Copyright 2002-2019 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.beans.factory.config;
018
019import java.util.LinkedHashMap;
020import java.util.Map;
021
022import org.springframework.beans.factory.FactoryBean;
023import org.springframework.beans.factory.InitializingBean;
024import org.springframework.lang.Nullable;
025
026/**
027 * Factory for a {@code Map} that reads from a YAML source, preserving the
028 * YAML-declared value types and their structure.
029 *
030 * <p>YAML is a nice human-readable format for configuration, and it has some
031 * useful hierarchical properties. It's more or less a superset of JSON, so it
032 * has a lot of similar features.
033 *
034 * <p>If multiple resources are provided the later ones will override entries in
035 * the earlier ones hierarchically; that is, all entries with the same nested key
036 * of type {@code Map} at any depth are merged. For example:
037 *
038 * <pre class="code">
039 * foo:
040 *   bar:
041 *    one: two
042 * three: four
043 * </pre>
044 *
045 * plus (later in the list)
046 *
047 * <pre class="code">
048 * foo:
049 *   bar:
050 *    one: 2
051 * five: six
052 * </pre>
053 *
054 * results in an effective input of
055 *
056 * <pre class="code">
057 * foo:
058 *   bar:
059 *    one: 2
060 * three: four
061 * five: six
062 * </pre>
063 *
064 * Note that the value of "foo" in the first document is not simply replaced
065 * with the value in the second, but its nested values are merged.
066 *
067 * <p>Requires SnakeYAML 1.18 or higher, as of Spring Framework 5.0.6.
068 *
069 * @author Dave Syer
070 * @author Juergen Hoeller
071 * @since 4.1
072 */
073public class YamlMapFactoryBean extends YamlProcessor implements FactoryBean<Map<String, Object>>, InitializingBean {
074
075        private boolean singleton = true;
076
077        @Nullable
078        private Map<String, Object> map;
079
080
081        /**
082         * Set if a singleton should be created, or a new object on each request
083     {@code true} (a singleton).
084         */
085        public void setSingleton(boolean singleton) {
086                this.singleton = singleton;
087        }
088
089        @Override
090        public boolean isSingleton() {
091                return this.singleton;
092        }
093
094        @Override
095        public void afterPropertiesSet() {
096                if (isSingleton()) {
097                        this.map = createMap();
098                }
099        }
100
101        @Override
102        @Nullable
103        public Map<String, Object> getObject() {
104                return (this.map != null ? this.map : createMap());
105        }
106
107        @Override
108        public Class<?> getObjectType() {
109                return Map.class;
110        }
111
112
113        /**
114         * Template method that subclasses may override to construct the object
115         * returned by this factory.
116         * <p>Invoked lazily the first time {@link #getObject()} is invoked in
117         * case of a shared singleton; else, on each {@link #getObject()} call.
118         * <p>The default implementation returns the merged {@code Map} instance.
119         * @return the object returned by this factory
120         * @see #process(MatchCallback)
121         */
122        protected Map<String, Object> createMap() {
123                Map<String, Object> result = new LinkedHashMap<>();
124                process((properties, map) -> merge(result, map));
125                return result;
126        }
127
128        @SuppressWarnings({"rawtypes", "unchecked"})
129        private void merge(Map<String, Object> output, Map<String, Object> map) {
130                map.forEach((key, value) -> {
131                        Object existing = output.get(key);
132                        if (value instanceof Map && existing instanceof Map) {
133                                // Inner cast required by Eclipse IDE.
134                                Map<String, Object> result = new LinkedHashMap<>((Map<String, Object>) existing);
135                                merge(result, (Map) value);
136                                output.put(key, result);
137                        }
138                        else {
139                                output.put(key, value);
140                        }
141                });
142        }
143
144}