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