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