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             final String user = getCurrentUserName();
472             NodeUtil.visit(getSession().getNode(destAbsPath), node -> updateActivationStatus(node, user));
473             getSession().save();
474         }
475 
476         @Override
477         public void copy(String srcWorkspace, String srcAbsPath, String destAbsPath) throws NoSuchWorkspaceException, ConstraintViolationException, VersionException, AccessDeniedException, PathNotFoundException, ItemExistsException, LockException, RepositoryException {
478             super.copy(srcWorkspace, srcAbsPath, destAbsPath);
479             final String workspaceName = super.getWrappedWorkspace().getName();
480             setCreatedDate(workspaceName, destAbsPath, true);
481             final String user = getCurrentUserName();
482             NodeUtil.visit(getSession().getNode(destAbsPath), node -> updateActivationStatus(node, user));
483             getSession().save();
484         }
485 
486         @Override
487         public void clone(String srcWorkspace, String srcAbsPath, String destAbsPath, boolean removeExisting) throws NoSuchWorkspaceException, ConstraintViolationException, VersionException, AccessDeniedException, PathNotFoundException, ItemExistsException, LockException, RepositoryException {
488             super.clone(srcWorkspace, srcAbsPath, destAbsPath, removeExisting);
489             // no update on clone. a) it doesn't work reliably (session refresh) and b) contract of clone is to make identical copy!
490         }
491 
492     }
493 
494     /**
495      * Wrapper keeping last update date up to date.
496      *
497      * @deprecated since 5.2.2 - pls use MgnlPropertySettingSessionWrapper instead.
498      */
499     @Deprecated
500     public class LastUpdateSessionWrapper extends MgnlPropertySettingSessionWrapper {
501         public LastUpdateSessionWrapper(final Session session, final MgnlPropertySettingContentDecorator contentDecorator) {
502             super(session, contentDecorator);
503         }
504     }
505 
506     /**
507      * Updates all specific properties upon certain actions. Right now it's just updating the destination parent page or content mgnl:lastModified property on move.
508      */
509     public class MgnlPropertySettingSessionWrapper extends ContentDecoratorSessionWrapper<MgnlPropertySettingContentDecorator> implements Session {
510 
511         public MgnlPropertySettingSessionWrapper(Session session, MgnlPropertySettingContentDecorator contentDecorator) {
512             super(session, contentDecorator);
513         }
514 
515         @Override
516         public void move(final String srcAbsPath, final String destAbsPath) throws ItemExistsException, PathNotFoundException, VersionException, ConstraintViolationException, LockException, RepositoryException {
517             super.move(srcAbsPath, destAbsPath);
518             // 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
519             boolean onSourcePath = MgnlContext.doInSystemContext(new JCRSessionOp<Boolean>(super.getWrappedSession().getWorkspace().getName()) {
520                 @Override
521                 public Boolean exec(Session session) throws RepositoryException {
522                     return session.itemExists(srcAbsPath);
523                 }
524             });
525             String aPath = onSourcePath ? srcAbsPath : destAbsPath;
526             updateLastModified(super.getWrappedSession(), aPath, true);
527             if (onSourcePath) {
528                 for (DirtyOp op : dirtyOps) {
529                     if (op.getPath().equals(srcAbsPath) || op.getPath().startsWith(srcAbsPath + "/")) {
530                         op.setPath(destAbsPath + StringUtils.substringAfter(op.getPath(), srcAbsPath));
531                     }
532                 }
533             }
534         }
535 
536         @Override
537         public void save() throws AccessDeniedException, ItemExistsException, ReferentialIntegrityException, ConstraintViolationException, InvalidItemStateException, VersionException, LockException, NoSuchNodeTypeException, RepositoryException {
538             String workspaceName = wrapped.getWorkspace().getName();
539             try {
540                 log.debug("saving session: {}::{}::sys:{}::{}", wrapped.toString(), workspaceName, MgnlContext.isSystemInstance(), Thread.currentThread().getName());
541                 super.save();
542             } catch (InvalidItemStateException e) {
543                 log.error("Failed to update LUD for session: {}::{}", wrapped.toString(), workspaceName, e);
544                 throw e;
545             }
546             if (!dirtyOps.isEmpty()) {
547                 Session sysSession = MgnlContext.getSystemContext().getJCRSession(workspaceName);
548                 if (sysSession instanceof DelegateSessionWrapper) {
549                     sysSession = ((DelegateSessionWrapper) sysSession).deepUnwrap(MgnlPropertySettingSessionWrapper.class);
550                 }
551                 applyPendingChanges(sysSession);
552                 sysSession.save();
553             }
554         }
555 
556         protected void applyPendingChanges(Session session) throws RepositoryException, PathNotFoundException {
557             while (!dirtyOps.isEmpty()) {
558                 DirtyOp dirty = dirtyOps.remove(0);
559                 if (session.nodeExists(dirty.getPath())) {
560                     log.debug("Updating {} with {}", dirty.getPath(), dirty.getUpdateDate());
561                     LastModified.update(session.getNode(dirty.getPath()), dirty.getUserName(), dirty.getUpdateDate());
562                 } else {
563                     // can happen when deleting something that was modified or moved within same session
564                     if (log.isDebugEnabled()) {
565                         log.warn("wanted to update {}:{} modified by:{} at {} but it's gone now.", session.getWorkspace().getName(), dirty.getPath(), dirty.getUserName(), dirty.getUpdateDate());
566                     }
567                 }
568             }
569         }
570 
571     }
572 
573     @Override
574     public Session wrapSession(Session session) {
575         return new MgnlPropertySettingSessionWrapper(session, this);
576     }
577 
578     @Override
579     public Workspace wrapWorkspace(Workspace workspace) {
580         return new MgnlPropertySettingWorkspaceWrapper(workspace, this);
581     }
582 
583     @Override
584     public Node wrapNode(Node node) {
585         return new MgnlPropertySettingNodeWrapper(node, this);
586     }
587 
588     @Override
589     public Property wrapProperty(Property property) {
590         return new LastUpdatePropertyWrapper(property, this);
591     }
592 
593     void updateLastModified(final Session session, final String destAbsPath, final boolean recursiveDown) throws RepositoryException, PathNotFoundException {
594         this.updateLastModified(session.getWorkspace().getName(), destAbsPath, recursiveDown);
595     }
596 
597     void updateLastModified(final String workspaceName, final String destAbsPath, final boolean recursiveDown) throws RepositoryException, PathNotFoundException {
598 
599         if ("/".equals(destAbsPath) && !recursiveDown) {
600             // we do not maintain lud on root node
601             return;
602         }
603         // one date for all children
604         final Calendar updateDate = Calendar.getInstance();
605 
606         // DO ALL IN SYSTEM CONTEXT !!! USER MIGHT NOT HAVE ENOUGH RIGHTS
607         MgnlContext.doInSystemContext(new ChangeLastUpdateDateOp(workspaceName, getCurrentUserName(), destAbsPath, updateDate, recursiveDown));
608 
609     }
610 
611     void updateLastModified(String workspaceName, String destAbsPath) throws RepositoryException, PathNotFoundException {
612         updateLastModified(workspaceName, destAbsPath, false);
613     }
614 
615     void updateLastModified(Session session, String destAbsPath) throws RepositoryException, PathNotFoundException {
616         updateLastModified(session, destAbsPath, false);
617     }
618 
619     void updateLastModifiedProperty(String workspaceName, String propertyName, String parentPath) throws RepositoryException {
620         if (shouldIgnoreUpdate(propertyName)) {
621             return;
622         }
623         updateLastModified(workspaceName, parentPath);
624     }
625 
626     void setCreatedDate(final String workspaceName, final String absPath) throws RepositoryException {
627         setCreatedDate(workspaceName, absPath, false);
628     }
629 
630     void setCreatedDate(final String workspaceName, final String absPath, final boolean recursiveDown) throws RepositoryException {
631         final Session session = MgnlContext.getJCRSession(workspaceName);
632 
633         // does it exist?
634         if (!session.itemExists(absPath)) {
635             // can't do anything.
636             if (log.isDebugEnabled()) {
637                 log.warn("Can't update {}. Path {}:{} does not exist.", NodeTypes.Created.NAME, workspaceName, absPath);
638             }
639             return;
640         }
641         Node node = session.getNode(absPath);
642 
643         // don't do anything on null nodes
644         if (node == null) {
645             return;
646         }
647 
648         // skip root
649         if (node.getDepth() == 0) {
650             return;
651         }
652 
653         // unwrap if required
654         if (node instanceof DelegateNodeWrapper) {
655             node = ((DelegateNodeWrapper) node).deepUnwrap(MgnlPropertySettingNodeWrapper.class);
656         }
657 
658         final String user = getCurrentUserName();
659 
660         // one date for all
661         final Calendar now = Calendar.getInstance();
662 
663         if (node.isNodeType(NodeTypes.Created.NAME)) {
664             log.debug("Setting {} on {} from {}:{}", NodeTypes.Created.NAME, node.getPath(), node.getSession().toString(), Thread.currentThread().getName());
665             NodeTypes.Created.set(node, user, now);
666         }
667 
668         if (recursiveDown) {
669             // trick or treat children
670             List<NodeIterator> iters = new ArrayList<>();
671             iters.add(node.getNodes());
672             while (!iters.isEmpty()) {
673                 List<NodeIterator> tmp = updateChildren(iters, user, now);
674                 iters.clear();
675                 iters.addAll(tmp);
676             }
677         }
678     }
679 
680     private void updateActivationStatus(Node node, String userName) throws RepositoryException {
681         if (NodeUtil.isNodeType(node, NodeTypes.Activatable.NAME)) {
682             NodeTypes.Activatable.update(node, userName, false);
683         }
684     }
685 
686     private List<NodeIterator> updateChildren(final List<NodeIterator> iters, final String user, final Calendar updateDate) {
687         List<NodeIterator> tmp = new ArrayList<>();
688         for (NodeIterator iterator : iters) {
689             while (iterator.hasNext()) {
690                 Node node = iterator.nextNode();
691                 try {
692                     if (node.isNodeType(NodeTypes.Created.NAME)) {
693                         NodeTypes.Created.set(node, user, updateDate);
694                     }
695                     tmp.add(node.getNodes());
696                 } catch (RepositoryException e) {
697                     log.error("Failed to set created date of {} with {}", node, e.getMessage(), e);
698                 }
699             }
700         }
701         return tmp;
702     }
703 
704     /**
705      * @return whether a modification to the property with the provided name should trigger an update of the last modification or not.
706      */
707     protected boolean shouldIgnoreUpdate(final String propertyName) {
708         // our update doesn't count as update (activation, versioning, last update, etc) - except for changing the template
709         return propertyName.startsWith(NodeTypes.JCR_PREFIX) || (propertyName.startsWith(NodeTypes.MGNL_PREFIX) && !SPECIAL_PROPERTY_NAMES.contains(propertyName));
710     }
711 
712     protected String getCurrentUserName() {
713         String userName = MgnlContext.getUser().getName();
714         if (MgnlContext.isSystemInstance()) {
715             // in system context try to obtain original non-system context and retrieve user from it
716             Context ctx = ((SystemContext) MgnlContext.getInstance()).getOriginalContext();
717             if (ctx != null && ctx.getUser() != null && !userName.equals(ctx.getUser().getName())) {
718                 // 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
719                 return "System [" + ctx.getUser().getName() + "]";
720             }
721         }
722         return userName;
723     }
724 }