001/* 002 * Copyright 2012-2018 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 * http://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.boot.test.util; 018 019import java.io.Closeable; 020import java.util.Arrays; 021import java.util.Collections; 022import java.util.LinkedHashMap; 023import java.util.Map; 024import java.util.Objects; 025import java.util.concurrent.Callable; 026import java.util.stream.Stream; 027import java.util.stream.StreamSupport; 028 029import org.springframework.boot.context.properties.source.ConfigurationPropertySources; 030import org.springframework.context.ApplicationContext; 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.MutablePropertySources; 036import org.springframework.core.env.PropertySource; 037import org.springframework.core.env.StandardEnvironment; 038import org.springframework.core.env.SystemEnvironmentPropertySource; 039import org.springframework.util.Assert; 040import org.springframework.util.StringUtils; 041 042/** 043 * Test utilities for adding properties. Properties can be applied to a Spring 044 * {@link Environment} or to the {@link System#getProperties() system environment}. 045 * 046 * @author Madhura Bhave 047 * @author Phillip Webb 048 * @author Stephane Nicoll 049 * @since 2.0.0 050 */ 051public final class TestPropertyValues { 052 053 private static final TestPropertyValues EMPTY = new TestPropertyValues( 054 Collections.emptyMap()); 055 056 private final Map<String, Object> properties; 057 058 private TestPropertyValues(Map<String, Object> properties) { 059 this.properties = Collections.unmodifiableMap(properties); 060 } 061 062 /** 063 * Builder method to add more properties. 064 * @param pairs the property pairs to add 065 * @return a new {@link TestPropertyValues} instance 066 */ 067 public TestPropertyValues and(String... pairs) { 068 return and(Arrays.stream(pairs).map(Pair::parse)); 069 } 070 071 private TestPropertyValues and(Stream<Pair> pairs) { 072 Map<String, Object> properties = new LinkedHashMap<>(this.properties); 073 pairs.filter(Objects::nonNull).forEach((pair) -> pair.addTo(properties)); 074 return new TestPropertyValues(properties); 075 } 076 077 /** 078 * Add the properties from the underlying map to the environment owned by an 079 * {@link ApplicationContext}. 080 * @param context the context with an environment to modify 081 */ 082 public void applyTo(ConfigurableApplicationContext context) { 083 applyTo(context.getEnvironment()); 084 } 085 086 /** 087 * Add the properties from the underlying map to the environment. The default property 088 * source used is {@link MapPropertySource}. 089 * @param environment the environment that needs to be modified 090 */ 091 public void applyTo(ConfigurableEnvironment environment) { 092 applyTo(environment, Type.MAP); 093 } 094 095 /** 096 * Add the properties from the underlying map to the environment using the specified 097 * property source type. 098 * @param environment the environment that needs to be modified 099 * @param type the type of {@link PropertySource} to be added. See {@link Type} 100 */ 101 public void applyTo(ConfigurableEnvironment environment, Type type) { 102 applyTo(environment, type, type.applySuffix("test")); 103 } 104 105 /** 106 * Add the properties from the underlying map to the environment using the specified 107 * property source type and name. 108 * @param environment the environment that needs to be modified 109 * @param type the type of {@link PropertySource} to be added. See {@link Type} 110 * @param name the name for the property source 111 */ 112 public void applyTo(ConfigurableEnvironment environment, Type type, String name) { 113 Assert.notNull(environment, "Environment must not be null"); 114 Assert.notNull(type, "Property source type must not be null"); 115 Assert.notNull(name, "Property source name must not be null"); 116 MutablePropertySources sources = environment.getPropertySources(); 117 addToSources(sources, type, name); 118 ConfigurationPropertySources.attach(environment); 119 } 120 121 /** 122 * Add the properties to the {@link System#getProperties() system properties} for the 123 * duration of the {@code call}, restoring previous values when the call completes. 124 * @param <T> the result type 125 * @param call the call to make 126 * @return the result of the call 127 */ 128 public <T> T applyToSystemProperties(Callable<T> call) { 129 try (SystemPropertiesHandler handler = new SystemPropertiesHandler()) { 130 return call.call(); 131 } 132 catch (Exception ex) { 133 rethrow(ex); 134 throw new IllegalStateException("Original cause not rethrown", ex); 135 } 136 } 137 138 @SuppressWarnings("unchecked") 139 private <E extends Throwable> void rethrow(Throwable e) throws E { 140 throw (E) e; 141 } 142 143 @SuppressWarnings("unchecked") 144 private void addToSources(MutablePropertySources sources, Type type, String name) { 145 if (sources.contains(name)) { 146 PropertySource<?> propertySource = sources.get(name); 147 if (propertySource.getClass() == type.getSourceClass()) { 148 ((Map<String, Object>) propertySource.getSource()) 149 .putAll(this.properties); 150 return; 151 } 152 } 153 Map<String, Object> source = new LinkedHashMap<>(this.properties); 154 sources.addFirst((type.equals(Type.MAP) ? new MapPropertySource(name, source) 155 : new SystemEnvironmentPropertySource(name, source))); 156 } 157 158 /** 159 * Return a new {@link TestPropertyValues} with the underlying map populated with the 160 * given property pairs. Name-value pairs can be specified with colon (":") or equals 161 * ("=") separators. 162 * @param pairs the name-value pairs for properties that need to be added to the 163 * environment 164 * @return the new instance 165 */ 166 public static TestPropertyValues of(String... pairs) { 167 return of(Stream.of(pairs)); 168 } 169 170 /** 171 * Return a new {@link TestPropertyValues} with the underlying map populated with the 172 * given property pairs. Name-value pairs can be specified with colon (":") or equals 173 * ("=") separators. 174 * @param pairs the name-value pairs for properties that need to be added to the 175 * environment 176 * @return the new instance 177 */ 178 public static TestPropertyValues of(Iterable<String> pairs) { 179 if (pairs == null) { 180 return empty(); 181 } 182 return of(StreamSupport.stream(pairs.spliterator(), false)); 183 } 184 185 /** 186 * Return a new {@link TestPropertyValues} with the underlying map populated with the 187 * given property pairs. Name-value pairs can be specified with colon (":") or equals 188 * ("=") separators. 189 * @param pairs the name-value pairs for properties that need to be added to the 190 * environment 191 * @return the new instance 192 */ 193 public static TestPropertyValues of(Stream<String> pairs) { 194 if (pairs == null) { 195 return empty(); 196 } 197 return empty().and(pairs.map(Pair::parse)); 198 } 199 200 /** 201 * Return an empty {@link TestPropertyValues} instance. 202 * @return an empty instance 203 */ 204 public static TestPropertyValues empty() { 205 return EMPTY; 206 } 207 208 /** 209 * The type of property source. 210 */ 211 public enum Type { 212 213 /** 214 * Used for {@link SystemEnvironmentPropertySource}. 215 */ 216 SYSTEM_ENVIRONMENT(SystemEnvironmentPropertySource.class, 217 StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME), 218 219 /** 220 * Used for {@link MapPropertySource}. 221 */ 222 MAP(MapPropertySource.class, null); 223 224 private final Class<? extends MapPropertySource> sourceClass; 225 226 private final String suffix; 227 228 Type(Class<? extends MapPropertySource> sourceClass, String suffix) { 229 this.sourceClass = sourceClass; 230 this.suffix = suffix; 231 } 232 233 public Class<? extends MapPropertySource> getSourceClass() { 234 return this.sourceClass; 235 } 236 237 protected String applySuffix(String name) { 238 return (this.suffix != null) ? name + "-" + this.suffix : name; 239 } 240 241 } 242 243 /** 244 * A single name value pair. 245 */ 246 public static class Pair { 247 248 private String name; 249 250 private String value; 251 252 public Pair(String name, String value) { 253 Assert.hasLength(name, "Name must not be empty"); 254 this.name = name; 255 this.value = value; 256 } 257 258 public void addTo(Map<String, Object> properties) { 259 properties.put(this.name, this.value); 260 } 261 262 public static Pair parse(String pair) { 263 int index = getSeparatorIndex(pair); 264 String name = (index > 0) ? pair.substring(0, index) : pair; 265 String value = (index > 0) ? pair.substring(index + 1) : ""; 266 return of(name.trim(), value.trim()); 267 } 268 269 private static int getSeparatorIndex(String pair) { 270 int colonIndex = pair.indexOf(':'); 271 int equalIndex = pair.indexOf('='); 272 if (colonIndex == -1) { 273 return equalIndex; 274 } 275 if (equalIndex == -1) { 276 return colonIndex; 277 } 278 return Math.min(colonIndex, equalIndex); 279 } 280 281 private static Pair of(String name, String value) { 282 if (StringUtils.isEmpty(name) && StringUtils.isEmpty(value)) { 283 return null; 284 } 285 return new Pair(name, value); 286 } 287 288 } 289 290 /** 291 * Handler to apply and restore system properties. 292 */ 293 private class SystemPropertiesHandler implements Closeable { 294 295 private final Map<String, String> previous; 296 297 SystemPropertiesHandler() { 298 this.previous = apply(TestPropertyValues.this.properties); 299 } 300 301 private Map<String, String> apply(Map<String, ?> properties) { 302 Map<String, String> previous = new LinkedHashMap<>(); 303 properties.forEach((name, value) -> previous.put(name, 304 setOrClear(name, (String) value))); 305 return previous; 306 } 307 308 @Override 309 public void close() { 310 this.previous.forEach(this::setOrClear); 311 } 312 313 private String setOrClear(String name, String value) { 314 Assert.notNull(name, "Name must not be null"); 315 if (StringUtils.isEmpty(value)) { 316 return (String) System.getProperties().remove(name); 317 } 318 return (String) System.getProperties().setProperty(name, value); 319 } 320 321 } 322 323}