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.lang.reflect.Method;
020import java.util.Map;
021import java.util.concurrent.ConcurrentHashMap;
022
023import org.springframework.context.ApplicationContext;
024import org.springframework.context.ConfigurableApplicationContext;
025import org.springframework.core.style.ToStringCreator;
026import org.springframework.lang.Nullable;
027import org.springframework.test.annotation.DirtiesContext.HierarchyMode;
028import org.springframework.test.context.CacheAwareContextLoaderDelegate;
029import org.springframework.test.context.MergedContextConfiguration;
030import org.springframework.test.context.TestContext;
031import org.springframework.util.Assert;
032import org.springframework.util.StringUtils;
033
034/**
035 * Default implementation of the {@link TestContext} interface.
036 *
037 * @author Sam Brannen
038 * @author Juergen Hoeller
039 * @author Rob Harrop
040 * @since 4.0
041 */
042public class DefaultTestContext implements TestContext {
043
044        private static final long serialVersionUID = -5827157174866681233L;
045
046        private final Map<String, Object> attributes = new ConcurrentHashMap<>(4);
047
048        private final CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate;
049
050        private final MergedContextConfiguration mergedContextConfiguration;
051
052        private final Class<?> testClass;
053
054        @Nullable
055        private volatile Object testInstance;
056
057        @Nullable
058        private volatile Method testMethod;
059
060        @Nullable
061        private volatile Throwable testException;
062
063
064        /**
065         * <em>Copy constructor</em> for creating a new {@code DefaultTestContext}
066         * based on the <em>attributes</em> and immutable state of the supplied context.
067         * <p><em>Immutable state</em> includes all arguments supplied to the
068         * {@linkplain #DefaultTestContext(Class, MergedContextConfiguration,
069         * CacheAwareContextLoaderDelegate) standard constructor}.
070         * @throws NullPointerException if the supplied {@code DefaultTestContext}
071         * is {@code null}
072         */
073        public DefaultTestContext(DefaultTestContext testContext) {
074                this(testContext.testClass, testContext.mergedContextConfiguration,
075                        testContext.cacheAwareContextLoaderDelegate);
076                this.attributes.putAll(testContext.attributes);
077        }
078
079        /**
080         * Construct a new {@code DefaultTestContext} from the supplied arguments.
081         * @param testClass the test class for this test context
082         * @param mergedContextConfiguration the merged application context
083         * configuration for this test context
084         * @param cacheAwareContextLoaderDelegate the delegate to use for loading
085         * and closing the application context for this test context
086         */
087        public DefaultTestContext(Class<?> testClass, MergedContextConfiguration mergedContextConfiguration,
088                        CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate) {
089
090                Assert.notNull(testClass, "Test Class must not be null");
091                Assert.notNull(mergedContextConfiguration, "MergedContextConfiguration must not be null");
092                Assert.notNull(cacheAwareContextLoaderDelegate, "CacheAwareContextLoaderDelegate must not be null");
093                this.testClass = testClass;
094                this.mergedContextConfiguration = mergedContextConfiguration;
095                this.cacheAwareContextLoaderDelegate = cacheAwareContextLoaderDelegate;
096        }
097
098        /**
099         * Determine if the {@linkplain ApplicationContext application context} for
100         * this test context is present in the context cache.
101         * @return {@code true} if the application context has already been loaded
102         * and stored in the context cache
103         * @since 5.2
104         * @see #getApplicationContext()
105         * @see CacheAwareContextLoaderDelegate#isContextLoaded
106         */
107        @Override
108        public boolean hasApplicationContext() {
109                return this.cacheAwareContextLoaderDelegate.isContextLoaded(this.mergedContextConfiguration);
110        }
111
112        /**
113         * Get the {@linkplain ApplicationContext application context} for this
114         * test context.
115         * <p>The default implementation delegates to the {@link CacheAwareContextLoaderDelegate}
116         * that was supplied when this {@code TestContext} was constructed.
117         * @throws IllegalStateException if the context returned by the context
118         * loader delegate is not <em>active</em> (i.e., has been closed)
119         * @see CacheAwareContextLoaderDelegate#loadContext
120         */
121        @Override
122        public ApplicationContext getApplicationContext() {
123                ApplicationContext context = this.cacheAwareContextLoaderDelegate.loadContext(this.mergedContextConfiguration);
124                if (context instanceof ConfigurableApplicationContext) {
125                        @SuppressWarnings("resource")
126                        ConfigurableApplicationContext cac = (ConfigurableApplicationContext) context;
127                        Assert.state(cac.isActive(), () ->
128                                        "The ApplicationContext loaded for [" + this.mergedContextConfiguration +
129                                        "] is not active. This may be due to one of the following reasons: " +
130                                        "1) the context was closed programmatically by user code; " +
131                                        "2) the context was closed during parallel test execution either " +
132                                        "according to @DirtiesContext semantics or due to automatic eviction " +
133                                        "from the ContextCache due to a maximum cache size policy.");
134                }
135                return context;
136        }
137
138        /**
139         * Mark the {@linkplain ApplicationContext application context} associated
140         * with this test context as <em>dirty</em> (i.e., by removing it from the
141         * context cache and closing it).
142         * <p>The default implementation delegates to the {@link CacheAwareContextLoaderDelegate}
143         * that was supplied when this {@code TestContext} was constructed.
144         * @see CacheAwareContextLoaderDelegate#closeContext
145         */
146        @Override
147        public void markApplicationContextDirty(@Nullable HierarchyMode hierarchyMode) {
148                this.cacheAwareContextLoaderDelegate.closeContext(this.mergedContextConfiguration, hierarchyMode);
149        }
150
151        @Override
152        public final Class<?> getTestClass() {
153                return this.testClass;
154        }
155
156        @Override
157        public final Object getTestInstance() {
158                Object testInstance = this.testInstance;
159                Assert.state(testInstance != null, "No test instance");
160                return testInstance;
161        }
162
163        @Override
164        public final Method getTestMethod() {
165                Method testMethod = this.testMethod;
166                Assert.state(testMethod != null, "No test method");
167                return testMethod;
168        }
169
170        @Override
171        @Nullable
172        public final Throwable getTestException() {
173                return this.testException;
174        }
175
176        @Override
177        public void updateState(@Nullable Object testInstance, @Nullable Method testMethod, @Nullable Throwable testException) {
178                this.testInstance = testInstance;
179                this.testMethod = testMethod;
180                this.testException = testException;
181        }
182
183        @Override
184        public void setAttribute(String name, @Nullable Object value) {
185                Assert.notNull(name, "Name must not be null");
186                synchronized (this.attributes) {
187                        if (value != null) {
188                                this.attributes.put(name, value);
189                        }
190                        else {
191                                this.attributes.remove(name);
192                        }
193                }
194        }
195
196        @Override
197        @Nullable
198        public Object getAttribute(String name) {
199                Assert.notNull(name, "Name must not be null");
200                return this.attributes.get(name);
201        }
202
203        @Override
204        @Nullable
205        public Object removeAttribute(String name) {
206                Assert.notNull(name, "Name must not be null");
207                return this.attributes.remove(name);
208        }
209
210        @Override
211        public boolean hasAttribute(String name) {
212                Assert.notNull(name, "Name must not be null");
213                return this.attributes.containsKey(name);
214        }
215
216        @Override
217        public String[] attributeNames() {
218                synchronized (this.attributes) {
219                        return StringUtils.toStringArray(this.attributes.keySet());
220                }
221        }
222
223
224        /**
225         * Provide a String representation of this test context's state.
226         */
227        @Override
228        public String toString() {
229                return new ToStringCreator(this)
230                                .append("testClass", this.testClass)
231                                .append("testInstance", this.testInstance)
232                                .append("testMethod", this.testMethod)
233                                .append("testException", this.testException)
234                                .append("mergedContextConfiguration", this.mergedContextConfiguration)
235                                .append("attributes", this.attributes)
236                                .toString();
237        }
238
239}