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}