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