View Javadoc
1   /**
2    * This file Copyright (c) 2013-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.jcr.wrapper;
35  
36  import info.magnolia.cms.security.JCRSessionOp;
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.LinkedList;
51  import java.util.List;
52  import java.util.Map;
53  
54  import javax.jcr.AccessDeniedException;
55  import javax.jcr.Binary;
56  import javax.jcr.InvalidItemStateException;
57  import javax.jcr.ItemExistsException;
58  import javax.jcr.NoSuchWorkspaceException;
59  import javax.jcr.Node;
60  import javax.jcr.NodeIterator;
61  import javax.jcr.PathNotFoundException;
62  import javax.jcr.Property;
63  import javax.jcr.ReferentialIntegrityException;
64  import javax.jcr.RepositoryException;
65  import javax.jcr.Session;
66  import javax.jcr.Value;
67  import javax.jcr.ValueFormatException;
68  import javax.jcr.Workspace;
69  import javax.jcr.lock.LockException;
70  import javax.jcr.nodetype.ConstraintViolationException;
71  import javax.jcr.nodetype.NoSuchNodeTypeException;
72  import javax.jcr.version.VersionException;
73  
74  import org.apache.commons.lang.ArrayUtils;
75  import org.apache.commons.lang.StringUtils;
76  import org.slf4j.Logger;
77  import org.slf4j.LoggerFactory;
78  
79  /**
80   * Decorator to set appropriate Mgnl specific properties (e.g. mgnl:lastModification, mgnl:created).
81   *
82   * Caution: upon workspace clone the wrapper does not update the lastModification date in order to preserve contract of clone (make an identical copy).
83   */
84  public class MgnlPropertySettingContentDecorator extends PropertyAndChildWrappingContentDecorator implements ContentDecorator {
85  
86      /**
87       * inner holder of node info for all nodes that need their lud modified.
88       */
89      // as long as this class stays bound to session in which it is created, we don't need to store workspace name
90      private class DirtyOp {
91  
92          private String path;
93          private String userName;
94          private Calendar updateDate;
95  
96          public DirtyOp(String path, String userName, Calendar updateDate) {
97              this.path = path;
98              this.userName = userName;
99              this.updateDate = updateDate;
100         }
101 
102         public String getPath() {
103             return path;
104         }
105 
106         public void setPath(String path) {
107             this.path = path;
108         }
109 
110         public String getUserName() {
111             return userName;
112         }
113 
114         public Calendar getUpdateDate() {
115             return updateDate;
116         }
117 
118     }
119 
120     private static final Logger log = LoggerFactory.getLogger(MgnlPropertySettingContentDecorator.class);
121 
122     // TODO: this behavior should be injectable w/ custom rules
123     /**
124      * Map below contains parent node type we try to resolve for LUD updates instead of the "current" path/node.
125      */
126     private static Map<String, String> parentNodeMappings = ArrayUtils.toMap(new String[][]{
127             {RepositoryConstants.WEBSITE, NodeTypes.Page.NAME},
128             {RepositoryConstants.USERS, NodeTypes.User.NAME}});
129 
130     protected List<DirtyOp> dirtyOps = new LinkedList<DirtyOp>();
131 
132 
133     /**
134      * Repository operation for update of mgnl:lastUpdated property.
135      */
136     final class ChangeLastUpdateDateOp extends MgnlContext.RepositoryOp {
137         private final String workspaceName;
138         private final String userName;
139         private final String destAbsPath;
140         private final Calendar updateDate;
141         private final boolean recursiveDown;
142 
143         ChangeLastUpdateDateOp(String workspaceName, String userName, String destAbsPath, Calendar updateDate, boolean recursiveDown) {
144             this.workspaceName = workspaceName;
145             this.userName = userName;
146             this.destAbsPath = destAbsPath;
147             this.updateDate = updateDate;
148             this.recursiveDown = recursiveDown;
149         }
150 
151         @Override
152         public void doExec() throws RepositoryException {
153             Session sysSession = MgnlContext.getJCRSession(workspaceName);
154 
155             // does it exist?
156             if (!sysSession.itemExists(destAbsPath)) {
157                 // can't do anything.
158                 if (log.isDebugEnabled()) {
159                     log.warn("Can't update mgnl:lastModified. Path {}:{} doesn't exist anymore.", workspaceName, destAbsPath);
160                 }
161                 return;
162             }
163             String srcAbsPath = sysSession.getNode(destAbsPath).getPath();
164 
165             Node node = resolveModifiedNode(workspaceName, destAbsPath, sysSession);
166 
167             // might be null also after the failed attempt to resolve parent node that need to be marked as modified
168             if (node == null) {
169                 node = sysSession.getNode(destAbsPath);
170             }
171 
172             if (node.isNodeType(NodeTypes.MetaData.NAME)) {
173                 // no LUD on old style of metadata
174                 return;
175             }
176 
177             // skip root node
178             if (node.getDepth() == 0) {
179                 return;
180             }
181 
182             // unwrap
183             if (node instanceof DelegateNodeWrapper) {
184                 node = ((DelegateNodeWrapper) node).deepUnwrap(MgnlPropertySettingNodeWrapper.class);
185             }
186 
187             log.debug("LUD on {} from {}:{}", node.getPath(), node.getSession().toString(), Thread.currentThread().getName());
188             if (node.isNodeType(LastModified.NAME)) {
189                 String resolvedPath = node.getPath();
190                 if (!destAbsPath.equals(srcAbsPath)) {
191                     // yeah, apparently that's what happens after non-yet-persisted in-session move
192                     if (!resolvedPath.equals(srcAbsPath) && !resolvedPath.equals(destAbsPath)) {
193                         // we are in real bind here ... what should be the dirty path?
194                         if (srcAbsPath.startsWith(resolvedPath)) {
195                             String hunk = StringUtils.substringAfter(srcAbsPath, resolvedPath);
196                             if (destAbsPath.endsWith(hunk)) {
197                                 resolvedPath = StringUtils.substringBefore(destAbsPath, hunk);
198                             }
199                         } else {
200                             resolvedPath = destAbsPath;
201                         }
202                     }
203                 }
204 
205                 dirtyOps.add(new DirtyOp(resolvedPath, userName, updateDate));
206             }
207 
208             if (recursiveDown) {
209                 // trick or treat children
210                 List<NodeIterator> iters = new ArrayList<NodeIterator>();
211                 iters.add(node.getNodes());
212                 while (!iters.isEmpty()) {
213                     List<NodeIterator> tmp = updateChildren(node.getPath(), destAbsPath, iters, updateDate);
214                     iters.clear();
215                     iters.addAll(tmp);
216                 }
217             }
218             // save ... sucks, but since we are in system context there's no other way
219             // we are overriding save on session and call it on sys ctx as well instead of saving here
220             return;
221         }
222 
223         private Node resolveModifiedNode(final String workspaceName, final String destAbsPath, Session sysSession) throws RepositoryException {
224             String parentNodeType = parentNodeMappings.get(workspaceName);
225             Node node = sysSession.getNode(destAbsPath);
226             if (parentNodeType == null) {
227                 return node;
228             }
229             while (node != null && !parentNodeType.equals(node.getPrimaryNodeType().getName()) && node.getDepth() > 0) {
230                 node = node.getParent();
231             }
232             return node;
233         }
234 
235         private List<NodeIterator> updateChildren(String srcAbsPath, String destAbsPath, List<NodeIterator> iters, Calendar updateDate) {
236             List<NodeIterator> tmp = new ArrayList<NodeIterator>();
237             for (NodeIterator iter : iters) {
238                 while (iter.hasNext()) {
239                     Node node = iter.nextNode();
240                     try {
241                         if (skipTypeInWorkspace(workspaceName, node.getPrimaryNodeType().getName())) {
242                             // do not update LUD of acls
243                             continue;
244                         }
245                         if (node.isNodeType(NodeTypes.MetaData.NAME)) {
246                             // no LUD on old style of metadata
247                             continue;
248                         }
249                         if (node.isNodeType(LastModified.NAME)) {
250                             dirtyOps.add(new DirtyOp(destAbsPath + StringUtils.removeStart(node.getPath(), srcAbsPath), userName, updateDate));
251                         }
252                         tmp.add(node.getNodes());
253                     } catch (RepositoryException e) {
254                         log.error("Failed to update last modified date of " + node + " with " + e.getMessage(), e);
255                     }
256                 }
257             }
258             return tmp;
259         }
260 
261         private boolean skipTypeInWorkspace(String workspaceName, String name) {
262             if (RepositoryConstants.USER_ROLES.equals(workspaceName)) {
263                 return !(NodeTypes.Role.NAME.equals(name) || NodeTypes.Folder.NAME.equals(name));
264             }
265             if (RepositoryConstants.WEBSITE.equals(workspaceName)) {
266                 return !NodeTypes.Page.NAME.equals(name);
267             }
268             return false;
269         }
270     }
271 
272     /**
273      * Updates parent page or parent content mgnl:lastUpdated property on modification.
274      */
275     public class LastUpdatePropertyWrapper extends ContentDecoratorPropertyWrapper<MgnlPropertySettingContentDecorator> implements Property {
276 
277         public LastUpdatePropertyWrapper(Property property, MgnlPropertySettingContentDecorator contentDecorator) {
278             super(property, contentDecorator);
279         }
280 
281         @Override
282         public void setValue(BigDecimal value) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
283             super.setValue(value);
284             this.updateLastModifiedProperty();
285         }
286 
287         @Override
288         public void setValue(Binary value) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
289             super.setValue(value);
290             this.updateLastModifiedProperty();
291         }
292 
293         @Override
294         public void setValue(boolean value) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
295             super.setValue(value);
296             this.updateLastModifiedProperty();
297         }
298 
299         @Override
300         public void setValue(Calendar value) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
301             super.setValue(value);
302             this.updateLastModifiedProperty();
303         }
304 
305         @Override
306         public void setValue(double value) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
307             super.setValue(value);
308             this.updateLastModifiedProperty();
309         }
310 
311         @Override
312         public void setValue(InputStream value) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
313             super.setValue(value);
314             this.updateLastModifiedProperty();
315         }
316 
317         @Override
318         public void setValue(long value) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
319             super.setValue(value);
320             this.updateLastModifiedProperty();
321         }
322 
323         @Override
324         public void setValue(Node value) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
325             super.setValue(value);
326             this.updateLastModifiedProperty();
327         }
328 
329         @Override
330         public void setValue(String value) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
331             super.setValue(value);
332             this.updateLastModifiedProperty();
333         }
334 
335         @Override
336         public void setValue(String[] values) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
337             super.setValue(values);
338             this.updateLastModifiedProperty();
339         }
340 
341         @Override
342         public void setValue(Value value) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
343             super.setValue(value);
344             this.updateLastModifiedProperty();
345         }
346 
347         @Override
348         public void setValue(Value[] values) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
349             super.setValue(values);
350             this.updateLastModifiedProperty();
351         }
352 
353         @Override
354         public void remove() throws VersionException, LockException, ConstraintViolationException, AccessDeniedException, RepositoryException {
355             Node parent = this.getParent();
356             String propertyName = this.getName();
357             super.remove();
358             if (shouldIgnoreUpdate(propertyName)) {
359                 return;
360             }
361             updateLastModified(parent.getSession(), parent.getPath());
362         }
363 
364         private void updateLastModifiedProperty() throws RepositoryException {
365             MgnlPropertySettingContentDecorator.this.updateLastModifiedProperty(getSession().getWorkspace().getName(), this.getName(), this.getParent().getPath());
366         }
367     }
368 
369     /**
370      * Wrapper keeping last update date up to date.
371      *
372      * @deprecated since 5.2.2 - pls use MgnlPropertySettingWorkspaceWrapper instead.
373      */
374     public class LastUpdateWorkspaceWrapper extends MgnlPropertySettingWorkspaceWrapper {
375 
376         protected LastUpdateWorkspaceWrapper(final Workspace workspace, final ContentDecorator contentDecorator) {
377             super(workspace, contentDecorator);
378         }
379     }
380 
381     /**
382      * Updates destination parent page or parent content mgnl:lastUpdated property on move or copy operations.
383      */
384     public class MgnlPropertySettingWorkspaceWrapper extends ContentDecoratorWorkspaceWrapper implements Workspace {
385 
386         protected MgnlPropertySettingWorkspaceWrapper(final Workspace workspace, final ContentDecorator contentDecorator) {
387             super(workspace, contentDecorator);
388         }
389 
390         @Override
391         public void move(String srcAbsPath, String destAbsPath) throws ConstraintViolationException, VersionException, AccessDeniedException, PathNotFoundException, ItemExistsException, LockException, RepositoryException {
392             super.move(srcAbsPath, destAbsPath);
393             updateLastModified(super.getWrappedWorkspace().getSession(), destAbsPath, true);
394         }
395 
396         @Override
397         public void copy(String srcAbsPath, String destAbsPath) throws ConstraintViolationException, VersionException, AccessDeniedException, PathNotFoundException, ItemExistsException, LockException, RepositoryException {
398             super.copy(srcAbsPath, destAbsPath);
399             final String workspaceName = super.getWrappedWorkspace().getName();
400             setCreatedDate(workspaceName, destAbsPath, true);
401         }
402 
403         @Override
404         public void copy(String srcWorkspace, String srcAbsPath, String destAbsPath) throws NoSuchWorkspaceException, ConstraintViolationException, VersionException, AccessDeniedException, PathNotFoundException, ItemExistsException, LockException, RepositoryException {
405             super.copy(srcWorkspace, srcAbsPath, destAbsPath);
406             final String workspaceName = super.getWrappedWorkspace().getName();
407             setCreatedDate(workspaceName, destAbsPath, true);
408         }
409 
410         @Override
411         public void clone(String srcWorkspace, String srcAbsPath, String destAbsPath, boolean removeExisting) throws NoSuchWorkspaceException, ConstraintViolationException, VersionException, AccessDeniedException, PathNotFoundException, ItemExistsException, LockException, RepositoryException {
412             super.clone(srcWorkspace, srcAbsPath, destAbsPath, removeExisting);
413             // no update on clone. a) it doesn't work reliably (session refresh) and b) contract of clone is to make identical copy!
414         }
415 
416     }
417 
418     /**
419      * Wrapper keeping last update date up to date.
420      *
421      * @deprecated since 5.2.2 - pls use MgnlPropertySettingSessionWrapper instead.
422      */
423     public class LastUpdateSessionWrapper extends MgnlPropertySettingSessionWrapper {
424         public LastUpdateSessionWrapper(final Session session, final MgnlPropertySettingContentDecorator contentDecorator) {
425             super(session, contentDecorator);
426         }
427     }
428 
429     /**
430      * Updates all specific properties upon certain actions. Right now it's just updating the destination parent page or content mgnl:lastModified property on move.
431      */
432     public class MgnlPropertySettingSessionWrapper extends ContentDecoratorSessionWrapper<MgnlPropertySettingContentDecorator> implements Session {
433 
434         public MgnlPropertySettingSessionWrapper(Session session, MgnlPropertySettingContentDecorator contentDecorator) {
435             super(session, contentDecorator);
436         }
437 
438         @Override
439         public void move(final String srcAbsPath, final String destAbsPath) throws ItemExistsException, PathNotFoundException, VersionException, ConstraintViolationException, LockException, RepositoryException {
440             super.move(srcAbsPath, destAbsPath);
441             // due to bug in some versions of jackrabbit we might have node showing itself as still being on a source path or already on a destination path
442             boolean onSourcePath = MgnlContext.doInSystemContext(new JCRSessionOp<Boolean>(super.getWrappedSession().getWorkspace().getName()) {
443                 @Override
444                 public Boolean exec(Session session) throws RepositoryException {
445                     return session.itemExists(srcAbsPath);
446                 };
447             });
448             String aPath = onSourcePath ? srcAbsPath : destAbsPath;
449             updateLastModified(super.getWrappedSession(), aPath, true);
450             if (onSourcePath) {
451                 for (DirtyOp op : dirtyOps) {
452                     if (op.getPath().equals(srcAbsPath) || op.getPath().startsWith(srcAbsPath + "/")) {
453                         op.setPath(destAbsPath + StringUtils.substringAfter(op.getPath(), srcAbsPath));
454                     }
455                 }
456             }
457         }
458 
459         @Override
460         public void save() throws AccessDeniedException, ItemExistsException, ReferentialIntegrityException, ConstraintViolationException, InvalidItemStateException, VersionException, LockException, NoSuchNodeTypeException, RepositoryException {
461             String workspaceName = wrapped.getWorkspace().getName();
462             try {
463                 log.debug("saving session: " + wrapped.toString() + "::" + workspaceName + "::sys:" + MgnlContext.isSystemInstance() + "::" + Thread.currentThread().getName());
464                 super.save();
465             } catch (InvalidItemStateException e) {
466                 log.error("Failed to update LUD for session: " + wrapped.toString() + "::" + workspaceName, e);
467                 throw e;
468             }
469             if (!dirtyOps.isEmpty()) {
470                 Session sysSession = MgnlContext.getSystemContext().getJCRSession(workspaceName);
471                 if (sysSession instanceof DelegateSessionWrapper) {
472                     sysSession = ((DelegateSessionWrapper) sysSession).deepUnwrap(MgnlPropertySettingSessionWrapper.class);
473                 }
474                 applyPendingChanges(sysSession);
475                 sysSession.save();
476             }
477         }
478 
479         protected void applyPendingChanges(Session session) throws RepositoryException, PathNotFoundException {
480             while (!dirtyOps.isEmpty()) {
481                 DirtyOp dirty = dirtyOps.remove(0);
482                 if (session.nodeExists(dirty.getPath())) {
483                     log.debug("Updating {} with {}", dirty.getPath(), dirty.getUpdateDate());
484                     LastModified.update(session.getNode(dirty.getPath()), dirty.getUserName(), dirty.getUpdateDate());
485                 } else {
486                     // can happen when deleting something that was modified or moved within same session
487                     if (log.isDebugEnabled()) {
488                         log.warn("wanted to update " + session.getWorkspace().getName() + ":" + dirty.getPath() + " modified by:" + dirty.getUserName() + " at " + dirty.getUpdateDate() + " but it's gone now.");
489                     }
490                 }
491             }
492         }
493 
494     }
495 
496     @Override
497     public Session wrapSession(Session session) {
498         return new MgnlPropertySettingSessionWrapper(session, this);
499     }
500 
501     @Override
502     public Workspace wrapWorkspace(Workspace workspace) {
503         return new MgnlPropertySettingWorkspaceWrapper(workspace, this);
504     }
505 
506     @Override
507     public Node wrapNode(Node node) {
508         return new MgnlPropertySettingNodeWrapper(node, this);
509     }
510 
511     @Override
512     public Property wrapProperty(Property property) {
513         return new LastUpdatePropertyWrapper(property, this);
514     }
515 
516     void updateLastModified(final Session session, final String destAbsPath, final boolean recursiveDown) throws RepositoryException, PathNotFoundException {
517         this.updateLastModified(session.getWorkspace().getName(), destAbsPath, recursiveDown);
518     }
519 
520     void updateLastModified(final String workspaceName, final String destAbsPath, final boolean recursiveDown) throws RepositoryException, PathNotFoundException {
521 
522         if ("/".equals(destAbsPath) && !recursiveDown) {
523             // we do not maintain lud on root node
524             return;
525         }
526         // one date for all children
527         final Calendar updateDate = Calendar.getInstance();
528 
529         // DO ALL IN SYSTEM CONTEXT !!! USER MIGHT NOT HAVE ENOUGH RIGHTS
530         MgnlContext.doInSystemContext(new ChangeLastUpdateDateOp(workspaceName, MgnlContext.getUser().getName(), destAbsPath, updateDate, recursiveDown));
531 
532     }
533 
534     void updateLastModified(String workspaceName, String destAbsPath) throws RepositoryException, PathNotFoundException {
535         updateLastModified(workspaceName, destAbsPath, false);
536     }
537 
538     void updateLastModified(Session session, String destAbsPath) throws RepositoryException, PathNotFoundException {
539         updateLastModified(session, destAbsPath, false);
540     }
541 
542     void updateLastModifiedProperty(String workspaceName, String propertyName, String parentPath) throws RepositoryException {
543         if (shouldIgnoreUpdate(propertyName)) {
544             return;
545         }
546         updateLastModified(workspaceName, parentPath);
547     }
548 
549     void setCreatedDate(final String workspaceName, final String absPath) throws RepositoryException {
550         setCreatedDate(workspaceName, absPath, false);
551     }
552 
553     void setCreatedDate(final String workspaceName, final String absPath, final boolean recursiveDown) throws RepositoryException {
554         final Session session = MgnlContext.getJCRSession(workspaceName);
555 
556         // does it exist?
557         if (!session.itemExists(absPath)) {
558             // can't do anything.
559             if (log.isDebugEnabled()) {
560                 log.warn("Can't update {}. Path {}:{} does not exist.", NodeTypes.Created.NAME, workspaceName, absPath);
561             }
562             return;
563         }
564         Node node = session.getNode(absPath);
565 
566         // don't do anything on null nodes
567         if (node == null) {
568             return;
569         }
570 
571         // skip root
572         if (node.getDepth() == 0) {
573             return;
574         }
575 
576         // unwrap if required
577         if (node instanceof DelegateNodeWrapper) {
578             node = ((DelegateNodeWrapper) node).deepUnwrap(MgnlPropertySettingNodeWrapper.class);
579         }
580 
581         final String user = MgnlContext.getUser().getName();
582 
583         // one date for all
584         final Calendar now = Calendar.getInstance();
585 
586         if (node.isNodeType(NodeTypes.Created.NAME)) {
587             log.debug("Setting {} on {} from {}:{}", NodeTypes.Created.NAME, node.getPath(), node.getSession().toString(), Thread.currentThread().getName());
588             NodeTypes.Created.set(node, user, now);
589         }
590 
591         if (recursiveDown) {
592             // trick or treat children
593             List<NodeIterator> iters = new ArrayList<NodeIterator>();
594             iters.add(node.getNodes());
595             while (!iters.isEmpty()) {
596                 List<NodeIterator> tmp = updateChildren(iters, user, now);
597                 iters.clear();
598                 iters.addAll(tmp);
599             }
600         }
601     }
602 
603     private List<NodeIterator> updateChildren(final List<NodeIterator> iters, final String user, final Calendar updateDate) {
604         List<NodeIterator> tmp = new ArrayList<NodeIterator>();
605         for (NodeIterator iterator : iters) {
606             while (iterator.hasNext()) {
607                 Node node = iterator.nextNode();
608                 try {
609                     if (node.isNodeType(NodeTypes.Created.NAME)) {
610                         NodeTypes.Created.set(node, user, updateDate);
611                     }
612                     tmp.add(node.getNodes());
613                 } catch (RepositoryException e) {
614                     log.error("Failed to set created date of " + node + " with " + e.getMessage(), e);
615                 }
616             }
617         }
618         return tmp;
619     }
620 
621     /**
622      * @return whether a modification to the property with the provided name should trigger an update of the last modification or not.
623      */
624     protected boolean shouldIgnoreUpdate(final String propertyName) {
625         // our update doesn't count as update (activation, versioning, last update, etc) - except for changing the template
626         return propertyName.startsWith(NodeTypes.JCR_PREFIX) || (propertyName.startsWith(NodeTypes.MGNL_PREFIX) && !NodeTypes.Renderable.TEMPLATE.equals(propertyName));
627     }
628 }