View Javadoc
1   /**
2    * This file Copyright (c) 2008-2015 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
32   *
33   */
34  package info.magnolia.module.cache.cachepolicy;
35  
36  import info.magnolia.cms.cache.CacheConstants;
37  import info.magnolia.cms.core.AggregationState;
38  import info.magnolia.cms.util.DeprecationUtil;
39  import info.magnolia.context.MgnlContext;
40  import info.magnolia.context.WebContext;
41  import info.magnolia.module.cache.Cache;
42  import info.magnolia.module.cache.CacheModule;
43  import info.magnolia.module.cache.CachePolicy;
44  import info.magnolia.module.cache.CachePolicyResult;
45  import info.magnolia.module.cache.FlushPolicy;
46  import info.magnolia.module.cache.cachekey.CacheKeyGenerator;
47  import info.magnolia.module.cache.cachekey.DefaultCacheKey;
48  import info.magnolia.module.cache.cachekey.DefaultCacheKeyGenerator;
49  import info.magnolia.module.cache.cachepolicy.voters.BrowserTtlVoter;
50  import info.magnolia.module.cache.cachepolicy.voters.ServerTtlVoter;
51  import info.magnolia.module.cache.cachepolicy.voters.TtlVoting;
52  import info.magnolia.module.cache.filter.CacheResponseWrapper;
53  import info.magnolia.module.cache.filter.UncacheableEntry;
54  import info.magnolia.objectfactory.Components;
55  import info.magnolia.voting.voters.VoterSet;
56  
57  import java.util.Collections;
58  import java.util.HashSet;
59  import java.util.Set;
60  
61  import javax.inject.Inject;
62  
63  import org.slf4j.Logger;
64  import org.slf4j.LoggerFactory;
65  
66  /**
67   * A basic CachePolicy driven by shouldBypassVoters. This policy implementation uses {@link info.magnolia.module.cache.cachekey.DefaultCacheKey} to identify each cache entry.
68   */
69  public class Default implements CachePolicy {
70  
71      public static final String UUID_KEY_MAP_KEY = "uuid-key-mapping";
72      private static final Logger log = LoggerFactory.getLogger(Default.class);
73  
74      private final CacheModule cacheModule;
75  
76      private CacheKeyGenerator<?> cacheKeyGenerator = new DefaultCacheKeyGenerator();
77      private VoterSet shouldBypassVoters;
78      private VoterSet<CacheResponseWrapper> ttlVoters = new VoterSet<>();
79  
80      private boolean refreshOnNoCacheRequests = false;
81  
82      /**
83       * @deprecated since 5.4. Use {@link #Default(CacheModule)} instead,
84       */
85      public Default() {
86          this(Components.getComponent(CacheModule.class));
87      }
88  
89      @Inject
90      public Default(CacheModule cacheModule) {
91          this.cacheModule = cacheModule;
92          ttlVoters.setVoting(new TtlVoting());
93          ttlVoters.addVoter(new ServerTtlVoter());
94          ttlVoters.addVoter(new BrowserTtlVoter());
95      }
96  
97      @Override
98      public CachePolicyResult shouldCache(final Cache cache, final AggregationState aggregationState, final FlushPolicy flushPolicy) {
99          final Object key = retrieveCacheKey(aggregationState);
100 
101         if (shouldBypass(aggregationState, key)) {
102             return new CachePolicyResult(CachePolicyResult.bypass, key, null);
103         }
104 
105         if (shouldRefresh(aggregationState, key)) {
106             log.debug("Cache refresh requested for {}", key);
107             return new CachePolicyResult(CachePolicyResult.store, key, null);
108         }
109 
110         // no multithreaded expiring cache can guarantee existence of the key between two subsequent calls without synchronization here on common object or mutex
111         // and synchronization on the cache is too heavy weight
112         // if (cache.hasElement(key)) {
113         // final Object entry = cache.get(key);
114         // ... so unless having mutexes for our own cache keys and being able to synchronize on that
115         // simply make sure there is just one call ... ever. Either underlying cache has it's own key-mutex system (like ehCache) or
116         // stale value returned by cache might result in entries being generated multiple times or served stale for while ... so choose your underlying cache impl carefully
117         // either way, Magnolia should not need to care nor should it play a police for cache implementation
118 
119         // for default cache impl (blocking ehCache) this call will block on given key, and on given key only, if previously requested until value is available or timeout occurs
120         final Object cachedEntry = cache.get(key);
121         // also assume that if the value can't be retrieved for given key, underlying cache is not a pig and will throw exception at some point releasing the thread and propagating the error to the user
122 
123         if (cachedEntry != null) {
124             if (cachedEntry instanceof UncacheableEntry) {
125                 return new CachePolicyResult(CachePolicyResult.bypass, key, null);
126             } else {
127                 return new CachePolicyResult(CachePolicyResult.useCache, key, cachedEntry);
128             }
129         }
130         return new CachePolicyResult(CachePolicyResult.store, key, null);
131     }
132 
133     /**
134      * Checks whether requested content should be served from cache or refreshed instead.
135      *
136      * @return True if cache entry for the key should be recreated, false otherwise.
137      */
138     protected boolean shouldRefresh(AggregationState aggregationState, Object key) {
139         if (isRefreshOnNoCacheRequests()) {
140             String cacheControl = ((WebContext) MgnlContext.getInstance()).getRequest().getHeader(CacheConstants.HEADER_CACHE_CONTROL);
141             String pragma = ((WebContext) MgnlContext.getInstance()).getRequest().getHeader(CacheConstants.HEADER_PRAGMA);
142             return (cacheControl != null && cacheControl.equals(CacheConstants.HEADER_VALUE_NO_CACHE)) || (pragma != null && pragma.equals(CacheConstants.HEADER_VALUE_NO_CACHE));
143         }
144         return false;
145     }
146 
147     protected boolean shouldBypass(AggregationState aggregationState, Object key) {
148         final String uri;
149         if (key instanceof DefaultCacheKey) {
150             uri = ((DefaultCacheKey) key).getUri();
151         } else {
152             uri = key.toString();
153         }
154         // true if shouldBypassVoters vote positively
155         if (shouldBypassVoters != null) {
156             return shouldBypassVoters.vote(uri) <= 0;
157         }
158         log.warn("No cache voter defined.");
159         return false;
160     }
161 
162     public Object retrieveCacheKey(final AggregationState aggregationState) {
163         CacheKeyGenerator<?> cacheKeyGenerator = this.getCacheKeyGenerator();
164 
165         XMagnoliaCacheAttribute attribute = MgnlContext.getAttribute(XMagnoliaCacheAttribute.NAME);
166         if (attribute != null && attribute.getKeyGenerator() != null) {
167             cacheKeyGenerator = attribute.getKeyGenerator();
168         }
169         return cacheKeyGenerator.generateKey(aggregationState);
170     }
171 
172     @Override
173     public Object[] retrieveCacheKeys(final String uuid, final String repository) {
174         final String uuidKey = repository + ":" + uuid;
175         final Set<Object> keys = getUUIDKeySetFromCacheSafely(uuidKey);
176         return keys.toArray();
177     }
178 
179     @Override
180     public void persistCacheKey(final String repo, final String uuid, final Object key) {
181         final String uuidKey = repo + ":" + uuid;
182         final Set<Object> uuidToCacheKeyMapping = getUUIDKeySetFromCacheSafely(uuidKey);
183         uuidToCacheKeyMapping.add(key);
184     }
185 
186     @Override
187     public Object[] removeCacheKeys(final String uuid, final String repository) {
188         final String uuidKey = repository + ":" + uuid;
189         final Set keys = getUUIDKeySetFromCacheSafely(uuidKey);
190         getUuidKeyCache().remove(uuidKey);
191         return keys.toArray();
192     }
193 
194     private Cache getUuidKeyCache() {
195         return cacheModule.getCacheFactory().getCache(UUID_KEY_MAP_KEY);
196     }
197 
198     /**
199      * Method to safely (without danger of blocking cache) obtain persistent mapping between UUIDs and cache keys.
200      */
201     private synchronized Set<Object> getUUIDKeySetFromCacheSafely(String uuidKey) {
202         final Cache cache = getUuidKeyCache();
203         synchronized (cache) {
204             Set<Object> keys = (Set<Object>) cache.get(uuidKey);
205             if (keys == null) {
206                 keys = Collections.synchronizedSet(new HashSet<Object>());
207                 cache.put(uuidKey, keys);
208             }
209             return keys;
210         }
211     }
212 
213     public boolean isRefreshOnNoCacheRequests() {
214         return this.refreshOnNoCacheRequests;
215     }
216 
217     public void setRefreshOnNoCacheRequests(boolean allowNoCacheHeader) {
218         this.refreshOnNoCacheRequests = allowNoCacheHeader;
219     }
220 
221     @Override
222     public VoterSet<CacheResponseWrapper> getTtlVoters() {
223         return ttlVoters;
224     }
225 
226     public void setTtlVoters(VoterSet<CacheResponseWrapper> ttlVoters) {
227         this.ttlVoters = ttlVoters;
228     }
229 
230     public VoterSet getShouldBypassVoters() {
231         return shouldBypassVoters;
232     }
233 
234     public void setShouldBypassVoters(VoterSet shouldBypassVoters) {
235         this.shouldBypassVoters = shouldBypassVoters;
236     }
237 
238     public CacheKeyGenerator<?> getCacheKeyGenerator() {
239         return cacheKeyGenerator;
240     }
241 
242     public void setCacheKeyGenerator(CacheKeyGenerator<?> cacheKeyGenerator) {
243         this.cacheKeyGenerator = cacheKeyGenerator;
244     }
245 
246     /**
247      * @deprecated since 5.4. Use {@link #getShouldBypassVoters()} instead.
248      */
249     @Deprecated
250     public VoterSet getVoters() {
251         DeprecationUtil.isDeprecated("Use info.magnolia.module.cache.cachepolicy.Default#getShouldBypassVoters instead.");
252         return shouldBypassVoters;
253     }
254 
255     /**
256      * @deprecated since 5.4. Use {@link #setShouldBypassVoters(info.magnolia.voting.voters.VoterSet)} ()} instead.
257      */
258     @Deprecated
259     public void setVoters(VoterSet voters) {
260         DeprecationUtil.isDeprecated("Use info.magnolia.module.cache.cachepolicy.Default#setShouldBypassVoters instead.");
261         this.shouldBypassVoters = voters;
262     }
263 }