/** * This file Copyright (c) 2013 Magnolia International * Ltd. (http://www.magnolia-cms.com). All rights reserved. * * * This file is dual-licensed under both the Magnolia * Network Agreement and the GNU General Public License. * You may elect to use one or the other of these licenses. * * This file is distributed in the hope that it will be * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the * implied warranty of MERCHANTABILITY or FITNESS FOR A * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT. * Redistribution, except as permitted by whichever of the GPL * or MNA you select, is prohibited. * * 1. For the GPL license (GPL), you can redistribute and/or * modify this file under the terms of the GNU General * Public License, Version 3, as published by the Free Software * Foundation. You should have received a copy of the GNU * General Public License, Version 3 along with this program; * if not, write to the Free Software Foundation, Inc., 51 * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * 2. For the Magnolia Network Agreement (MNA), this file * and the accompanying materials are made available under the * terms of the MNA which accompanies this distribution, and * is available at http://www.magnolia-cms.com/mna.html * * Any modifications to this file must keep this entire header * intact. * */ package info.magnolia.jcr.wrapper; import info.magnolia.cms.security.User; import info.magnolia.context.MgnlContext; import info.magnolia.jcr.decoration.ContentDecorator; import info.magnolia.jcr.decoration.ContentDecoratorPropertyWrapper; import info.magnolia.jcr.decoration.ContentDecoratorSessionWrapper; import info.magnolia.jcr.decoration.ContentDecoratorWorkspaceWrapper; import info.magnolia.jcr.util.NodeTypes; import info.magnolia.jcr.util.NodeTypes.LastModified; import info.magnolia.repository.RepositoryConstants; import java.io.InputStream; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Calendar; import java.util.List; import javax.jcr.AccessDeniedException; import javax.jcr.Binary; import javax.jcr.InvalidItemStateException; import javax.jcr.ItemExistsException; import javax.jcr.NoSuchWorkspaceException; import javax.jcr.Node; import javax.jcr.NodeIterator; import javax.jcr.PathNotFoundException; import javax.jcr.Property; import javax.jcr.ReferentialIntegrityException; import javax.jcr.RepositoryException; import javax.jcr.Session; import javax.jcr.Value; import javax.jcr.ValueFormatException; import javax.jcr.Workspace; import javax.jcr.lock.LockException; import javax.jcr.nodetype.ConstraintViolationException; import javax.jcr.nodetype.NoSuchNodeTypeException; import javax.jcr.version.VersionException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Decorator to keep activation status of content up to date. */ public class LastUpdateContentDecorator extends PropertyAndChildWrappingContentDecorator implements ContentDecorator { private static final Logger log = LoggerFactory.getLogger(LastUpdateContentDecorator.class); protected boolean isSysSessionDirty; /** * Updates parent page or parent content mgnl:lastUpdated property on modification. */ public class LastUpdatePropertyWrapper extends ContentDecoratorPropertyWrapper implements Property { public LastUpdatePropertyWrapper(Property property, LastUpdateContentDecorator contentDecorator) { super(property, contentDecorator); } @Override public void setValue(BigDecimal value) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException { super.setValue(value); this.updateLastModifiedProperty(); } @Override public void setValue(Binary value) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException { super.setValue(value); this.updateLastModifiedProperty(); } @Override public void setValue(boolean value) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException { super.setValue(value); this.updateLastModifiedProperty(); } @Override public void setValue(Calendar value) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException { super.setValue(value); this.updateLastModifiedProperty(); } @Override public void setValue(double value) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException { super.setValue(value); this.updateLastModifiedProperty(); } @Override public void setValue(InputStream value) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException { super.setValue(value); this.updateLastModifiedProperty(); } @Override public void setValue(long value) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException { super.setValue(value); this.updateLastModifiedProperty(); } @Override public void setValue(Node value) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException { super.setValue(value); this.updateLastModifiedProperty(); } @Override public void setValue(String value) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException { super.setValue(value); this.updateLastModifiedProperty(); } @Override public void setValue(String[] values) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException { super.setValue(values); this.updateLastModifiedProperty(); } @Override public void setValue(Value value) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException { super.setValue(value); this.updateLastModifiedProperty(); } @Override public void setValue(Value[] values) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException { super.setValue(values); this.updateLastModifiedProperty(); } @Override public void remove() throws VersionException, LockException, ConstraintViolationException, AccessDeniedException, RepositoryException { Node parent = this.getParent(); super.remove(); updateLastModified(parent.getSession(), parent.getPath()); } private void updateLastModifiedProperty() throws RepositoryException { LastUpdateContentDecorator.this.updateLastModifiedProperty(getSession(), this.getName(), this.getParent().getPath()); } } /** * Updates destination parent page or parent content mgnl:lastUpdated property on move or copy operations. */ public class LastUpdateWorkspaceWrapper extends ContentDecoratorWorkspaceWrapper implements Workspace { protected LastUpdateWorkspaceWrapper(Workspace workspace, ContentDecorator contentDecorator) { super(workspace, contentDecorator); } @Override public void move(String srcAbsPath, String destAbsPath) throws ConstraintViolationException, VersionException, AccessDeniedException, PathNotFoundException, ItemExistsException, LockException, RepositoryException { super.move(srcAbsPath, destAbsPath); updateLastModified(super.getWrappedWorkspace().getSession(), destAbsPath, true); } @Override public void copy(String srcAbsPath, String destAbsPath) throws ConstraintViolationException, VersionException, AccessDeniedException, PathNotFoundException, ItemExistsException, LockException, RepositoryException { super.copy(srcAbsPath, destAbsPath); updateLastModified(super.getWrappedWorkspace().getSession(), destAbsPath, true); } @Override public void copy(String srcWorkspace, String srcAbsPath, String destAbsPath) throws NoSuchWorkspaceException, ConstraintViolationException, VersionException, AccessDeniedException, PathNotFoundException, ItemExistsException, LockException, RepositoryException { super.copy(srcWorkspace, srcAbsPath, destAbsPath); updateLastModified(super.getWrappedWorkspace().getSession(), destAbsPath, true); } @Override public void clone(String srcWorkspace, String srcAbsPath, String destAbsPath, boolean removeExisting) throws NoSuchWorkspaceException, ConstraintViolationException, VersionException, AccessDeniedException, PathNotFoundException, ItemExistsException, LockException, RepositoryException { super.clone(srcWorkspace, srcAbsPath, destAbsPath, removeExisting); // no update on clone. a) it doesn't work reliably (session refresh) and b) contract of clone is to make identical copy! } } /** * Updates destination parent page or content mgnl:lastModified property on move. */ public class LastUpdateSessionWrapper extends ContentDecoratorSessionWrapper implements Session { public LastUpdateSessionWrapper(Session session, LastUpdateContentDecorator contentDecorator) { super(session, contentDecorator); } @Override public void move(String srcAbsPath, String destAbsPath) throws ItemExistsException, PathNotFoundException, VersionException, ConstraintViolationException, LockException, RepositoryException { super.move(srcAbsPath, destAbsPath); updateLastModified(super.getWrappedSession(), destAbsPath, true); } @Override public void save() throws AccessDeniedException, ItemExistsException, ReferentialIntegrityException, ConstraintViolationException, InvalidItemStateException, VersionException, LockException, NoSuchNodeTypeException, RepositoryException { super.save(); if (isSysSessionDirty) { Session sysSession = MgnlContext.getSystemContext().getJCRSession(this.getWorkspace().getName()); if (sysSession instanceof DelegateSessionWrapper) { sysSession = ((DelegateSessionWrapper) sysSession).deepUnwrap(LastUpdateSessionWrapper.class); } sysSession.save(); isSysSessionDirty = false; } } } @Override public Session wrapSession(Session session) { return new LastUpdateSessionWrapper(session, this); } @Override public Workspace wrapWorkspace(Workspace workspace) { return new LastUpdateWorkspaceWrapper(workspace, this); } @Override public Node wrapNode(Node node) { // TODO: why can't this be done by default? Wrappers are typed! instead we just wrap in CDNW ... looks like a bug/improvement ... should wrap in wrapper imposed by this decorator instead return new LastUpdateNodeWrapper(node, this); } @Override public Property wrapProperty(Property property) { return new LastUpdatePropertyWrapper(property, this); } private void updateLastModified(final Session session, final String destAbsPath, final boolean recursiveDown) throws RepositoryException, PathNotFoundException { if ("/".equals(destAbsPath) && !recursiveDown) { // we do not maintain lud on root node return; } final String workspaceName = session.getWorkspace().getName(); User user = MgnlContext.getUser(); final String username = user == null ? "not available" : user.getName(); // one date for all children final Calendar updateDate = Calendar.getInstance(); // DO ALL IN SYSTEM CONTEXT !!! USER MIGHT NOT HAVE ENOUGH RIGHTS MgnlContext.doInSystemContext(new MgnlContext.VoidOp() { @Override public void doExec() { try { Session sysSession = MgnlContext.getJCRSession(workspaceName); // does it exist? if (!sysSession.itemExists(destAbsPath)) { // can't do anything. if (log.isDebugEnabled()) { log.warn("Can't update mgnl:lastModified. Path {}:{} doesn't exist anymore.", workspaceName, destAbsPath); } return; } Node node = null; // TODO: this behavior should be configurable/injectable w/ custom rules // NOTE: never let had implement a rule engine if (RepositoryConstants.WEBSITE.equals(workspaceName)) { // bubble up to a mgnl:page in website workspace node = sysSession.getNode(destAbsPath); while (node != null && !NodeTypes.Page.NAME.equals(node.getPrimaryNodeType().getName()) && node.getDepth() > 0) { node = node.getParent(); } } // might be null also after the while loop above if (node == null) { node = sysSession.getNode(destAbsPath); } // unwrap if (node instanceof DelegateNodeWrapper) { node = ((DelegateNodeWrapper) node).deepUnwrap(LastUpdateNodeWrapper.class); } NodeTypes.LastModified.update(node); if (recursiveDown) { // trick or treat children List iters = new ArrayList(); iters.add(node.getNodes()); while (!iters.isEmpty()) { List tmp = updateChildren(iters, username, updateDate); iters.clear(); iters.addAll(tmp); } } // save ... sucks, but since we are in system context there's no other way // we are overriding save on session and call it on sys ctx as well instead of saving here isSysSessionDirty = true; } catch (RepositoryException e) { log.error("Failed to update last modified date with " + e.getMessage(), e); } } private List updateChildren(List iters, String username, Calendar updateDate) { List tmp = new ArrayList(); for (NodeIterator iter : iters) { while (iter.hasNext()) { Node node = iter.nextNode(); try { if (RepositoryConstants.WEBSITE.equals(workspaceName) && !NodeTypes.Page.NAME.equals(node.getPrimaryNodeType().getName())) { // do not update LUD of components and areas continue; } LastModified.update(node, username, updateDate); tmp.add(node.getNodes()); } catch (RepositoryException e) { log.error("Failed to update last modified date of " + node + " with " + e.getMessage(), e); } } } return tmp; } }); } void updateLastModified(Session session, String destAbsPath) throws RepositoryException, PathNotFoundException { updateLastModified(session, destAbsPath, false); } void updateLastModifiedProperty(Session session, String property, String parentPath) throws RepositoryException { if (property.startsWith(NodeTypes.MGNL_PREFIX) || property.startsWith(NodeTypes.JCR_PREFIX)) { // our update doesn't count as update (activation, versioning, last update, etc) return; } updateLastModified(session, parentPath); } }