View Javadoc

1   /**
2    * This file Copyright (c) 2013 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.jcr.wrapper;
35  
36  import info.magnolia.cms.security.User;
37  import info.magnolia.context.MgnlContext;
38  import info.magnolia.jcr.decoration.ContentDecorator;
39  import info.magnolia.jcr.decoration.ContentDecoratorPropertyWrapper;
40  import info.magnolia.jcr.decoration.ContentDecoratorSessionWrapper;
41  import info.magnolia.jcr.decoration.ContentDecoratorWorkspaceWrapper;
42  import info.magnolia.jcr.util.NodeTypes;
43  import info.magnolia.jcr.util.NodeTypes.LastModified;
44  import info.magnolia.repository.RepositoryConstants;
45  
46  import java.io.InputStream;
47  import java.math.BigDecimal;
48  import java.util.ArrayList;
49  import java.util.Calendar;
50  import java.util.List;
51  import java.util.Map;
52  
53  import javax.jcr.AccessDeniedException;
54  import javax.jcr.Binary;
55  import javax.jcr.InvalidItemStateException;
56  import javax.jcr.ItemExistsException;
57  import javax.jcr.NoSuchWorkspaceException;
58  import javax.jcr.Node;
59  import javax.jcr.NodeIterator;
60  import javax.jcr.PathNotFoundException;
61  import javax.jcr.Property;
62  import javax.jcr.ReferentialIntegrityException;
63  import javax.jcr.RepositoryException;
64  import javax.jcr.Session;
65  import javax.jcr.Value;
66  import javax.jcr.ValueFormatException;
67  import javax.jcr.Workspace;
68  import javax.jcr.lock.LockException;
69  import javax.jcr.nodetype.ConstraintViolationException;
70  import javax.jcr.nodetype.NoSuchNodeTypeException;
71  import javax.jcr.version.VersionException;
72  
73  import org.apache.commons.lang.ArrayUtils;
74  import org.slf4j.Logger;
75  import org.slf4j.LoggerFactory;
76  
77  /**
78   * Decorator to keep Last Update Data (LUD) status of content up to date.
79   */
80  public class LastUpdateContentDecorator extends PropertyAndChildWrappingContentDecorator implements ContentDecorator {
81  
82      private static final Logger log = LoggerFactory.getLogger(LastUpdateContentDecorator.class);
83  
84      // TODO: this behavior should be injectable w/ custom rules
85      /**
86       * Map below contains parent node type we try to resolve for LUD updates instead of the "current" path/node.
87       */
88      private static Map<String, String> parentNodeMappings = ArrayUtils.toMap(new String[][] {
89              { RepositoryConstants.WEBSITE, NodeTypes.Page.NAME },
90              { RepositoryConstants.USERS, NodeTypes.User.NAME } });
91  
92      protected boolean isSysSessionDirty;
93  
94  
95      /**
96       * Repository operation for update of mgnl:lastUpdated property.
97       */
98      final class ChangeLastUpdateDateOp extends MgnlContext.RepositoryOp {
99          private final String workspaceName;
100         private final String username;
101         private final String destAbsPath;
102         private final Calendar updateDate;
103         private final boolean recursiveDown;
104 
105         ChangeLastUpdateDateOp(String workspaceName, String username, String destAbsPath, Calendar updateDate, boolean recursiveDown) {
106             this.workspaceName = workspaceName;
107             this.username = username;
108             this.destAbsPath = destAbsPath;
109             this.updateDate = updateDate;
110             this.recursiveDown = recursiveDown;
111         }
112 
113         @Override
114         public void doExec() throws RepositoryException {
115             Session sysSession = MgnlContext.getJCRSession(workspaceName);
116 
117             // does it exist?
118             if (!sysSession.itemExists(destAbsPath)) {
119                 // can't do anything.
120                 if (log.isDebugEnabled()) {
121                     log.warn("Can't update mgnl:lastModified. Path {}:{} doesn't exist anymore.", workspaceName, destAbsPath);
122                 }
123                 return;
124             }
125             Node node = null;
126 
127             node = resolveModifiedNode(workspaceName, destAbsPath, sysSession);
128 
129             // might be null also after the while loop above
130             if (node == null) {
131                 node = sysSession.getNode(destAbsPath);
132             }
133 
134             if (node.isNodeType(NodeTypes.MetaData.NAME)) {
135                 // no LUD on old style of metadata
136                 return;
137             }
138 
139             // skip root
140             if (node.getDepth() == 0) {
141                 return;
142             }
143 
144             // unwrap
145             if (node instanceof DelegateNodeWrapper) {
146                 node = ((DelegateNodeWrapper) node).deepUnwrap(LastUpdateNodeWrapper.class);
147             }
148 
149             log.debug("LUD on {} from {}:{}", node.getPath(), node.getSession().toString(), Thread.currentThread().getName());
150             if (node.isNodeType(NodeTypes.LastModified.NAME)) {
151                 NodeTypes.LastModified.update(node);
152             }
153 
154             if (recursiveDown) {
155                 // trick or treat children
156                 List<NodeIterator> iters = new ArrayList<NodeIterator>();
157                 iters.add(node.getNodes());
158                 while (!iters.isEmpty()) {
159                     List<NodeIterator> tmp = updateChildren(iters, username, updateDate);
160                     iters.clear();
161                     iters.addAll(tmp);
162                 }
163             }
164             // save ... sucks, but since we are in system context there's no other way
165             // we are overriding save on session and call it on sys ctx as well instead of saving here
166             isSysSessionDirty = true;
167             return;
168         }
169 
170         private Node resolveModifiedNode(final String workspaceName, final String destAbsPath, Session sysSession) throws RepositoryException {
171             String parentNodeType = parentNodeMappings.get(workspaceName);
172             Node node = sysSession.getNode(destAbsPath);
173             if (parentNodeType == null) {
174                 return node;
175             }
176             while (node != null && !parentNodeType.equals(node.getPrimaryNodeType().getName()) && node.getDepth() > 0) {
177                 node = node.getParent();
178             }
179             return node;
180         }
181 
182         private List<NodeIterator> updateChildren(List<NodeIterator> iters, String username, Calendar updateDate) {
183             List<NodeIterator> tmp = new ArrayList<NodeIterator>();
184             for (NodeIterator iter : iters) {
185                 while (iter.hasNext()) {
186                     Node node = iter.nextNode();
187                     try {
188                         if (RepositoryConstants.WEBSITE.equals(workspaceName) && !NodeTypes.Page.NAME.equals(node.getPrimaryNodeType().getName())) {
189                             // do not update LUD of components and areas
190                             continue;
191                         }
192                         if (node.isNodeType(NodeTypes.MetaData.NAME)) {
193                             // no LUD on old style of metadata
194                             continue;
195                         }
196                         if (node.isNodeType(NodeTypes.LastModified.NAME)) {
197                             LastModified.update(node, username, updateDate);
198                         }
199                         tmp.add(node.getNodes());
200                     } catch (RepositoryException e) {
201                         log.error("Failed to update last modified date of " + node + " with " + e.getMessage(), e);
202                     }
203                 }
204             }
205             return tmp;
206         }
207     }
208 
209     /**
210      * Updates parent page or parent content mgnl:lastUpdated property on modification.
211      */
212     public class LastUpdatePropertyWrapper extends ContentDecoratorPropertyWrapper<LastUpdateContentDecorator> implements Property {
213 
214         public LastUpdatePropertyWrapper(Property property, LastUpdateContentDecorator contentDecorator) {
215             super(property, contentDecorator);
216         }
217 
218         @Override
219         public void setValue(BigDecimal value) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
220             super.setValue(value);
221             this.updateLastModifiedProperty();
222         }
223 
224         @Override
225         public void setValue(Binary value) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
226             super.setValue(value);
227             this.updateLastModifiedProperty();
228         }
229 
230         @Override
231         public void setValue(boolean value) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
232             super.setValue(value);
233             this.updateLastModifiedProperty();
234         }
235 
236         @Override
237         public void setValue(Calendar value) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
238             super.setValue(value);
239             this.updateLastModifiedProperty();
240         }
241 
242         @Override
243         public void setValue(double value) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
244             super.setValue(value);
245             this.updateLastModifiedProperty();
246         }
247 
248         @Override
249         public void setValue(InputStream value) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
250             super.setValue(value);
251             this.updateLastModifiedProperty();
252         }
253 
254         @Override
255         public void setValue(long value) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
256             super.setValue(value);
257             this.updateLastModifiedProperty();
258         }
259 
260         @Override
261         public void setValue(Node value) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
262             super.setValue(value);
263             this.updateLastModifiedProperty();
264         }
265 
266         @Override
267         public void setValue(String value) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
268             super.setValue(value);
269             this.updateLastModifiedProperty();
270         }
271 
272         @Override
273         public void setValue(String[] values) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
274             super.setValue(values);
275             this.updateLastModifiedProperty();
276         }
277 
278         @Override
279         public void setValue(Value value) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
280             super.setValue(value);
281             this.updateLastModifiedProperty();
282         }
283 
284         @Override
285         public void setValue(Value[] values) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
286             super.setValue(values);
287             this.updateLastModifiedProperty();
288         }
289 
290         @Override
291         public void remove() throws VersionException, LockException, ConstraintViolationException, AccessDeniedException, RepositoryException {
292             Node parent = this.getParent();
293             String propertyName = this.getName();
294             super.remove();
295             if (shouldIgnoreUpdate(propertyName)) {
296                 return;
297             }
298             updateLastModified(parent.getSession(), parent.getPath());
299         }
300 
301         private void updateLastModifiedProperty() throws RepositoryException {
302             LastUpdateContentDecorator.this.updateLastModifiedProperty(getSession().getWorkspace().getName(), this.getName(), this.getParent().getPath());
303         }
304     }
305 
306     /**
307      * Updates destination parent page or parent content mgnl:lastUpdated property on move or copy operations.
308      */
309     public class LastUpdateWorkspaceWrapper extends ContentDecoratorWorkspaceWrapper implements Workspace {
310 
311         protected LastUpdateWorkspaceWrapper(Workspace workspace, ContentDecorator contentDecorator) {
312             super(workspace, contentDecorator);
313         }
314 
315         @Override
316         public void move(String srcAbsPath, String destAbsPath) throws ConstraintViolationException, VersionException, AccessDeniedException, PathNotFoundException, ItemExistsException, LockException, RepositoryException {
317             super.move(srcAbsPath, destAbsPath);
318             updateLastModified(super.getWrappedWorkspace().getSession(), destAbsPath, true);
319         }
320 
321         @Override
322         public void copy(String srcAbsPath, String destAbsPath) throws ConstraintViolationException, VersionException, AccessDeniedException, PathNotFoundException, ItemExistsException, LockException, RepositoryException {
323             super.copy(srcAbsPath, destAbsPath);
324             updateLastModified(super.getWrappedWorkspace().getSession(), destAbsPath, true);
325         }
326 
327         @Override
328         public void copy(String srcWorkspace, String srcAbsPath, String destAbsPath) throws NoSuchWorkspaceException, ConstraintViolationException, VersionException, AccessDeniedException, PathNotFoundException, ItemExistsException, LockException, RepositoryException {
329             super.copy(srcWorkspace, srcAbsPath, destAbsPath);
330             updateLastModified(super.getWrappedWorkspace().getSession(), destAbsPath, true);
331         }
332 
333         @Override
334         public void clone(String srcWorkspace, String srcAbsPath, String destAbsPath, boolean removeExisting) throws NoSuchWorkspaceException, ConstraintViolationException, VersionException, AccessDeniedException, PathNotFoundException, ItemExistsException, LockException, RepositoryException {
335             super.clone(srcWorkspace, srcAbsPath, destAbsPath, removeExisting);
336             // no update on clone. a) it doesn't work reliably (session refresh) and b) contract of clone is to make identical copy!
337         }
338 
339     }
340 
341     /**
342      * Updates destination parent page or content mgnl:lastModified property on move.
343      */
344     public class LastUpdateSessionWrapper extends ContentDecoratorSessionWrapper<LastUpdateContentDecorator> implements Session {
345 
346         public LastUpdateSessionWrapper(Session session, LastUpdateContentDecorator contentDecorator) {
347             super(session, contentDecorator);
348         }
349 
350         @Override
351         public void move(String srcAbsPath, String destAbsPath) throws ItemExistsException, PathNotFoundException, VersionException, ConstraintViolationException, LockException, RepositoryException {
352             super.move(srcAbsPath, destAbsPath);
353             // update src path, item is not moved yet
354             updateLastModified(super.getWrappedSession(), srcAbsPath, true);
355         }
356 
357         @Override
358         public void save() throws AccessDeniedException, ItemExistsException, ReferentialIntegrityException, ConstraintViolationException, InvalidItemStateException, VersionException, LockException, NoSuchNodeTypeException, RepositoryException {
359             String workspaceName = wrapped.getWorkspace().getName();
360             try {
361                 log.debug("saving session: " + wrapped.toString() + "::" + workspaceName + "::sys:" + MgnlContext.isSystemInstance() + "::" + Thread.currentThread().getName());
362                 super.save();
363                 if (MgnlContext.isSystemInstance()) {
364                     isSysSessionDirty = false;
365                 }
366             } catch (InvalidItemStateException e) {
367                 log.error("Failed to update LUD for session: " + wrapped.toString() + "::" + workspaceName, e);
368                 throw e;
369             }
370             if (isSysSessionDirty) {
371                 Session sysSession = MgnlContext.getSystemContext().getJCRSession(workspaceName);
372                 if (sysSession instanceof DelegateSessionWrapper) {
373                     sysSession = ((DelegateSessionWrapper) sysSession).deepUnwrap(LastUpdateSessionWrapper.class);
374                 }
375                 if (!wrapped.toString().equals(sysSession.toString())) {
376                     log.debug("Forcing saving of : " + sysSession.toString() + "::" + workspaceName);
377                     sysSession.save();
378                 }
379                 isSysSessionDirty = false;
380             }
381         }
382 
383     }
384 
385     @Override
386     public Session wrapSession(Session session) {
387 
388         return new LastUpdateSessionWrapper(session, this);
389     }
390 
391     @Override
392     public Workspace wrapWorkspace(Workspace workspace) {
393         return new LastUpdateWorkspaceWrapper(workspace, this);
394     }
395 
396     @Override
397     public Node wrapNode(Node node) {
398         return new LastUpdateNodeWrapper(node, this);
399     }
400 
401     @Override
402     public Property wrapProperty(Property property) {
403         return new LastUpdatePropertyWrapper(property, this);
404     }
405 
406     void updateLastModified(final Session session, final String destAbsPath, final boolean recursiveDown) throws RepositoryException, PathNotFoundException {
407         this.updateLastModified(session.getWorkspace().getName(), destAbsPath, recursiveDown);
408     }
409 
410     void updateLastModified(final String workspaceName, final String destAbsPath, final boolean recursiveDown) throws RepositoryException, PathNotFoundException {
411 
412         if ("/".equals(destAbsPath) && !recursiveDown) {
413             // we do not maintain lud on root node
414             return;
415         }
416         User user = MgnlContext.getUser();
417         final String username = user == null ? "not available" : user.getName();
418         // one date for all children
419         final Calendar updateDate = Calendar.getInstance();
420 
421         // DO ALL IN SYSTEM CONTEXT !!! USER MIGHT NOT HAVE ENOUGH RIGHTS
422         MgnlContext.doInSystemContext(new ChangeLastUpdateDateOp(workspaceName, username, destAbsPath, updateDate, recursiveDown));
423 
424     }
425 
426     void updateLastModified(String workspaceName, String destAbsPath) throws RepositoryException, PathNotFoundException {
427         updateLastModified(workspaceName, destAbsPath, false);
428     }
429 
430     void updateLastModified(Session session, String destAbsPath) throws RepositoryException, PathNotFoundException {
431         updateLastModified(session, destAbsPath, false);
432     }
433 
434     void updateLastModifiedProperty(String workspaceName, String propertyName, String parentPath) throws RepositoryException {
435         if (shouldIgnoreUpdate(propertyName)) {
436             return;
437         }
438         updateLastModified(workspaceName, parentPath);
439     }
440 
441     /**
442      * @return whether a modification to the property with the provided name should trigger an update of the last modification or not.
443      */
444     protected boolean shouldIgnoreUpdate(final String propertyName) {
445         // our update doesn't count as update (activation, versioning, last update, etc) - except for changing the template
446         return propertyName.startsWith(NodeTypes.JCR_PREFIX) || (propertyName.startsWith(NodeTypes.MGNL_PREFIX) && !NodeTypes.Renderable.TEMPLATE.equals(propertyName));
447     }
448 }