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}