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.cache;
018
019import java.util.ArrayList;
020import java.util.Collections;
021import java.util.HashSet;
022import java.util.LinkedHashMap;
023import java.util.List;
024import java.util.Map;
025import java.util.Set;
026import java.util.concurrent.ConcurrentHashMap;
027import java.util.concurrent.atomic.AtomicInteger;
028
029import org.apache.commons.logging.Log;
030import org.apache.commons.logging.LogFactory;
031
032import org.springframework.context.ApplicationContext;
033import org.springframework.context.ConfigurableApplicationContext;
034import org.springframework.core.style.ToStringCreator;
035import org.springframework.test.annotation.DirtiesContext.HierarchyMode;
036import org.springframework.test.context.MergedContextConfiguration;
037import org.springframework.util.Assert;
038
039/**
040 * Default implementation of the {@link ContextCache} API.
041 *
042 * <p>Uses a synchronized {@link Map} configured with a maximum size
043 * and a <em>least recently used</em> (LRU) eviction policy to cache
044 * {@link ApplicationContext} instances.
045 *
046 * <p>The maximum size may be supplied as a {@linkplain #DefaultContextCache(int)
047 * constructor argument} or set via a system property or Spring property named
048 * {@code spring.test.context.cache.maxSize}.
049 *
050 * @author Sam Brannen
051 * @author Juergen Hoeller
052 * @since 2.5
053 * @see ContextCacheUtils#retrieveMaxCacheSize()
054 */
055public class DefaultContextCache implements ContextCache {
056
057        private static final Log statsLogger = LogFactory.getLog(CONTEXT_CACHE_LOGGING_CATEGORY);
058
059        /**
060         * Map of context keys to Spring {@code ApplicationContext} instances.
061         */
062        private final Map<MergedContextConfiguration, ApplicationContext> contextMap =
063                        Collections.synchronizedMap(new LruCache(32, 0.75f));
064
065        /**
066         * Map of parent keys to sets of children keys, representing a top-down <em>tree</em>
067         * of context hierarchies. This information is used for determining which subtrees
068         * need to be recursively removed and closed when removing a context that is a parent
069         * of other contexts.
070         */
071        private final Map<MergedContextConfiguration, Set<MergedContextConfiguration>> hierarchyMap =
072                        new ConcurrentHashMap<MergedContextConfiguration, Set<MergedContextConfiguration>>(32);
073
074        private final int maxSize;
075
076        private final AtomicInteger hitCount = new AtomicInteger();
077
078        private final AtomicInteger missCount = new AtomicInteger();
079
080
081        /**
082         * Create a new {@code DefaultContextCache} using the maximum cache size
083         * obtained via {@link ContextCacheUtils#retrieveMaxCacheSize()}.
084         * @since 4.3
085         * @see #DefaultContextCache(int)
086         * @see ContextCacheUtils#retrieveMaxCacheSize()
087         */
088        public DefaultContextCache() {
089                this(ContextCacheUtils.retrieveMaxCacheSize());
090        }
091
092        /**
093         * Create a new {@code DefaultContextCache} using the supplied maximum
094         * cache size.
095         * @param maxSize the maximum cache size
096         * @throws IllegalArgumentException if the supplied {@code maxSize} value
097         * is not positive
098         * @since 4.3
099         * @see #DefaultContextCache()
100         */
101        public DefaultContextCache(int maxSize) {
102                Assert.isTrue(maxSize > 0, "'maxSize' must be positive");
103                this.maxSize = maxSize;
104        }
105
106
107        /**
108         * {@inheritDoc}
109         */
110        @Override
111        public boolean contains(MergedContextConfiguration key) {
112                Assert.notNull(key, "Key must not be null");
113                return this.contextMap.containsKey(key);
114        }
115
116        /**
117         * {@inheritDoc}
118         */
119        @Override
120        public ApplicationContext get(MergedContextConfiguration key) {
121                Assert.notNull(key, "Key must not be null");
122                ApplicationContext context = this.contextMap.get(key);
123                if (context == null) {
124                        this.missCount.incrementAndGet();
125                }
126                else {
127                        this.hitCount.incrementAndGet();
128                }
129                return context;
130        }
131
132        /**
133         * {@inheritDoc}
134         */
135        @Override
136        public void put(MergedContextConfiguration key, ApplicationContext context) {
137                Assert.notNull(key, "Key must not be null");
138                Assert.notNull(context, "ApplicationContext must not be null");
139
140                this.contextMap.put(key, context);
141                MergedContextConfiguration child = key;
142                MergedContextConfiguration parent = child.getParent();
143                while (parent != null) {
144                        Set<MergedContextConfiguration> list = this.hierarchyMap.get(parent);
145                        if (list == null) {
146                                list = new HashSet<MergedContextConfiguration>();
147                                this.hierarchyMap.put(parent, list);
148                        }
149                        list.add(child);
150                        child = parent;
151                        parent = child.getParent();
152                }
153        }
154
155        /**
156         * {@inheritDoc}
157         */
158        @Override
159        public void remove(MergedContextConfiguration key, HierarchyMode hierarchyMode) {
160                Assert.notNull(key, "Key must not be null");
161
162                // startKey is the level at which to begin clearing the cache, depending
163                // on the configured hierarchy mode.
164                MergedContextConfiguration startKey = key;
165                if (hierarchyMode == HierarchyMode.EXHAUSTIVE) {
166                        while (startKey.getParent() != null) {
167                                startKey = startKey.getParent();
168                        }
169                }
170
171                List<MergedContextConfiguration> removedContexts = new ArrayList<MergedContextConfiguration>();
172                remove(removedContexts, startKey);
173
174                // Remove all remaining references to any removed contexts from the
175                // hierarchy map.
176                for (MergedContextConfiguration currentKey : removedContexts) {
177                        for (Set<MergedContextConfiguration> children : this.hierarchyMap.values()) {
178                                children.remove(currentKey);
179                        }
180                }
181
182                // Remove empty entries from the hierarchy map.
183                for (MergedContextConfiguration currentKey : this.hierarchyMap.keySet()) {
184                        if (this.hierarchyMap.get(currentKey).isEmpty()) {
185                                this.hierarchyMap.remove(currentKey);
186                        }
187                }
188        }
189
190        private void remove(List<MergedContextConfiguration> removedContexts, MergedContextConfiguration key) {
191                Assert.notNull(key, "Key must not be null");
192
193                Set<MergedContextConfiguration> children = this.hierarchyMap.get(key);
194                if (children != null) {
195                        for (MergedContextConfiguration child : children) {
196                                // Recurse through lower levels
197                                remove(removedContexts, child);
198                        }
199                        // Remove the set of children for the current context from the hierarchy map.
200                        this.hierarchyMap.remove(key);
201                }
202
203                // Physically remove and close leaf nodes first (i.e., on the way back up the
204                // stack as opposed to prior to the recursive call).
205                ApplicationContext context = this.contextMap.remove(key);
206                if (context instanceof ConfigurableApplicationContext) {
207                        ((ConfigurableApplicationContext) context).close();
208                }
209                removedContexts.add(key);
210        }
211
212        /**
213         * {@inheritDoc}
214         */
215        @Override
216        public int size() {
217                return this.contextMap.size();
218        }
219
220        /**
221         * Get the maximum size of this cache.
222         */
223        public int getMaxSize() {
224                return this.maxSize;
225        }
226
227        /**
228         * {@inheritDoc}
229         */
230        @Override
231        public int getParentContextCount() {
232                return this.hierarchyMap.size();
233        }
234
235        /**
236         * {@inheritDoc}
237         */
238        @Override
239        public int getHitCount() {
240                return this.hitCount.get();
241        }
242
243        /**
244         * {@inheritDoc}
245         */
246        @Override
247        public int getMissCount() {
248                return this.missCount.get();
249        }
250
251        /**
252         * {@inheritDoc}
253         */
254        @Override
255        public void reset() {
256                synchronized (this.contextMap) {
257                        clear();
258                        clearStatistics();
259                }
260        }
261
262        /**
263         * {@inheritDoc}
264         */
265        @Override
266        public void clear() {
267                synchronized (this.contextMap) {
268                        this.contextMap.clear();
269                        this.hierarchyMap.clear();
270                }
271        }
272
273        /**
274         * {@inheritDoc}
275         */
276        @Override
277        public void clearStatistics() {
278                synchronized (this.contextMap) {
279                        this.hitCount.set(0);
280                        this.missCount.set(0);
281                }
282        }
283
284        /**
285         * {@inheritDoc}
286         */
287        @Override
288        public void logStatistics() {
289                if (statsLogger.isDebugEnabled()) {
290                        statsLogger.debug("Spring test ApplicationContext cache statistics: " + this);
291                }
292        }
293
294        /**
295         * Generate a text string containing the implementation type of this
296         * cache and its statistics.
297         * <p>The string returned by this method contains all information
298         * required for compliance with the contract for {@link #logStatistics()}.
299         * @return a string representation of this cache, including statistics
300         */
301        @Override
302        public String toString() {
303                return new ToStringCreator(this)
304                                .append("size", size())
305                                .append("maxSize", getMaxSize())
306                                .append("parentContextCount", getParentContextCount())
307                                .append("hitCount", getHitCount())
308                                .append("missCount", getMissCount())
309                                .toString();
310        }
311
312
313        /**
314         * Simple cache implementation based on {@link LinkedHashMap} with a maximum
315         * size and a <em>least recently used</em> (LRU) eviction policy that
316         * properly closes application contexts.
317         * @since 4.3
318         */
319        @SuppressWarnings("serial")
320        private class LruCache extends LinkedHashMap<MergedContextConfiguration, ApplicationContext> {
321
322                /**
323                 * Create a new {@code LruCache} with the supplied initial capacity
324                 * and load factor.
325                 * @param initialCapacity the initial capacity
326                 * @param loadFactor the load factor
327                 */
328                LruCache(int initialCapacity, float loadFactor) {
329                        super(initialCapacity, loadFactor, true);
330                }
331
332                @Override
333                protected boolean removeEldestEntry(Map.Entry<MergedContextConfiguration, ApplicationContext> eldest) {
334                        if (this.size() > DefaultContextCache.this.getMaxSize()) {
335                                // Do NOT delete "DefaultContextCache.this."; otherwise, we accidentally
336                                // invoke java.util.Map.remove(Object, Object).
337                                DefaultContextCache.this.remove(eldest.getKey(), HierarchyMode.CURRENT_LEVEL);
338                        }
339
340                        // Return false since we invoke a custom eviction algorithm.
341                        return false;
342                }
343        }
344
345}