/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.jackrabbit.core.version;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import javax.jcr.AccessDeniedException;
import javax.jcr.MergeException;
import javax.jcr.Node;
import javax.jcr.PropertyType;
import javax.jcr.RepositoryException;
import javax.jcr.UnsupportedRepositoryOperationException;
import javax.jcr.version.Version;
import javax.jcr.version.VersionException;
import org.apache.jackrabbit.core.HierarchyManager;
import org.apache.jackrabbit.core.ItemValidator;
import org.apache.jackrabbit.core.id.ItemId;
import org.apache.jackrabbit.core.id.NodeId;
import org.apache.jackrabbit.core.security.authorization.Permission;
import org.apache.jackrabbit.core.session.SessionContext;
import org.apache.jackrabbit.core.state.ChildNodeEntry;
import org.apache.jackrabbit.core.state.ItemStateException;
import org.apache.jackrabbit.core.state.PropertyState;
import org.apache.jackrabbit.core.state.UpdatableItemStateManager;
import org.apache.jackrabbit.core.value.InternalValue;
import org.apache.jackrabbit.spi.Name;
import org.apache.jackrabbit.spi.commons.name.NameConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The JCR Version Manager impementation is split in several classes in order to
* group related methods together.
* </p>
* this class provides methods for the merge operations.
*/
abstract public class VersionManagerImplMerge extends VersionManagerImplRestore {
/**
* default logger
*/
private static final Logger log = LoggerFactory.getLogger(VersionManagerImplMerge.class);
/**
* Creates a new version manager for the given session
*
* @param context component context of the current session
* @param stateMgr the underlying state manager
* @param hierMgr local hierarchy manager
*/
protected VersionManagerImplMerge(
SessionContext context, UpdatableItemStateManager stateMgr,
HierarchyManager hierMgr) {
super(context, stateMgr, hierMgr);
}
/**
* Merges/Updates this node with its corresponding ones
*
* @param state state to merge or update
* @param srcRoot src workspace root node
* @param failedIds list of failed ids
* @param bestEffort best effor flag
* @param shallow is shallow flag
* @throws RepositoryException if an error occurs
* @throws ItemStateException if an error occurs
*/
protected void merge(NodeStateEx state, NodeStateEx srcRoot,
List<ItemId> failedIds,
boolean bestEffort, boolean shallow)
throws RepositoryException, ItemStateException {
if (shallow) {
// If <code>isShallow</code> is <code>true</code> and this node is not
// versionable, then this method returns and no changes are made.
if (!state.getEffectiveNodeType().includesNodeType(NameConstants.MIX_VERSIONABLE)) {
return;
}
}
NodeStateEx srcNode = getCorrespondingNode(state, srcRoot);
if (srcNode == null) {
// If this node (the one on which merge is called) does not have a corresponding
// node in the indicated workspace, then the merge method returns quietly and no
// changes are made.
return;
}
WriteOperation ops = startWriteOperation();
try {
internalMerge(state, srcRoot, failedIds, bestEffort, shallow);
state.store();
ops.save();
} finally {
ops.close();
}
}
/**
* Merges/Updates this node with its corresponding ones
*
* @param state state to merge or update
* @param srcRoot src workspace root node
* @param failedIds list of failed ids
* @param bestEffort best effor flag
* @param shallow is shallow flag
* @throws RepositoryException if an error occurs
* @throws ItemStateException if an error occurs
*/
private void internalMerge(NodeStateEx state, NodeStateEx srcRoot,
List<ItemId> failedIds,
boolean bestEffort, boolean shallow)
throws RepositoryException, ItemStateException {
NodeStateEx srcNode = doMergeTest(state, srcRoot, failedIds, bestEffort);
if (srcNode == null) {
if (!shallow) {
// leave, iterate over children, but ignore non-versionable child
// nodes (see JCR-1046)
for (NodeStateEx n: state.getChildNodes()) {
if (n.getEffectiveNodeType().includesNodeType(NameConstants.MIX_VERSIONABLE)) {
internalMerge(n, srcRoot, failedIds, bestEffort, shallow);
}
}
}
return;
}
// check lock and hold status if node exists
checkModify(state, ItemValidator.CHECK_LOCK | ItemValidator.CHECK_HOLD, Permission.NONE);
// remove all properties that are not present in srcNode
for (PropertyState prop: state.getProperties()) {
if (!srcNode.hasProperty(prop.getName())) {
state.removeProperty(prop.getName());
}
}
// update all properties from the src node
for (PropertyState prop: srcNode.getProperties()) {
Name propName = prop.getName();
// ignore system types
if (propName.equals(NameConstants.JCR_PRIMARYTYPE)
|| propName.equals(NameConstants.JCR_MIXINTYPES)
|| propName.equals(NameConstants.JCR_UUID)) {
continue;
}
state.copyFrom(prop);
}
// update the mixin types
state.setMixins(srcNode.getState().getMixinTypeNames());
// remove the child nodes in N but not in N'
LinkedList<ChildNodeEntry> toDelete = new LinkedList<ChildNodeEntry>();
for (ChildNodeEntry entry: state.getState().getChildNodeEntries()) {
if (!srcNode.getState().hasChildNodeEntry(entry.getName(), entry.getIndex())) {
toDelete.add(entry);
}
}
for (ChildNodeEntry entry: toDelete) {
state.removeNode(entry.getName(), entry.getIndex());
}
state.store();
// add source ones
for (ChildNodeEntry entry: srcNode.getState().getChildNodeEntries()) {
NodeStateEx child = state.getNode(entry.getName(), entry.getIndex());
if (child == null) {
// if destination workspace already has such an node, remove it
if (state.hasNode(entry.getId())) {
child = state.getNode(entry.getId());
NodeStateEx parent = child.getParent();
parent.removeNode(child);
parent.store();
}
// create new child
NodeStateEx srcChild = srcNode.getNode(entry.getId());
child = state.addNode(entry.getName(), srcChild.getState().getNodeTypeName(), srcChild.getNodeId());
child.setMixins(srcChild.getState().getMixinTypeNames());
// copy src child
state.store();
internalMerge(child, srcRoot, null, bestEffort, false);
} else if (!shallow) {
// recursively merge
internalMerge(child, srcRoot, failedIds, bestEffort, false);
}
}
}
/**
* Returns the corresponding node in the workspace of the given session.
* <p/>
* Given a node N1 in workspace W1, its corresponding node N2 in workspace
* W2 is defined as follows:
* <ul>
* <li>If N1 is the root node of W1 then N2 is the root node of W2.
* <li>If N1 is referenceable (has a UUID) then N2 is the node in W2 with
* the same UUID.
* <li>If N1 is not referenceable (does not have a UUID) then there is some
* node M1 which is either the nearest ancestor of N1 that is
* referenceable, or is the root node of W1. If the corresponding node
* of M1 is M2 in W2, then N2 is the node with the same relative path
* from M2 as N1 has from M1.
* </ul>
*
* @param state N1
* @param srcRoot the root node state of W2
* @return the corresponding node or <code>null</code> if no corresponding
* node exists.
* @throws RepositoryException If another error occurs.
*/
private NodeStateEx getCorrespondingNode(NodeStateEx state, NodeStateEx srcRoot)
throws RepositoryException {
// search nearest ancestor that is referenceable
NodeStateEx m1 = state;
LinkedList<ChildNodeEntry> elements = new LinkedList<ChildNodeEntry>();
while (m1.getParentId() != null &&
!m1.getEffectiveNodeType().includesNodeType(NameConstants.MIX_REFERENCEABLE)) {
NodeStateEx parent = m1.getParent();
elements.addFirst(parent.getState().getChildNodeEntry(m1.getNodeId()));
m1 = parent;
}
// check if corresponding ancestor exists
if (srcRoot.hasNode(m1.getNodeId())) {
NodeStateEx m2 = srcRoot.getNode(m1.getNodeId());
Iterator<ChildNodeEntry> iter = elements.iterator();
while (iter.hasNext() && m2 != null) {
ChildNodeEntry e = iter.next();
m2 = m2.getNode(e.getName(), e.getIndex());
}
return m2;
} else {
return null;
}
}
/**
* Performs the merge test. If the result is 'update', then the corresponding
* source node is returned. if the result is 'leave' or 'besteffort-fail'
* then <code>null</code> is returned. If the result of the merge test is
* 'fail' with bestEffort set to <code>false</code> a MergeException is
* thrown.
* <p/>
* jsr170 - 8.2.10 Merge:
* [...]
* <ul>
* <li>If N is currently checked-in then:</li>
* <ul>
* <li>If V' is a successor (to any degree) of V, then the merge result
* for N is update.
* </li>
* <li>If V' is a predecessor (to any degree) of V or if V and
* V' are identical (i.e., are actually the same version),
* then the merge result for N is leave.
* </li>
* <li>If V is neither a successor of, predecessor of, nor
* identical with V', then the merge result for N is failed.
* This is the case where N and N' represent divergent
* branches of the version graph, thus determining the
* result of a merge is non-trivial.
* </li>
* </ul>
* <li>If N is currently checked-out then:</li>
* <ul>
* <li>If V' is a predecessor (to any degree) of V or if V and
* V' are identical (i.e., are actually the same version),
* then the merge result for N is leave.
* </li>
* <li>If any other relationship holds between V and V',
* then the merge result for N is fail.
* </li>
* </ul>
* </ul>
*
* @param state state to test
* @param srcRoot root node state of the source workspace
* @param failedIds the list to store the failed node ids.
* @param bestEffort the best effort flag
* @return the corresponding source node or <code>null</code>
* @throws RepositoryException if an error occurs.
* @throws AccessDeniedException if access is denied
*/
private NodeStateEx doMergeTest(NodeStateEx state, NodeStateEx srcRoot, List<ItemId> failedIds, boolean bestEffort)
throws RepositoryException, AccessDeniedException {
// If N does not have a corresponding node then the merge result for N is leave.
NodeStateEx srcNode = getCorrespondingNode(state, srcRoot);
if (srcNode == null) {
return null;
}
// if not versionable, update
if (!state.getEffectiveNodeType().includesNodeType(NameConstants.MIX_VERSIONABLE) || failedIds == null) {
return srcNode;
}
// if source node is not versionable, leave
if (!srcNode.getEffectiveNodeType().includesNodeType(NameConstants.MIX_VERSIONABLE)) {
return null;
}
// test versions. the following code could be simplified but is
// intentionally expanded for follow the spec.
InternalVersion v = getBaseVersion(state);
InternalVersion vp = getBaseVersion(srcNode);
if (!isCheckedOut(state)) {
// If N is currently checked-in then:
if (vp.isMoreRecent(v)) {
// - If V' is an eventual successor of V, then the merge result for N is update.
return srcNode;
} else if (v.equals(vp) || v.isMoreRecent(vp)) {
// - If V' is an eventual predecessor of V or if V and V' are identical (i.e., are
// actually the same version), then the merge result for N is leave.
return null;
} else {
// - If V is neither an eventual successor of, eventual predecessor of, nor
// identical with V', then the merge result for N is failed. This is the case
// where N and N' represent divergent branches of the version graph.
// failed is covered below
}
} else {
// If N is currently checked-out then:
if (v.equals(vp) || v.isMoreRecent(vp)) {
// - If V' is an eventual predecessor of V or if V and V' are identical (i.e., are
// actually the same version), then the merge result for
// N is leave.
return null;
} else {
// - If any other relationship holds between V and V', then the merge result
// for N is fail.
// failed is covered below
}
}
if (vp.isMoreRecent(v) && !isCheckedOut(state)) {
// I f V' is a successor (to any degree) of V, then the merge result for
// N is update. This case can be thought of as the case where N' is
// "newer" than N and therefore N should be updated to reflect N'.
return srcNode;
} else if (v.equals(vp) || v.isMoreRecent(vp)) {
// If V' is a predecessor (to any degree) of V or if V and V' are
// identical (i.e., are actually the same version), then the merge
// result for N is leave. This case can be thought of as the case where
// N' is "older" or the "same age" as N and therefore N should be left alone.
return null;
} else {
// If V is neither a successor of, predecessor of, nor identical
// with V', then the merge result for N is failed. This is the case
// where N and N' represent divergent branches of the version graph,
// thus determining the result of a merge is non-trivial.
if (bestEffort) {
// add 'offending' version to jcr:mergeFailed property
Set<NodeId> set = getMergeFailed(state);
set.add(vp.getId());
setMergeFailed(state, set);
failedIds.add(state.getNodeId());
state.store();
return null;
} else {
String msg =
"Unable to merge nodes. Violating versions. " + safeGetJCRPath(state);
log.debug(msg);
throw new MergeException(msg);
}
}
}
/**
* Perform {@link Node#cancelMerge(Version)} or {@link Node#doneMerge(Version)}
* depending on the value of <code>cancel</code>.
* @param state state to finish
* @param version version
* @param cancel flag inidicates if this is a cancel operation
* @throws RepositoryException if an error occurs
*/
protected void finishMerge(NodeStateEx state, Version version, boolean cancel)
throws RepositoryException {
// check versionable
if (!checkVersionable(state)) {
throw new UnsupportedRepositoryOperationException("Node not full versionable: " + safeGetJCRPath(state));
}
// check if version is in mergeFailed list
Set<NodeId> failed = getMergeFailed(state);
NodeId versionId = ((VersionImpl) version).getNodeId();
if (!failed.remove(versionId)) {
String msg =
"Unable to finish merge. Specified version is not in"
+ " jcr:mergeFailed property: " + safeGetJCRPath(state);
log.error(msg);
throw new VersionException(msg);
}
WriteOperation ops = startWriteOperation();
try {
// remove version from mergeFailed list
setMergeFailed(state, failed);
if (!cancel) {
// add version to jcr:predecessors list
InternalValue[] vals = state.getPropertyValues(NameConstants.JCR_PREDECESSORS);
InternalValue[] v = new InternalValue[vals.length + 1];
for (int i = 0; i < vals.length; i++) {
v[i] = InternalValue.create(vals[i].getNodeId());
}
v[vals.length] = InternalValue.create(versionId);
state.setPropertyValues(NameConstants.JCR_PREDECESSORS, PropertyType.REFERENCE, v, true);
}
state.store();
ops.save();
} catch (ItemStateException e) {
throw new RepositoryException(e);
} finally {
ops.close();
}
}
/**
* Returns a set of nodeid from the jcr:mergeFailed property of the given state
* @param state the state
* @return set of node ids
* @throws RepositoryException if an error occurs
*/
private Set<NodeId> getMergeFailed(NodeStateEx state)
throws RepositoryException {
Set<NodeId> set = new HashSet<NodeId>();
if (state.hasProperty(NameConstants.JCR_MERGEFAILED)) {
InternalValue[] vals = state.getPropertyValues(NameConstants.JCR_MERGEFAILED);
for (InternalValue val : vals) {
set.add(val.getNodeId());
}
}
return set;
}
/**
* Updates the merge failed property of the given state/
* @param state the state to update
* @param set the set of ids
* @throws RepositoryException if an error occurs
*/
private void setMergeFailed(NodeStateEx state, Set<NodeId> set)
throws RepositoryException {
if (set.isEmpty()) {
state.removeProperty(NameConstants.JCR_MERGEFAILED);
} else {
InternalValue[] vals = new InternalValue[set.size()];
Iterator<NodeId> iter = set.iterator();
int i = 0;
while (iter.hasNext()) {
NodeId id = iter.next();
vals[i++] = InternalValue.create(id);
}
state.setPropertyValues(NameConstants.JCR_MERGEFAILED, PropertyType.REFERENCE, vals, true);
}
}
/**
* Merge the given activity to this workspace
*
* @param activity internal activity
* @param failedIds list of failed ids
* @throws RepositoryException if an error occurs
*/
protected void merge(InternalActivity activity, List<ItemId> failedIds)
throws RepositoryException {
VersionSet changeSet = activity.getChangeSet();
WriteOperation ops = startWriteOperation();
try {
Iterator<NodeId> iter = changeSet.versions().keySet().iterator();
while (iter.hasNext()) {
InternalVersion v = changeSet.versions().remove(iter.next());
NodeStateEx state = getNodeStateEx(v.getFrozenNode().getFrozenId());
if (state != null) {
InternalVersion base = getBaseVersion(state);
// if base version is newer than version, add to failed list
// but merge it anyways
if (base.isMoreRecent(v)) {
failedIds.add(state.getNodeId());
// add it to the jcr:mergeFailed property
Set<NodeId> set = getMergeFailed(state);
set.add(base.getId());
setMergeFailed(state, set);
state.store();
} else {
for (InternalVersion restored: internalRestore(state, v, changeSet, true)) {
changeSet.versions().remove(restored.getVersionHistory().getId());
}
}
}
// reset iterator
iter = changeSet.versions().keySet().iterator();
}
ops.save();
} catch (ItemStateException e) {
throw new RepositoryException(e);
} finally {
ops.close();
}
}
}