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;
018
019import java.io.Serializable;
020import java.util.Arrays;
021import java.util.Collections;
022import java.util.LinkedHashSet;
023import java.util.Set;
024
025import org.springframework.context.ApplicationContext;
026import org.springframework.context.ApplicationContextInitializer;
027import org.springframework.context.ConfigurableApplicationContext;
028import org.springframework.core.style.ToStringCreator;
029import org.springframework.util.Assert;
030import org.springframework.util.ObjectUtils;
031import org.springframework.util.StringUtils;
032
033/**
034 * {@code MergedContextConfiguration} encapsulates the <em>merged</em>
035 * context configuration declared on a test class and all of its superclasses
036 * via {@link ContextConfiguration @ContextConfiguration},
037 * {@link ActiveProfiles @ActiveProfiles}, and
038 * {@link TestPropertySource @TestPropertySource}.
039 *
040 * <p>Merged context resource locations, annotated classes, active profiles,
041 * property resource locations, and in-lined properties represent all declared
042 * values in the test class hierarchy taking into consideration the semantics
043 * of the {@link ContextConfiguration#inheritLocations},
044 * {@link ActiveProfiles#inheritProfiles},
045 * {@link TestPropertySource#inheritLocations}, and
046 * {@link TestPropertySource#inheritProperties} flags.
047 *
048 * <p>A {@link SmartContextLoader} uses {@code MergedContextConfiguration}
049 * to load an {@link org.springframework.context.ApplicationContext ApplicationContext}.
050 *
051 * <p>{@code MergedContextConfiguration} is also used by the
052 * {@link org.springframework.test.context.cache.ContextCache ContextCache}
053 * as the key for caching an
054 * {@link org.springframework.context.ApplicationContext ApplicationContext}
055 * that was loaded using properties of this {@code MergedContextConfiguration}.
056 *
057 * @author Sam Brannen
058 * @author Phillip Webb
059 * @since 3.1
060 * @see ContextConfiguration
061 * @see ContextHierarchy
062 * @see ActiveProfiles
063 * @see TestPropertySource
064 * @see ContextConfigurationAttributes
065 * @see SmartContextLoader#loadContext(MergedContextConfiguration)
066 */
067public class MergedContextConfiguration implements Serializable {
068
069        private static final long serialVersionUID = -3290560718464957422L;
070
071        private static final String[] EMPTY_STRING_ARRAY = new String[0];
072
073        private static final Class<?>[] EMPTY_CLASS_ARRAY = new Class<?>[0];
074
075        private static final Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> EMPTY_INITIALIZER_CLASSES =
076                        Collections.<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> emptySet();
077
078        private static final Set<ContextCustomizer> EMPTY_CONTEXT_CUSTOMIZERS = Collections.<ContextCustomizer> emptySet();
079
080
081        private final Class<?> testClass;
082
083        private final String[] locations;
084
085        private final Class<?>[] classes;
086
087        private final Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> contextInitializerClasses;
088
089        private final String[] activeProfiles;
090
091        private final String[] propertySourceLocations;
092
093        private final String[] propertySourceProperties;
094
095        private final Set<ContextCustomizer> contextCustomizers;
096
097        private final ContextLoader contextLoader;
098
099        private final CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate;
100
101        private final MergedContextConfiguration parent;
102
103
104        private static String[] processStrings(String[] array) {
105                return (array != null ? array : EMPTY_STRING_ARRAY);
106        }
107
108        private static Class<?>[] processClasses(Class<?>[] classes) {
109                return (classes != null ? classes : EMPTY_CLASS_ARRAY);
110        }
111
112        private static Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> processContextInitializerClasses(
113                        Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> contextInitializerClasses) {
114
115                return (contextInitializerClasses != null ?
116                                Collections.unmodifiableSet(contextInitializerClasses) : EMPTY_INITIALIZER_CLASSES);
117        }
118
119        private static Set<ContextCustomizer> processContextCustomizers(Set<ContextCustomizer> contextCustomizers) {
120                return (contextCustomizers != null ?
121                                Collections.unmodifiableSet(contextCustomizers) : EMPTY_CONTEXT_CUSTOMIZERS);
122        }
123
124        private static String[] processActiveProfiles(String[] activeProfiles) {
125                if (activeProfiles == null) {
126                        return EMPTY_STRING_ARRAY;
127                }
128
129                // Active profiles must be unique
130                Set<String> profilesSet = new LinkedHashSet<String>(Arrays.asList(activeProfiles));
131                return StringUtils.toStringArray(profilesSet);
132        }
133
134        /**
135         * Generate a null-safe {@link String} representation of the supplied
136         * {@link ContextLoader} based solely on the fully qualified name of the
137         * loader or &quot;null&quot; if the supplied loaded is {@code null}.
138         */
139        protected static String nullSafeToString(ContextLoader contextLoader) {
140                return (contextLoader != null ? contextLoader.getClass().getName() : "null");
141        }
142
143
144        /**
145         * Create a new {@code MergedContextConfiguration} instance for the
146         * supplied parameters.
147         * <p>Delegates to
148         * {@link #MergedContextConfiguration(Class, String[], Class[], Set, String[], String[], String[], ContextLoader, CacheAwareContextLoaderDelegate, MergedContextConfiguration)}.
149         * @param testClass the test class for which the configuration was merged
150         * @param locations the merged context resource locations
151         * @param classes the merged annotated classes
152         * @param activeProfiles the merged active bean definition profiles
153         * @param contextLoader the resolved {@code ContextLoader}
154         */
155        public MergedContextConfiguration(Class<?> testClass, String[] locations, Class<?>[] classes,
156                        String[] activeProfiles, ContextLoader contextLoader) {
157
158                this(testClass, locations, classes, null, activeProfiles, contextLoader);
159        }
160
161        /**
162         * Create a new {@code MergedContextConfiguration} instance for the
163         * supplied parameters.
164         * <p>Delegates to
165         * {@link #MergedContextConfiguration(Class, String[], Class[], Set, String[], String[], String[], ContextLoader, CacheAwareContextLoaderDelegate, MergedContextConfiguration)}.
166         * @param testClass the test class for which the configuration was merged
167         * @param locations the merged context resource locations
168         * @param classes the merged annotated classes
169         * @param contextInitializerClasses the merged context initializer classes
170         * @param activeProfiles the merged active bean definition profiles
171         * @param contextLoader the resolved {@code ContextLoader}
172         * @see #MergedContextConfiguration(Class, String[], Class[], Set, String[], ContextLoader, CacheAwareContextLoaderDelegate, MergedContextConfiguration)
173         */
174        public MergedContextConfiguration(Class<?> testClass, String[] locations, Class<?>[] classes,
175                        Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> contextInitializerClasses,
176                        String[] activeProfiles, ContextLoader contextLoader) {
177
178                this(testClass, locations, classes, contextInitializerClasses, activeProfiles, contextLoader, null, null);
179        }
180
181        /**
182         * Create a new {@code MergedContextConfiguration} instance for the
183         * supplied parameters.
184         * <p>Delegates to
185         * {@link #MergedContextConfiguration(Class, String[], Class[], Set, String[], String[], String[], ContextLoader, CacheAwareContextLoaderDelegate, MergedContextConfiguration)}.
186         * @param testClass the test class for which the configuration was merged
187         * @param locations the merged context resource locations
188         * @param classes the merged annotated classes
189         * @param contextInitializerClasses the merged context initializer classes
190         * @param activeProfiles the merged active bean definition profiles
191         * @param contextLoader the resolved {@code ContextLoader}
192         * @param cacheAwareContextLoaderDelegate a cache-aware context loader
193         * delegate with which to retrieve the parent context
194         * @param parent the parent configuration or {@code null} if there is no parent
195         * @since 3.2.2
196         */
197        public MergedContextConfiguration(Class<?> testClass, String[] locations, Class<?>[] classes,
198                        Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> contextInitializerClasses,
199                        String[] activeProfiles, ContextLoader contextLoader,
200                        CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate, MergedContextConfiguration parent) {
201
202                this(testClass, locations, classes, contextInitializerClasses, activeProfiles, null, null, contextLoader,
203                        cacheAwareContextLoaderDelegate, parent);
204        }
205
206        /**
207         * Create a new {@code MergedContextConfiguration} instance by copying
208         * all fields from the supplied {@code MergedContextConfiguration}.
209         * @since 4.1
210         */
211        public MergedContextConfiguration(MergedContextConfiguration mergedConfig) {
212                this(mergedConfig.testClass, mergedConfig.locations, mergedConfig.classes,
213                        mergedConfig.contextInitializerClasses, mergedConfig.activeProfiles, mergedConfig.propertySourceLocations,
214                        mergedConfig.propertySourceProperties, mergedConfig.contextCustomizers,
215                        mergedConfig.contextLoader, mergedConfig.cacheAwareContextLoaderDelegate, mergedConfig.parent);
216        }
217
218        /**
219         * Create a new {@code MergedContextConfiguration} instance for the
220         * supplied parameters.
221         * <p>If a {@code null} value is supplied for {@code locations},
222         * {@code classes}, {@code activeProfiles}, {@code propertySourceLocations},
223         * or {@code propertySourceProperties} an empty array will be stored instead.
224         * If a {@code null} value is supplied for the
225         * {@code contextInitializerClasses} an empty set will be stored instead.
226         * Furthermore, active profiles will be sorted, and duplicate profiles
227         * will be removed.
228         * @param testClass the test class for which the configuration was merged
229         * @param locations the merged context resource locations
230         * @param classes the merged annotated classes
231         * @param contextInitializerClasses the merged context initializer classes
232         * @param activeProfiles the merged active bean definition profiles
233         * @param propertySourceLocations the merged {@code PropertySource} locations
234         * @param propertySourceProperties the merged {@code PropertySource} properties
235         * @param contextLoader the resolved {@code ContextLoader}
236         * @param cacheAwareContextLoaderDelegate a cache-aware context loader
237         * delegate with which to retrieve the parent context
238         * @param parent the parent configuration or {@code null} if there is no parent
239         * @since 4.1
240         */
241        public MergedContextConfiguration(Class<?> testClass, String[] locations, Class<?>[] classes,
242                        Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> contextInitializerClasses,
243                        String[] activeProfiles, String[] propertySourceLocations, String[] propertySourceProperties,
244                        ContextLoader contextLoader, CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate,
245                        MergedContextConfiguration parent) {
246                this(testClass, locations, classes, contextInitializerClasses, activeProfiles,
247                                propertySourceLocations, propertySourceProperties,
248                                EMPTY_CONTEXT_CUSTOMIZERS, contextLoader,
249                                cacheAwareContextLoaderDelegate, parent);
250        }
251
252        /**
253         * Create a new {@code MergedContextConfiguration} instance for the
254         * supplied parameters.
255         * <p>If a {@code null} value is supplied for {@code locations},
256         * {@code classes}, {@code activeProfiles}, {@code propertySourceLocations},
257         * or {@code propertySourceProperties} an empty array will be stored instead.
258         * If a {@code null} value is supplied for {@code contextInitializerClasses}
259         * or {@code contextCustomizers}, an empty set will be stored instead.
260         * Furthermore, active profiles will be sorted, and duplicate profiles
261         * will be removed.
262         * @param testClass the test class for which the configuration was merged
263         * @param locations the merged context resource locations
264         * @param classes the merged annotated classes
265         * @param contextInitializerClasses the merged context initializer classes
266         * @param activeProfiles the merged active bean definition profiles
267         * @param propertySourceLocations the merged {@code PropertySource} locations
268         * @param propertySourceProperties the merged {@code PropertySource} properties
269         * @param contextCustomizers the context customizers
270         * @param contextLoader the resolved {@code ContextLoader}
271         * @param cacheAwareContextLoaderDelegate a cache-aware context loader
272         * delegate with which to retrieve the parent context
273         * @param parent the parent configuration or {@code null} if there is no parent
274         * @since 4.3
275         */
276        public MergedContextConfiguration(Class<?> testClass, String[] locations, Class<?>[] classes,
277                        Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> contextInitializerClasses,
278                        String[] activeProfiles, String[] propertySourceLocations, String[] propertySourceProperties,
279                        Set<ContextCustomizer> contextCustomizers, ContextLoader contextLoader,
280                        CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate, MergedContextConfiguration parent) {
281
282                this.testClass = testClass;
283                this.locations = processStrings(locations);
284                this.classes = processClasses(classes);
285                this.contextInitializerClasses = processContextInitializerClasses(contextInitializerClasses);
286                this.activeProfiles = processActiveProfiles(activeProfiles);
287                this.propertySourceLocations = processStrings(propertySourceLocations);
288                this.propertySourceProperties = processStrings(propertySourceProperties);
289                this.contextCustomizers = processContextCustomizers(contextCustomizers);
290                this.contextLoader = contextLoader;
291                this.cacheAwareContextLoaderDelegate = cacheAwareContextLoaderDelegate;
292                this.parent = parent;
293        }
294
295
296        /**
297         * Get the {@linkplain Class test class} associated with this
298         * {@code MergedContextConfiguration}.
299         */
300        public Class<?> getTestClass() {
301                return this.testClass;
302        }
303
304        /**
305         * Get the merged resource locations for {@code ApplicationContext}
306         * configuration files for the {@linkplain #getTestClass() test class}.
307         * <p>Context resource locations typically represent XML configuration
308         * files or Groovy scripts.
309         */
310        public String[] getLocations() {
311                return this.locations;
312        }
313
314        /**
315         * Get the merged annotated classes for the {@linkplain #getTestClass() test class}.
316         */
317        public Class<?>[] getClasses() {
318                return this.classes;
319        }
320
321        /**
322         * Determine if this {@code MergedContextConfiguration} instance has
323         * path-based context resource locations.
324         * @return {@code true} if the {@link #getLocations() locations} array is not empty
325         * @since 4.0.4
326         * @see #hasResources()
327         * @see #hasClasses()
328         */
329        public boolean hasLocations() {
330                return !ObjectUtils.isEmpty(getLocations());
331        }
332
333        /**
334         * Determine if this {@code MergedContextConfiguration} instance has
335         * class-based resources.
336         * @return {@code true} if the {@link #getClasses() classes} array is not empty
337         * @since 4.0.4
338         * @see #hasResources()
339         * @see #hasLocations()
340         */
341        public boolean hasClasses() {
342                return !ObjectUtils.isEmpty(getClasses());
343        }
344
345        /**
346         * Determine if this {@code MergedContextConfiguration} instance has
347         * either path-based context resource locations or class-based resources.
348         * @return {@code true} if either the {@link #getLocations() locations}
349         * or the {@link #getClasses() classes} array is not empty
350         * @since 4.0.4
351         * @see #hasLocations()
352         * @see #hasClasses()
353         */
354        public boolean hasResources() {
355                return (hasLocations() || hasClasses());
356        }
357
358        /**
359         * Get the merged {@code ApplicationContextInitializer} classes for the
360         * {@linkplain #getTestClass() test class}.
361         */
362        public Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> getContextInitializerClasses() {
363                return this.contextInitializerClasses;
364        }
365
366        /**
367         * Get the merged active bean definition profiles for the
368         * {@linkplain #getTestClass() test class}.
369         * @see ActiveProfiles
370         */
371        public String[] getActiveProfiles() {
372                return this.activeProfiles;
373        }
374
375        /**
376         * Get the merged resource locations for test {@code PropertySources}
377         * for the {@linkplain #getTestClass() test class}.
378         * @see TestPropertySource#locations
379         * @see java.util.Properties
380         */
381        public String[] getPropertySourceLocations() {
382                return this.propertySourceLocations;
383        }
384
385        /**
386         * Get the merged test {@code PropertySource} properties for the
387         * {@linkplain #getTestClass() test class}.
388         * <p>Properties will be loaded into the {@code Environment}'s set of
389         * {@code PropertySources}.
390         * @see TestPropertySource#properties
391         * @see java.util.Properties
392         */
393        public String[] getPropertySourceProperties() {
394                return this.propertySourceProperties;
395        }
396
397        /**
398         * Get the merged {@link ContextCustomizer ContextCustomizers} that will be applied
399         * when the application context is loaded.
400         */
401        public Set<ContextCustomizer> getContextCustomizers() {
402                return this.contextCustomizers;
403        }
404
405        /**
406         * Get the resolved {@link ContextLoader} for the {@linkplain #getTestClass() test class}.
407         */
408        public ContextLoader getContextLoader() {
409                return this.contextLoader;
410        }
411
412        /**
413         * Get the {@link MergedContextConfiguration} for the parent application context
414         * in a context hierarchy.
415         * @return the parent configuration or {@code null} if there is no parent
416         * @see #getParentApplicationContext()
417         * @since 3.2.2
418         */
419        public MergedContextConfiguration getParent() {
420                return this.parent;
421        }
422
423        /**
424         * Get the parent {@link ApplicationContext} for the context defined by this
425         * {@code MergedContextConfiguration} from the context cache.
426         * <p>If the parent context has not yet been loaded, it will be loaded, stored
427         * in the cache, and then returned.
428         * @return the parent {@code ApplicationContext} or {@code null} if there is no parent
429         * @see #getParent()
430         * @since 3.2.2
431         */
432        public ApplicationContext getParentApplicationContext() {
433                if (this.parent == null) {
434                        return null;
435                }
436                Assert.state(this.cacheAwareContextLoaderDelegate != null,
437                                "Cannot retrieve a parent application context without access to the CacheAwareContextLoaderDelegate");
438                return this.cacheAwareContextLoaderDelegate.loadContext(this.parent);
439        }
440
441
442        /**
443         * Determine if the supplied object is equal to this {@code MergedContextConfiguration}
444         * instance by comparing both object's {@linkplain #getLocations() locations},
445         * {@linkplain #getClasses() annotated classes},
446         * {@linkplain #getContextInitializerClasses() context initializer classes},
447         * {@linkplain #getActiveProfiles() active profiles},
448         * {@linkplain #getPropertySourceLocations() property source locations},
449         * {@linkplain #getPropertySourceProperties() property source properties},
450         * {@linkplain #getParent() parents}, and the fully qualified names of their
451         * {@link #getContextLoader() ContextLoaders}.
452         */
453        @Override
454        public boolean equals(Object other) {
455                if (this == other) {
456                        return true;
457                }
458                if (other == null || other.getClass() != getClass()) {
459                        return false;
460                }
461
462                MergedContextConfiguration otherConfig = (MergedContextConfiguration) other;
463                if (!Arrays.equals(this.locations, otherConfig.locations)) {
464                        return false;
465                }
466                if (!Arrays.equals(this.classes, otherConfig.classes)) {
467                        return false;
468                }
469                if (!this.contextInitializerClasses.equals(otherConfig.contextInitializerClasses)) {
470                        return false;
471                }
472                if (!Arrays.equals(this.activeProfiles, otherConfig.activeProfiles)) {
473                        return false;
474                }
475                if (!Arrays.equals(this.propertySourceLocations, otherConfig.propertySourceLocations)) {
476                        return false;
477                }
478                if (!Arrays.equals(this.propertySourceProperties, otherConfig.propertySourceProperties)) {
479                        return false;
480                }
481                if (!this.contextCustomizers.equals(otherConfig.contextCustomizers)) {
482                        return false;
483                }
484
485                if (this.parent == null) {
486                        if (otherConfig.parent != null) {
487                                return false;
488                        }
489                }
490                else if (!this.parent.equals(otherConfig.parent)) {
491                        return false;
492                }
493
494                if (!nullSafeToString(this.contextLoader).equals(nullSafeToString(otherConfig.contextLoader))) {
495                        return false;
496                }
497
498                return true;
499        }
500
501        /**
502         * Generate a unique hash code for all properties of this
503         * {@code MergedContextConfiguration} excluding the
504         * {@linkplain #getTestClass() test class}.
505         */
506        @Override
507        public int hashCode() {
508                int result = Arrays.hashCode(this.locations);
509                result = 31 * result + Arrays.hashCode(this.classes);
510                result = 31 * result + this.contextInitializerClasses.hashCode();
511                result = 31 * result + Arrays.hashCode(this.activeProfiles);
512                result = 31 * result + Arrays.hashCode(this.propertySourceLocations);
513                result = 31 * result + Arrays.hashCode(this.propertySourceProperties);
514                result = 31 * result + this.contextCustomizers.hashCode();
515                result = 31 * result + (this.parent != null ? this.parent.hashCode() : 0);
516                result = 31 * result + nullSafeToString(this.contextLoader).hashCode();
517                return result;
518        }
519
520        /**
521         * Provide a String representation of the {@linkplain #getTestClass() test class},
522         * {@linkplain #getLocations() locations}, {@linkplain #getClasses() annotated classes},
523         * {@linkplain #getContextInitializerClasses() context initializer classes},
524         * {@linkplain #getActiveProfiles() active profiles},
525         * {@linkplain #getPropertySourceLocations() property source locations},
526         * {@linkplain #getPropertySourceProperties() property source properties},
527         * {@linkplain #getContextCustomizers() context customizers},
528         * the name of the {@link #getContextLoader() ContextLoader}, and the
529         * {@linkplain #getParent() parent configuration}.
530         */
531        @Override
532        public String toString() {
533                return new ToStringCreator(this)
534                                .append("testClass", this.testClass)
535                                .append("locations", ObjectUtils.nullSafeToString(this.locations))
536                                .append("classes", ObjectUtils.nullSafeToString(this.classes))
537                                .append("contextInitializerClasses", ObjectUtils.nullSafeToString(this.contextInitializerClasses))
538                                .append("activeProfiles", ObjectUtils.nullSafeToString(this.activeProfiles))
539                                .append("propertySourceLocations", ObjectUtils.nullSafeToString(this.propertySourceLocations))
540                                .append("propertySourceProperties", ObjectUtils.nullSafeToString(this.propertySourceProperties))
541                                .append("contextCustomizers", this.contextCustomizers)
542                                .append("contextLoader", nullSafeToString(this.contextLoader))
543                                .append("parent", this.parent)
544                                .toString();
545        }
546
547}