/**
* 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.beans.config.ContentRepository;
import info.magnolia.cms.core.MgnlNodeType;
import info.magnolia.cms.core.Path;
import info.magnolia.context.MgnlContext;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.jcr.ImportUUIDBehavior;
import javax.jcr.ItemNotFoundException;
import javax.jcr.LoginException;
import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.Property;
import javax.jcr.PropertyIterator;
import javax.jcr.PropertyType;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.nodetype.ConstraintViolationException;
import javax.jcr.nodetype.NodeType;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.jackrabbit.commons.iterator.FilteringNodeIterator;
import org.apache.jackrabbit.commons.predicate.Predicate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Util to copy nodes and hierarchies between workspaces. A {@link Content.ContentFilter} defines what such a copy process includes.
* This is used to copy pages to the version workspace. While the paragraph nodes have to be copied the sub-pages should not.
*
* @version $Id$
*/
public final class CopyUtil {
/**
* Logger.
*/
private static Logger log = LoggerFactory.getLogger(CopyUtil.class);
private static final CopyUtil thisInstance = new CopyUtil();
private CopyUtil() {
}
public static CopyUtil getInstance() {
return thisInstance;
}
/**
* Copy given node to the version store using specified filter.
* @param source
* @param filter
*/
void copyToversion(Node source, Predicate filter) throws RepositoryException {
// first check if the node already exist
Node root;
try {
root = this.getSession().getNodeByIdentifier(source.getUUID());
if (root.getParent().getName().equalsIgnoreCase(VersionManager.TMP_REFERENCED_NODES)) {
root.getSession().move(root.getPath(), "/" + root.getName());
}
this.removeProperties(root);
// copy root properties
this.updateProperties(source, root);
this.updateNodeTypes(source, root);
root.save();
}
catch (ItemNotFoundException e) {
// create root for this versionable node
try {
this.importNode(this.getSession().getRootNode(), source);
}
catch (IOException ioe) {
throw new RepositoryException("Failed to import node in magnolia version store : " + ioe.getMessage());
}
root = this.getSession().getNodeByIdentifier(source.getUUID());
// copy root properties
// this.updateProperties(source, root);
// save parent node since this node is newly created
getSession().getRootNode().save();
}
// copy all child nodes
NodeIterator children = new FilteringNodeIterator(source.getNodes(), filter);
while (children.hasNext()) {
Node child = children.nextNode();
this.clone(child, root, filter, true);
}
this.removeNonExistingChildNodes(source, root, filter);
}
private void updateNodeTypes(Node source, Node root) throws RepositoryException {
List<String> targetNodeTypes = new ArrayList<String>();
for (NodeType t : root.getMixinNodeTypes()) {
targetNodeTypes.add(t.getName());
}
NodeType[] nodeTypes = source.getMixinNodeTypes();
for (NodeType type : nodeTypes) {
root.addMixin(type.getName());
targetNodeTypes.remove(type.getName());
}
// remove all mixins not found in the original except MIX_VERSIONABLE
for (String nodeType : targetNodeTypes) {
if (MgnlNodeType.MIX_VERSIONABLE.equals(nodeType)) {
continue;
}
root.removeMixin(nodeType);
}
}
/**
* Copy source to destination using the provided filter.
* @param source node in version store
* @param destination which needs to be restored
* @param filter this must be the same filter as used while creating this version
*/
void copyFromVersion(Node source, Node destination, Predicate filter) throws RepositoryException {
// merge top node properties
this.removeProperties(destination);
this.updateProperties(source, destination);
// copy all nodes from version store
this.copyAllChildNodes(source, destination, filter);
// remove all non existing nodes
this.removeNonExistingChildNodes(source, destination, filter);
this.removeNonExistingMixins(source, destination);
}
private void removeNonExistingMixins(Node source, Node destination) throws RepositoryException {
List<String> destNodeTypes = new ArrayList<String>();
// has to match mixin names as mixin instances to not equal()
for (NodeType nt : destination.getMixinNodeTypes()) {
destNodeTypes.add(nt.getName());
}
// remove all that still exist in source
for (NodeType nt :source.getMixinNodeTypes()) {
destNodeTypes.remove(nt.getName());
}
// un-mix the rest
for (String type : destNodeTypes) {
destination.removeMixin(type);
}
}
/**
* Recursively removes all child nodes from node using specified filter.
*/
private void removeNonExistingChildNodes(Node source, Node destination, Predicate filter)
throws RepositoryException {
// collect all uuids from the source node hierarchy using the given filter
NodeIterator children = new FilteringNodeIterator(destination.getNodes(), filter);
while (children.hasNext()) {
Node child = children.nextNode();
// check if this child exist in source, if not remove it
if (child.getDefinition().isAutoCreated()) {
continue;
}
try {
source.getSession().getNodeByUUID(child.getUUID());
// if exist its ok, recursively remove all sub nodes
this.removeNonExistingChildNodes(source, child, filter);
}
catch (ItemNotFoundException e) {
PropertyIterator referencedProperties = child.getReferences();
if (referencedProperties.getSize() > 0) {
// remove all referenced properties, its safe since source workspace cannot have these
// properties if node with this UUID does not exist
while (referencedProperties.hasNext()) {
referencedProperties.nextProperty().remove();
}
}
child.remove();
}
}
}
/**
* Copy all child nodes from node1 to node2.
*/
private void copyAllChildNodes(Node node1, Node node2, Predicate filter)
throws RepositoryException {
NodeIterator children = new FilteringNodeIterator(node1.getNodes(), filter);
while (children.hasNext()) {
Node child = children.nextNode();
this.clone(child, node2, filter, true);
}
}
public void clone(Node node, Node parent, Predicate filter, boolean removeExisting)
throws RepositoryException {
try {
// it seems to be a bug in jackrabbit - cloning does not work if the node with the same uuid
// exist, "removeExisting" has no effect
// if node exist with the same UUID, simply update non propected properties
String workspaceName = ContentRepository.getInternalWorkspaceName(parent.getSession().getWorkspace().getName());
Node existingNode = getSession(workspaceName).getNodeByIdentifier(node.getUUID());
if (removeExisting) {
existingNode.remove();
parent.save();
this.clone(node, parent);
return;
}
this.removeProperties(existingNode);
this.updateProperties(node, existingNode);
NodeIterator children = new FilteringNodeIterator(node.getNodes(), filter);
while (children.hasNext()) {
this.clone(children.nextNode(), existingNode, filter, removeExisting);
}
}
catch (ItemNotFoundException e) {
// its safe to clone if UUID does not exist in this workspace
this.clone(node, parent);
}
}
private void clone(Node node, Node parent) throws RepositoryException {
if (node.getDefinition().isAutoCreated()) {
Node destination = parent.getNode(node.getName());
this.removeProperties(destination);
this.updateProperties(node, destination);
}
else {
String parH = parent.getPath();
log.debug("workspace level clone from {}:{} to {}:{}", new Object[] { node.getSession().getWorkspace().getName(), node.getPath(), parent.getSession().getWorkspace().getName(), parent.getPath() });
parent.getSession().getWorkspace().clone(
node.getSession().getWorkspace().getName(),
node.getPath(),
parH + (parH != null && parH.endsWith("/") ? "" :"/") + node.getName(),
true);
}
}
/**
* Remove all properties under the given node.
*/
private void removeProperties(Node node) throws RepositoryException {
PropertyIterator properties = node.getProperties();
while (properties.hasNext()) {
Property property = properties.nextProperty();
if (property.getDefinition().isProtected() || property.getDefinition().isMandatory()) {
continue;
}
try {
property.remove();
}
catch (ConstraintViolationException e) {
if (log.isDebugEnabled()) {
log.debug("Property " + property.getName() + " is a reserved property");
}
}
}
}
/**
* Import while preserving UUID, parameters supplied must be from separate workspaces.
* @param parent under which the specified node will be imported
* @param node
* @throws RepositoryException
* @throws IOException if failed to import or export
*/
private void importNode(Node parent, Node node) throws RepositoryException, IOException {
File file = File.createTempFile("mgnl", null, Path.getTempDirectory());
FileOutputStream outStream = new FileOutputStream(file);
node.getSession().getWorkspace().getSession().exportSystemView(node.getPath(), outStream, false, true);
outStream.flush();
IOUtils.closeQuietly(outStream);
FileInputStream inStream = new FileInputStream(file);
parent.getSession().getWorkspace().getSession().importXML(
parent.getPath(),
inStream,
ImportUUIDBehavior.IMPORT_UUID_COLLISION_REMOVE_EXISTING);
IOUtils.closeQuietly(inStream);
file.delete();
}
/**
* Merge all non reserved properties.
*/
private void updateProperties(Node source, Node destination) throws RepositoryException {
PropertyIterator properties = source.getProperties();
while (properties.hasNext()) {
Property property = properties.nextProperty();
// exclude system property Rule and Version specific properties which were created on version
if (property.getName().equalsIgnoreCase(VersionManager.PROPERTY_RULE)) {
continue;
}
try {
if (property.getDefinition().isProtected()) {
continue;
}
if ("jcr:isCheckedOut".equals(property.getName())) {
// do not attempt to restore isCheckedOut property as it makes no sense to restore versioned node with
// checkedOut status and value for this property might not be set even though the property itself is set.
// Since JCR-1272 attempt to restore the property with no value will end up with RepositoryException instead
// of ConstraintViolationException and hence will not be caught by the catch{} block below.
continue;
}
if (property.getType() == PropertyType.REFERENCE) {
// first check for the referenced node existence
try {
getSession(destination.getSession().getWorkspace().getName())
.getNodeByIdentifier(property.getString());
}
catch (ItemNotFoundException e) {
if (!StringUtils.equalsIgnoreCase(
destination.getSession().getWorkspace().getName(),
VersionManager.VERSION_WORKSPACE)) {
throw e;
}
// get referenced node under temporary store
// use jcr import, there is no other way to get a node without sub hierarchy
Node referencedNode = getSession(source.getSession().getWorkspace().getName()).getNodeByIdentifier(
property.getString());
try {
this.importNode(getTemporaryPath(), referencedNode);
this.removeProperties(getSession().getNodeByIdentifier(property.getString()));
getTemporaryPath().save();
}
catch (IOException ioe) {
log.error("Failed to import referenced node", ioe);
}
}
}
if (property.getDefinition().isMultiple()) {
destination.setProperty(property.getName(), property.getValues());
}
else {
destination.setProperty(property.getName(), property.getValue());
}
}
catch (ConstraintViolationException e) {
if (log.isDebugEnabled()) {
log.debug("Property " + property.getName() + " is a reserved property");
}
}
}
}
/**
* Get version store session.
* @throws RepositoryException
* @throws LoginException
*/
private Session getSession() throws LoginException, RepositoryException {
return MgnlContext.getJCRSession(VersionManager.VERSION_WORKSPACE);
}
/**
* Get session of the specified workspace.
* @param workspaceId
* @throws RepositoryException
* @throws LoginException
*/
private Session getSession(String workspaceId) throws LoginException, RepositoryException {
return MgnlContext.getJCRSession(workspaceId);
}
/**
* Get temporary node.
*/
private Node getTemporaryPath() throws RepositoryException {
return getSession().getNode("/" + VersionManager.TMP_REFERENCED_NODES);
}
}