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}