View Javadoc

1   /**
2    * This file Copyright (c) 2013-2014 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.context.MgnlContext;
37  import info.magnolia.jcr.decoration.ContentDecorator;
38  import info.magnolia.jcr.decoration.ContentDecoratorPropertyWrapper;
39  import info.magnolia.jcr.decoration.ContentDecoratorSessionWrapper;
40  import info.magnolia.jcr.decoration.ContentDecoratorWorkspaceWrapper;
41  import info.magnolia.jcr.util.NodeTypes;
42  import info.magnolia.jcr.util.NodeTypes.LastModified;
43  import info.magnolia.repository.RepositoryConstants;
44  
45  import java.io.InputStream;
46  import java.math.BigDecimal;
47  import java.util.ArrayList;
48  import java.util.Calendar;
49  import java.util.List;
50  import java.util.Map;
51  
52  import javax.jcr.AccessDeniedException;
53  import javax.jcr.Binary;
54  import javax.jcr.InvalidItemStateException;
55  import javax.jcr.ItemExistsException;
56  import javax.jcr.NoSuchWorkspaceException;
57  import javax.jcr.Node;
58  import javax.jcr.NodeIterator;
59  import javax.jcr.PathNotFoundException;
60  import javax.jcr.Property;
61  import javax.jcr.ReferentialIntegrityException;
62  import javax.jcr.RepositoryException;
63  import javax.jcr.Session;
64  import javax.jcr.Value;
65  import javax.jcr.ValueFormatException;
66  import javax.jcr.Workspace;
67  import javax.jcr.lock.LockException;
68  import javax.jcr.nodetype.ConstraintViolationException;
69  import javax.jcr.nodetype.NoSuchNodeTypeException;
70  import javax.jcr.version.VersionException;
71  
72  import org.apache.commons.lang.ArrayUtils;
73  import org.slf4j.Logger;
74  import org.slf4j.LoggerFactory;
75  
76  /**
77   * Decorator to set appropriate Mgnl specific properties (e.g. mgnl:lastModification, mgnl:created).
78   */
79  public class MgnlPropertySettingContentDecorator extends PropertyAndChildWrappingContentDecorator implements ContentDecorator {
80  
81      private static final Logger log = LoggerFactory.getLogger(MgnlPropertySettingContentDecorator.class);
82  
83      // TODO: this behavior should be injectable w/ custom rules
84      /**
85       * Map below contains parent node type we try to resolve for LUD updates instead of the "current" path/node.
86       */
87      private static Map<String, String> parentNodeMappings = ArrayUtils.toMap(new String[][] {
88              { RepositoryConstants.WEBSITE, NodeTypes.Page.NAME },
89              { RepositoryConstants.USERS, NodeTypes.User.NAME } });
90  
91      protected boolean isSysSessionDirty;
92  
93  
94      /**
95       * Repository operation for update of mgnl:lastUpdated property.
96       */
97      final class ChangeLastUpdateDateOp extends MgnlContext.RepositoryOp {
98          private final String workspaceName;
99          private final String userName;
100         private final String destAbsPath;
101         private final Calendar updateDate;
102         private final boolean recursiveDown;
103 
104         ChangeLastUpdateDateOp(String workspaceName, String userName, String destAbsPath, Calendar updateDate, boolean recursiveDown) {
105             this.workspaceName = workspaceName;
106             this.userName = userName;
107             this.destAbsPath = destAbsPath;
108             this.updateDate = updateDate;
109             this.recursiveDown = recursiveDown;
110         }
111 
112         @Override
113         public void doExec() throws RepositoryException {
114             Session sysSession = MgnlContext.getJCRSession(workspaceName);
115 
116             // does it exist?
117             if (!sysSession.itemExists(destAbsPath)) {
118                 // can't do anything.
119                 if (log.isDebugEnabled()) {
120                     log.warn("Can't update mgnl:lastModified. Path {}:{} doesn't exist anymore.", workspaceName, destAbsPath);
121                 }
122                 return;
123             }
124             Node node = resolveModifiedNode(workspaceName, destAbsPath, sysSession);
125 
126             // might be null also after the while loop above
127             if (node == null) {
128                 node = sysSession.getNode(destAbsPath);
129             }
130 
131             if (node.isNodeType(NodeTypes.MetaData.NAME)) {
132                 // no LUD on old style of metadata
133                 return;
134             }
135 
136             // skip root
137             if (node.getDepth() == 0) {
138                 return;
139             }
140 
141             // unwrap
142             if (node instanceof DelegateNodeWrapper) {
143                 node = ((DelegateNodeWrapper) node).deepUnwrap(MgnlPropertySettingNodeWrapper.class);
144             }
145 
146             log.debug("LUD on {} from {}:{}", node.getPath(), node.getSession().toString(), Thread.currentThread().getName());
147             if (node.isNodeType(LastModified.NAME)) {
148                 LastModified.update(node, userName, updateDate);
149             }
150 
151             if (recursiveDown) {
152                 // trick or treat children
153                 List<NodeIterator> iters = new ArrayList<NodeIterator>();
154                 iters.add(node.getNodes());
155                 while (!iters.isEmpty()) {
156                     List<NodeIterator> tmp = updateChildren(iters, updateDate);
157                     iters.clear();
158                     iters.addAll(tmp);
159                 }
160             }
161             // save ... sucks, but since we are in system context there's no other way
162             // we are overriding save on session and call it on sys ctx as well instead of saving here
163             isSysSessionDirty = true;
164             return;
165         }
166 
167         private Node resolveModifiedNode(final String workspaceName, final String destAbsPath, Session sysSession) throws RepositoryException {
168             String parentNodeType = parentNodeMappings.get(workspaceName);
169             Node node = sysSession.getNode(destAbsPath);
170             if (parentNodeType == null) {
171                 return node;
172             }
173             while (node != null && !parentNodeType.equals(node.getPrimaryNodeType().getName()) && node.getDepth() > 0) {
174                 node = node.getParent();
175             }
176             return node;
177         }
178 
179         private List<NodeIterator> updateChildren(List<NodeIterator> iters, Calendar updateDate) {
180             List<NodeIterator> tmp = new ArrayList<NodeIterator>();
181             for (NodeIterator iter : iters) {
182                 while (iter.hasNext()) {
183                     Node node = iter.nextNode();
184                     try {
185                         if (skipTypeInWorkspace(workspaceName, node.getPrimaryNodeType().getName())) {
186                             // do not update LUD of acls
187                             continue;
188                         }
189                         if (node.isNodeType(NodeTypes.MetaData.NAME)) {
190                             // no LUD on old style of metadata
191                             continue;
192                         }
193                         if (node.isNodeType(LastModified.NAME)) {
194                             LastModified.update(node, userName, updateDate);
195                         }
196                         tmp.add(node.getNodes());
197                     } catch (RepositoryException e) {
198                         log.error("Failed to update last modified date of " + node + " with " + e.getMessage(), e);
199                     }
200                 }
201             }
202             return tmp;
203         }
204 
205         private boolean skipTypeInWorkspace(String workspaceName, String name) {
206             if (RepositoryConstants.USER_ROLES.equals(workspaceName)) {
207                 return !(NodeTypes.Role.NAME.equals(name) || NodeTypes.Folder.NAME.equals(name));
208             }
209             if (RepositoryConstants.WEBSITE.equals(workspaceName)) {
210                 return !NodeTypes.Page.NAME.equals(name);
211             }
212             return false;
213         }
214     }
215 
216     /**
217      * Updates parent page or parent content mgnl:lastUpdated property on modification.
218      */
219     public class LastUpdatePropertyWrapper extends ContentDecoratorPropertyWrapper<MgnlPropertySettingContentDecorator> implements Property {
220 
221         public LastUpdatePropertyWrapper(Property property, MgnlPropertySettingContentDecorator contentDecorator) {
222             super(property, contentDecorator);
223         }
224 
225         @Override
226         public void setValue(BigDecimal value) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
227             super.setValue(value);
228             this.updateLastModifiedProperty();
229         }
230 
231         @Override
232         public void setValue(Binary value) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
233             super.setValue(value);
234             this.updateLastModifiedProperty();
235         }
236 
237         @Override
238         public void setValue(boolean value) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
239             super.setValue(value);
240             this.updateLastModifiedProperty();
241         }
242 
243         @Override
244         public void setValue(Calendar value) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
245             super.setValue(value);
246             this.updateLastModifiedProperty();
247         }
248 
249         @Override
250         public void setValue(double value) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
251             super.setValue(value);
252             this.updateLastModifiedProperty();
253         }
254 
255         @Override
256         public void setValue(InputStream value) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
257             super.setValue(value);
258             this.updateLastModifiedProperty();
259         }
260 
261         @Override
262         public void setValue(long value) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
263             super.setValue(value);
264             this.updateLastModifiedProperty();
265         }
266 
267         @Override
268         public void setValue(Node value) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
269             super.setValue(value);
270             this.updateLastModifiedProperty();
271         }
272 
273         @Override
274         public void setValue(String value) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
275             super.setValue(value);
276             this.updateLastModifiedProperty();
277         }
278 
279         @Override
280         public void setValue(String[] values) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
281             super.setValue(values);
282             this.updateLastModifiedProperty();
283         }
284 
285         @Override
286         public void setValue(Value value) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
287             super.setValue(value);
288             this.updateLastModifiedProperty();
289         }
290 
291         @Override
292         public void setValue(Value[] values) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
293             super.setValue(values);
294             this.updateLastModifiedProperty();
295         }
296 
297         @Override
298         public void remove() throws VersionException, LockException, ConstraintViolationException, AccessDeniedException, RepositoryException {
299             Node parent = this.getParent();
300             String propertyName = this.getName();
301             super.remove();
302             if (shouldIgnoreUpdate(propertyName)) {
303                 return;
304             }
305             updateLastModified(parent.getSession(), parent.getPath());
306         }
307 
308         private void updateLastModifiedProperty() throws RepositoryException {
309             MgnlPropertySettingContentDecorator.this.updateLastModifiedProperty(getSession().getWorkspace().getName(), this.getName(), this.getParent().getPath());
310         }
311     }
312 
313     /**
314      * Wrapper keeping last update date up to date.
315      *
316      * @deprecated since 5.2.2 - pls use MgnlPropertySettingWorkspaceWrapper instead.
317      */
318     public class LastUpdateWorkspaceWrapper extends MgnlPropertySettingWorkspaceWrapper {
319 
320         protected LastUpdateWorkspaceWrapper(final Workspace workspace, final ContentDecorator contentDecorator) {
321             super(workspace, contentDecorator);
322         }
323     }
324 
325     /**
326      * Updates destination parent page or parent content mgnl:lastUpdated property on move or copy operations.
327      */
328     public class MgnlPropertySettingWorkspaceWrapper extends ContentDecoratorWorkspaceWrapper implements Workspace {
329 
330         protected MgnlPropertySettingWorkspaceWrapper(final Workspace workspace, final ContentDecorator contentDecorator) {
331             super(workspace, contentDecorator);
332         }
333 
334         @Override
335         public void move(String srcAbsPath, String destAbsPath) throws ConstraintViolationException, VersionException, AccessDeniedException, PathNotFoundException, ItemExistsException, LockException, RepositoryException {
336             super.move(srcAbsPath, destAbsPath);
337             updateLastModified(super.getWrappedWorkspace().getSession(), destAbsPath, true);
338         }
339 
340         @Override
341         public void copy(String srcAbsPath, String destAbsPath) throws ConstraintViolationException, VersionException, AccessDeniedException, PathNotFoundException, ItemExistsException, LockException, RepositoryException {
342             super.copy(srcAbsPath, destAbsPath);
343             final String workspaceName = super.getWrappedWorkspace().getName();
344             setCreatedDate(workspaceName, destAbsPath, true);
345         }
346 
347         @Override
348         public void copy(String srcWorkspace, String srcAbsPath, String destAbsPath) throws NoSuchWorkspaceException, ConstraintViolationException, VersionException, AccessDeniedException, PathNotFoundException, ItemExistsException, LockException, RepositoryException {
349             super.copy(srcWorkspace, srcAbsPath, destAbsPath);
350             final String workspaceName = super.getWrappedWorkspace().getName();
351             setCreatedDate(workspaceName, destAbsPath, true);
352         }
353 
354         @Override
355         public void clone(String srcWorkspace, String srcAbsPath, String destAbsPath, boolean removeExisting) throws NoSuchWorkspaceException, ConstraintViolationException, VersionException, AccessDeniedException, PathNotFoundException, ItemExistsException, LockException, RepositoryException {
356             super.clone(srcWorkspace, srcAbsPath, destAbsPath, removeExisting);
357             // no update on clone. a) it doesn't work reliably (session refresh) and b) contract of clone is to make identical copy!
358         }
359 
360     }
361 
362     /**
363      * Wrapper keeping last update date up to date.
364      *
365      * @deprecated since 5.2.2 - pls use MgnlPropertySettingSessionWrapper instead.
366      */
367     public class LastUpdateSessionWrapper extends MgnlPropertySettingSessionWrapper {
368         public LastUpdateSessionWrapper(final Session session, final MgnlPropertySettingContentDecorator contentDecorator) {
369             super(session, contentDecorator);
370         }
371     }
372 
373     /**
374      * Updates all specific properties upon certain actions. Right now it's just updating the destination parent page or content mgnl:lastModified property on move.
375      */
376     public class MgnlPropertySettingSessionWrapper extends ContentDecoratorSessionWrapper<MgnlPropertySettingContentDecorator> implements Session {
377 
378         public MgnlPropertySettingSessionWrapper(Session session, MgnlPropertySettingContentDecorator contentDecorator) {
379             super(session, contentDecorator);
380         }
381 
382         @Override
383         public void move(String srcAbsPath, String destAbsPath) throws ItemExistsException, PathNotFoundException, VersionException, ConstraintViolationException, LockException, RepositoryException {
384             super.move(srcAbsPath, destAbsPath);
385             // update src path, item is not moved yet
386             updateLastModified(super.getWrappedSession(), srcAbsPath, true);
387         }
388 
389         @Override
390         public void save() throws AccessDeniedException, ItemExistsException, ReferentialIntegrityException, ConstraintViolationException, InvalidItemStateException, VersionException, LockException, NoSuchNodeTypeException, RepositoryException {
391             String workspaceName = wrapped.getWorkspace().getName();
392             try {
393                 log.debug("saving session: " + wrapped.toString() + "::" + workspaceName + "::sys:" + MgnlContext.isSystemInstance() + "::" + Thread.currentThread().getName());
394                 super.save();
395                 if (MgnlContext.isSystemInstance()) {
396                     isSysSessionDirty = false;
397                 }
398             } catch (InvalidItemStateException e) {
399                 log.error("Failed to update LUD for session: " + wrapped.toString() + "::" + workspaceName, e);
400                 throw e;
401             }
402             if (isSysSessionDirty) {
403                 Session sysSession = MgnlContext.getSystemContext().getJCRSession(workspaceName);
404                 if (sysSession instanceof DelegateSessionWrapper) {
405                     sysSession = ((DelegateSessionWrapper) sysSession).deepUnwrap(LastUpdateSessionWrapper.class);
406                 }
407                 if (!wrapped.toString().equals(sysSession.toString())) {
408                     log.debug("Forcing saving of : " + sysSession.toString() + "::" + workspaceName);
409                     sysSession.save();
410                 }
411                 isSysSessionDirty = false;
412             }
413         }
414 
415     }
416 
417     @Override
418     public Session wrapSession(Session session) {
419         return new MgnlPropertySettingSessionWrapper(session, this);
420     }
421 
422     @Override
423     public Workspace wrapWorkspace(Workspace workspace) {
424         return new MgnlPropertySettingWorkspaceWrapper(workspace, this);
425     }
426 
427     @Override
428     public Node wrapNode(Node node) {
429         return new MgnlPropertySettingNodeWrapper(node, this);
430     }
431 
432     @Override
433     public Property wrapProperty(Property property) {
434         return new LastUpdatePropertyWrapper(property, this);
435     }
436 
437     void updateLastModified(final Session session, final String destAbsPath, final boolean recursiveDown) throws RepositoryException, PathNotFoundException {
438         this.updateLastModified(session.getWorkspace().getName(), destAbsPath, recursiveDown);
439     }
440 
441     void updateLastModified(final String workspaceName, final String destAbsPath, final boolean recursiveDown) throws RepositoryException, PathNotFoundException {
442 
443         if ("/".equals(destAbsPath) && !recursiveDown) {
444             // we do not maintain lud on root node
445             return;
446         }
447         // one date for all children
448         final Calendar updateDate = Calendar.getInstance();
449 
450         // DO ALL IN SYSTEM CONTEXT !!! USER MIGHT NOT HAVE ENOUGH RIGHTS
451         MgnlContext.doInSystemContext(new ChangeLastUpdateDateOp(workspaceName, MgnlContext.getUser().getName(), destAbsPath, updateDate, recursiveDown));
452 
453     }
454 
455     void updateLastModified(String workspaceName, String destAbsPath) throws RepositoryException, PathNotFoundException {
456         updateLastModified(workspaceName, destAbsPath, false);
457     }
458 
459     void updateLastModified(Session session, String destAbsPath) throws RepositoryException, PathNotFoundException {
460         updateLastModified(session, destAbsPath, false);
461     }
462 
463     void updateLastModifiedProperty(String workspaceName, String propertyName, String parentPath) throws RepositoryException {
464         if (shouldIgnoreUpdate(propertyName)) {
465             return;
466         }
467         updateLastModified(workspaceName, parentPath);
468     }
469 
470     void setCreatedDate(final String workspaceName, final String absPath) throws RepositoryException {
471         setCreatedDate(workspaceName, absPath, false);
472     }
473 
474     void setCreatedDate(final String workspaceName, final String absPath, final boolean recursiveDown) throws RepositoryException {
475         final Session session = MgnlContext.getJCRSession(workspaceName);
476 
477         // does it exist?
478         if (!session.itemExists(absPath)) {
479             // can't do anything.
480             if (log.isDebugEnabled()) {
481                 log.warn("Can't update {}. Path {}:{} does not exist.", NodeTypes.Created.NAME, workspaceName, absPath);
482             }
483             return;
484         }
485         Node node = session.getNode(absPath);
486 
487         // skip root
488         if (node.getDepth() == 0) {
489             return;
490         }
491         // don't do anything on null nodes
492         if (node == null) {
493             return;
494         }
495 
496         // unwrap if required
497         if (node instanceof DelegateNodeWrapper) {
498             node = ((DelegateNodeWrapper) node).deepUnwrap(MgnlPropertySettingNodeWrapper.class);
499         }
500 
501         final String user = MgnlContext.getUser().getName();
502 
503         // one date for all
504         final Calendar now = Calendar.getInstance();
505 
506         if (node.isNodeType(NodeTypes.Created.NAME)) {
507             log.debug("Setting {} on {} from {}:{}", NodeTypes.Created.NAME, node.getPath(), node.getSession().toString(), Thread.currentThread().getName());
508             NodeTypes.Created.set(node, user, now);
509         }
510 
511         if (recursiveDown) {
512             // trick or treat children
513             List<NodeIterator> iters = new ArrayList<NodeIterator>();
514             iters.add(node.getNodes());
515             while (!iters.isEmpty()) {
516                 List<NodeIterator> tmp = updateChildren(iters, user, now);
517                 iters.clear();
518                 iters.addAll(tmp);
519             }
520         }
521     }
522 
523     private List<NodeIterator> updateChildren(final List<NodeIterator> iters, final String user, final Calendar updateDate) {
524         List<NodeIterator> tmp = new ArrayList<NodeIterator>();
525         for (NodeIterator iterator : iters) {
526             while (iterator.hasNext()) {
527                 Node node = iterator.nextNode();
528                 try {
529                     if (node.isNodeType(NodeTypes.Created.NAME)) {
530                         NodeTypes.Created.set(node, user, updateDate);
531                     }
532                     tmp.add(node.getNodes());
533                 } catch (RepositoryException e) {
534                     log.error("Failed to set created date of " + node + " with " + e.getMessage(), e);
535                 }
536             }
537         }
538         return tmp;
539     }
540 
541     /**
542      * @return whether a modification to the property with the provided name should trigger an update of the last modification or not.
543      */
544     protected boolean shouldIgnoreUpdate(final String propertyName) {
545         // our update doesn't count as update (activation, versioning, last update, etc) - except for changing the template
546         return propertyName.startsWith(NodeTypes.JCR_PREFIX) || (propertyName.startsWith(NodeTypes.MGNL_PREFIX) && !NodeTypes.Renderable.TEMPLATE.equals(propertyName));
547     }
548 }