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