/*
* 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.state;
import org.apache.jackrabbit.core.id.ItemId;
import org.apache.jackrabbit.core.id.NodeId;
import org.apache.jackrabbit.core.id.PropertyId;
import org.apache.jackrabbit.core.nodetype.EffectiveNodeType;
import org.apache.jackrabbit.spi.Name;
import org.apache.jackrabbit.spi.QNodeDefinition;
import org.apache.jackrabbit.spi.QPropertyDefinition;
import org.apache.jackrabbit.spi.commons.name.NameConstants;
import javax.jcr.nodetype.NoSuchNodeTypeException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;
/**
* Internal utility class used for merging concurrent changes that occurred
* on different <code>NodeState</code> instances representing the same node.
* <p>
* See http://issues.apache.org/jira/browse/JCR-584.
*/
class NodeStateMerger {
/**
* Tries to silently merge the given <code>state</code> with its
* externally (e.g. through another session) modified overlayed state
* in order to avoid an <code>InvalidItemStateException</code>.
* <p>
* See http://issues.apache.org/jira/browse/JCR-584.
* See also http://issues.apache.org/jira/browse/JCR-3290.
*
* @param state node state whose modified overlayed state should be
* merged
* @param context used for analyzing the context of the modifications
* @return true if the changes could be successfully merged into the
* given node state; false otherwise
*/
static boolean merge(NodeState state, MergeContext context) {
NodeState overlayedState = (NodeState) state.getOverlayedState();
if (overlayedState == null
|| state.getModCount() == overlayedState.getModCount()) {
return false;
}
synchronized (overlayedState) {
synchronized (state) {
/**
* some examples for trivial non-conflicting changes:
* - s1 added child node a, s2 removes child node b
* - s1 adds child node a, s2 adds child node b
* - s1 adds child node a, s2 adds mixin type
*
* conflicting changes causing staleness:
* - s1 added non-sns child node or property a,
* s2 added non-sns child node or property a => name clash
* - either session reordered child nodes
* (some combinations could possibly be merged)
* - either session moved node
*/
// compare current state with externally modified overlayed
// state and determine what has been changed by whom
// child node entries order
if (!state.getReorderedChildNodeEntries().isEmpty()) {
// for now we don't even try to merge the result of
// a reorder operation
return false;
}
// the primary node type
if (!state.getNodeTypeName().equals(overlayedState.getNodeTypeName())) {
// the primary node type has changed either in 'state' or 'overlayedState'.
return false;
}
// mixin types
if (!mergeMixinTypes(state, overlayedState, context)) {
return false;
}
// parent id
if (state.getParentId() != null
&& !state.getParentId().equals(overlayedState.getParentId())) {
return false;
}
// child node entries
if (!state.getChildNodeEntries().equals(
overlayedState.getChildNodeEntries())) {
ArrayList<ChildNodeEntry> added = new ArrayList<ChildNodeEntry>();
ArrayList<ChildNodeEntry> removed = new ArrayList<ChildNodeEntry>();
for (ChildNodeEntry cne : state.getAddedChildNodeEntries()) {
// locally added or moved?
if (context.isAdded(cne.getId()) || (context.isModified(cne.getId()) && isParent(state, cne, context))) {
// a new child node entry has been added to this state;
// check for name collisions with other state
if (overlayedState.hasChildNodeEntry(cne.getName())) {
// conflicting names
if (cne.getIndex() < 2) {
// check if same-name siblings are allowed
if (!context.allowsSameNameSiblings(cne.getId())) {
return false;
}
}
// assume same-name siblings are allowed since index is >= 2
}
added.add(cne);
}
}
for (ChildNodeEntry cne : state.getRemovedChildNodeEntries()) {
// locally removed?
if (context.isDeleted(cne.getId()) || context.isModified(cne.getId())) {
// a child node entry has been removed from this node state
removed.add(cne);
}
}
// copy child node entries from other state and
// re-apply changes made on this state
state.setChildNodeEntries(overlayedState.getChildNodeEntries());
for (ChildNodeEntry cne : removed) {
state.removeChildNodeEntry(cne.getId());
}
for (ChildNodeEntry cne : added) {
state.addChildNodeEntry(cne.getName(), cne.getId());
}
}
// property names
if (!state.getPropertyNames().equals(
overlayedState.getPropertyNames())) {
HashSet<Name> added = new HashSet<Name>();
HashSet<Name> removed = new HashSet<Name>();
for (Name name : state.getAddedPropertyNames()) {
PropertyId propId =
new PropertyId(state.getNodeId(), name);
if (context.isAdded(propId)) {
added.add(name);
}
}
for (Name name : state.getRemovedPropertyNames()) {
PropertyId propId =
new PropertyId(state.getNodeId(), name);
if (context.isDeleted(propId)) {
// a property name has been removed from this state
removed.add(name);
}
}
// copy property names from other and
// re-apply changes made on this state
state.setPropertyNames(overlayedState.getPropertyNames());
for (Name name : added) {
state.addPropertyName(name);
}
for (Name name : removed) {
state.removePropertyName(name);
}
}
// finally sync modification count
state.setModCount(overlayedState.getModCount());
return true;
}
}
}
/**
*
* @param state
* @param overlayedState
* @return true if the mixin type names are the same in both node states or
* if the mixin modifications do not conflict (and could be merged); false
* otherwise.
*/
private static boolean mergeMixinTypes(NodeState state, NodeState overlayedState, MergeContext ctx) {
Set<Name> mixins = new HashSet<Name>(state.getMixinTypeNames());
Set<Name> overlayedMixins = new HashSet<Name>(overlayedState.getMixinTypeNames());
if (mixins.equals(overlayedMixins)) {
// no net effect modifications at all -> merge child items defined
// by the mixins according to the general rule.
return true;
}
PropertyId mixinPropId = new PropertyId(state.getNodeId(), NameConstants.JCR_MIXINTYPES);
boolean mergeDone;
if (ctx.isAdded(mixinPropId)) {
// jcr:mixinTypes property was created for 'state'.
// changes is safe (without need to merge),
// - overlayed state doesn't have any mixins OR
// - overlayed state got the same (or a subset) added
// and non of the items defined by the new mixin(s) collides with
// existing items on the overlayed state
if (overlayedMixins.isEmpty() || mixins.containsAll(overlayedMixins)) {
mixins.removeAll(overlayedMixins);
mergeDone = !conflicts(state, mixins, ctx, true);
} else {
// different mixins added in overlayedState and state
// -> don't merge
mergeDone = false;
}
} else if (ctx.isDeleted(mixinPropId)) {
// jcr:mixinTypes property was removed in 'state'.
// we can't determine if there was any change to mixin types in the
// overlayed state.
// -> don't merge.
mergeDone = false;
} else if (ctx.isModified(mixinPropId)) {
/* jcr:mixinTypes property was modified in 'state'.
NOTE: if the mixins of the overlayed state was modified as well
the property (jcr:mixinTypes) cannot not be persisted (stale).
since there is not way to determine if the overlayed mixins have
been modified just check for conflicts related to a net mixin
addition.
*/
if (mixins.containsAll(overlayedMixins)) {
// net result of modifications is only addition.
// -> so far the changes are save if there are no conflicts
// caused by mixins modification in 'state'.
// NOTE: the save may still fail if the mixin property has
// been modified in the overlayed state as well.
mixins.removeAll(overlayedMixins);
mergeDone = !conflicts(state, mixins, ctx, true);
} else {
// net result is either a removal in 'state' or modifications
// in both node states.
// -> don't merge.
mergeDone = false;
}
} else {
// jcr:mixinTypes property was added or modified in the overlayed
// state but neither added nor modified in 'state'.
if (overlayedMixins.containsAll(mixins)) {
// the modification in the overlayed state only includes the
// addition of mixin node types, but no removal.
// -> need to check if any added items from state would
// collide with the items defined by the new mixin on the
// overlayed state.
overlayedMixins.removeAll(mixins);
if (!conflicts(state, overlayedMixins, ctx, false)) {
// update the mixin names in 'state'. the child items defined
// by the new mixins will be added later on during merge of
// child nodes and properties.
state.setMixinTypeNames(overlayedMixins);
mergeDone = true;
} else {
mergeDone = false;
}
} else {
// either remove-mixin(s) or both add and removal of mixin in
// the overlayed state.
// -> we cannot merge easily
mergeDone = false;
}
}
return mergeDone;
}
/**
*
* @param state The state of the node to be saved.
* @param addedMixins The added mixins to be used for testing
* @param ctx
* @param compareToOverlayed
* @return true if a conflict can be determined, false otherwise.
*/
private static boolean conflicts(NodeState state,
Set<Name> addedMixins,
MergeContext ctx, boolean compareToOverlayed) {
try {
// check for all added mixin types in one state if there are colliding
// child items in the other state.
// this is currently a simple check for named item definitions;
// if the mixin defines residual item definitions -> return false.
for (Name mixinName : addedMixins) {
EffectiveNodeType ent = ctx.getEffectiveNodeType(mixinName);
if (ent.getUnnamedItemDefs().length > 0) {
// the mixin defines residual child definitions -> cannot
// easily determine conflicts
return false;
}
NodeState overlayed = (NodeState) state.getOverlayedState();
for (ChildNodeEntry cne : state.getChildNodeEntries()) {
if (ent.getNamedNodeDefs(cne.getName()).length > 0) {
if (ctx.isAdded(cne.getId()) || isAutoCreated(cne, ent)) {
if (!compareToOverlayed || overlayed.hasChildNodeEntry(cne.getName())) {
return true;
}
} // else: neither added nor autocreated in 'state' .
} // else: child node not defined by the added mixin type
}
for (Name propName : state.getPropertyNames()) {
if (ent.getNamedPropDefs(propName).length > 0) {
PropertyId pid = new PropertyId(state.getNodeId(), propName);
if (ctx.isAdded(pid) || isAutoCreated(propName, ent)) {
if (!compareToOverlayed || overlayed.hasPropertyName(propName)) {
return true;
}
} // else: neither added nor autocreated in 'state'
} // else: property not defined by added mixin
}
}
} catch (NoSuchNodeTypeException e) {
// unable to determine collision
return true;
}
// no conflict detected
return false;
}
private static boolean isParent(NodeState state, ChildNodeEntry entry, MergeContext context) {
try {
return state.getId().equals(context.getNodeState(entry.getId()).getParentId());
} catch (ItemStateException e) {
return false;
}
}
private static boolean isAutoCreated(ChildNodeEntry cne, EffectiveNodeType ent) {
for (QNodeDefinition def : ent.getAutoCreateNodeDefs()) {
if (def.getName().equals(cne.getName())) {
return true;
}
}
return false;
}
private static boolean isAutoCreated(Name propertyName, EffectiveNodeType ent) {
for (QPropertyDefinition def : ent.getAutoCreatePropDefs()) {
if (def.getName().equals(propertyName)) {
return true;
}
}
return false;
}
//-----------------------------------------------------< inner interfaces >
/**
* The context of a modification.
*/
static interface MergeContext {
boolean isAdded(ItemId id);
boolean isDeleted(ItemId id);
boolean isModified(ItemId id);
boolean allowsSameNameSiblings(NodeId id);
EffectiveNodeType getEffectiveNodeType(Name ntName) throws NoSuchNodeTypeException;
NodeState getNodeState(NodeId id) throws ItemStateException;
}
}