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