/** * This file Copyright (c) 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.jcr.wrapper; import info.magnolia.jcr.iterator.RangeIteratorImpl; import info.magnolia.jcr.util.NodeUtil; import java.util.ArrayList; import java.util.Collection; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.TreeMap; import javax.jcr.Node; import javax.jcr.NodeIterator; import javax.jcr.PathNotFoundException; import javax.jcr.Property; import javax.jcr.PropertyIterator; import javax.jcr.RepositoryException; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * This wrapper allows extending other nodes (mainly useful to extend configurations). A node can define * a property with the name 'extends'. Its value is either an absolute or relative path. The merge is then performed as follows: * * <ul> * <li>properties are merged and values are overwritten * <li>sub nodes are merged, the original order is guaranteed, new nodes are added at the end of the list * </ul> * * The mechanism supports multiple inheritances as such: * <ul> * <li>the node the current node inherits from can again extend a node * <li>nodes laying deeper in the hierarchy can extend an other node * </ul> * * @see {@link InheritanceNodeWrapper} a class supporting content inheritance. */ public class ExtendingNodeWrapper extends ChildWrappingNodeWrapper { protected static final String EXTENDING_NODE_PROPERTY = "extends"; protected static final String EXTENDING_NODE_PROPERTY_OVERRIDE = "override"; private static final Logger log = LoggerFactory.getLogger(ExtendingNodeWrapper.class); /** * Are we extending or not? */ private boolean extending; /** * Node that is being extended. */ private Node extendedNode; /** * Default constructor. * @param node */ public ExtendingNodeWrapper(Node wrappedNode) { this(wrappedNode, false); } /** * Call directly for test purposes only. * * @param node * @param failOnError */ public ExtendingNodeWrapper(Node wrappedNode, boolean failOnError) { super(wrappedNode); try { final boolean hasExtendingProperty = getWrappedNode().hasProperty(EXTENDING_NODE_PROPERTY); if (hasExtendingProperty) { extending = true; Property extendingNodeProperty = getWrappedNode().getProperty(EXTENDING_NODE_PROPERTY); String extendingNodePath = extendingNodeProperty.getString(); if (StringUtils.isBlank(extendingNodePath) || extendingNodePath.equals(EXTENDING_NODE_PROPERTY_OVERRIDE)) { extending = false; } if (extending) { if (isExists(extendingNodePath, getWrappedNode())) { // support multiple inheritance if (extendingNodePath.startsWith("/")) { extendedNode = getWrappedNode().getSession().getNode(extendingNodePath); } else { extendedNode = getWrappedNode().getNode(extendingNodePath); } if (!NodeUtil.isSame(getWrappedNode(), extendedNode)) { extendedNode = wrapIfNeeded(extendedNode); } else { // nodes are the same so we will not extend. extendedNode = null; extending = false; log.error("Node can't self-extend: " + getWrappedNode().getPath()); } } else { String message = "Can't find referenced node for value: " + wrapped; log.error(message); extending = false; if (failOnError) { throw new RuntimeException(message); } } } } } catch (RepositoryException e) { throw new RuntimeException("Can't wrap node [" + wrapped + "]", e); } } /** * Does not support the extends property but chains the two nodes directly. Each node is * wrapped internally to ensure that each of them support the extends property for themselves. * @param wrappedNode * @param extendedNode */ protected ExtendingNodeWrapper(Node wrappedNode, Node extendedNode) { super(wrapIfNeeded(wrappedNode)); extending = true; try { if (getWrappedNode().hasProperty(EXTENDING_NODE_PROPERTY)) { Property extendingNodeProperty = getWrappedNode().getProperty(EXTENDING_NODE_PROPERTY); // check if override is not forced extending = !extendingNodeProperty.getString().equals(EXTENDING_NODE_PROPERTY_OVERRIDE); } } catch (RepositoryException e) { throw new RuntimeException("Can't determine extends point for node [" + wrappedNode + "]", e); } // might extend further more this.extendedNode = wrapIfNeeded(extendedNode); } @Override public boolean hasNode(String relPath) throws RepositoryException { if(getWrappedNode().hasNode(relPath)) { return true; } else if(extending && extendedNode.hasNode(relPath)) { return true; } else { return false; } } @Override public Node getNode(String relPath) throws PathNotFoundException, RepositoryException { if (getWrappedNode().hasNode(relPath)) { return wrapNode(getWrappedNode().getNode(relPath)); } else if (extending && extendedNode.hasNode(relPath)) { return(extendedNode.getNode(relPath)); } else { throw new PathNotFoundException("Node does not exists: [" + relPath + "]"); } } @Override public NodeIterator getNodes() throws RepositoryException { return this.getNodes("*"); } @Override public NodeIterator getNodes(String[] nameGlobs) throws RepositoryException { return this.getNodes(StringUtils.join(nameGlobs, " | ")); } @Override public NodeIterator getNodes(String namePattern) throws RepositoryException { Collection<Node> children = NodeUtil.getSortedCollectionFromNodeIterator(getWrappedNode().getNodes()); if (extending) { Collection<Node> extendedNodeChildren = NodeUtil.getSortedCollectionFromNodeIterator(extendedNode.getNodes()); Map<String, Node> merged = new LinkedHashMap<String, Node>(); for (Node content : extendedNodeChildren) { merged.put(content.getName(), content); } for (Node content : children) { merged.put(content.getName(), content); } return new NodeIteratorImpl(wrapNodes(merged.values())); } return new NodeIteratorImpl(wrapNodes(children)); } @Override public boolean hasProperty(String relPath) throws RepositoryException { // extending property should be hidden if (relPath.equals(EXTENDING_NODE_PROPERTY)) { return false; } else if (getWrappedNode().hasProperty(relPath)) { return true; } else if (extending && extendedNode.hasProperty(relPath)) { return true; } else { return false; } } @Override public Property getProperty(String relPath) throws PathNotFoundException, RepositoryException { // extending property should be hidden if (relPath.equals(EXTENDING_NODE_PROPERTY)) { throw new PathNotFoundException("Cannont access property [" + getWrappedNode().getPath() + "." + relPath + "]"); } else if (getWrappedNode().hasProperty(relPath)) { return getWrappedNode().getProperty(relPath); } else if (extending && extendedNode.hasProperty(relPath)) { return extendedNode.getProperty(relPath); } else { throw new RepositoryException("Can't read property from extended node [" + extendedNode + "]"); } } @Override public PropertyIterator getProperties() throws RepositoryException { return this.getProperties("*"); } @Override public PropertyIterator getProperties(String[] nameGlobs) throws RepositoryException { return this.getProperties(StringUtils.join(nameGlobs, " | ")); } @Override public PropertyIterator getProperties(String namePattern) throws RepositoryException { Collection<Property> properties = getPropertiesAsList(getWrappedNode(), namePattern); if (extending) { Collection<Property> inheritedProperties = getPropertiesAsList(extendedNode, namePattern); Map<String, Property> merged = new TreeMap<String, Property>(); for (Property prop : inheritedProperties) { merged.put(prop.getName(), prop); } for (Property prop : properties) { merged.put(prop.getName(), prop); } return new PropertyIteratorImpl(merged.values()); } return new PropertyIteratorImpl(properties); } @Override public Node wrapNode(Node node) { // get the same subnode of the extended content try { if (extending && extendedNode.hasNode(node.getName())) { Node extendedSubNode = extendedNode.getNode(node.getName()); return new ExtendingNodeWrapper(node, extendedSubNode); } } catch (RepositoryException e) { throw new RuntimeException("Can't wrap " + node, e); } return wrapIfNeeded(node); } @Override public Node getWrappedNode() { if (wrapped instanceof ExtendingNodeWrapper) { ExtendingNodeWrapper extendingNodeWrapper = (ExtendingNodeWrapper) wrapped; if (!extendingNodeWrapper.extending) { return extendingNodeWrapper.getWrappedNode(); } } return wrapped; } @Override public void setWrappedNode(Node node) { // this wrapper can be used multiple times (multiple inheritance) super.wrapped = node; } /** * @return true if node extends another, false otherwise */ public boolean isExtending() { return extending; } /** * Check if node exists. * @param nodePath * @param parent * @return True if exists, otherwise false. * @throws RepositoryException */ private boolean isExists(String nodePath, Node parent) throws RepositoryException { if (nodePath.startsWith("/")) { return getWrappedNode().getSession().itemExists(nodePath); } return parent.hasNode(nodePath); } /** * Wraps node if needed. * @param node * @return wrapped node */ private static Node wrapIfNeeded(Node node) { if (node instanceof ExtendingNodeWrapper) { return node; } return new ExtendingNodeWrapper(node); } /** * * @param values * @return Collection of wrapped nodes. */ private Collection<Node> wrapNodes(Collection<Node> collection) { Collection<Node> wrappedNodes = new ArrayList<Node>(); for (Node node : collection) { wrappedNodes.add(wrapNode(node)); } return wrappedNodes; } /** * Gets all properties from node and returns them as {@link java.util.List}. * Also filters out "extends" property. * @param node * @param namePattern * @return List of node properties. * @throws RepositoryException */ private static List<Property> getPropertiesAsList(Node node, String namePattern) throws RepositoryException { List<Property> properties = new ArrayList<Property>(); PropertyIterator it = node.getProperties(namePattern); while(it.hasNext()) { Property prop = (Property) it.next(); if (!prop.getName().equals(EXTENDING_NODE_PROPERTY)) { properties.add(prop); } } return properties; } private static class PropertyIteratorImpl extends RangeIteratorImpl<Property> implements PropertyIterator { public PropertyIteratorImpl(Collection<Property> properties) { super(properties); } @Override public Property nextProperty() { return super.next(); } } private static class NodeIteratorImpl extends RangeIteratorImpl<Node> implements NodeIterator { public NodeIteratorImpl(Collection<Node> nodes) { super(nodes); } @Override public Node nextNode() { return super.next(); } } }