/* * Copyright (C) 2009 eXo Platform SAS. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.exoplatform.services.jcr.impl.core.version; import org.exoplatform.services.jcr.core.nodetype.NodeTypeDataManager; import org.exoplatform.services.jcr.dataflow.DataManager; import org.exoplatform.services.jcr.dataflow.ItemState; import org.exoplatform.services.jcr.datamodel.InternalQName; import org.exoplatform.services.jcr.datamodel.ItemType; import org.exoplatform.services.jcr.datamodel.NodeData; import org.exoplatform.services.jcr.datamodel.PropertyData; import org.exoplatform.services.jcr.datamodel.QPath; import org.exoplatform.services.jcr.datamodel.QPathEntry; import org.exoplatform.services.jcr.datamodel.ValueData; import org.exoplatform.services.jcr.impl.Constants; import org.exoplatform.services.jcr.impl.core.SessionDataManager; import org.exoplatform.services.jcr.impl.core.SessionImpl; import org.exoplatform.services.jcr.impl.dataflow.AbstractItemDataCopyVisitor; import org.exoplatform.services.jcr.impl.dataflow.ItemDataCopyVisitor; import org.exoplatform.services.jcr.impl.dataflow.ItemDataRemoveVisitor; import org.exoplatform.services.jcr.impl.dataflow.TransientNodeData; import org.exoplatform.services.jcr.impl.dataflow.TransientPropertyData; import org.exoplatform.services.jcr.impl.dataflow.ValueDataUtil; import org.exoplatform.services.jcr.impl.dataflow.session.SessionChangesLog; import org.exoplatform.services.log.ExoLogger; import org.exoplatform.services.log.Log; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Stack; import javax.jcr.MergeException; import javax.jcr.RepositoryException; /** * Created by The eXo Platform SAS 06.02.2007 Traverse through merging nodes * (destenation) and do merge to correspondent version states. * * @author <a href="mailto:peter.nedonosko@exoplatform.com.ua">Peter Nedonosko</a> * @version $Id: ItemDataMergeVisitor.java 14100 2008-05-12 10:53:47Z gazarenkov $ */ public class ItemDataMergeVisitor extends AbstractItemDataCopyVisitor { protected static int NONE = -1; protected static int LEAVE = 0; protected static int FAIL = 1; protected static int UPDATE = 2; protected static Log log = ExoLogger.getLogger("exo.jcr.component.core.ItemDataMergeVisitor"); protected final SessionImpl mergeSession; protected final SessionImpl corrSession; protected final Map<String, String> failed; protected final boolean bestEffort; protected final Stack<ContextParent> parents = new Stack<ContextParent>(); protected final SessionChangesLog changes; protected class RemoveVisitor extends ItemDataRemoveVisitor { RemoveVisitor() throws RepositoryException { super(mergeSession.getTransientNodesManager(), null, mergeSession.getWorkspace().getNodeTypesHolder(), mergeSession.getAccessManager(), mergeSession.getUserState()); } @Override protected void validateReferential(NodeData node) throws RepositoryException { // no REFERENCE validation here } }; private class ContextParent { private final NodeData parent; private final List<NodeData> corrChildNodes; private final int result; private ContextParent(NodeData parent, List<NodeData> corrChildNodes, int result) { this.parent = parent; this.corrChildNodes = corrChildNodes; this.result = result; } private ContextParent(NodeData parent, int result) { this(parent, null, result); } public NodeData getParent() { return parent; } public List<NodeData> getCorrChildNodes() { return corrChildNodes; } public int getResult() { return result; } } public ItemDataMergeVisitor(SessionImpl mergeSession, SessionImpl corrSession, Map<String, String> failed, boolean bestEffort) { super(mergeSession.getTransientNodesManager().getTransactManager()); this.corrSession = corrSession; this.mergeSession = mergeSession; this.bestEffort = bestEffort; this.failed = failed; this.changes = new SessionChangesLog(mergeSession); } @Override protected void entering(NodeData mergeNode, int level) throws RepositoryException { if (level == 0) { // initial - merge root node doMerge(mergeNode); } else if (parents.size() > 0) { ContextParent context = parents.peek(); if (context.getResult() == UPDATE) { // doUpdate() work... if (context.getCorrChildNodes().remove(mergeNode)) { // let C be the set of nodes in S and in S' // for each child node m of n in C domerge(m). doMerge(mergeNode); } else { // let D be the set of nodes in S but not in S'. // remove from n all child nodes in D. changes.add(new ItemState(new TransientNodeData(mergeNode.getQPath(), mergeNode.getIdentifier(), mergeNode.getPersistedVersion(), mergeNode.getPrimaryTypeName(), mergeNode.getMixinTypeNames(), mergeNode.getOrderNumber(), mergeNode.getParentIdentifier(), mergeNode.getACL()), ItemState.DELETED, true, context.getParent().getQPath(), true)); } } else if (context.getResult() == LEAVE) { // doLeave() work... // for each child node c of n domerge(c). doMerge(mergeNode); } else { // impossible... log.warn("Result is undefined for merge node " + mergeNode.getQPath().getAsString()); } } else { log.warn("Has no parent for merge node " + mergeNode.getQPath().getAsString()); } } @Override protected void entering(PropertyData mergeProperty, int level) throws RepositoryException { // remove any property, merged will be added in doMerge() --> doUpdate() } @Override protected void leaving(PropertyData mergeProperty, int level) throws RepositoryException { } @Override protected void leaving(NodeData mergeNode, int level) throws RepositoryException { if (parents.size() > 0) { ContextParent context = parents.pop(); if (context.getResult() == UPDATE) { // for each child node of n' in D' copy it (and its subtree) to n // as a new child node (if an incoming node has the same // UUID as a node already existing in this workspace, // the already existing node is removed). SessionDataManager mergeDataManager = mergeSession.getTransientNodesManager(); for (NodeData corrNode : context.getCorrChildNodes()) { NodeData existedSameIdentifier = (NodeData)mergeDataManager.getItemData(corrNode.getIdentifier()); if (existedSameIdentifier != null) { // if an incoming node has the same // UUID as a node already existing in this workspace, // the already existing node is removed RemoveVisitor remover = new RemoveVisitor(); existedSameIdentifier.accept(remover); changes.addAll(remover.getRemovedStates()); } ItemDataCopyVisitor copier = new ItemDataCopyVisitor(context.getParent(), corrNode.getQPath().getName(), mergeSession .getWorkspace().getNodeTypesHolder(), mergeDataManager, mergeDataManager, true); corrNode.accept(copier); changes.addAll(copier.getItemAddStates()); } } } } // -------------------- merge actions ------------ protected void doMerge(NodeData mergeNode) throws RepositoryException { // let n' be the corresponding node of n in ws'. // find corr node for this node NodeData corrNode = getCorrNodeData(mergeNode); if (corrNode != null) { NodeData mergeVersion = getBaseVersionData(mergeNode, mergeSession); if (mergeVersion != null) { NodeData corrVersion = getBaseVersionData(corrNode, corrSession); if (corrVersion != null) { // let v be base version of n. // let v' be base version of n'. SessionDataManager mergeDataManager = mergeSession.getTransientNodesManager(); PropertyData isCheckedOutProperty = (PropertyData)mergeDataManager.getItemData(mergeNode, new QPathEntry(Constants.JCR_ISCHECKEDOUT, 0), ItemType.PROPERTY); if (!ValueDataUtil.getBoolean(isCheckedOutProperty.getValues().get(0)) && isSuccessor(mergeVersion, corrVersion)) { // if v' is a successor of v and // n is not checked-in doupdate(n, n'). doUpdate(mergeNode, corrNode); } else if (mergeVersion.getQPath().equals(corrVersion.getQPath()) || isPredecessor(mergeVersion, corrVersion)) { // else if v is equal to or a predecessor of v' doleave(n). doLeave(mergeNode); } else { // else dofail(n, v'). doFail(mergeNode, corrVersion); } } else { // else if n' is not versionable doleave(n) doLeave(mergeNode); } } else { // else if n is not versionable doupdate(n, n') doUpdate(mergeNode, corrNode); } } else { // if no such n' doleave(n). doLeave(mergeNode); } } protected void doLeave(NodeData mergeNode) throws RepositoryException { // for each child node c of n domerge(c). // ...back to visitor parents.push(new ContextParent(mergeNode, LEAVE)); } protected void doUpdate(NodeData mergeNode, NodeData corrNode) throws RepositoryException { DataManager mergeDataManager = mergeSession.getTransientNodesManager().getTransactManager(); QPath mergePath = mergeNode.getQPath(); TransientNodeData mergedNode = new TransientNodeData(mergePath, mergeNode.getIdentifier(), mergeNode.getPersistedVersion(), corrNode.getPrimaryTypeName(), corrNode.getMixinTypeNames(), mergeNode.getOrderNumber(), mergeNode.getParentIdentifier(), mergeNode.getACL()); if (!mergeNode.getIdentifier().equals(corrNode.getIdentifier())) { NodeData existedSameIdentifier = (NodeData)mergeDataManager.getItemData(corrNode.getIdentifier()); if (existedSameIdentifier != null) { // if an incoming node has the same // UUID as a node already existing in this workspace, // the already existing node is removed RemoveVisitor remover = new RemoveVisitor(); existedSameIdentifier.accept(remover); changes.addAll(remover.getRemovedStates()); } } changes.add(new ItemState(mergedNode, ItemState.UPDATED, true, mergeNode.getQPath(), true)); // replace set of properties of n with those of n'. DataManager corrDataManager = corrSession.getTransientNodesManager().getTransactManager(); List<PropertyData> corrChildProps = corrDataManager.getChildPropertiesData(corrNode); List<PropertyData> mergeChildProps = mergeDataManager.getChildPropertiesData(mergeNode); Map<InternalQName, PropertyData> existedProps = new HashMap<InternalQName, PropertyData>(); for (PropertyData cp : mergeChildProps) { TransientPropertyData existed = new TransientPropertyData(cp.getQPath(), cp.getIdentifier(), cp.getPersistedVersion(), cp.getType(), cp.getParentIdentifier(), cp.isMultiValued(), copyValues(cp)); changes.add(new ItemState(existed, ItemState.DELETED, true, mergedNode.getQPath(), true)); existedProps.put(existed.getQPath().getName(), existed); } for (PropertyData cp : corrChildProps) { PropertyData existed = existedProps.get(cp.getQPath().getName()); TransientPropertyData mcp = new TransientPropertyData(QPath.makeChildPath(mergePath, cp.getQPath().getName()), existed != null ? existed.getIdentifier() : cp.getIdentifier(), existed != null ? existed.getPersistedVersion() : cp.getPersistedVersion(), cp.getType(), mergedNode.getIdentifier(), cp.isMultiValued(), copyValues(cp)); changes.add(new ItemState(mcp, ItemState.ADDED, true, mergedNode.getQPath(), true)); } List<NodeData> childNodes = corrDataManager.getChildNodesData(corrNode); parents.push(new ContextParent(mergedNode, childNodes, UPDATE)); // let S be the set of child nodes of n. // let S' be the set of child nodes of n'. // judging by the name of the child node: // let C be the set of nodes in S and in S' // let D be the set of nodes in S but not in S'. // let D' be the set of nodes in S' but not in S. // remove from n all child nodes in D. <<< will occurs in doMerge() on // particular child // for each child node of n' in D' copy it (and its subtree) to n // as a new child node (if an incoming node has the same // UUID as a node already existing in this workspace, // the already existing node is removed) <<< will occurs in doMerge() on // particular child // for each child node m of n in C domerge(m). } protected void doFail(NodeData mergeNode, NodeData corrVersion) throws RepositoryException { if (bestEffort) { // else add UUID of v' (if not already present) to the // jcr:mergeFailed property of n, // add UUID of n to failedset, // doleave(n). failed.put(mergeNode.getIdentifier(), corrVersion.getIdentifier()); doLeave(mergeNode); } else { // if bestEffort = false throw MergeException throw new MergeException("Merging of node " + mergeSession.getLocationFactory().createJCRPath(mergeNode.getQPath()).getAsString(false) + " failed"); } } // -------------------- utils -------------------- protected NodeData getBaseVersionData(final NodeData node, final SessionImpl session) throws RepositoryException { NodeTypeDataManager ntManager = session.getWorkspace().getNodeTypesHolder(); if (ntManager.isNodeType(Constants.MIX_VERSIONABLE, node.getPrimaryTypeName(), node.getMixinTypeNames())) { SessionDataManager dmanager = session.getTransientNodesManager(); PropertyData bvProperty = (PropertyData)dmanager.getItemData(node, new QPathEntry(Constants.JCR_BASEVERSION, 0), ItemType.PROPERTY); return (NodeData)dmanager.getItemData(ValueDataUtil.getString(bvProperty.getValues().get(0))); } return null; // non versionable } protected NodeData getCorrNodeData(final NodeData mergeNode) throws RepositoryException { final QPath mergePath = mergeNode.getQPath(); SessionDataManager corrDataManager = corrSession.getTransientNodesManager(); SessionDataManager mergeDataManager = mergeSession.getTransientNodesManager(); NodeTypeDataManager mergeNtManager = mergeSession.getWorkspace().getNodeTypesHolder(); if (mergeNtManager.isNodeType(Constants.MIX_REFERENCEABLE, mergeNode.getPrimaryTypeName(), mergeNode.getMixinTypeNames())) { // by UUID return (NodeData)corrDataManager.getItemData(mergeNode.getIdentifier()); } // by location for (int i = 1; i <= mergePath.getDepth(); i++) { final QPath ancesstorPath = mergePath.makeAncestorPath(i); NodeData mergeAncestor = (NodeData)mergeDataManager.getItemData(ancesstorPath); if (mergeAncestor != null && mergeNtManager.isNodeType(Constants.MIX_REFERENCEABLE, mergeAncestor.getPrimaryTypeName(), mergeAncestor.getMixinTypeNames())) { NodeData corrAncestor = (NodeData)corrDataManager.getItemData(mergeAncestor.getIdentifier()); if (corrAncestor != null) { QPathEntry[] relPathEntries = mergePath.getRelPath(mergePath.getDepth() - i); return (NodeData)corrDataManager.getItemData(corrAncestor, relPathEntries, ItemType.NODE); } } } return (NodeData)corrDataManager.getItemData(mergePath); } /** * Is a predecessor of the merge version */ protected boolean isPredecessor(NodeData mergeVersion, NodeData corrVersion) throws RepositoryException { SessionDataManager mergeDataManager = mergeSession.getTransientNodesManager(); PropertyData predecessorsProperty = (PropertyData)mergeDataManager.getItemData(mergeVersion, new QPathEntry(Constants.JCR_PREDECESSORS, 0), ItemType.PROPERTY); if (predecessorsProperty != null) { for (ValueData pv : predecessorsProperty.getValues()) { String pidentifier = ValueDataUtil.getString(pv); if (pidentifier.equals(corrVersion.getIdentifier())) { return true; // got it } // search in predecessors of the predecessor NodeData predecessor = (NodeData)mergeDataManager.getItemData(pidentifier); if (predecessor != null) { if (isPredecessor(predecessor, corrVersion)) { return true; } } else { throw new RepositoryException("Merge. Predecessor is not found by uuid " + pidentifier + ". Version " + mergeSession.getLocationFactory().createJCRPath(mergeVersion.getQPath()).getAsString(false)); } } // else it's a root version } return false; } /** * Is a successor of the merge version */ protected boolean isSuccessor(NodeData mergeVersion, NodeData corrVersion) throws RepositoryException { SessionDataManager mergeDataManager = mergeSession.getTransientNodesManager(); PropertyData successorsProperty = (PropertyData)mergeDataManager.getItemData(mergeVersion, new QPathEntry(Constants.JCR_SUCCESSORS, 0), ItemType.PROPERTY); if (successorsProperty != null) { for (ValueData sv : successorsProperty.getValues()) { String sidentifier = ValueDataUtil.getString(sv); if (sidentifier.equals(corrVersion.getIdentifier())) { return true; // got it } // search in successors of the successor NodeData successor = (NodeData)mergeDataManager.getItemData(sidentifier); if (successor != null) { if (isSuccessor(successor, corrVersion)) { return true; } } else { throw new RepositoryException("Merge. Ssuccessor is not found by uuid " + sidentifier + ". Version " + mergeSession.getLocationFactory().createJCRPath(mergeVersion.getQPath()).getAsString(false)); } } // else it's a end of version graph node } return false; } public SessionChangesLog getMergeChanges() { return changes; } }