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;
35  
36  import info.magnolia.cms.util.DeprecationUtil;
37  import info.magnolia.cms.util.ObservationUtil;
38  import info.magnolia.objectfactory.Components;
39  import info.magnolia.repository.RepositoryManager;
40  
41  import java.util.ArrayList;
42  import java.util.HashMap;
43  import java.util.Iterator;
44  import java.util.List;
45  import java.util.Map;
46  
47  import javax.inject.Inject;
48  import javax.jcr.RepositoryException;
49  import javax.jcr.observation.Event;
50  import javax.jcr.observation.EventIterator;
51  import javax.jcr.observation.EventListener;
52  
53  import org.apache.commons.lang3.StringUtils;
54  import org.slf4j.Logger;
55  import org.slf4j.LoggerFactory;
56  
57  /**
58   * Implemenation of the <code>FlushPolicy</code> providing functionality for triggering flush operation based on changes in observed workspaces.
59   */
60  public abstract class AbstractListeningFlushPolicy implements FlushPolicy {
61  
62      private static final Logger log = LoggerFactory.getLogger(AbstractListeningFlushPolicy.class);
63  
64      private List<String> workspaces;
65      private List<String> excludedWorkspaces = new ArrayList<String>();
66      private Map<String, EventListener> registeredListeners = new HashMap<>();
67  
68      private final CacheModule cacheModule;
69      private final RepositoryManager repositoryManager;
70  
71      /**
72       * @deprecated since 5.4. Use {@link #AbstractListeningFlushPolicy(CacheModule, RepositoryManager)} instead.
73       */
74      public AbstractListeningFlushPolicy() {
75          this.cacheModule = Components.getComponent(CacheModule.class);
76          this.repositoryManager = Components.getComponent(RepositoryManager.class);
77      }
78  
79      @Inject
80      public AbstractListeningFlushPolicy(CacheModule cacheModule, RepositoryManager repositoryManager) {
81          this.cacheModule = cacheModule;
82          this.repositoryManager = repositoryManager;
83      }
84  
85      @Override
86      public void start(Cache cache) {
87          for (Iterator<String> iter = this.getWorkspacesToProcess().iterator(); iter.hasNext(); ) {
88              final String workspace = iter.next();
89              try {
90                  if (repositoryManager.getWorkspaceMapping(workspace) != null) {
91                      final CacheCleaner cacheCleaner = new CacheCleaner(cache, workspace);
92                      final EventListener listener = ObservationUtil.instanciateDeferredEventListener(cacheCleaner, 5000, 30000);
93                      final String path = this.getPath(cache);
94                      ObservationUtil.registerChangeListener(workspace, path, listener);
95                      registeredListeners.put(workspace + ":" + path, listener);
96                  }
97              } catch (Exception e) {
98                  log.warn("Failed to register cache flushing observation for workspace '{}'. "
99                          + "Publishing any content to this workspace will not result in update of the cache. Please flush the cache manually.", workspace, e.getMessage());
100             }
101         }
102     }
103 
104     protected String getPath(Cache cache) {
105         return "/";
106     }
107 
108     @Override
109     public void stop(Cache cache) {
110         for (Map.Entry<String, EventListener> entry : registeredListeners.entrySet()) {
111             if (entry.getValue() == null) {
112                 // happens on restart of cache module after someone configures new listener repository ... we are trying to stop the listener which was not created yet
113                 continue;
114             }
115             ObservationUtil.unregisterChangeListener(StringUtils.substringBefore(entry.getKey(), ":"), entry.getValue());
116         }
117     }
118 
119     private List<String> getWorkspacesToProcess() {
120         if (this.getWorkspaces() != null) {
121             return this.getWorkspaces();
122         } else {
123             ArrayList<String> workspaces = new ArrayList<String>(repositoryManager.getWorkspaceNames());
124             workspaces.removeAll(this.getExcludedWorkspaces());
125             return workspaces;
126         }
127     }
128 
129     /**
130      * Implement this method to react on buffered events on a given cache and repository.
131      *
132      * @return true if single events should be processed as well, false otherwise.
133      */
134     protected abstract boolean preHandleEvents(Cache cache, String repository);
135 
136     /**
137      * Implement this method to wrap up flushing process after all single events have been processed.
138      * This method will be invoked only if {@link #preHandleEvents(Cache, String)} returns true;
139      */
140     protected abstract void postHandleEvents(Cache cache, String repository);
141 
142     /**
143      * Implement this method to react on each and every event on a given cache and repository,
144      * even if multiple where buffered.
145      * This method will be invoked only if {@link #preHandleEvents(Cache, String)} returns true;
146      */
147     protected abstract void handleSingleEvent(Cache cache, String repository, Event event);
148 
149     /**
150      * Flushes all content related to given uuid&repository combination from provided cache.
151      * Note that more then only one pages can be flushed when this method is called.
152      */
153     protected void flushByUUID(String uuid, String repository, Cache cache) {
154 
155         final ContentCachingConfiguration config = cacheModule.getContentCaching(cache.getName());
156         final CachePolicy policy = config.getCachePolicy();
157         if (policy == null) {
158             // no cache policy, no cache key, nothing to flush here ...
159             return;
160         }
161         // do NOT remove key mappings, just retrieve them. Rather try remove not existent items then leaving entries in other caches that might depend on the same mapping
162         Object[] cacheEntryKeys = config.getCachePolicy().retrieveCacheKeys(uuid, repository);
163         log.debug("Flushing {} due to detected content {}:{} update.", cacheEntryKeys, repository, uuid);
164 
165         if (cacheEntryKeys == null || cacheEntryKeys.length == 0) {
166             // nothing to remove
167             return;
168         }
169         for (Object key : cacheEntryKeys) {
170             cache.remove(key);
171         }
172         // we are done here
173     }
174 
175     /**
176      * Event listener triggering the cleanup of the cache.
177      */
178     protected class CacheCleaner implements EventListener {
179         private final Cache cache;
180         private final String repository;
181 
182         public CacheCleaner(Cache cache, String repository) {
183             this.cache = cache;
184             this.repository = repository;
185         }
186 
187         @Override
188         public void onEvent(EventIterator events) {
189             List<Event> eventList = new ArrayList<Event>();
190             // do not react on jcr: specific events. Those are sent to every registered workspace when any of the workspaces stores new version of its content
191             while (events.hasNext()) {
192                 final Event event = events.nextEvent();
193                 try {
194                     if (!event.getPath().startsWith("/jcr:")) {
195                         eventList.add(event);
196                     }
197                 } catch (RepositoryException e) {
198                     log.warn("Failed to process an event {}, the observation based cache flushing might not have been fully completed.", event.toString());
199                 }
200             }
201             if (eventList.isEmpty()) {
202                 return;
203             }
204             // if there are still any events left, continue here
205             boolean shouldContinue = preHandleEvents(cache, repository);
206             if (shouldContinue) {
207                 for (Event event : eventList) {
208                     handleSingleEvent(cache, repository, event);
209                 }
210                 postHandleEvents(cache, repository);
211             }
212         }
213     }
214 
215     // --------- config methods below
216 
217     /**
218      * The workspaces to which the listener is attached - upon any event on these,
219      * the cache is cleared.
220      */
221     public List<String> getWorkspaces() {
222         return workspaces;
223     }
224 
225     public void setWorkspaces(List<String> workspaces) {
226         if (!this.getExcludedWorkspaces().isEmpty()) {
227             log.error("You should configure only 'workspaces' or 'excludedWorkspaces' on {}. Not both of them.", this.getClass());
228             return;
229         }
230         this.workspaces = workspaces;
231     }
232 
233     /**
234      * The workspaces to which the listener is NOT attached.
235      */
236     public List<String> getExcludedWorkspaces() {
237         return excludedWorkspaces;
238     }
239 
240     public void setExcludedWorkspaces(List<String> excludedWorkspaces) {
241         if (this.getWorkspaces() != null) {
242             log.error("You should configure only 'workspaces' or 'excludedWorkspaces' on {}. Not both of them.", this.getClass());
243             return;
244         }
245         this.excludedWorkspaces = excludedWorkspaces;
246     }
247 
248     /**
249      * @deprecated since 5.4. Use {@link #getWorkspaces()} instead.
250      */
251     public List<String> getRepositories() {
252         DeprecationUtil.isDeprecated("Use info.magnolia.module.cache.AbstractListeningFlushPolicy#getWorkspaces instead.");
253         return this.getWorkspaces();
254     }
255 
256     /**
257      * @deprecated since 5.4. Use {@link #setWorkspaces(List)} instead.
258      */
259     public void setRepositories(List<String> repositories) {
260         DeprecationUtil.isDeprecated("Use info.magnolia.module.cache.AbstractListeningFlushPolicy#setWorkspaces instead.");
261         this.setWorkspaces(repositories);
262     }
263 
264     /**
265      * @deprecated since 5.4. Use {@link #setWorkspaces(List)} instead.
266      */
267     public void addRepository(String repository) {
268         DeprecationUtil.isDeprecated("Use info.magnolia.module.cache.AbstractListeningFlushPolicy#setWorkspaces instead.");
269         workspaces.add(repository);
270     }
271 }