Index: oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/UpdateOp.java =================================================================== --- oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/UpdateOp.java (revision 1710731) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/UpdateOp.java (working copy) @@ -245,16 +245,26 @@ } /** + * Checks if the property is equal to the given value. + * + * @param property the name of the property or map. + * @param value the value to compare to ({@code null} checks both for non-existence and the value being null) + */ + void equals(@Nonnull String property, @Nullable Object value) { + equals(property, null, value); + } + + /** * Checks if the property or map entry is equal to the given value. * * @param property the name of the property or map. * @param revision the revision within the map or {@code null} if this check * is for a property. - * @param value the value to compare to. + * @param value the value to compare to ({@code null} checks both for non-existence and the value being null) */ void equals(@Nonnull String property, @Nullable Revision revision, - @Nonnull Object value) { + @Nullable Object value) { if (isNew) { throw new IllegalStateException("Cannot perform equals check on new document"); } @@ -263,6 +273,34 @@ } /** + * Checks if the property does not exist or is not equal to the given value. + * + * @param property the name of the property or map. + * @param value the value to compare to. + */ + void notEquals(@Nonnull String property, @Nullable Object value) { + notEquals(property, null, value); + } + + /** + * Checks if the property or map entry does not exist or is not equal to the given value. + * + * @param property the name of the property or map. + * @param revision the revision within the map or {@code null} if this check + * is for a property. + * @param value the value to compare to. + */ + void notEquals(@Nonnull String property, + @Nullable Revision revision, + @Nullable Object value) { + if (isNew) { + throw new IllegalStateException("Cannot perform notEquals check on new document"); + } + getOrCreateConditions().put(new Key(property, revision), + Condition.newNotEqualsCondition(value)); + } + + /** * Increment the value. * * @param property the key @@ -415,8 +453,12 @@ /** * Checks if a map entry equals a given value. */ - EQUALS + EQUALS, + /** + * Checks if a map entry does not equal a given value. + */ + NOTEQUALS } /** @@ -440,10 +482,20 @@ * @param value the value to compare to. * @return the equals condition. */ - public static Condition newEqualsCondition(@Nonnull Object value) { - return new Condition(Type.EQUALS, checkNotNull(value)); + public static Condition newEqualsCondition(@Nullable Object value) { + return new Condition(Type.EQUALS, value); } + /** + * Creates a new notEquals condition with the given value. + * + * @param value the value to compare to. + * @return the notEquals condition. + */ + public static Condition newNotEqualsCondition(@Nullable Object value) { + return new Condition(Type.NOTEQUALS, value); + } + @Override public String toString() { return type + " " + value; Index: oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/UpdateUtils.java =================================================================== --- oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/UpdateUtils.java (revision 1710731) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/UpdateUtils.java (working copy) @@ -134,7 +134,7 @@ return false; } } - } else if (c.type == Condition.Type.EQUALS) { + } else if (c.type == Condition.Type.EQUALS || c.type == Condition.Type.NOTEQUALS) { if (r != null) { if (value instanceof Map) { value = ((Map) value).get(r); @@ -142,8 +142,11 @@ value = null; } } - if (!Objects.equal(value, c.value)) { + boolean equal = Objects.equal(value, c.value); + if (c.type == Condition.Type.EQUALS && !equal) { return false; + } else if (c.type == Condition.Type.NOTEQUALS && equal) { + return false; } } else { throw new IllegalArgumentException("Unknown condition: " + c.type); Index: oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/mongo/MongoDocumentStore.java =================================================================== --- oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/mongo/MongoDocumentStore.java (revision 1710731) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/document/mongo/MongoDocumentStore.java (working copy) @@ -1272,6 +1272,9 @@ case EQUALS: query.and(k.toString()).is(c.value); break; + case NOTEQUALS: + query.and(k.toString()).notEquals(c.value); + break; } } Index: oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/BasicDocumentStoreTest.java =================================================================== --- oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/BasicDocumentStoreTest.java (revision 1710731) +++ oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/BasicDocumentStoreTest.java (working copy) @@ -68,6 +68,119 @@ } @Test + public void testConditionalUpdate() { + String id = this.getClass().getName() + ".testConditionalUpdate"; + + // remove if present + NodeDocument nd = super.ds.find(Collection.NODES, id); + if (nd != null) { + super.ds.remove(Collection.NODES, id); + } + + String existingProp = "_recoverylock"; + String existingRevisionProp = "recoverylock"; + String nonExistingProp = "_qux"; + String nonExistingRevisionProp = "qux"; + Revision r = new Revision(1, 1, 1); + + // add + UpdateOp up = new UpdateOp(id, true); + up.set("_id", id); + up.set(existingProp, "lock"); + up.setMapEntry(existingRevisionProp, r, "lock"); + assertTrue(super.ds.create(Collection.NODES, Collections.singletonList(up))); + + // updates + up = new UpdateOp(id, false); + up.set("_id", id); + up.notEquals(nonExistingProp, "none"); + NodeDocument result = super.ds.findAndUpdate(Collection.NODES, up); + assertNotNull(result); + + up = new UpdateOp(id, false); + up.set("_id", id); + up.equals(nonExistingProp, null); + result = super.ds.findAndUpdate(Collection.NODES, up); + assertNotNull(result); + + up = new UpdateOp(id, false); + up.set("_id", id); + up.notEquals(nonExistingRevisionProp, r, "none"); + result = super.ds.findAndUpdate(Collection.NODES, up); + assertNotNull(result); + + up = new UpdateOp(id, false); + up.set("_id", id); + up.equals(nonExistingRevisionProp, r, null); + result = super.ds.findAndUpdate(Collection.NODES, up); + assertNotNull(result); + + up = new UpdateOp(id, false); + up.set("_id", id); + up.equals(existingProp, "none"); + result = super.ds.findAndUpdate(Collection.NODES, up); + assertNull(result); + + up = new UpdateOp(id, false); + up.set("_id", id); + up.equals(existingProp, null); + result = super.ds.findAndUpdate(Collection.NODES, up); + assertNull(result); + + up = new UpdateOp(id, false); + up.set("_id", id); + up.equals(existingRevisionProp, r, "none"); + result = super.ds.findAndUpdate(Collection.NODES, up); + assertNull(result); + + up = new UpdateOp(id, false); + up.set("_id", id); + up.equals(existingRevisionProp, r, null); + result = super.ds.findAndUpdate(Collection.NODES, up); + assertNull(result); + + up = new UpdateOp(id, false); + up.set("_id", id); + up.notEquals(existingProp, "lock"); + result = super.ds.findAndUpdate(Collection.NODES, up); + assertNull(result); + + up = new UpdateOp(id, false); + up.set("_id", id); + up.equals(existingProp, null); + result = super.ds.findAndUpdate(Collection.NODES, up); + assertNull(result); + + up = new UpdateOp(id, false); + up.set("_id", id); + up.notEquals(existingRevisionProp, r, "lock"); + result = super.ds.findAndUpdate(Collection.NODES, up); + assertNull(result); + + up = new UpdateOp(id, false); + up.set("_id", id); + up.equals(existingRevisionProp, r, null); + result = super.ds.findAndUpdate(Collection.NODES, up); + assertNull(result); + + up = new UpdateOp(id, false); + up.set("_id", id); + up.equals(existingProp, "lock"); + up.set(existingProp, "none"); + result = super.ds.findAndUpdate(Collection.NODES, up); + assertNotNull(result); + + up = new UpdateOp(id, false); + up.set("_id", id); + up.equals(existingRevisionProp, r, "lock"); + up.setMapEntry(existingRevisionProp, r, "none"); + result = super.ds.findAndUpdate(Collection.NODES, up); + assertNotNull(result); + + removeMe.add(id); + } + + @Test public void testMaxIdAscii() { int result = testMaxId(true); assertTrue("needs to support keys of 512 bytes length, but only supports " + result, result >= 512); Index: oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/UpdateOpTest.java =================================================================== --- oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/UpdateOpTest.java (revision 1710731) +++ oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/UpdateOpTest.java (working copy) @@ -16,8 +16,6 @@ */ package org.apache.jackrabbit.oak.plugins.document; -import com.google.common.collect.Lists; - import org.junit.Test; import static com.google.common.collect.Lists.newArrayList; @@ -44,15 +42,15 @@ UpdateOp.Key k3 = new UpdateOp.Key("foo", null); assertTrue(k1.equals(k3)); assertTrue(k3.equals(k1)); - + UpdateOp.Key k4 = new UpdateOp.Key("foo", r1); assertFalse(k4.equals(k3)); assertFalse(k3.equals(k4)); - + UpdateOp.Key k5 = new UpdateOp.Key("foo", r2); assertFalse(k5.equals(k4)); assertFalse(k4.equals(k5)); - + UpdateOp.Key k6 = new UpdateOp.Key("foo", r1); assertTrue(k6.equals(k4)); assertTrue(k4.equals(k6)); @@ -144,6 +142,37 @@ } @Test + public void notEqualsTest() { + Revision r = Revision.newRevision(1); + UpdateOp op = new UpdateOp("id", true); + try { + op.notEquals("p", r, "v"); + fail("expected " + IllegalStateException.class.getName()); + } catch (IllegalStateException e) { + // expected + } + op = new UpdateOp("id", false); + op.notEquals("p", r, "v"); + assertEquals(1, op.getConditions().size()); + UpdateOp.Key key = op.getConditions().keySet().iterator().next(); + assertEquals(r, key.getRevision()); + assertEquals("p", key.getName()); + UpdateOp.Condition c = op.getConditions().get(key); + assertEquals(UpdateOp.Condition.Type.NOTEQUALS, c.type); + assertEquals("v", c.value); + + op = new UpdateOp("id", false); + op.notEquals("p", r, null); + assertEquals(1, op.getConditions().size()); + key = op.getConditions().keySet().iterator().next(); + assertEquals(r, key.getRevision()); + assertEquals("p", key.getName()); + c = op.getConditions().get(key); + assertEquals(UpdateOp.Condition.Type.NOTEQUALS, c.type); + assertEquals(null, c.value); + } + + @Test public void getChanges() { UpdateOp op = new UpdateOp("id", false); assertEquals(0, op.getChanges().size()); Index: oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/UpdateUtilsTest.java =================================================================== --- oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/UpdateUtilsTest.java (revision 1710731) +++ oak-core/src/test/java/org/apache/jackrabbit/oak/plugins/document/UpdateUtilsTest.java (working copy) @@ -111,32 +111,84 @@ assertTrue(UpdateUtils.checkConditions(d, op.getConditions())); op = newUpdateOp(id); + op.notEquals("t", r, "value"); + assertFalse(UpdateUtils.checkConditions(d, op.getConditions())); + + op = newUpdateOp(id); op.equals("t", r, "foo"); assertFalse(UpdateUtils.checkConditions(d, op.getConditions())); op = newUpdateOp(id); + op.notEquals("t", r, "foo"); + assertTrue(UpdateUtils.checkConditions(d, op.getConditions())); + + op = newUpdateOp(id); op.equals("t", Revision.newRevision(1), "value"); assertFalse(UpdateUtils.checkConditions(d, op.getConditions())); op = newUpdateOp(id); - op.equals("t", null, "value"); + op.notEquals("t", Revision.newRevision(1), "value"); + assertTrue(UpdateUtils.checkConditions(d, op.getConditions())); + + op = newUpdateOp(id); + op.equals("t", "value"); assertFalse(UpdateUtils.checkConditions(d, op.getConditions())); op = newUpdateOp(id); + op.notEquals("t", "value"); + assertTrue(UpdateUtils.checkConditions(d, op.getConditions())); + + op = newUpdateOp(id); op.equals("p", r, 42L); assertFalse(UpdateUtils.checkConditions(d, op.getConditions())); op = newUpdateOp(id); - op.equals("p", null, 42L); + op.notEquals("p", r, 42L); assertTrue(UpdateUtils.checkConditions(d, op.getConditions())); op = newUpdateOp(id); - op.equals("p", null, 7L); + op.equals("p", 42L); + assertTrue(UpdateUtils.checkConditions(d, op.getConditions())); + + op = newUpdateOp(id); + op.notEquals("p", 42L); assertFalse(UpdateUtils.checkConditions(d, op.getConditions())); + op = newUpdateOp(id); + op.equals("p", 7L); + assertFalse(UpdateUtils.checkConditions(d, op.getConditions())); + op = newUpdateOp(id); + op.notEquals("p", 7L); + assertTrue(UpdateUtils.checkConditions(d, op.getConditions())); + + // check on non-existing property + op = newUpdateOp(id); + op.notEquals("other", 7L); + assertTrue(UpdateUtils.checkConditions(d, op.getConditions())); + + op = newUpdateOp(id); + op.notEquals("other", r, 7L); + assertTrue(UpdateUtils.checkConditions(d, op.getConditions())); + + op = newUpdateOp(id); + op.notEquals("other", r, null); + assertFalse(UpdateUtils.checkConditions(d, op.getConditions())); + + op = newUpdateOp(id); + op.equals("other", r, null); + assertTrue(UpdateUtils.checkConditions(d, op.getConditions())); + + // check null + op = newUpdateOp(id); + op.notEquals("p", null); + assertTrue(UpdateUtils.checkConditions(d, op.getConditions())); + + op = newUpdateOp(id); + op.notEquals("other", r, null); + assertFalse(UpdateUtils.checkConditions(d, op.getConditions())); } - + private static UpdateOp newUpdateOp(String id) { return new UpdateOp(id, false); }