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.context.MgnlContext;
39  import info.magnolia.context.WebContext;
40  import info.magnolia.module.cache.Cache;
41  import info.magnolia.module.cache.CacheModule;
42  import info.magnolia.module.cache.CachePolicy;
43  import info.magnolia.module.cache.CachePolicyResult;
44  import info.magnolia.module.cache.FlushPolicy;
45  import info.magnolia.module.cache.cachekey.CacheKeyGenerator;
46  import info.magnolia.module.cache.cachekey.DefaultCacheKey;
47  import info.magnolia.module.cache.cachekey.DefaultCacheKeyGenerator;
48  import info.magnolia.module.cache.cachepolicy.instructor.CacheInstructor;
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.voting.voters.VoterSet;
55  
56  import java.util.Collections;
57  import java.util.HashSet;
58  import java.util.Set;
59  
60  import javax.inject.Inject;
61  import javax.inject.Provider;
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 CacheKeyGenerator<?> cacheKeyGenerator = new DefaultCacheKeyGenerator();
75      private VoterSet shouldBypassVoters;
76      private VoterSet<CacheResponseWrapper> ttlVoters = new VoterSet<>();
77  
78      private boolean refreshOnNoCacheRequests = false;
79  
80      private final CacheModule cacheModule;
81      private final Provider<CacheInstructor> cacheInstructorProvider;
82  
83      @Inject
84      public Default(CacheModule cacheModule, Provider<CacheInstructor> cacheInstructorProvider) {
85          this.cacheModule = cacheModule;
86          this.cacheInstructorProvider = cacheInstructorProvider;
87          ttlVoters.setVoting(new TtlVoting());
88          ttlVoters.addVoter(new ServerTtlVoter(cacheInstructorProvider));
89          ttlVoters.addVoter(new BrowserTtlVoter());
90      }
91  
92  
93      @Override
94      public CachePolicyResult shouldCache(final Cache cache, final AggregationState aggregationState, final FlushPolicy flushPolicy) {
95          final Object key = retrieveCacheKey(aggregationState);
96  
97          if (shouldBypass(aggregationState, key)) {
98              return new CachePolicyResult(CachePolicyResult.bypass, key, null);
99          }
100 
101         if (shouldRefresh(aggregationState, key)) {
102             log.debug("Cache refresh requested for {}", key);
103             return new CachePolicyResult(CachePolicyResult.store, key, null);
104         }
105 
106         // no multithreaded expiring cache can guarantee existence of the key between two subsequent calls without synchronization here on common object or mutex
107         // and synchronization on the cache is too heavy weight
108         // if (cache.hasElement(key)) {
109         // final Object entry = cache.get(key);
110         // ... so unless having mutexes for our own cache keys and being able to synchronize on that
111         // simply make sure there is just one call ... ever. Either underlying cache has it's own key-mutex system (like ehCache) or
112         // 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
113         // either way, Magnolia should not need to care nor should it play a police for cache implementation
114 
115         // 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
116         final Object cachedEntry = cache.get(key);
117         // 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
118 
119         if (cachedEntry != null) {
120             if (cachedEntry instanceof UncacheableEntry) {
121                 return new CachePolicyResult(CachePolicyResult.bypass, key, null);
122             } else {
123                 return new CachePolicyResult(CachePolicyResult.useCache, key, cachedEntry);
124             }
125         }
126         return new CachePolicyResult(CachePolicyResult.store, key, null);
127     }
128 
129     /**
130      * Checks whether requested content should be served from cache or refreshed instead.
131      *
132      * @return True if cache entry for the key should be recreated, false otherwise.
133      */
134     protected boolean shouldRefresh(AggregationState aggregationState, Object key) {
135         if (isRefreshOnNoCacheRequests()) {
136             String cacheControl = ((WebContext) MgnlContext.getInstance()).getRequest().getHeader(CacheConstants.HEADER_CACHE_CONTROL);
137             String pragma = ((WebContext) MgnlContext.getInstance()).getRequest().getHeader(CacheConstants.HEADER_PRAGMA);
138             return (cacheControl != null && cacheControl.equals(CacheConstants.HEADER_VALUE_NO_CACHE)) || (pragma != null && pragma.equals(CacheConstants.HEADER_VALUE_NO_CACHE));
139         }
140         return false;
141     }
142 
143     protected boolean shouldBypass(AggregationState aggregationState, Object key) {
144         final String uri;
145         if (key instanceof DefaultCacheKey) {
146             uri = ((DefaultCacheKey) key).getUri();
147         } else {
148             uri = key.toString();
149         }
150         // true if shouldBypassVoters vote positively
151         if (shouldBypassVoters != null) {
152             return shouldBypassVoters.vote(uri) <= 0;
153         }
154         log.warn("No cache voter defined.");
155         return false;
156     }
157 
158     public Object retrieveCacheKey(final AggregationState aggregationState) {
159         CacheKeyGenerator<?> cacheKeyGenerator = cacheInstructorProvider.get().getKeyGenerator();
160         if (cacheKeyGenerator == null) {
161             cacheKeyGenerator = getCacheKeyGenerator();
162         }
163         return cacheKeyGenerator.generateKey(aggregationState);
164     }
165 
166     @Override
167     public Object[] retrieveCacheKeys(final String uuid, final String repository) {
168         final String uuidKey = repository + ":" + uuid;
169         final Set<Object> keys = getUUIDKeySetFromCacheSafely(uuidKey);
170         return keys.toArray();
171     }
172 
173     @Override
174     public void persistCacheKey(final String repo, final String uuid, final Object key) {
175         final String uuidKey = repo + ":" + uuid;
176         final Set<Object> uuidToCacheKeyMapping = getUUIDKeySetFromCacheSafely(uuidKey);
177         uuidToCacheKeyMapping.add(key);
178     }
179 
180     @Override
181     public Object[] removeCacheKeys(final String uuid, final String repository) {
182         final String uuidKey = repository + ":" + uuid;
183         final Set keys = getUUIDKeySetFromCacheSafely(uuidKey);
184         getUuidKeyCache().remove(uuidKey);
185         return keys.toArray();
186     }
187 
188     private Cache getUuidKeyCache() {
189         return cacheModule.getCacheFactory().getCache(UUID_KEY_MAP_KEY);
190     }
191 
192     /**
193      * Method to safely (without danger of blocking cache) obtain persistent mapping between UUIDs and cache keys.
194      */
195     private synchronized Set<Object> getUUIDKeySetFromCacheSafely(String uuidKey) {
196         final Cache cache = getUuidKeyCache();
197         synchronized (cache) {
198             Set<Object> keys = (Set<Object>) cache.get(uuidKey);
199             if (keys == null) {
200                 keys = Collections.synchronizedSet(new HashSet<Object>());
201                 cache.put(uuidKey, keys);
202             }
203             return keys;
204         }
205     }
206 
207     public boolean isRefreshOnNoCacheRequests() {
208         return this.refreshOnNoCacheRequests;
209     }
210 
211     public void setRefreshOnNoCacheRequests(boolean allowNoCacheHeader) {
212         this.refreshOnNoCacheRequests = allowNoCacheHeader;
213     }
214 
215     @Override
216     public VoterSet<CacheResponseWrapper> getTtlVoters() {
217         return ttlVoters;
218     }
219 
220     public void setTtlVoters(VoterSet<CacheResponseWrapper> ttlVoters) {
221         this.ttlVoters = ttlVoters;
222     }
223 
224     public VoterSet getShouldBypassVoters() {
225         return shouldBypassVoters;
226     }
227 
228     public void setShouldBypassVoters(VoterSet shouldBypassVoters) {
229         this.shouldBypassVoters = shouldBypassVoters;
230     }
231 
232     public CacheKeyGenerator<?> getCacheKeyGenerator() {
233         return cacheKeyGenerator;
234     }
235 
236     public void setCacheKeyGenerator(CacheKeyGenerator<?> cacheKeyGenerator) {
237         this.cacheKeyGenerator = cacheKeyGenerator;
238     }
239 }