/**
* This file Copyright (c) 2003-2012 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.cms.core.version;
import info.magnolia.cms.core.AbstractContent;
import info.magnolia.cms.core.Content;
import info.magnolia.cms.core.DefaultContent;
import info.magnolia.cms.core.HierarchyManager;
import info.magnolia.cms.core.ItemType;
import info.magnolia.cms.core.NodeData;
import info.magnolia.cms.security.AccessDeniedException;
import info.magnolia.cms.security.AccessManager;
import info.magnolia.cms.security.Permission;
import info.magnolia.cms.util.ContentWrapper;
import info.magnolia.cms.util.NodeDataWrapper;
import info.magnolia.cms.util.Rule;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import javax.jcr.Node;
import javax.jcr.PathNotFoundException;
import javax.jcr.PropertyType;
import javax.jcr.RepositoryException;
import javax.jcr.Value;
import javax.jcr.Workspace;
import javax.jcr.lock.Lock;
import javax.jcr.lock.LockException;
import javax.jcr.nodetype.NodeType;
import javax.jcr.nodetype.NodeTypeManager;
import javax.jcr.version.Version;
import javax.jcr.version.VersionHistory;
import javax.jcr.version.VersionIterator;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Wraps a versioned node (frozen node) and allows traversing the hierarchy as if the node where in the original place.
*
* @version $Id$
*/
public class ContentVersion extends DefaultContent {
/**
* Makes sure that the handle hides the fact that the nodes live in the version store.
* @version $Id$
*
*/
private final class ContentVersionChildWrapper extends ContentWrapper {
private final Content parent;
private ContentVersionChildWrapper(Content wrappedContent, Content parent) {
super(wrappedContent);
this.parent = parent;
}
@Override
public Content getParent() throws RepositoryException {
return parent;
}
/**
* Show the original path not the one from the version store.
*/
@Override
public String getHandle() {
try {
return getParent().getHandle() + "/" + getName();
}
catch (RepositoryException e) {
throw new RuntimeException("Can't create handle for versioned node.", e);
}
}
/**
* We have to wrap the node data to make sure that the handle is correct.
*/
@Override
public NodeData newNodeDataInstance(String name, int type, boolean createIfNotExisting) throws AccessDeniedException, RepositoryException {
return new NodeDataWrapper(super.newNodeDataInstance(name, type, createIfNotExisting)) {
@Override
public String getHandle() {
return ContentVersionChildWrapper.this.getHandle() + "/" + getName();
}
};
}
@Override
protected Content wrap(Content node) {
return new ContentVersionChildWrapper(node, this);
}
}
private static Logger log = LoggerFactory.getLogger(ContentVersion.class);
/**
* User who created this version.
*/
public static final String VERSION_USER = "versionUser";
/**
* Name of the base node.
*/
public static final String NAME = "name";
/**
* Version node (nt:version).
*/
private final Version state;
/**
* The node as existing in the workspace. Not the version node.
*/
private final Content base;
/**
* Rule used to create this version.
*/
private Rule rule;
private final Version versionedNode;
public ContentVersion(Version thisVersion, Content base) throws RepositoryException {
if (thisVersion == null) {
throw new RepositoryException("Failed to get ContentVersion, version does not exist");
}
this.versionedNode = thisVersion;
if (thisVersion instanceof VersionedNode) {
this.state = ((VersionedNode) thisVersion).unwrap();
} else {
this.state = thisVersion;
}
this.base = base;
//setting permissions should be no longer necessary as it is handled by jcr itself
// this.hierarchyManager = new HierarchyManagerWrapper(base.getHierarchyManager()) {
// private AccessManagerImpl accessManager;
//
// {
// // child nodes (and metaData if nothing else) depends on this to have access when root access is restricted for given user
// List<Permission> permissions = new ArrayList<Permission>(getWrappedHierarchyManager().getAccessManager().getPermissionList());
// PermissionImpl p = new PermissionImpl();
// p.setPattern(new SimpleUrlPattern("/jcr:system/jcr:versionStorage/*"));
// // read only
// p.setPermissions(8);
// permissions.add(p);
// // use dedicated AM and not the one base share with its parent
// accessManager = new AccessManagerImpl();
// accessManager.setPermissionList(permissions);
// }
//
// @Override
// public AccessManager getAccessManager() {
// return accessManager;
// }
// };
this.init();
}
/**
* Set frozen node of this version as working node.
* @throws RepositoryException
*/
private void init() throws RepositoryException {
this.setNode(this.state.getNode(ItemType.JCR_FROZENNODE));
try {
if (!StringUtils.equalsIgnoreCase(this.state.getName(), VersionManager.ROOT_VERSION)) {
this.rule = VersionManager.getInstance().getUsedFilter(this.getJCRNode());
}
}
catch (Exception e) {
log.error(e.getMessage(), e);
}
if (this.rule == null) {
log.info("failed to get filter used for creating this version, use open filter");
this.rule = new Rule();
}
}
/**
* Get creation date of this version.
* @throws RepositoryException
* @return creation date as calendar
*/
public Calendar getCreated() throws RepositoryException {
return this.state.getCreated();
}
/**
* Return the name of the version represented by this object.
* @return the versions name
* @throws RepositoryException
*/
public String getVersionLabel() throws RepositoryException {
return this.state.getName();
}
/**
* Get containing version history.
* @throws RepositoryException
* @return version history associated to this version
*/
public VersionHistory getContainingHistory() throws RepositoryException {
return this.state.getContainingHistory();
}
/**
* The original name of the node.
*/
@Override
public String getName() {
try {
return VersionManager.getInstance().getSystemNode(this.getJCRNode()).getProperty(NAME).getString();
}
catch (RepositoryException re) {
log.error("Failed to retrieve name from version system node", re);
return "";
}
}
/**
* The name of the user who created this version.
*/
public String getUserName() {
try {
return VersionManager.getInstance().getSystemNode(this.getJCRNode()).getProperty(VERSION_USER).getString();
}
catch (RepositoryException re) {
log.error("Failed to retrieve user from version system node", re);
return "";
}
}
/**
* Get original path of this versioned content.
*/
@Override
public String getHandle() {
return this.base.getHandle();
}
/**
* Returns a direct child if it was included in the version. Otherwise it tries to get the child from the original place.
* The versioning rule is respected.
*/
@Override
public Content getContent(String name) throws PathNotFoundException, RepositoryException, AccessDeniedException {
//first we have to check if this is a direct child
if(super.hasContent(name)){
return new ContentVersionChildWrapper(super.getContent(name), this);
}
Content content = base.getContent(name);
// only return the node if it was excluded from the versioning, otherwise the node is new
if(!rule.isAllowed(content.getJCRNode())){
return content;
}
throw new PathNotFoundException(base.getHandle() + "/" + name);
}
/**
* Uses the same approach as {@link #getContent(String)}.
*/
@Override
public boolean hasContent(String name) throws RepositoryException {
if(super.hasContent(name)){
return true;
}
else if(base.hasContent(name)){
Content content = base.getContent(name);
// only return the node if it was excluded from the versioning, otherwise the node is new
if(!rule.isAllowed(content.getJCRNode())){
return true;
}
}
return false;
}
/**
* Throws an {@link AccessDeniedException} as versions are read only.
*/
@Override
public Content createContent(String name) throws AccessDeniedException {
throw new AccessDeniedException("Not allowed to write on version preview");
}
/**
* Throws an {@link AccessDeniedException} as versions are read only.
*/
@Override
public Content createContent(String name, String contentType) throws AccessDeniedException {
throw new AccessDeniedException("Not allowed to write on version preview");
}
/**
* Throws an {@link AccessDeniedException} as versions are read only.
*/
@Override
public Content createContent(String name, ItemType contentType) throws AccessDeniedException {
throw new AccessDeniedException("Not allowed to write on version preview");
}
/**
* Throws an {@link AccessDeniedException} as versions are read only.
*/
@Override
public NodeData createNodeData(String name) throws AccessDeniedException {
throw new AccessDeniedException("Not allowed to write on version preview");
}
/**
* Throws an {@link AccessDeniedException} as versions are read only.
*/
public NodeData createNodeData(String name, Value value, int type) throws AccessDeniedException {
throw new AccessDeniedException("Not allowed to write on version preview");
}
/**
* Throws an {@link AccessDeniedException} as versions are read only.
*/
@Override
public NodeData createNodeData(String name, Value value) throws AccessDeniedException {
throw new AccessDeniedException("Not allowed to write on version preview");
}
/**
* Throws an {@link AccessDeniedException} as versions are read only.
*/
@Override
public NodeData createNodeData(String name, int type) throws AccessDeniedException {
throw new AccessDeniedException("Not allowed to write on version preview");
}
/**
* Throws an {@link AccessDeniedException} as versions are read only.
*/
@Override
public void deleteNodeData(String name) throws RepositoryException {
throw new AccessDeniedException("Not allowed to write on version preview");
}
/**
* Throws an {@link AccessDeniedException} as versions are read only.
*/
@Override
public void updateMetaData() throws AccessDeniedException {
throw new AccessDeniedException("Not allowed to write on version preview");
}
/**
* All {@link #getChildren()} methods delegate to this method. We combine the direct children and children
* from the current node which were not included by the version rule.
*/
@Override
public Collection<Content> getChildren(ContentFilter filter, String namePattern, Comparator<Content> orderCriteria) {
ArrayList<Content> result = new ArrayList<Content>();
result.addAll(wrap(super.getChildren(filter, namePattern, orderCriteria)));
Collection<Content> transientChildren = ((AbstractContent)this.base).getChildren(filter, namePattern, orderCriteria);
for (Content transientChild : transientChildren) {
try {
if(!rule.isAllowed(transientChild.getJCRNode())){
result.add(transientChild);
}
}
catch (RepositoryException e) {
throw new RuntimeException("Can't determine node type of " + transientChild, e);
}
}
return result;
}
private Collection<Content> wrap(Collection<Content> children) {
List<Content> transformed = new ArrayList<Content>();
for (Content child : children) {
transformed.add(new ContentVersionChildWrapper(child, this));
}
return transformed;
}
/**
* @return Boolean, if sub node(s) exists
*/
@Override
public boolean hasChildren() {
return (this.getChildren().size() > 0);
}
/**
* @param contentType JCR node type as configured
* @return Boolean, if sub <code>collectionType</code> exists
*/
@Override
public boolean hasChildren(String contentType) {
return (this.getChildren(contentType).size() > 0);
}
/**
* Returns the parent of the base node.
*/
@Override
public Content getParent() throws PathNotFoundException, RepositoryException, AccessDeniedException {
return this.base.getParent();
}
@Override
public Content getAncestor(int level) throws PathNotFoundException, RepositoryException, AccessDeniedException {
return this.base.getAncestor(level);
}
/**
* Convenience method for taglib.
* @return Content representing node on level 0
* @throws javax.jcr.RepositoryException if an error occurs
*/
@Override
public Collection<Content> getAncestors() throws PathNotFoundException, RepositoryException {
return this.base.getAncestors();
}
/**
* Get node level from the ROOT node : FIXME implement getDepth in javax.jcr.
* @return level at which current node exist, relative to the ROOT node
* @throws javax.jcr.PathNotFoundException
* @throws javax.jcr.RepositoryException if an error occurs
*/
@Override
public int getLevel() throws PathNotFoundException, RepositoryException {
return this.base.getLevel();
}
/**
* Throws an {@link AccessDeniedException} as versions are read only.
*/
@Override
public void orderBefore(String srcName, String beforeName) throws RepositoryException {
throw new AccessDeniedException("Not allowed to write on version preview");
}
/**
* This method returns the index of this node within the ordered set of its same-name sibling nodes. This index is
* the one used to address same-name siblings using the square-bracket notation, e.g., /a[3]/b[4]. Note that the
* index always starts at 1 (not 0), for compatibility with XPath. As a result, for nodes that do not have
* same-name-siblings, this method will always return 1.
* @return The index of this node within the ordered set of its same-name sibling nodes.
* @throws javax.jcr.RepositoryException if an error occurs
*/
@Override
public int getIndex() throws RepositoryException {
return this.base.getIndex();
}
/**
* Returns primary node type definition of the associated Node of this object.
* @throws RepositoryException if an error occurs
*/
@Override
public NodeType getNodeType() throws RepositoryException {
log.warn("This is a Version node, it will always return NT_FROZEN as node type.");
log.warn("Use getNodeTypeName to retrieve base node primary type");
return super.getNodeType();
}
/**
* Throws an {@link AccessDeniedException} as versions are read only.
*/
@Override
public void restore(String versionName, boolean removeExisting) throws RepositoryException {
throw new AccessDeniedException("Not allowed to write on version preview");
}
/**
* Throws an {@link AccessDeniedException} as versions are read only.
*/
@Override
public void restore(Version version, boolean removeExisting) throws RepositoryException {
throw new AccessDeniedException("Not allowed to write on version preview");
}
/**
* Throws an {@link AccessDeniedException} as versions are read only.
*/
@Override
public void restore(Version version, String relPath, boolean removeExisting) throws RepositoryException {
throw new AccessDeniedException("Not allowed to write on version preview");
}
/**
* Throws an {@link AccessDeniedException} as versions are read only.
*/
@Override
public void restoreByLabel(String versionLabel, boolean removeExisting) throws RepositoryException {
throw new AccessDeniedException("Not allowed to write on version preview");
}
/**
* Throws an {@link AccessDeniedException} as versions are read only.
*/
@Override
public Version addVersion() throws RepositoryException {
throw new AccessDeniedException("Not allowed to add version on version preview");
}
/**
* Throws an {@link AccessDeniedException} as versions are read only.
*/
@Override
public Version addVersion(Rule rule) throws RepositoryException {
throw new AccessDeniedException("Not allowed to add version on version preview");
}
/**
* Returns always false as verions are read only.
*/
@Override
public boolean isModified() {
log.error("Not valid for version");
return false;
}
/**
* Throws an {@link AccessDeniedException} as versions are read only.
*/
@Override
public VersionHistory getVersionHistory() throws RepositoryException {
throw new AccessDeniedException("Not allowed to read VersionHistory of Version");
}
/**
* Throws an {@link AccessDeniedException} as versions are read only.
*/
@Override
public VersionIterator getAllVersions() throws RepositoryException {
throw new AccessDeniedException("Not allowed to get VersionIterator of Version");
}
/**
* Throws an {@link AccessDeniedException} as versions are read only.
*/
@Override
public ContentVersion getBaseVersion() throws RepositoryException {
throw new AccessDeniedException("Not allowed to get base version of Version");
}
/**
* Throws an {@link AccessDeniedException} as versions are read only.
*/
@Override
public ContentVersion getVersionedContent(Version version) throws RepositoryException {
throw new AccessDeniedException("Not allowed to get preview of Version itself");
}
/**
* Throws an {@link AccessDeniedException} as versions are read only.
*/
@Override
public ContentVersion getVersionedContent(String versionName) throws RepositoryException {
throw new AccessDeniedException("Not allowed to get preview of Version itself");
}
/**
* Throws an {@link AccessDeniedException} as versions are read only.
*/
@Override
public void save() throws RepositoryException {
throw new AccessDeniedException("Not allowed to write on version preview");
}
/**
* Checks for the allowed access rights.
* @param permissions as defined in javax.jcr.Permission
* @return true is the current user has specified access on this node.
*/
@Override
public boolean isGranted(long permissions) {
return (permissions & Permission.READ) == permissions;
}
/**
* Throws an {@link AccessDeniedException} as versions are read only.
*/
@Override
public void delete() throws RepositoryException {
throw new AccessDeniedException("Not allowed to write on version preview");
}
/**
* Throws an {@link AccessDeniedException} as versions are read only.
*/
@Override
public void delete(String path) throws RepositoryException {
throw new AccessDeniedException("Not allowed to write on version preview");
}
/**
* UUID of the node refrenced by this object.
* @return uuid
*/
@Override
public String getUUID() {
return this.base.getUUID();
}
/**
* Throws an {@link AccessDeniedException} as versions are read only.
*/
@Override
public void addMixin(String type) throws RepositoryException {
throw new AccessDeniedException("Not allowed to write on version preview");
}
/**
* Throws an {@link AccessDeniedException} as versions are read only.
*/
@Override
public void removeMixin(String type) throws RepositoryException {
throw new AccessDeniedException("Not allowed to write on version preview");
}
/**
* Throws an {@link AccessDeniedException} as versions are read only.
*/
@Override
public Lock lock(boolean isDeep, boolean isSessionScoped) throws LockException, RepositoryException {
throw new AccessDeniedException("Lock not supported on version preview");
}
/**
* Throws an {@link AccessDeniedException} as versions are read only.
*/
@Override
public Lock lock(boolean isDeep, boolean isSessionScoped, long yieldFor) throws LockException, RepositoryException {
throw new AccessDeniedException("Lock not supported on version preview");
}
/**
* Throws an {@link AccessDeniedException} as versions are read only.
*/
@Override
public Lock getLock() throws LockException, RepositoryException {
throw new AccessDeniedException("Lock not supported on version preview");
}
/**
* Throws an {@link AccessDeniedException} as versions are read only.
*/
@Override
public void unlock() throws LockException, RepositoryException {
throw new AccessDeniedException("Lock not supported on version preview");
}
/**
* Throws an {@link AccessDeniedException} as versions are read only.
*/
@Override
public boolean holdsLock() throws RepositoryException {
throw new AccessDeniedException("Lock not supported on version preview");
}
/**
* Throws an {@link AccessDeniedException} as versions are read only.
*/
@Override
public boolean isLocked() throws RepositoryException {
throw new AccessDeniedException("Lock not supported on version preview");
}
/**
* Get hierarchy manager if previously set for this object.
* @return HierarchyManager
*/
@Override
public HierarchyManager getHierarchyManager() {
return this.base.getHierarchyManager();
}
/**
* Get access manager if previously set for this object.
* @return AccessManager
* @deprecated use getHierarchyManager instead
*/
@Deprecated
@Override
public AccessManager getAccessManager() {
return this.base.getAccessManager();
}
@Override
public Workspace getWorkspace() throws RepositoryException {
return this.base.getWorkspace();
}
@Override
public boolean hasNodeData(String name) throws RepositoryException {
if (this.node.hasProperty(name)) {
return true;
}
if (this.node.hasNode(name) && this.node.getNode(name).getProperty("jcr:frozenPrimaryType").getValue().getString().equals(ItemType.NT_RESOURCE)) {
return true;
}
return false;
}
@Override
protected int determineNodeDataType(String name) {
// FIXME: maybe delegate to NodeDataImplementations?
try {
if (this.node.hasProperty(name)) {
return this.node.getProperty(name).getType();
}
if (this.node.hasNode(name) && this.node.getNode(name).getProperty("jcr:frozenPrimaryType").getValue().getString().equals(ItemType.NT_RESOURCE)) {
return PropertyType.BINARY;
}
}
catch (RepositoryException e) {
throw new IllegalStateException("Can't determine property type of [" + getHandle() + "/" + name + "]", e);
}
return PropertyType.UNDEFINED;
}
@Override
public NodeType[] getMixinNodeTypes() throws RepositoryException {
Value[] vals = this.node.getProperty("jcr:frozenMixinTypes").getValues();
NodeTypeManager typeMan = getJCRNode().getSession().getWorkspace().getNodeTypeManager();
NodeType[] types = new NodeType[vals.length];
int i = 0;
for (Value val : vals) {
types[i++] = typeMan.getNodeType(val.getString());
}
return types;
}
// public List<ContentVersion> getPredecessors() throws RepositoryException {
// List<ContentVersion> list = new ArrayList<ContentVersion>();
// for (Version v : this.state.getPredecessors()) {
// list.add(new ContentVersion(v, this.base));
// }
// return list;
// }
public Version[] getPredecessors() throws RepositoryException {
// would prefer to return the above version (List of ContentVersions), but since our other APIs return jcr Version as well ...
return this.state.getPredecessors();
}
@Override
public Node getJCRNode() {
// we seem to build content version from 2 different types of nodes - from Version and from jcr:frozenNode
try {
if (versionedNode.hasNode("jcr:frozenNode")) {
return versionedNode.getFrozenNode();
}
} catch (RepositoryException e) {
log.error("Failed to retrieve frozen node from version " + versionedNode, e);
}
return versionedNode;
}
}