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