| /* |
| * 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()); |
| } |
| } |
| |
| } |