Index: src/main/java/org/apache/jackrabbit/oak/plugins/commit/JcrConflictHandler.java =================================================================== --- src/main/java/org/apache/jackrabbit/oak/plugins/commit/JcrConflictHandler.java (revision 1641392) +++ src/main/java/org/apache/jackrabbit/oak/plugins/commit/JcrConflictHandler.java (working copy) @@ -29,8 +29,8 @@ * The conflict handler is a composite of {@link ChildOrderConflictHandler} * and {@link AnnotatingConflictHandler}. */ - public static final ConflictHandler JCR_CONFLICT_HANDLER = - new ChildOrderConflictHandler(new AnnotatingConflictHandler()); + public static final ConflictHandler JCR_CONFLICT_HANDLER = new ChildOrderConflictHandler( + new JcrLastModifiedConflictHandler(new AnnotatingConflictHandler())); private JcrConflictHandler() { } Index: src/main/java/org/apache/jackrabbit/oak/plugins/commit/JcrLastModifiedConflictHandler.java =================================================================== --- src/main/java/org/apache/jackrabbit/oak/plugins/commit/JcrLastModifiedConflictHandler.java (revision 0) +++ src/main/java/org/apache/jackrabbit/oak/plugins/commit/JcrLastModifiedConflictHandler.java (revision 0) @@ -0,0 +1,96 @@ +/* + * 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 java.util.Calendar; + +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.spi.commit.ConflictHandler; +import org.apache.jackrabbit.oak.spi.state.NodeBuilder; + +import static org.apache.jackrabbit.util.ISO8601.parse; +import static org.apache.jackrabbit.JcrConstants.JCR_LASTMODIFIED; + +public class JcrLastModifiedConflictHandler extends ConflictHandlerWrapper { + + public JcrLastModifiedConflictHandler(ConflictHandler handler) { + super(handler); + } + + @Override + public Resolution addExistingProperty(NodeBuilder parent, + PropertyState ours, PropertyState theirs) { + if (isNamedProperty(ours)) { + merge(parent, ours, theirs); + return Resolution.MERGED; + } + return handler.addExistingProperty(parent, ours, theirs); + } + + @Override + public Resolution changeDeletedProperty(NodeBuilder parent, + PropertyState ours) { + if (isNamedProperty(ours)) { + return Resolution.THEIRS; + } + return handler.changeDeletedProperty(parent, ours); + } + + @Override + public Resolution changeChangedProperty(NodeBuilder parent, + PropertyState ours, PropertyState theirs) { + if (isNamedProperty(ours)) { + merge(parent, ours, theirs); + return Resolution.MERGED; + } + return handler.changeChangedProperty(parent, ours, theirs); + } + + private static void merge(NodeBuilder parent, PropertyState ours, + PropertyState theirs) { + Calendar o = parse(ours.getValue(Type.DATE)); + Calendar t = parse(theirs.getValue(Type.DATE)); + // pick&set newer one + if (o.before(t)) { + parent.setProperty(JCR_LASTMODIFIED, t); + } + parent.setProperty(JCR_LASTMODIFIED, o); + } + + @Override + public Resolution deleteDeletedProperty(NodeBuilder parent, + PropertyState ours) { + if (isNamedProperty(ours)) { + return Resolution.THEIRS; + } + return handler.deleteDeletedProperty(parent, ours); + } + + @Override + public Resolution deleteChangedProperty(NodeBuilder parent, + PropertyState theirs) { + if (isNamedProperty(theirs)) { + return Resolution.THEIRS; + } + return handler.deleteChangedProperty(parent, theirs); + } + + private static boolean isNamedProperty(PropertyState p) { + return JCR_LASTMODIFIED.equals(p.getName()); + } +} Index: src/test/java/org/apache/jackrabbit/oak/kernel/NodeStoreTest.java =================================================================== --- src/test/java/org/apache/jackrabbit/oak/kernel/NodeStoreTest.java (revision 1641392) +++ src/test/java/org/apache/jackrabbit/oak/kernel/NodeStoreTest.java (working copy) @@ -26,11 +26,12 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.junit.Assume.assumeTrue; -import static org.junit.runners.Parameterized.Parameters; import java.util.ArrayList; import java.util.Arrays; +import java.util.Calendar; import java.util.Collection; +import java.util.Date; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -39,6 +40,7 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; +import org.apache.jackrabbit.JcrConstants; import org.apache.jackrabbit.oak.NodeStoreFixture; import org.apache.jackrabbit.oak.api.CommitFailedException; import org.apache.jackrabbit.oak.api.PropertyState; @@ -57,11 +59,13 @@ import org.apache.jackrabbit.oak.spi.state.NodeBuilder; import org.apache.jackrabbit.oak.spi.state.NodeState; import org.apache.jackrabbit.oak.spi.state.NodeStore; +import org.apache.jackrabbit.oak.util.ToStringUtils; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; @RunWith(value = Parameterized.class) public class NodeStoreTest { @@ -149,6 +153,37 @@ } @Test + public void addExistingNodeJCRLastModified() throws CommitFailedException { + // FIXME OAK-1550 Incorrect handling of addExistingNode conflict in NodeStore + assumeTrue(fixture != NodeStoreFixture.MONGO_MK); + assumeTrue(fixture != NodeStoreFixture.MONGO_NS); + + CommitHook hook = new CompositeHook( + new ConflictHook(JcrConflictHandler.JCR_CONFLICT_HANDLER), + new EditorHook(new ConflictValidatorProvider()) + ); + + NodeBuilder b1 = store.getRoot().builder(); + NodeBuilder b2 = store.getRoot().builder(); + + b1.setChildNode("addExistingNodeJCRLastModified").setProperty(JcrConstants.JCR_LASTMODIFIED, + Calendar.getInstance()); + try { + TimeUnit.MILLISECONDS.sleep(25); + } catch (InterruptedException e) { + // + } + b2.setChildNode("addExistingNodeJCRLastModified").setProperty(JcrConstants.JCR_LASTMODIFIED, + Calendar.getInstance()); + + b1.setChildNode("conflict"); + b2.setChildNode("conflict"); + + store.merge(b1, hook, CommitInfo.EMPTY); + store.merge(b2, hook, CommitInfo.EMPTY); + } + + @Test public void simpleMerge() throws CommitFailedException { NodeBuilder rootBuilder = store.getRoot().builder(); NodeBuilder testBuilder = rootBuilder.child("test");