View Javadoc
1   /**
2    * This file Copyright (c) 2008-2018 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.instructor.CacheInstructor;
50  import info.magnolia.module.cache.cachepolicy.voters.BrowserTtlVoter;
51  import info.magnolia.module.cache.cachepolicy.voters.ServerTtlVoter;
52  import info.magnolia.module.cache.cachepolicy.voters.TtlVoting;
53  import info.magnolia.module.cache.filter.CacheResponseWrapper;
54  import info.magnolia.module.cache.filter.UncacheableEntry;
55  import info.magnolia.objectfactory.Components;
56  import info.magnolia.voting.voters.VoterSet;
57  
58  import java.util.Collections;
59  import java.util.HashSet;
60  import java.util.Set;
61  
62  import javax.inject.Inject;
63  import javax.inject.Provider;
64  
65  import org.slf4j.Logger;
66  import org.slf4j.LoggerFactory;
67  
68  /**
69   * A basic CachePolicy driven by shouldBypassVoters. This policy implementation uses {@link info.magnolia.module.cache.cachekey.DefaultCacheKey} to identify each cache entry.
70   */
71  public class Default implements CachePolicy {
72  
73      public static final String UUID_KEY_MAP_KEY = "uuid-key-mapping";
74      private static final Logger log = LoggerFactory.getLogger(Default.class);
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      private final CacheModule cacheModule;
83      private final Provider<CacheInstructor> cacheInstructorProvider;
84  
85      @Inject
86      public Default(CacheModule cacheModule, Provider<CacheInstructor> cacheInstructorProvider) {
87          this.cacheModule = cacheModule;
88          this.cacheInstructorProvider = cacheInstructorProvider;
89          ttlVoters.setVoting(new TtlVoting());
90          ttlVoters.addVoter(new ServerTtlVoter(cacheInstructorProvider));
91          ttlVoters.addVoter(new BrowserTtlVoter());
92      }
93  
94      /**
95       * @deprecated since 5.5. Use {@link #Default(info.magnolia.module.cache.CacheModule, Provider)} instead.
96       */
97      @Deprecated
98      public Default(CacheModule cacheModule) {
99          this(cacheModule, new Provider<CacheInstructor>() {
100             @Override
101             public CacheInstructor get() {
102                 return Components.getComponent(CacheInstructor.class);
103             }
104         });
105     }
106 
107     /**
108      * @deprecated since 5.4. Use {@link #Default(info.magnolia.module.cache.CacheModule, Provider)} instead,
109      */
110     public Default() {
111         this(Components.getComponent(CacheModule.class));
112     }
113 
114     @Override
115     public CachePolicyResult shouldCache(final Cache cache, final AggregationState aggregationState, final FlushPolicy flushPolicy) {
116         final Object key = retrieveCacheKey(aggregationState);
117 
118         if (shouldBypass(aggregationState, key)) {
119             return new CachePolicyResult(CachePolicyResult.bypass, key, null);
120         }
121 
122         if (shouldRefresh(aggregationState, key)) {
123             log.debug("Cache refresh requested for {}", key);
124             return new CachePolicyResult(CachePolicyResult.store, key, null);
125         }
126 
127         // no multithreaded expiring cache can guarantee existence of the key between two subsequent calls without synchronization here on common object or mutex
128         // and synchronization on the cache is too heavy weight
129         // if (cache.hasElement(key)) {
130         // final Object entry = cache.get(key);
131         // ... so unless having mutexes for our own cache keys and being able to synchronize on that
132         // simply make sure there is just one call ... ever. Either underlying cache has it's own key-mutex system (like ehCache) or
133         // 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
134         // either way, Magnolia should not need to care nor should it play a police for cache implementation
135 
136         // 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
137         final Object cachedEntry = cache.get(key);
138         // 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
139 
140         if (cachedEntry != null) {
141             if (cachedEntry instanceof UncacheableEntry) {
142                 return new CachePolicyResult(CachePolicyResult.bypass, key, null);
143             } else {
144                 return new CachePolicyResult(CachePolicyResult.useCache, key, cachedEntry);
145             }
146         }
147         return new CachePolicyResult(CachePolicyResult.store, key, null);
148     }
149 
150     /**
151      * Checks whether requested content should be served from cache or refreshed instead.
152      *
153      * @return True if cache entry for the key should be recreated, false otherwise.
154      */
155     protected boolean shouldRefresh(AggregationState aggregationState, Object key) {
156         if (isRefreshOnNoCacheRequests()) {
157             String cacheControl = ((WebContext) MgnlContext.getInstance()).getRequest().getHeader(CacheConstants.HEADER_CACHE_CONTROL);
158             String pragma = ((WebContext) MgnlContext.getInstance()).getRequest().getHeader(CacheConstants.HEADER_PRAGMA);
159             return (cacheControl != null && cacheControl.equals(CacheConstants.HEADER_VALUE_NO_CACHE)) || (pragma != null && pragma.equals(CacheConstants.HEADER_VALUE_NO_CACHE));
160         }
161         return false;
162     }
163 
164     protected boolean shouldBypass(AggregationState aggregationState, Object key) {
165         final String uri;
166         if (key instanceof DefaultCacheKey) {
167             uri = ((DefaultCacheKey) key).getUri();
168         } else {
169             uri = key.toString();
170         }
171         // true if shouldBypassVoters vote positively
172         if (shouldBypassVoters != null) {
173             return shouldBypassVoters.vote(uri) <= 0;
174         }
175         log.warn("No cache voter defined.");
176         return false;
177     }
178 
179     public Object retrieveCacheKey(final AggregationState aggregationState) {
180         CacheKeyGenerator<?> cacheKeyGenerator = cacheInstructorProvider.get().getKeyGenerator();
181         if (cacheKeyGenerator == null) {
182             cacheKeyGenerator = getCacheKeyGenerator();
183         }
184         return cacheKeyGenerator.generateKey(aggregationState);
185     }
186 
187     @Override
188     public Object[] retrieveCacheKeys(final String uuid, final String repository) {
189         final String uuidKey = repository + ":" + uuid;
190         final Set<Object> keys = getUUIDKeySetFromCacheSafely(uuidKey);
191         return keys.toArray();
192     }
193 
194     @Override
195     public void persistCacheKey(final String repo, final String uuid, final Object key) {
196         final String uuidKey = repo + ":" + uuid;
197         final Set<Object> uuidToCacheKeyMapping = getUUIDKeySetFromCacheSafely(uuidKey);
198         uuidToCacheKeyMapping.add(key);
199     }
200 
201     @Override
202     public Object[] removeCacheKeys(final String uuid, final String repository) {
203         final String uuidKey = repository + ":" + uuid;
204         final Set keys = getUUIDKeySetFromCacheSafely(uuidKey);
205         getUuidKeyCache().remove(uuidKey);
206         return keys.toArray();
207     }
208 
209     private Cache getUuidKeyCache() {
210         return cacheModule.getCacheFactory().getCache(UUID_KEY_MAP_KEY);
211     }
212 
213     /**
214      * Method to safely (without danger of blocking cache) obtain persistent mapping between UUIDs and cache keys.
215      */
216     private synchronized Set<Object> getUUIDKeySetFromCacheSafely(String uuidKey) {
217         final Cache cache = getUuidKeyCache();
218         synchronized (cache) {
219             Set<Object> keys = (Set<Object>) cache.get(uuidKey);
220             if (keys == null) {
221                 keys = Collections.synchronizedSet(new HashSet<Object>());
222                 cache.put(uuidKey, keys);
223             }
224             return keys;
225         }
226     }
227 
228     public boolean isRefreshOnNoCacheRequests() {
229         return this.refreshOnNoCacheRequests;
230     }
231 
232     public void setRefreshOnNoCacheRequests(boolean allowNoCacheHeader) {
233         this.refreshOnNoCacheRequests = allowNoCacheHeader;
234     }
235 
236     @Override
237     public VoterSet<CacheResponseWrapper> getTtlVoters() {
238         return ttlVoters;
239     }
240 
241     public void setTtlVoters(VoterSet<CacheResponseWrapper> ttlVoters) {
242         this.ttlVoters = ttlVoters;
243     }
244 
245     public VoterSet getShouldBypassVoters() {
246         return shouldBypassVoters;
247     }
248 
249     public void setShouldBypassVoters(VoterSet shouldBypassVoters) {
250         this.shouldBypassVoters = shouldBypassVoters;
251     }
252 
253     public CacheKeyGenerator<?> getCacheKeyGenerator() {
254         return cacheKeyGenerator;
255     }
256 
257     public void setCacheKeyGenerator(CacheKeyGenerator<?> cacheKeyGenerator) {
258         this.cacheKeyGenerator = cacheKeyGenerator;
259     }
260 
261     /**
262      * @deprecated since 5.4. Use {@link #getShouldBypassVoters()} instead.
263      */
264     @Deprecated
265     public VoterSet getVoters() {
266         DeprecationUtil.isDeprecated("Use info.magnolia.module.cache.cachepolicy.Default#getShouldBypassVoters instead.");
267         return shouldBypassVoters;
268     }
269 
270     /**
271      * @deprecated since 5.4. Use {@link #setShouldBypassVoters(info.magnolia.voting.voters.VoterSet)} ()} instead.
272      */
273     @Deprecated
274     public void setVoters(VoterSet voters) {
275         DeprecationUtil.isDeprecated("Use info.magnolia.module.cache.cachepolicy.Default#setShouldBypassVoters instead.");
276         this.shouldBypassVoters = voters;
277     }
278 }