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 (skipTypeInWorkspace(workspaceName, node.getPrimaryNodeType().getName())) {
189                             // do not update LUD of acls
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         private boolean skipTypeInWorkspace(String workspaceName, String name) {
209             if (RepositoryConstants.USER_ROLES.equals(workspaceName)) {
210                 return !(NodeTypes.Role.NAME.equals(name) || NodeTypes.Folder.NAME.equals(name));
211             }
212             if (RepositoryConstants.WEBSITE.equals(workspaceName)) {
213                 return !NodeTypes.Page.NAME.equals(name);
214             }
215             return false;
216         }
217     }
218 
219     /**
220      * Updates parent page or parent content mgnl:lastUpdated property on modification.
221      */
222     public class LastUpdatePropertyWrapper extends ContentDecoratorPropertyWrapper<LastUpdateContentDecorator> implements Property {
223 
224         public LastUpdatePropertyWrapper(Property property, LastUpdateContentDecorator contentDecorator) {
225             super(property, contentDecorator);
226         }
227 
228         @Override
229         public void setValue(BigDecimal value) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
230             super.setValue(value);
231             this.updateLastModifiedProperty();
232         }
233 
234         @Override
235         public void setValue(Binary value) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
236             super.setValue(value);
237             this.updateLastModifiedProperty();
238         }
239 
240         @Override
241         public void setValue(boolean value) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
242             super.setValue(value);
243             this.updateLastModifiedProperty();
244         }
245 
246         @Override
247         public void setValue(Calendar value) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
248             super.setValue(value);
249             this.updateLastModifiedProperty();
250         }
251 
252         @Override
253         public void setValue(double value) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
254             super.setValue(value);
255             this.updateLastModifiedProperty();
256         }
257 
258         @Override
259         public void setValue(InputStream value) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
260             super.setValue(value);
261             this.updateLastModifiedProperty();
262         }
263 
264         @Override
265         public void setValue(long value) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
266             super.setValue(value);
267             this.updateLastModifiedProperty();
268         }
269 
270         @Override
271         public void setValue(Node value) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
272             super.setValue(value);
273             this.updateLastModifiedProperty();
274         }
275 
276         @Override
277         public void setValue(String value) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
278             super.setValue(value);
279             this.updateLastModifiedProperty();
280         }
281 
282         @Override
283         public void setValue(String[] values) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
284             super.setValue(values);
285             this.updateLastModifiedProperty();
286         }
287 
288         @Override
289         public void setValue(Value value) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
290             super.setValue(value);
291             this.updateLastModifiedProperty();
292         }
293 
294         @Override
295         public void setValue(Value[] values) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
296             super.setValue(values);
297             this.updateLastModifiedProperty();
298         }
299 
300         @Override
301         public void remove() throws VersionException, LockException, ConstraintViolationException, AccessDeniedException, RepositoryException {
302             Node parent = this.getParent();
303             String propertyName = this.getName();
304             super.remove();
305             if (shouldIgnoreUpdate(propertyName)) {
306                 return;
307             }
308             updateLastModified(parent.getSession(), parent.getPath());
309         }
310 
311         private void updateLastModifiedProperty() throws RepositoryException {
312             LastUpdateContentDecorator.this.updateLastModifiedProperty(getSession().getWorkspace().getName(), this.getName(), this.getParent().getPath());
313         }
314     }
315 
316     /**
317      * Updates destination parent page or parent content mgnl:lastUpdated property on move or copy operations.
318      */
319     public class LastUpdateWorkspaceWrapper extends ContentDecoratorWorkspaceWrapper implements Workspace {
320 
321         protected LastUpdateWorkspaceWrapper(Workspace workspace, ContentDecorator contentDecorator) {
322             super(workspace, contentDecorator);
323         }
324 
325         @Override
326         public void move(String srcAbsPath, String destAbsPath) throws ConstraintViolationException, VersionException, AccessDeniedException, PathNotFoundException, ItemExistsException, LockException, RepositoryException {
327             super.move(srcAbsPath, destAbsPath);
328             updateLastModified(super.getWrappedWorkspace().getSession(), destAbsPath, true);
329         }
330 
331         @Override
332         public void copy(String srcAbsPath, String destAbsPath) throws ConstraintViolationException, VersionException, AccessDeniedException, PathNotFoundException, ItemExistsException, LockException, RepositoryException {
333             super.copy(srcAbsPath, destAbsPath);
334             updateLastModified(super.getWrappedWorkspace().getSession(), destAbsPath, true);
335         }
336 
337         @Override
338         public void copy(String srcWorkspace, String srcAbsPath, String destAbsPath) throws NoSuchWorkspaceException, ConstraintViolationException, VersionException, AccessDeniedException, PathNotFoundException, ItemExistsException, LockException, RepositoryException {
339             super.copy(srcWorkspace, srcAbsPath, destAbsPath);
340             updateLastModified(super.getWrappedWorkspace().getSession(), destAbsPath, true);
341         }
342 
343         @Override
344         public void clone(String srcWorkspace, String srcAbsPath, String destAbsPath, boolean removeExisting) throws NoSuchWorkspaceException, ConstraintViolationException, VersionException, AccessDeniedException, PathNotFoundException, ItemExistsException, LockException, RepositoryException {
345             super.clone(srcWorkspace, srcAbsPath, destAbsPath, removeExisting);
346             // no update on clone. a) it doesn't work reliably (session refresh) and b) contract of clone is to make identical copy!
347         }
348 
349     }
350 
351     /**
352      * Updates destination parent page or content mgnl:lastModified property on move.
353      */
354     public class LastUpdateSessionWrapper extends ContentDecoratorSessionWrapper<LastUpdateContentDecorator> implements Session {
355 
356         public LastUpdateSessionWrapper(Session session, LastUpdateContentDecorator contentDecorator) {
357             super(session, contentDecorator);
358         }
359 
360         @Override
361         public void move(String srcAbsPath, String destAbsPath) throws ItemExistsException, PathNotFoundException, VersionException, ConstraintViolationException, LockException, RepositoryException {
362             super.move(srcAbsPath, destAbsPath);
363             // update src path, item is not moved yet
364             updateLastModified(super.getWrappedSession(), srcAbsPath, true);
365         }
366 
367         @Override
368         public void save() throws AccessDeniedException, ItemExistsException, ReferentialIntegrityException, ConstraintViolationException, InvalidItemStateException, VersionException, LockException, NoSuchNodeTypeException, RepositoryException {
369             String workspaceName = wrapped.getWorkspace().getName();
370             try {
371                 log.debug("saving session: " + wrapped.toString() + "::" + workspaceName + "::sys:" + MgnlContext.isSystemInstance() + "::" + Thread.currentThread().getName());
372                 super.save();
373                 if (MgnlContext.isSystemInstance()) {
374                     isSysSessionDirty = false;
375                 }
376             } catch (InvalidItemStateException e) {
377                 log.error("Failed to update LUD for session: " + wrapped.toString() + "::" + workspaceName, e);
378                 throw e;
379             }
380             if (isSysSessionDirty) {
381                 Session sysSession = MgnlContext.getSystemContext().getJCRSession(workspaceName);
382                 if (sysSession instanceof DelegateSessionWrapper) {
383                     sysSession = ((DelegateSessionWrapper) sysSession).deepUnwrap(LastUpdateSessionWrapper.class);
384                 }
385                 if (!wrapped.toString().equals(sysSession.toString())) {
386                     log.debug("Forcing saving of : " + sysSession.toString() + "::" + workspaceName);
387                     sysSession.save();
388                 }
389                 isSysSessionDirty = false;
390             }
391         }
392 
393     }
394 
395     @Override
396     public Session wrapSession(Session session) {
397 
398         return new LastUpdateSessionWrapper(session, this);
399     }
400 
401     @Override
402     public Workspace wrapWorkspace(Workspace workspace) {
403         return new LastUpdateWorkspaceWrapper(workspace, this);
404     }
405 
406     @Override
407     public Node wrapNode(Node node) {
408         return new LastUpdateNodeWrapper(node, this);
409     }
410 
411     @Override
412     public Property wrapProperty(Property property) {
413         return new LastUpdatePropertyWrapper(property, this);
414     }
415 
416     void updateLastModified(final Session session, final String destAbsPath, final boolean recursiveDown) throws RepositoryException, PathNotFoundException {
417         this.updateLastModified(session.getWorkspace().getName(), destAbsPath, recursiveDown);
418     }
419 
420     void updateLastModified(final String workspaceName, final String destAbsPath, final boolean recursiveDown) throws RepositoryException, PathNotFoundException {
421 
422         if ("/".equals(destAbsPath) && !recursiveDown) {
423             // we do not maintain lud on root node
424             return;
425         }
426         User user = MgnlContext.getUser();
427         final String username = user == null ? "not available" : user.getName();
428         // one date for all children
429         final Calendar updateDate = Calendar.getInstance();
430 
431         // DO ALL IN SYSTEM CONTEXT !!! USER MIGHT NOT HAVE ENOUGH RIGHTS
432         MgnlContext.doInSystemContext(new ChangeLastUpdateDateOp(workspaceName, username, destAbsPath, updateDate, recursiveDown));
433 
434     }
435 
436     void updateLastModified(String workspaceName, String destAbsPath) throws RepositoryException, PathNotFoundException {
437         updateLastModified(workspaceName, destAbsPath, false);
438     }
439 
440     void updateLastModified(Session session, String destAbsPath) throws RepositoryException, PathNotFoundException {
441         updateLastModified(session, destAbsPath, false);
442     }
443 
444     void updateLastModifiedProperty(String workspaceName, String propertyName, String parentPath) throws RepositoryException {
445         if (shouldIgnoreUpdate(propertyName)) {
446             return;
447         }
448         updateLastModified(workspaceName, parentPath);
449     }
450 
451     /**
452      * @return whether a modification to the property with the provided name should trigger an update of the last modification or not.
453      */
454     protected boolean shouldIgnoreUpdate(final String propertyName) {
455         // our update doesn't count as update (activation, versioning, last update, etc) - except for changing the template
456         return propertyName.startsWith(NodeTypes.JCR_PREFIX) || (propertyName.startsWith(NodeTypes.MGNL_PREFIX) && !NodeTypes.Renderable.TEMPLATE.equals(propertyName));
457     }
458 }