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