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.test.context.support;
018
019import java.io.IOException;
020import java.io.StringReader;
021import java.util.ArrayList;
022import java.util.Arrays;
023import java.util.LinkedHashMap;
024import java.util.List;
025import java.util.Map;
026import java.util.Properties;
027
028import org.apache.commons.logging.Log;
029import org.apache.commons.logging.LogFactory;
030
031import org.springframework.context.ConfigurableApplicationContext;
032import org.springframework.core.annotation.MergedAnnotation;
033import org.springframework.core.annotation.MergedAnnotations;
034import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
035import org.springframework.core.env.ConfigurableEnvironment;
036import org.springframework.core.env.Environment;
037import org.springframework.core.env.MapPropertySource;
038import org.springframework.core.env.PropertySource;
039import org.springframework.core.env.PropertySources;
040import org.springframework.core.io.Resource;
041import org.springframework.core.io.ResourceLoader;
042import org.springframework.core.io.support.ResourcePropertySource;
043import org.springframework.test.context.TestPropertySource;
044import org.springframework.test.context.util.TestContextResourceUtils;
045import org.springframework.util.Assert;
046import org.springframework.util.ObjectUtils;
047import org.springframework.util.StringUtils;
048
049/**
050 * Utility methods for working with {@link TestPropertySource @TestPropertySource}
051 * and adding test {@link PropertySource PropertySources} to the {@code Environment}.
052 *
053 * <p>Primarily intended for use within the framework.
054 *
055 * @author Sam Brannen
056 * @author Anatoliy Korovin
057 * @author Phillip Webb
058 * @since 4.1
059 * @see TestPropertySource
060 */
061public abstract class TestPropertySourceUtils {
062
063        /**
064         * The name of the {@link MapPropertySource} created from <em>inlined properties</em>.
065         * @since 4.1.5
066         * @see #addInlinedPropertiesToEnvironment
067         */
068        public static final String INLINED_PROPERTIES_PROPERTY_SOURCE_NAME = "Inlined Test Properties";
069
070        private static final Log logger = LogFactory.getLog(TestPropertySourceUtils.class);
071
072
073        static MergedTestPropertySources buildMergedTestPropertySources(Class<?> testClass) {
074                MergedAnnotations mergedAnnotations = MergedAnnotations.from(testClass, SearchStrategy.TYPE_HIERARCHY);
075                return (mergedAnnotations.isPresent(TestPropertySource.class) ? mergeTestPropertySources(mergedAnnotations) :
076                                MergedTestPropertySources.empty());
077        }
078
079        private static MergedTestPropertySources mergeTestPropertySources(MergedAnnotations mergedAnnotations) {
080                List<TestPropertySourceAttributes> attributesList = resolveTestPropertySourceAttributes(mergedAnnotations);
081                return new MergedTestPropertySources(mergeLocations(attributesList), mergeProperties(attributesList));
082        }
083
084        private static List<TestPropertySourceAttributes> resolveTestPropertySourceAttributes(
085                        MergedAnnotations mergedAnnotations) {
086
087                List<TestPropertySourceAttributes> attributesList = new ArrayList<>();
088                mergedAnnotations.stream(TestPropertySource.class)
089                                .forEach(annotation -> addOrMergeTestPropertySourceAttributes(attributesList, annotation));
090                return attributesList;
091        }
092
093        private static void addOrMergeTestPropertySourceAttributes(List<TestPropertySourceAttributes> attributesList,
094                        MergedAnnotation<TestPropertySource> current) {
095
096                if (attributesList.isEmpty()) {
097                        attributesList.add(new TestPropertySourceAttributes(current));
098                }
099                else {
100                        TestPropertySourceAttributes previous = attributesList.get(attributesList.size() - 1);
101                        if (previous.canMergeWith(current)) {
102                                previous.mergeWith(current);
103                        }
104                        else {
105                                attributesList.add(new TestPropertySourceAttributes(current));
106                        }
107                }
108        }
109
110        private static String[] mergeLocations(List<TestPropertySourceAttributes> attributesList) {
111                List<String> locations = new ArrayList<>();
112                for (TestPropertySourceAttributes attrs : attributesList) {
113                        if (logger.isTraceEnabled()) {
114                                logger.trace(String.format("Processing locations for TestPropertySource attributes %s", attrs));
115                        }
116                        String[] locationsArray = TestContextResourceUtils.convertToClasspathResourcePaths(
117                                        attrs.getDeclaringClass(), true, attrs.getLocations());
118                        locations.addAll(0, Arrays.asList(locationsArray));
119                        if (!attrs.isInheritLocations()) {
120                                break;
121                        }
122                }
123                return StringUtils.toStringArray(locations);
124        }
125
126        private static String[] mergeProperties(List<TestPropertySourceAttributes> attributesList) {
127                List<String> properties = new ArrayList<>();
128                for (TestPropertySourceAttributes attrs : attributesList) {
129                        if (logger.isTraceEnabled()) {
130                                logger.trace(String.format("Processing inlined properties for TestPropertySource attributes %s", attrs));
131                        }
132                        String[] attrProps = attrs.getProperties();
133                        properties.addAll(0, Arrays.asList(attrProps));
134                        if (!attrs.isInheritProperties()) {
135                                break;
136                        }
137                }
138                return StringUtils.toStringArray(properties);
139        }
140
141        /**
142         * Add the {@link Properties} files from the given resource {@code locations}
143         * to the {@link Environment} of the supplied {@code context}.
144         * <p>This method simply delegates to
145         * {@link #addPropertiesFilesToEnvironment(ConfigurableEnvironment, ResourceLoader, String...)}.
146         * @param context the application context whose environment should be updated;
147         * never {@code null}
148         * @param locations the resource locations of {@code Properties} files to add
149         * to the environment; potentially empty but never {@code null}
150         * @throws IllegalStateException if an error occurs while processing a properties file
151         * @since 4.1.5
152         * @see ResourcePropertySource
153         * @see TestPropertySource#locations
154         * @see #addPropertiesFilesToEnvironment(ConfigurableEnvironment, ResourceLoader, String...)
155         */
156        public static void addPropertiesFilesToEnvironment(ConfigurableApplicationContext context, String... locations) {
157                Assert.notNull(context, "'context' must not be null");
158                Assert.notNull(locations, "'locations' must not be null");
159                addPropertiesFilesToEnvironment(context.getEnvironment(), context, locations);
160        }
161
162        /**
163         * Add the {@link Properties} files from the given resource {@code locations}
164         * to the supplied {@link ConfigurableEnvironment environment}.
165         * <p>Property placeholders in resource locations (i.e., <code>${...}</code>)
166         * will be {@linkplain Environment#resolveRequiredPlaceholders(String) resolved}
167         * against the {@code Environment}.
168         * <p>Each properties file will be converted to a {@link ResourcePropertySource}
169         * that will be added to the {@link PropertySources} of the environment with
170         * highest precedence.
171         * @param environment the environment to update; never {@code null}
172         * @param resourceLoader the {@code ResourceLoader} to use to load each resource;
173         * never {@code null}
174         * @param locations the resource locations of {@code Properties} files to add
175         * to the environment; potentially empty but never {@code null}
176         * @throws IllegalStateException if an error occurs while processing a properties file
177         * @since 4.3
178         * @see ResourcePropertySource
179         * @see TestPropertySource#locations
180         * @see #addPropertiesFilesToEnvironment(ConfigurableApplicationContext, String...)
181         */
182        public static void addPropertiesFilesToEnvironment(ConfigurableEnvironment environment,
183                        ResourceLoader resourceLoader, String... locations) {
184
185                Assert.notNull(environment, "'environment' must not be null");
186                Assert.notNull(resourceLoader, "'resourceLoader' must not be null");
187                Assert.notNull(locations, "'locations' must not be null");
188                try {
189                        for (String location : locations) {
190                                String resolvedLocation = environment.resolveRequiredPlaceholders(location);
191                                Resource resource = resourceLoader.getResource(resolvedLocation);
192                                environment.getPropertySources().addFirst(new ResourcePropertySource(resource));
193                        }
194                }
195                catch (IOException ex) {
196                        throw new IllegalStateException("Failed to add PropertySource to Environment", ex);
197                }
198        }
199
200        /**
201         * Add the given <em>inlined properties</em> to the {@link Environment} of the
202         * supplied {@code context}.
203         * <p>This method simply delegates to
204         * {@link #addInlinedPropertiesToEnvironment(ConfigurableEnvironment, String[])}.
205         * @param context the application context whose environment should be updated;
206         * never {@code null}
207         * @param inlinedProperties the inlined properties to add to the environment;
208         * potentially empty but never {@code null}
209         * @since 4.1.5
210         * @see TestPropertySource#properties
211         * @see #addInlinedPropertiesToEnvironment(ConfigurableEnvironment, String[])
212         */
213        public static void addInlinedPropertiesToEnvironment(ConfigurableApplicationContext context, String... inlinedProperties) {
214                Assert.notNull(context, "'context' must not be null");
215                Assert.notNull(inlinedProperties, "'inlinedProperties' must not be null");
216                addInlinedPropertiesToEnvironment(context.getEnvironment(), inlinedProperties);
217        }
218
219        /**
220         * Add the given <em>inlined properties</em> (in the form of <em>key-value</em>
221         * pairs) to the supplied {@link ConfigurableEnvironment environment}.
222         * <p>All key-value pairs will be added to the {@code Environment} as a
223         * single {@link MapPropertySource} with the highest precedence.
224         * <p>For details on the parsing of <em>inlined properties</em>, consult the
225         * Javadoc for {@link #convertInlinedPropertiesToMap}.
226         * @param environment the environment to update; never {@code null}
227         * @param inlinedProperties the inlined properties to add to the environment;
228         * potentially empty but never {@code null}
229         * @since 4.1.5
230         * @see MapPropertySource
231         * @see #INLINED_PROPERTIES_PROPERTY_SOURCE_NAME
232         * @see TestPropertySource#properties
233         * @see #convertInlinedPropertiesToMap
234         */
235        public static void addInlinedPropertiesToEnvironment(ConfigurableEnvironment environment, String... inlinedProperties) {
236                Assert.notNull(environment, "'environment' must not be null");
237                Assert.notNull(inlinedProperties, "'inlinedProperties' must not be null");
238                if (!ObjectUtils.isEmpty(inlinedProperties)) {
239                        if (logger.isDebugEnabled()) {
240                                logger.debug("Adding inlined properties to environment: " +
241                                                ObjectUtils.nullSafeToString(inlinedProperties));
242                        }
243                        MapPropertySource ps = (MapPropertySource)
244                                        environment.getPropertySources().get(INLINED_PROPERTIES_PROPERTY_SOURCE_NAME);
245                        if (ps == null) {
246                                ps = new MapPropertySource(INLINED_PROPERTIES_PROPERTY_SOURCE_NAME, new LinkedHashMap<>());
247                                environment.getPropertySources().addFirst(ps);
248                        }
249                        ps.getSource().putAll(convertInlinedPropertiesToMap(inlinedProperties));
250                }
251        }
252
253        /**
254         * Convert the supplied <em>inlined properties</em> (in the form of <em>key-value</em>
255         * pairs) into a map keyed by property name, preserving the ordering of property names
256         * in the returned map.
257         * <p>Parsing of the key-value pairs is achieved by converting all pairs
258         * into <em>virtual</em> properties files in memory and delegating to
259         * {@link Properties#load(java.io.Reader)} to parse each virtual file.
260         * <p>For a full discussion of <em>inlined properties</em>, consult the Javadoc
261         * for {@link TestPropertySource#properties}.
262         * @param inlinedProperties the inlined properties to convert; potentially empty
263         * but never {@code null}
264         * @return a new, ordered map containing the converted properties
265         * @throws IllegalStateException if a given key-value pair cannot be parsed, or if
266         * a given inlined property contains multiple key-value pairs
267         * @since 4.1.5
268         * @see #addInlinedPropertiesToEnvironment(ConfigurableEnvironment, String[])
269         */
270        public static Map<String, Object> convertInlinedPropertiesToMap(String... inlinedProperties) {
271                Assert.notNull(inlinedProperties, "'inlinedProperties' must not be null");
272                Map<String, Object> map = new LinkedHashMap<>();
273                Properties props = new Properties();
274
275                for (String pair : inlinedProperties) {
276                        if (!StringUtils.hasText(pair)) {
277                                continue;
278                        }
279                        try {
280                                props.load(new StringReader(pair));
281                        }
282                        catch (Exception ex) {
283                                throw new IllegalStateException("Failed to load test environment property from [" + pair + "]", ex);
284                        }
285                        Assert.state(props.size() == 1, () -> "Failed to load exactly one test environment property from [" + pair + "]");
286                        for (String name : props.stringPropertyNames()) {
287                                map.put(name, props.getProperty(name));
288                        }
289                        props.clear();
290                }
291
292                return map;
293        }
294
295}