View Javadoc

1   /**
2    * This file Copyright (c) 2008-2010 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 java.util.Collections;
37  import java.util.HashSet;
38  import java.util.Locale;
39  import java.util.Map;
40  import java.util.Set;
41  
42  import info.magnolia.cms.core.AggregationState;
43  import info.magnolia.context.MgnlContext;
44  import info.magnolia.context.WebContext;
45  import info.magnolia.module.ModuleRegistry;
46  import info.magnolia.module.cache.Cache;
47  import info.magnolia.module.cache.CacheFactory;
48  import info.magnolia.module.cache.CacheModule;
49  import info.magnolia.module.cache.CachePolicy;
50  import info.magnolia.module.cache.CachePolicyResult;
51  import info.magnolia.module.cache.DefaultCacheKey;
52  import info.magnolia.module.cache.FlushPolicy;
53  import info.magnolia.voting.voters.VoterSet;
54  
55  import org.slf4j.Logger;
56  import org.slf4j.LoggerFactory;
57  
58  /**
59   * A basic CachePolicy driven by voters. This policy implementation uses
60   * {@link info.magnolia.module.cache.DefaultCacheKey} to identify each cache entry.
61   *
62   * @author gjoseph
63   * @version $Revision: 1821 $ ($Author: fgiust $)
64   */
65  public class Default implements CachePolicy {
66  
67      public static final String UUID_KEY_MAP_KEY = "uuid-key-mapping";
68      private static final Logger log = LoggerFactory.getLogger(Default.class);
69  
70      private VoterSet voters;
71  
72      private boolean refreshOnNoCacheRequests = false;
73  
74      public CachePolicyResult shouldCache(final Cache cache, final AggregationState aggregationState, final FlushPolicy flushPolicy) {
75          final Object key = retrieveCacheKey(aggregationState);
76  
77          if (shouldBypass(aggregationState, key)) {
78              return new CachePolicyResult(CachePolicyResult.bypass, key, null);
79          }
80  
81          if (shouldRefresh(aggregationState, key)) {
82              log.debug("Cache refresh requested for {}", key);
83              return new CachePolicyResult(CachePolicyResult.store, key, null);
84          }
85  
86          // no multithreaded expiring cache can guarantee existence of the key between two subsequent calls without synchronization here on common object or mutex
87          // and synchronization on the cache is too heavy weight
88          // if (cache.hasElement(key)) {
89          //    final Object entry = cache.get(key);
90          // ... so unless having mutexes for our own cache keys and being able to synchronize on that
91          // simply make sure there is just one call ... ever. Either underlying cache has it's own key-mutex system (like ehCache) or
92          // 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
93          // either way, Magnolia should not need to care nor should it play a police for cache implementation
94  
95          // 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
96          final Object cachedEntry = cache.get(key);
97          // 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
98  
99         if (cachedEntry != null) {
100             return new CachePolicyResult(CachePolicyResult.useCache, key, cachedEntry);
101         } else {
102             return new CachePolicyResult(CachePolicyResult.store, key, null);
103         }
104     }
105 
106     /**
107      * Checks whether requested content should be served from cache or refreshed instead.
108      * @return True if cache entry for the key should be recreated, false otherwise.
109      */
110     protected boolean shouldRefresh(AggregationState aggregationState, Object key) {
111         if(isRefreshOnNoCacheRequests()){
112             String cacheControl = ((WebContext) MgnlContext.getInstance()).getRequest().getHeader("Cache-Control");
113             // TODO: check for pragma as well?? RFC says "HTTP/1.1 caches SHOULD treat "Pragma:
114             // no-cache" as if the client had sent "Cache-Control: no-cache"
115             return cacheControl != null && cacheControl.equals("no-cache");
116         }
117         return false;
118     }
119 
120     protected boolean shouldBypass(AggregationState aggregationState, Object key) {
121         final String uri;
122         if (key instanceof DefaultCacheKey) {
123             uri = ((DefaultCacheKey) key).getUri();
124         } else {
125             uri = key.toString();
126         }
127         // true if voters vote positively
128         return voters.vote(uri) <= 0;
129     }
130 
131     public Object retrieveCacheKey(final AggregationState aggregationState) {
132         // get original URI - not using current URI since we want to cache original URIs, not those we forward to (parameters in virtual URIs, i18n, ...)
133         final String uri = aggregationState.getOriginalURI();
134 
135         // get serverName and request params and from WebContext
136         final String serverName;
137         final Map<String, String> params;
138         if (MgnlContext.isWebContext()) {
139             serverName = MgnlContext.getWebContext().getRequest().getServerName();
140             params = MgnlContext.getWebContext().getParameters();
141         } else {
142             serverName = null;
143             params = null;
144         }
145 
146         // get locale
147         final String localeStr;
148         final Locale locale = aggregationState.getLocale();
149         if(locale != null){
150             localeStr = locale.toString();
151          } else {
152             localeStr = null;
153          }
154 
155         // create composite key so we can easily check each part of it later
156         return new DefaultCacheKey(uri, serverName, localeStr, params);
157     }
158 
159     public Object[] retrieveCacheKeys(final String uuid, final String repository) {
160         final String uuidKey = repository + ":" + uuid;
161         final Set<Object> keys = getUUIDKeySetFromCacheSafely(uuidKey);
162         return keys.toArray();
163     }
164 
165     public void persistCacheKey(final String repo, final String uuid, final Object key) {
166         final String uuidKey = repo + ":" + uuid;
167         final Set<Object> uuidToCacheKeyMapping = getUUIDKeySetFromCacheSafely(uuidKey);
168         uuidToCacheKeyMapping.add(key);
169     }
170 
171     public VoterSet getVoters() {
172         return voters;
173     }
174 
175     public void setVoters(VoterSet voters) {
176         this.voters = voters;
177     }
178 
179     public Object[] removeCacheKeys(final String uuid, final String repository) {
180         final String uuidKey = repository + ":" + uuid;
181         final Set keys = getUUIDKeySetFromCacheSafely(uuidKey);
182         getUuidKeyCache().remove(uuidKey);
183         return keys.toArray();
184     }
185 
186     private Cache getUuidKeyCache() {
187         final CacheFactory factory = ModuleRegistry.Factory.getInstance().getModuleInstance(CacheModule.class).getCacheFactory();
188         return factory.getCache(UUID_KEY_MAP_KEY);
189     }
190 
191     /**
192      * Method to safely (without danger of blocking cache) obtain persistent mapping between UUIDs and cache keys.
193      */
194     private synchronized Set<Object> getUUIDKeySetFromCacheSafely(String uuidKey) {
195         final Cache cache = getUuidKeyCache();
196         synchronized (cache) {
197             Set<Object> keys = (Set<Object>) cache.get(uuidKey);
198             if (keys == null) {
199                 keys = Collections.synchronizedSet(new HashSet<Object>());
200                 cache.put(uuidKey, keys);
201             }
202             return keys;
203         }
204     }
205 
206     public boolean isRefreshOnNoCacheRequests() {
207         return this.refreshOnNoCacheRequests;
208     }
209 
210     public void setRefreshOnNoCacheRequests(boolean allowNoCacheHeader) {
211         this.refreshOnNoCacheRequests = allowNoCacheHeader;
212     }
213 }