blob: 0492e737ec8f26d87b5a9e8ef369dfca4bc03efb [file] [log] [blame]
/*
* 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.oak.plugins.commit;
import static org.apache.jackrabbit.oak.api.Type.NAME;
import static org.apache.jackrabbit.oak.spi.state.ConflictAnnotatingRebaseDiff.BASE;
import static org.apache.jackrabbit.oak.spi.state.ConflictAnnotatingRebaseDiff.CONFLICT;
import static org.apache.jackrabbit.oak.spi.state.ConflictAnnotatingRebaseDiff.OURS;
import static org.apache.jackrabbit.oak.spi.state.ConflictType.ADD_EXISTING_NODE;
import static org.apache.jackrabbit.oak.spi.state.ConflictType.ADD_EXISTING_PROPERTY;
import static org.apache.jackrabbit.oak.spi.state.ConflictType.CHANGE_CHANGED_PROPERTY;
import static org.apache.jackrabbit.oak.spi.state.ConflictType.CHANGE_DELETED_NODE;
import static org.apache.jackrabbit.oak.spi.state.ConflictType.CHANGE_DELETED_PROPERTY;
import static org.apache.jackrabbit.oak.spi.state.ConflictType.DELETE_CHANGED_NODE;
import static org.apache.jackrabbit.oak.spi.state.ConflictType.DELETE_CHANGED_PROPERTY;
import static org.apache.jackrabbit.oak.spi.state.ConflictType.DELETE_DELETED_NODE;
import static org.apache.jackrabbit.oak.spi.state.ConflictType.DELETE_DELETED_PROPERTY;
import java.util.Map;
import java.util.Set;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Sets;
import org.apache.jackrabbit.oak.api.PropertyState;
import org.apache.jackrabbit.oak.json.JsopDiff;
import org.apache.jackrabbit.oak.plugins.memory.PropertyBuilder;
import org.apache.jackrabbit.oak.plugins.tree.TreeConstants;
import org.apache.jackrabbit.oak.spi.commit.ThreeWayConflictHandler;
import org.apache.jackrabbit.oak.spi.commit.ThreeWayConflictHandler.Resolution;
import org.apache.jackrabbit.oak.spi.state.ChildNodeEntry;
import org.apache.jackrabbit.oak.spi.state.ConflictAnnotatingRebaseDiff;
import org.apache.jackrabbit.oak.spi.state.ConflictType;
import org.apache.jackrabbit.oak.spi.state.DefaultNodeStateDiff;
import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
import org.apache.jackrabbit.oak.spi.state.NodeState;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* MergingNodeStateDiff... TODO
*/
public final class MergingNodeStateDiff extends DefaultNodeStateDiff {
private static final Logger LOG = LoggerFactory.getLogger(MergingNodeStateDiff.class);
private final NodeState parent;
private final NodeBuilder target;
private final ThreeWayConflictHandler conflictHandler;
private MergingNodeStateDiff(NodeState parent, NodeBuilder target, ThreeWayConflictHandler conflictHandler) {
this.parent = parent;
this.target = target;
this.conflictHandler = conflictHandler;
}
static NodeState merge(NodeState fromState, NodeState toState, ThreeWayConflictHandler conflictHandler) {
return merge(fromState, toState, toState.builder(), conflictHandler).getNodeState();
}
private static NodeBuilder merge(NodeState fromState, NodeState toState, NodeBuilder target,
ThreeWayConflictHandler conflictHandler) {
toState.compareAgainstBaseState(fromState,
new MergingNodeStateDiff(toState, target, conflictHandler));
return target;
}
//------------------------------------------------------< NodeStateDiff >---
@Override
public boolean childNodeAdded(String name, NodeState after) {
if (CONFLICT.equals(name)) {
for (ChildNodeEntry conflict : after.getChildNodeEntries()) {
resolveConflict(ConflictType.fromName(conflict.getName()), conflict.getNodeState());
}
target.getChildNode(CONFLICT).remove();
}
return true;
}
@Override
public boolean childNodeChanged(String name, NodeState before, NodeState after) {
if (target.hasChildNode(name)) {
merge(before, after, target.getChildNode(name), conflictHandler);
}
return true;
}
//------------------------------------------------------------< private >---
private void resolveConflict(ConflictType conflictType, NodeState conflictInfo) {
PropertyConflictHandler propertyConflictHandler = propertyConflictHandlers.get(conflictType);
if (propertyConflictHandler != null) {
NodeState oursNS = conflictInfo.getChildNode(OURS);
NodeState baseNS = conflictInfo.getChildNode(BASE);
Set<String> processed = Sets.newHashSet();
for (PropertyState ours : oursNS.getProperties()) {
String name = ours.getName();
processed.add(name);
PropertyState base = baseNS.getProperty(name);
PropertyState theirs = parent.getProperty(name);
Resolution resolution = propertyConflictHandler.resolve(ours, theirs, base);
applyPropertyResolution(resolution, conflictType, name, ours);
}
for (PropertyState base : baseNS.getProperties()) {
String name = base.getName();
if (processed.contains(name)) {
continue;
}
PropertyState theirs = parent.getProperty(name);
Resolution resolution = propertyConflictHandler.resolve(null, theirs, base);
applyPropertyResolution(resolution, conflictType, name, null);
}
} else {
NodeConflictHandler nodeConflictHandler = nodeConflictHandlers.get(conflictType);
if (nodeConflictHandler != null) {
NodeState oursNS = conflictInfo.getChildNode(OURS);
NodeState baseNS = conflictInfo.getChildNode(BASE);
Set<String> candidates = Sets.union(Sets.newHashSet(oursNS.getChildNodeNames()),
Sets.newHashSet(baseNS.getChildNodeNames()));
for (String name : candidates) {
NodeState ours = oursNS.getChildNode(name);
NodeState base = baseNS.getChildNode(name);
NodeState theirs = parent.getChildNode(name);
Resolution resolution = nodeConflictHandler.resolve(name, ours, theirs, base);
applyResolution(resolution, conflictType, name, ours);
if (LOG.isDebugEnabled()) {
String diff = JsopDiff.diffToJsop(base, theirs);
LOG.debug(
"{} resolved conflict of type {} with resolution {} on node {}, conflict trace {}",
nodeConflictHandler, conflictType, resolution,
name, diff);
}
}
}
else {
LOG.warn("Ignoring unknown conflict '" + conflictType + '\'');
}
}
NodeBuilder conflictMarker = getConflictMarker(conflictType);
if (conflictMarker != null) {
assert conflictMarker.getChildNode(ConflictAnnotatingRebaseDiff.BASE).getChildNodeCount(1) == 0;
assert conflictMarker.getChildNode(ConflictAnnotatingRebaseDiff.OURS).getChildNodeCount(1) == 0;
}
}
private void applyPropertyResolution(Resolution resolution, ConflictType conflictType, String name, PropertyState ours) {
NodeBuilder conflictMarker = getConflictMarker(conflictType);
if (resolution == Resolution.OURS) {
if (DELETE_CHANGED_PROPERTY == conflictType || DELETE_DELETED_PROPERTY == conflictType) {
target.removeProperty(name);
} else {
target.setProperty(ours);
}
}
NodeBuilder baseClean = conflictMarker.getChildNode(ConflictAnnotatingRebaseDiff.BASE);
if (baseClean.exists()) {
baseClean.removeProperty(name);
}
NodeBuilder oursClean = conflictMarker.getChildNode(ConflictAnnotatingRebaseDiff.OURS);
if (oursClean.exists()) {
oursClean.removeProperty(name);
}
}
private void applyResolution(Resolution resolution, ConflictType conflictType, String name, NodeState ours) {
NodeBuilder conflictMarker = getConflictMarker(conflictType);
if (resolution == Resolution.OURS) {
if (DELETE_CHANGED_NODE == conflictType || DELETE_DELETED_NODE == conflictType) {
removeChild(target, name);
} else {
addChild(target, name, ours);
}
}
NodeBuilder baseClean = conflictMarker.getChildNode(ConflictAnnotatingRebaseDiff.BASE);
if (baseClean.exists()) {
baseClean.getChildNode(name).remove();
}
NodeBuilder oursClean = conflictMarker.getChildNode(ConflictAnnotatingRebaseDiff.OURS);
if (oursClean.exists()) {
oursClean.getChildNode(name).remove();
}
}
private NodeBuilder getConflictMarker(ConflictType conflictType) {
final String conflictName = conflictType.getName();
if (target.hasChildNode(CONFLICT)) {
NodeBuilder conflict = target.child(CONFLICT);
if (conflict.hasChildNode(conflictName)) {
return conflict.child(conflictName);
}
}
return null;
}
private interface PropertyConflictHandler {
Resolution resolve(PropertyState ours, PropertyState theirs, PropertyState base);
}
private interface NodeConflictHandler {
Resolution resolve(String name, NodeState ours, NodeState theirs, NodeState base);
}
private final Map<ConflictType, PropertyConflictHandler> propertyConflictHandlers = ImmutableMap.of(
ADD_EXISTING_PROPERTY, new PropertyConflictHandler() {
@Override
public Resolution resolve(PropertyState ours, PropertyState theirs, PropertyState base) {
return conflictHandler.addExistingProperty(target, ours, theirs);
}
@Override
public String toString() {
return "PropertyConflictHandler<ADD_EXISTING_PROPERTY>";
}
},
CHANGE_DELETED_PROPERTY, new PropertyConflictHandler() {
@Override
public Resolution resolve(PropertyState ours, PropertyState theirs, PropertyState base) {
return conflictHandler.changeDeletedProperty(target, ours, base);
}
@Override
public String toString() {
return "PropertyConflictHandler<CHANGE_DELETED_PROPERTY>";
}
},
CHANGE_CHANGED_PROPERTY, new PropertyConflictHandler() {
@Override
public Resolution resolve(PropertyState ours, PropertyState theirs, PropertyState base) {
return conflictHandler.changeChangedProperty(target, ours, theirs, base);
}
@Override
public String toString() {
return "PropertyConflictHandler<CHANGE_CHANGED_PROPERTY>";
}
},
DELETE_DELETED_PROPERTY, new PropertyConflictHandler() {
@Override
public Resolution resolve(PropertyState ours, PropertyState theirs, PropertyState base) {
return conflictHandler.deleteDeletedProperty(target, base);
}
@Override
public String toString() {
return "PropertyConflictHandler<DELETE_DELETED_PROPERTY>";
}
},
DELETE_CHANGED_PROPERTY, new PropertyConflictHandler() {
@Override
public Resolution resolve(PropertyState ours, PropertyState theirs, PropertyState base) {
return conflictHandler.deleteChangedProperty(target, theirs, base);
}
@Override
public String toString() {
return "PropertyConflictHandler<DELETE_CHANGED_PROPERTY>";
}
}
);
private final Map<ConflictType, NodeConflictHandler> nodeConflictHandlers = ImmutableMap.of(
ADD_EXISTING_NODE, new NodeConflictHandler() {
@Override
public Resolution resolve(String name, NodeState ours, NodeState theirs, NodeState base) {
return conflictHandler.addExistingNode(target, name, ours, theirs);
}
@Override
public String toString() {
return "NodeConflictHandler<ADD_EXISTING_NODE>";
}
},
CHANGE_DELETED_NODE, new NodeConflictHandler() {
@Override
public Resolution resolve(String name, NodeState ours, NodeState theirs, NodeState base) {
return conflictHandler.changeDeletedNode(target, name, ours, base);
}
@Override
public String toString() {
return "NodeConflictHandler<CHANGE_DELETED_NODE>";
}
},
DELETE_CHANGED_NODE, new NodeConflictHandler() {
@Override
public Resolution resolve(String name, NodeState ours, NodeState theirs, NodeState base) {
return conflictHandler.deleteChangedNode(target, name, theirs, base);
}
@Override
public String toString() {
return "NodeConflictHandler<DELETE_CHANGED_NODE>";
}
},
DELETE_DELETED_NODE, new NodeConflictHandler() {
@Override
public Resolution resolve(String name, NodeState ours, NodeState theirs, NodeState base) {
return conflictHandler.deleteDeletedNode(target, name, base);
}
@Override
public String toString() {
return "NodeConflictHandler<DELETE_DELETED_NODE>";
}
}
);
private static void addChild(NodeBuilder target, String name, NodeState state) {
target.setChildNode(name, state);
PropertyState childOrder = target.getProperty(TreeConstants.OAK_CHILD_ORDER);
if (childOrder != null) {
PropertyBuilder<String> builder = PropertyBuilder.copy(NAME, childOrder);
builder.addValue(name);
target.setProperty(builder.getPropertyState());
}
}
private static void removeChild(NodeBuilder target, String name) {
target.getChildNode(name).remove();
PropertyState childOrder = target.getProperty(TreeConstants.OAK_CHILD_ORDER);
if (childOrder != null) {
PropertyBuilder<String> builder = PropertyBuilder.copy(NAME, childOrder);
builder.removeValue(name);
target.setProperty(builder.getPropertyState());
}
}
}