View Javadoc

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