You are viewing a plain text version of this content. The canonical link for it is here.
Posted to oak-commits@jackrabbit.apache.org by ch...@apache.org on 2017/10/03 05:06:58 UTC

svn commit: r1810633 - in /jackrabbit/oak/trunk/oak-lucene/src: main/java/org/apache/jackrabbit/oak/plugins/index/lucene/ main/java/org/apache/jackrabbit/oak/plugins/index/lucene/property/ test/java/org/apache/jackrabbit/oak/plugins/index/lucene/ test/...

Author: chetanm
Date: Tue Oct  3 05:06:57 2017
New Revision: 1810633

URL: http://svn.apache.org/viewvc?rev=1810633&view=rev
Log:
OAK-6535 - Synchronous Lucene Property Indexes

Add support for enforcing uniqueness constraint check as part of commit

Also wires up the PropertyIndexUpdateCallback with LuceneIndexEditorProvider
which completes the update flow for sync property indexes

Added:
    jackrabbit/oak/trunk/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/property/LuceneIndexPropertyQuery.java   (with props)
    jackrabbit/oak/trunk/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/property/PropertyIndexQuery.java   (with props)
    jackrabbit/oak/trunk/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/property/PropertyQuery.java   (with props)
    jackrabbit/oak/trunk/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/property/UniquenessConstraintValidator.java   (with props)
    jackrabbit/oak/trunk/oak-lucene/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/property/LuceneIndexPropertyQueryTest.java   (with props)
    jackrabbit/oak/trunk/oak-lucene/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/property/SynchronousPropertyIndexTest.java   (with props)
    jackrabbit/oak/trunk/oak-lucene/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/property/UniquenessConstraintValidatorTest.java   (with props)
Modified:
    jackrabbit/oak/trunk/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexEditor.java
    jackrabbit/oak/trunk/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexEditorProvider.java
    jackrabbit/oak/trunk/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/PropertyUpdateCallback.java
    jackrabbit/oak/trunk/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/property/PropertyIndexUpdateCallback.java
    jackrabbit/oak/trunk/oak-lucene/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexEditor2Test.java
    jackrabbit/oak/trunk/oak-lucene/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/property/HybridPropertyIndexStorageTest.java

Modified: jackrabbit/oak/trunk/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexEditor.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexEditor.java?rev=1810633&r1=1810632&r2=1810633&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexEditor.java (original)
+++ jackrabbit/oak/trunk/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexEditor.java Tue Oct  3 05:06:57 2017
@@ -150,6 +150,11 @@ public class LuceneIndexEditor implement
         }
 
         if (parent == null) {
+            PropertyUpdateCallback callback = context.getPropertyUpdateCallback();
+            if (callback != null) {
+                callback.done();
+            }
+
             try {
                 context.closeWriter();
             } catch (IOException e) {

Modified: jackrabbit/oak/trunk/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexEditorProvider.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexEditorProvider.java?rev=1810633&r1=1810632&r2=1810633&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexEditorProvider.java (original)
+++ jackrabbit/oak/trunk/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexEditorProvider.java Tue Oct  3 05:06:57 2017
@@ -33,6 +33,9 @@ import org.apache.jackrabbit.oak.plugins
 import org.apache.jackrabbit.oak.plugins.index.lucene.hybrid.IndexingQueue;
 import org.apache.jackrabbit.oak.plugins.index.lucene.hybrid.LocalIndexWriterFactory;
 import org.apache.jackrabbit.oak.plugins.index.lucene.hybrid.LuceneDocumentHolder;
+import org.apache.jackrabbit.oak.plugins.index.lucene.property.LuceneIndexPropertyQuery;
+import org.apache.jackrabbit.oak.plugins.index.lucene.property.PropertyIndexUpdateCallback;
+import org.apache.jackrabbit.oak.plugins.index.lucene.property.PropertyQuery;
 import org.apache.jackrabbit.oak.plugins.index.lucene.writer.DefaultIndexWriterFactory;
 import org.apache.jackrabbit.oak.plugins.index.lucene.writer.LuceneIndexWriterConfig;
 import org.apache.jackrabbit.oak.plugins.index.lucene.writer.LuceneIndexWriterFactory;
@@ -135,6 +138,9 @@ public class LuceneIndexEditorProvider i
             LuceneIndexWriterFactory writerFactory = null;
             IndexDefinition indexDefinition = null;
             boolean asyncIndexing = true;
+            String indexPath = indexingContext.getIndexPath();
+            PropertyIndexUpdateCallback propertyUpdateCallback = null;
+
             if (nrtIndexingEnabled() && !indexingContext.isAsync() && IndexDefinition.supportsSyncOrNRTIndexing(definition)) {
 
                 //Would not participate in reindexing. Only interested in
@@ -155,14 +161,15 @@ public class LuceneIndexEditorProvider i
 
                 //TODO Also check if index has been done once
 
+
                 writerFactory = new LocalIndexWriterFactory(getDocumentHolder(commitContext),
-                        indexingContext.getIndexPath());
+                        indexPath);
 
                 //IndexDefinition from tracker might differ from one passed here for reindexing
                 //case which should be fine. However reusing existing definition would avoid
                 //creating definition instance for each commit as this gets executed for each commit
                 if (indexTracker != null){
-                    indexDefinition = indexTracker.getIndexDefinition(indexingContext.getIndexPath());
+                    indexDefinition = indexTracker.getIndexDefinition(indexPath);
                     if (indexDefinition != null && !indexDefinition.hasMatchingNodeTypeReg(root)){
                         log.debug("Detected change in NodeType registry for index {}. Would not use " +
                                 "existing index definition", indexDefinition.getIndexPath());
@@ -170,6 +177,19 @@ public class LuceneIndexEditorProvider i
                     }
                 }
 
+                if (indexDefinition == null) {
+                    indexDefinition = IndexDefinition.newBuilder(root, definition.getNodeState(),
+                            indexPath).build();
+                }
+
+                if (indexDefinition.hasSyncPropertyDefinitions()) {
+                    propertyUpdateCallback = new PropertyIndexUpdateCallback(indexPath, definition);
+                    if (indexTracker != null) {
+                        PropertyQuery query = new LuceneIndexPropertyQuery(indexTracker, indexPath);
+                        propertyUpdateCallback.getUniquenessConstraintValidator().setSecondStore(query);
+                    }
+                }
+
                 //Pass on a read only builder to ensure that nothing gets written
                 //at all to NodeStore for local indexing.
                 //TODO [hybrid] This would cause issue with Facets as for faceted fields
@@ -186,6 +206,8 @@ public class LuceneIndexEditorProvider i
 
             LuceneIndexEditorContext context = new LuceneIndexEditorContext(root, definition, indexDefinition, callback,
                     writerFactory, extractedTextCache, augmentorFactory, indexingContext, asyncIndexing);
+
+            context.setPropertyUpdateCallback(propertyUpdateCallback);
             return new LuceneIndexEditor(context);
         }
         return null;

Modified: jackrabbit/oak/trunk/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/PropertyUpdateCallback.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/PropertyUpdateCallback.java?rev=1810633&r1=1810632&r2=1810633&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/PropertyUpdateCallback.java (original)
+++ jackrabbit/oak/trunk/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/PropertyUpdateCallback.java Tue Oct  3 05:06:57 2017
@@ -21,6 +21,7 @@ package org.apache.jackrabbit.oak.plugin
 
 import javax.annotation.Nullable;
 
+import org.apache.jackrabbit.oak.api.CommitFailedException;
 import org.apache.jackrabbit.oak.api.PropertyState;
 
 /**
@@ -42,4 +43,11 @@ public interface PropertyUpdateCallback
     void propertyUpdated(String nodePath, String propertyRelativePath, PropertyDefinition pd,
                          @Nullable  PropertyState before, @Nullable PropertyState after);
 
+    /**
+     * Invoked after editor has traversed all the changes
+     *
+     * @throws CommitFailedException in case some validation fails
+     */
+    void done() throws CommitFailedException;
+
 }

Added: jackrabbit/oak/trunk/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/property/LuceneIndexPropertyQuery.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/property/LuceneIndexPropertyQuery.java?rev=1810633&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/property/LuceneIndexPropertyQuery.java (added)
+++ jackrabbit/oak/trunk/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/property/LuceneIndexPropertyQuery.java Tue Oct  3 05:06:57 2017
@@ -0,0 +1,76 @@
+/*
+ * 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.index.lucene.property;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.jackrabbit.oak.plugins.index.lucene.IndexNode;
+import org.apache.jackrabbit.oak.plugins.index.lucene.IndexTracker;
+import org.apache.jackrabbit.oak.plugins.index.lucene.util.PathStoredFieldVisitor;
+import org.apache.lucene.index.IndexReader;
+import org.apache.lucene.index.Term;
+import org.apache.lucene.search.ScoreDoc;
+import org.apache.lucene.search.TermQuery;
+import org.apache.lucene.search.TopDocs;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Performs simple property=value query against a Lucene index
+ */
+public class LuceneIndexPropertyQuery implements PropertyQuery {
+    private static final Logger log = LoggerFactory.getLogger(LuceneIndexPropertyQuery.class);
+    private final IndexTracker tracker;
+    private final String indexPath;
+
+    public LuceneIndexPropertyQuery(IndexTracker tracker, String indexPath) {
+        this.tracker = tracker;
+        this.indexPath = indexPath;
+    }
+
+    @Override
+    public Iterable<String> getIndexedPaths(String propertyRelativePath, String value) {
+        List<String> indexPaths = new ArrayList<>(2);
+        IndexNode indexNode = tracker.acquireIndexNode(indexPath);
+        if (indexNode != null) {
+            try {
+                TermQuery query = new TermQuery(new Term(propertyRelativePath, value));
+                //By design such query should not result in more than 1 result.
+                //So just use 10 as batch size
+                TopDocs docs = indexNode.getSearcher().search(query, 10);
+
+                IndexReader reader = indexNode.getSearcher().getIndexReader();
+                for (ScoreDoc d : docs.scoreDocs) {
+                    PathStoredFieldVisitor visitor = new PathStoredFieldVisitor();
+                    reader.document(d.doc, visitor);
+                    indexPaths.add(visitor.getPath());
+                }
+            } catch (IOException e) {
+                log.warn("Error occurred while checking index {} for unique value " +
+                        "[{}] for [{}]", indexPath,value, propertyRelativePath, e);
+            } finally {
+                indexNode.release();
+            }
+        }
+        return indexPaths;
+    }
+}

Propchange: jackrabbit/oak/trunk/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/property/LuceneIndexPropertyQuery.java
------------------------------------------------------------------------------
    svn:eol-style = native

Added: jackrabbit/oak/trunk/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/property/PropertyIndexQuery.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/property/PropertyIndexQuery.java?rev=1810633&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/property/PropertyIndexQuery.java (added)
+++ jackrabbit/oak/trunk/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/property/PropertyIndexQuery.java Tue Oct  3 05:06:57 2017
@@ -0,0 +1,49 @@
+/*
+ * 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.index.lucene.property;
+
+import org.apache.jackrabbit.oak.api.Type;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+
+import static org.apache.jackrabbit.oak.plugins.index.lucene.property.HybridPropertyIndexUtil.PROPERTY_INDEX;
+
+/**
+ * Performs simple property=value query against a unique property index storage
+ */
+class PropertyIndexQuery implements PropertyQuery {
+    private final NodeBuilder builder;
+
+    public PropertyIndexQuery(NodeBuilder builder) {
+        this.builder = builder;
+    }
+
+    @Override
+    public Iterable<String> getIndexedPaths(String propertyRelativePath, String value) {
+        NodeBuilder idxb = getIndexNode(propertyRelativePath);
+        NodeBuilder entry = idxb.child(value);
+        return entry.getProperty("entry").getValue(Type.STRINGS);
+    }
+
+    private NodeBuilder getIndexNode(String propertyRelativePath) {
+        NodeBuilder propertyIndex = builder.child(PROPERTY_INDEX);
+        String nodeName = HybridPropertyIndexUtil.getNodeName(propertyRelativePath);
+        return propertyIndex.child(nodeName);
+    }
+}

Propchange: jackrabbit/oak/trunk/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/property/PropertyIndexQuery.java
------------------------------------------------------------------------------
    svn:eol-style = native

Modified: jackrabbit/oak/trunk/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/property/PropertyIndexUpdateCallback.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/property/PropertyIndexUpdateCallback.java?rev=1810633&r1=1810632&r2=1810633&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/property/PropertyIndexUpdateCallback.java (original)
+++ jackrabbit/oak/trunk/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/property/PropertyIndexUpdateCallback.java Tue Oct  3 05:06:57 2017
@@ -25,6 +25,7 @@ import java.util.Set;
 import javax.annotation.Nullable;
 import javax.jcr.PropertyType;
 
+import org.apache.jackrabbit.oak.api.CommitFailedException;
 import org.apache.jackrabbit.oak.api.PropertyState;
 import org.apache.jackrabbit.oak.plugins.index.lucene.PropertyDefinition;
 import org.apache.jackrabbit.oak.plugins.index.lucene.PropertyUpdateCallback;
@@ -47,10 +48,12 @@ public class PropertyIndexUpdateCallback
 
     private final NodeBuilder builder;
     private final String indexPath;
+    private final UniquenessConstraintValidator uniquenessConstraintValidator;
 
     public PropertyIndexUpdateCallback(String indexPath, NodeBuilder builder) {
         this.builder = builder;
         this.indexPath = indexPath;
+        this.uniquenessConstraintValidator = new UniquenessConstraintValidator(indexPath, builder);
     }
 
     @Override
@@ -80,7 +83,7 @@ public class PropertyIndexUpdateCallback
                         null,
                         beforeKeys,
                         afterKeys);
-                //TODO Query to check if unique
+                uniquenessConstraintValidator.add(propertyRelativePath, afterKeys);
             } else {
                 ContentMirrorStoreStrategy s = new ContentMirrorStoreStrategy();
                 s.update(ofInstance(indexNode),
@@ -91,7 +94,15 @@ public class PropertyIndexUpdateCallback
                         afterKeys);
             }
         }
+    }
+
+    @Override
+    public void done() throws CommitFailedException {
+        uniquenessConstraintValidator.validate();
+    }
 
+    public UniquenessConstraintValidator getUniquenessConstraintValidator() {
+        return uniquenessConstraintValidator;
     }
 
     private NodeBuilder getIndexNode(String propertyRelativePath, boolean unique) {

Added: jackrabbit/oak/trunk/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/property/PropertyQuery.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/property/PropertyQuery.java?rev=1810633&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/property/PropertyQuery.java (added)
+++ jackrabbit/oak/trunk/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/property/PropertyQuery.java Tue Oct  3 05:06:57 2017
@@ -0,0 +1,32 @@
+/*
+ * 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.index.lucene.property;
+
+import java.util.Collections;
+
+/**
+ * Abstraction to enable performing simple property=value query
+ * across various types of storage
+ */
+public interface PropertyQuery {
+    PropertyQuery DEFAULT = (propertyRelativePath, value) -> Collections.emptyList();
+
+    Iterable<String> getIndexedPaths(String propertyRelativePath, String value);
+}

Propchange: jackrabbit/oak/trunk/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/property/PropertyQuery.java
------------------------------------------------------------------------------
    svn:eol-style = native

Added: jackrabbit/oak/trunk/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/property/UniquenessConstraintValidator.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/property/UniquenessConstraintValidator.java?rev=1810633&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/property/UniquenessConstraintValidator.java (added)
+++ jackrabbit/oak/trunk/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/property/UniquenessConstraintValidator.java Tue Oct  3 05:06:57 2017
@@ -0,0 +1,81 @@
+/*
+ * 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.index.lucene.property;
+
+import java.util.Map;
+import java.util.Set;
+
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Multimap;
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.apache.jackrabbit.oak.api.CommitFailedException.CONSTRAINT;
+
+/**
+ * Performs validation related to unique index by ensuring that for
+ * given property value only one indexed entry is present. The query
+ * is performed against multiple stores
+ *
+ *   - Property storage - Stores the recently added unique keys via UniqueStore strategy
+ *   - Lucene storage - Stores the long term index in lucene
+ */
+public class UniquenessConstraintValidator {
+    private final String indexPath;
+    private final Multimap<String, String> uniqueKeys = HashMultimap.create();
+    private final PropertyQuery firstStore;
+    private PropertyQuery secondStore = PropertyQuery.DEFAULT;
+
+    public UniquenessConstraintValidator(String indexPath, NodeBuilder builder) {
+        this.indexPath = indexPath;
+        this.firstStore = new PropertyIndexQuery(builder);
+    }
+
+    public void add(String propertyRelativePath, Set<String> afterKeys) {
+        uniqueKeys.putAll(propertyRelativePath, afterKeys);
+    }
+
+    public void validate() throws CommitFailedException {
+        for (Map.Entry<String, String> e : uniqueKeys.entries()) {
+            String propertyRelativePath = e.getKey();
+            Iterable<String> indexedPaths = getIndexedPaths(propertyRelativePath, e.getValue());
+            Set<String> allPaths = ImmutableSet.copyOf(indexedPaths);
+            if (allPaths.size() > 1) {
+                String msg = String.format("Uniqueness constraint violated for property [%s] for " +
+                        "index [%s]. IndexedPaths %s", propertyRelativePath, indexPath, allPaths);
+                throw new CommitFailedException(CONSTRAINT, 30, msg);
+            }
+        }
+    }
+
+    public void setSecondStore(PropertyQuery secondStore) {
+        this.secondStore = checkNotNull(secondStore);
+    }
+
+    private Iterable<String> getIndexedPaths(String propertyRelativePath, String value) {
+        return Iterables.concat(
+                firstStore.getIndexedPaths(propertyRelativePath, value),
+                secondStore.getIndexedPaths(propertyRelativePath, value)
+        );
+    }
+}

Propchange: jackrabbit/oak/trunk/oak-lucene/src/main/java/org/apache/jackrabbit/oak/plugins/index/lucene/property/UniquenessConstraintValidator.java
------------------------------------------------------------------------------
    svn:eol-style = native

Modified: jackrabbit/oak/trunk/oak-lucene/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexEditor2Test.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-lucene/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexEditor2Test.java?rev=1810633&r1=1810632&r2=1810633&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-lucene/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexEditor2Test.java (original)
+++ jackrabbit/oak/trunk/oak-lucene/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/LuceneIndexEditor2Test.java Tue Oct  3 05:06:57 2017
@@ -105,10 +105,12 @@ public class LuceneIndexEditor2Test {
         NodeBuilder builder = before.builder();
         builder.child("a").setProperty("foo", "bar");
         builder.child("a").setProperty("foo2", "bar");
+        builder.child("a").child("b");
 
         before = hook.processCommit(root, builder.getNodeState(), CommitInfo.EMPTY);
         propCallback.state.assertState("/a", "foo", UpdateState.ADDED);
         assertEquals(1, propCallback.invocationCount);
+        assertEquals(1, propCallback.doneInvocationCount);
         propCallback.reset();
 
         //Property updated
@@ -215,6 +217,7 @@ public class LuceneIndexEditor2Test {
     private static class TestPropertyUpdateCallback implements PropertyUpdateCallback {
         int invocationCount;
         CallbackState state;
+        int doneInvocationCount;
 
         @Override
         public void propertyUpdated(String nodePath, String propertyRelativePath, PropertyDefinition pd,
@@ -231,9 +234,15 @@ public class LuceneIndexEditor2Test {
             invocationCount++;
         }
 
+        @Override
+        public void done() throws CommitFailedException {
+            doneInvocationCount++;
+        }
+
         void reset(){
             state = null;
             invocationCount = 0;
+            doneInvocationCount = 0;
         }
     }
 

Modified: jackrabbit/oak/trunk/oak-lucene/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/property/HybridPropertyIndexStorageTest.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-lucene/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/property/HybridPropertyIndexStorageTest.java?rev=1810633&r1=1810632&r2=1810633&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-lucene/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/property/HybridPropertyIndexStorageTest.java (original)
+++ jackrabbit/oak/trunk/oak-lucene/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/property/HybridPropertyIndexStorageTest.java Tue Oct  3 05:06:57 2017
@@ -27,6 +27,7 @@ import org.apache.jackrabbit.oak.api.Pro
 import org.apache.jackrabbit.oak.plugins.index.Cursors;
 import org.apache.jackrabbit.oak.plugins.index.lucene.IndexDefinition;
 import org.apache.jackrabbit.oak.plugins.index.lucene.PropertyDefinition;
+import org.apache.jackrabbit.oak.plugins.index.lucene.PropertyUpdateCallback;
 import org.apache.jackrabbit.oak.plugins.index.lucene.util.IndexDefinitionBuilder;
 import org.apache.jackrabbit.oak.query.NodeStateNodeTypeInfoProvider;
 import org.apache.jackrabbit.oak.query.QueryEngineSettings;
@@ -132,11 +133,15 @@ public class HybridPropertyIndexStorageT
     public void uniqueProperty() throws Exception{
         defnb.indexRule("nt:base").property("foo").unique();
 
-        newCallback().propertyUpdated("/a", "foo", pd("foo"),
+        PropertyUpdateCallback callback = newCallback();
+
+        callback.propertyUpdated("/a", "foo", pd("foo"),
                 null, createProperty("foo", "bar"));
-        newCallback().propertyUpdated("/b", "foo", pd("foo"),
+        callback.propertyUpdated("/b", "foo", pd("foo"),
                 null, createProperty("foo", "bar2"));
 
+        callback.done();
+
         assertThat(query("foo", newString("bar")), containsInAnyOrder("/a"));
     }
 
@@ -153,7 +158,7 @@ public class HybridPropertyIndexStorageT
         newCallback().propertyUpdated("/a", "foo", pd("foo"),
                 createProperty("foo", "bar"), null);
 
-        // /b should not come as pruning is disabled
+        // /b should not come as pruning is enabled
         assertThat(query("foo", newString("bar")), empty());
     }
 

Added: jackrabbit/oak/trunk/oak-lucene/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/property/LuceneIndexPropertyQueryTest.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-lucene/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/property/LuceneIndexPropertyQueryTest.java?rev=1810633&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-lucene/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/property/LuceneIndexPropertyQueryTest.java (added)
+++ jackrabbit/oak/trunk/oak-lucene/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/property/LuceneIndexPropertyQueryTest.java Tue Oct  3 05:06:57 2017
@@ -0,0 +1,67 @@
+/*
+ * 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.index.lucene.property;
+
+import com.google.common.collect.Iterables;
+import org.apache.jackrabbit.oak.plugins.index.IndexUpdateProvider;
+import org.apache.jackrabbit.oak.plugins.index.lucene.IndexTracker;
+import org.apache.jackrabbit.oak.plugins.index.lucene.LuceneIndexEditorProvider;
+import org.apache.jackrabbit.oak.plugins.index.lucene.util.IndexDefinitionBuilder;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EditorHook;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.apache.jackrabbit.oak.spi.state.NodeState;
+import org.junit.Test;
+
+import static org.apache.jackrabbit.oak.InitialContent.INITIAL_CONTENT;
+import static org.apache.jackrabbit.oak.plugins.index.lucene.TestUtil.child;
+import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.junit.Assert.*;
+
+public class LuceneIndexPropertyQueryTest {
+    private NodeState root = INITIAL_CONTENT;
+    private NodeBuilder builder = root.builder();
+    private IndexTracker tracker = new IndexTracker();
+
+    private String indexPath  = "/oak:index/foo";
+    private IndexDefinitionBuilder defnb = new IndexDefinitionBuilder(child(builder, indexPath));
+
+    private LuceneIndexPropertyQuery query = new LuceneIndexPropertyQuery(tracker, indexPath);
+
+    @Test
+    public void simplePropertyIndex() throws Exception{
+        defnb.noAsync();
+        defnb.indexRule("nt:base").property("foo").propertyIndex();
+
+        assertEquals(0,Iterables.size(query.getIndexedPaths("foo", "bar")));
+
+        NodeState before = builder.getNodeState();
+        builder.child("a").setProperty("foo", "bar");
+        builder.child("b").setProperty("foo", "bar");
+        NodeState after = builder.getNodeState();
+
+        EditorHook hook = new EditorHook(new IndexUpdateProvider(new LuceneIndexEditorProvider()));
+        NodeState indexed = hook.processCommit(before, after, CommitInfo.EMPTY);
+        tracker.update(indexed);
+
+        assertThat(query.getIndexedPaths("foo", "bar"),
+                containsInAnyOrder("/a", "/b"));
+    }
+}
\ No newline at end of file

Propchange: jackrabbit/oak/trunk/oak-lucene/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/property/LuceneIndexPropertyQueryTest.java
------------------------------------------------------------------------------
    svn:eol-style = native

Added: jackrabbit/oak/trunk/oak-lucene/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/property/SynchronousPropertyIndexTest.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-lucene/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/property/SynchronousPropertyIndexTest.java?rev=1810633&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-lucene/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/property/SynchronousPropertyIndexTest.java (added)
+++ jackrabbit/oak/trunk/oak-lucene/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/property/SynchronousPropertyIndexTest.java Tue Oct  3 05:06:57 2017
@@ -0,0 +1,199 @@
+/*
+ * 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.index.lucene.property;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.jackrabbit.oak.InitialContent;
+import org.apache.jackrabbit.oak.Oak;
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.api.ContentRepository;
+import org.apache.jackrabbit.oak.api.Tree;
+import org.apache.jackrabbit.oak.commons.PathUtils;
+import org.apache.jackrabbit.oak.commons.concurrent.ExecutorCloser;
+import org.apache.jackrabbit.oak.plugins.index.AsyncIndexUpdate;
+import org.apache.jackrabbit.oak.plugins.index.counter.NodeCounterEditorProvider;
+import org.apache.jackrabbit.oak.plugins.index.lucene.IndexCopier;
+import org.apache.jackrabbit.oak.plugins.index.lucene.IndexTracker;
+import org.apache.jackrabbit.oak.plugins.index.lucene.LuceneIndexEditorProvider;
+import org.apache.jackrabbit.oak.plugins.index.lucene.LuceneIndexProvider;
+import org.apache.jackrabbit.oak.plugins.index.lucene.TestUtil;
+import org.apache.jackrabbit.oak.plugins.index.lucene.hybrid.IndexingQueue;
+import org.apache.jackrabbit.oak.plugins.index.lucene.util.IndexDefinitionBuilder;
+import org.apache.jackrabbit.oak.plugins.index.nodetype.NodeTypeIndexProvider;
+import org.apache.jackrabbit.oak.plugins.index.property.PropertyIndexEditorProvider;
+import org.apache.jackrabbit.oak.plugins.memory.MemoryNodeStore;
+import org.apache.jackrabbit.oak.query.AbstractQueryTest;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EmptyHook;
+import org.apache.jackrabbit.oak.spi.commit.Observer;
+import org.apache.jackrabbit.oak.spi.mount.Mounts;
+import org.apache.jackrabbit.oak.spi.query.QueryIndexProvider;
+import org.apache.jackrabbit.oak.spi.security.OpenSecurityProvider;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.apache.jackrabbit.oak.spi.state.NodeStore;
+import org.apache.jackrabbit.oak.spi.whiteboard.Whiteboard;
+import org.apache.jackrabbit.oak.spi.whiteboard.WhiteboardUtils;
+import org.junit.After;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import static org.apache.jackrabbit.oak.api.CommitFailedException.CONSTRAINT;
+import static org.apache.jackrabbit.oak.commons.PathUtils.concat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.mock;
+
+public class SynchronousPropertyIndexTest extends AbstractQueryTest {
+    private ExecutorService executorService = Executors.newFixedThreadPool(2);
+
+    @Rule
+    public TemporaryFolder temporaryFolder = new TemporaryFolder(new File("target"));
+
+    private LuceneIndexProvider luceneIndexProvider;
+    private IndexingQueue queue = mock(IndexingQueue.class);
+    private NodeStore nodeStore = new MemoryNodeStore();
+    private Whiteboard wb;
+
+
+    private IndexDefinitionBuilder defnb = new IndexDefinitionBuilder();
+    private String indexPath  = "/oak:index/foo";
+
+    @After
+    public void tearDown() throws IOException {
+        luceneIndexProvider.close();
+        new ExecutorCloser(executorService).close();
+    }
+
+    @Override
+    protected ContentRepository createRepository() {
+        IndexCopier copier;
+        try {
+            copier = new IndexCopier(executorService, temporaryFolder.getRoot());
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+
+        IndexTracker tracker = new IndexTracker();
+        luceneIndexProvider = new LuceneIndexProvider(tracker);
+        LuceneIndexEditorProvider editorProvider = new LuceneIndexEditorProvider(copier,
+                tracker,
+                null,
+                null,
+                Mounts.defaultMountInfoProvider());
+
+        editorProvider.setIndexingQueue(queue);
+
+        Oak oak = new Oak(nodeStore)
+                .with(new InitialContent())
+                .with(new OpenSecurityProvider())
+                .with((QueryIndexProvider) luceneIndexProvider)
+                .with((Observer) luceneIndexProvider)
+                .with(editorProvider)
+                .with(new PropertyIndexEditorProvider())
+                .with(new NodeTypeIndexProvider())
+                .with(new NodeCounterEditorProvider())
+                //Effectively disable async indexing auto run
+                //such that we can control run timing as per test requirement
+                .withAsyncIndexing("async", TimeUnit.DAYS.toSeconds(1));
+
+        wb = oak.getWhiteboard();
+        return oak.createContentRepository();
+    }
+
+    @Test
+    public void uniquePropertyCommit() throws Exception{
+        defnb.async("async", "nrt");
+        defnb.indexRule("nt:base").property("foo").propertyIndex().unique();
+
+        addIndex(indexPath, defnb);
+        root.commit();
+
+        createPath("/a").setProperty("foo", "bar");
+        root.commit();
+
+        createPath("/b").setProperty("foo", "bar");
+        try {
+            root.commit();
+            fail();
+        } catch (CommitFailedException e) {
+            assertEquals(CONSTRAINT, e.getType());
+        }
+    }
+
+    @Test
+    public void uniquePropertyCommit_Async() throws Exception{
+        defnb.async("async", "nrt");
+        defnb.indexRule("nt:base").property("foo").propertyIndex().unique();
+
+        addIndex(indexPath, defnb);
+        root.commit();
+
+        createPath("/a").setProperty("foo", "bar");
+        root.commit();
+        runAsyncIndex();
+
+        //Remove the :property-index node to simulate bucket change
+        //This time commit would trigger a lucene query
+        NodeBuilder builder = nodeStore.getRoot().builder();
+        String propIdxStorePath = concat(indexPath, HybridPropertyIndexUtil.PROPERTY_INDEX);
+        NodeBuilder propIndex = TestUtil.child(builder, propIdxStorePath);
+        propIndex.remove();
+        nodeStore.merge(builder, EmptyHook.INSTANCE, CommitInfo.EMPTY);
+        root.refresh();
+
+        createPath("/b").setProperty("foo", "bar");
+        try {
+            root.commit();
+            fail();
+        } catch (CommitFailedException e) {
+            assertEquals(CONSTRAINT, e.getType());
+        }
+    }
+
+    private void runAsyncIndex() {
+        AsyncIndexUpdate async = (AsyncIndexUpdate) WhiteboardUtils.getService(wb,
+                Runnable.class, input -> input instanceof AsyncIndexUpdate);
+        assertNotNull(async);
+        async.run();
+        if (async.isFailing()) {
+            fail("AsyncIndexUpdate failed");
+        }
+        root.refresh();
+    }
+
+    private void addIndex(String indexPath, IndexDefinitionBuilder defnb){
+        defnb.build(createPath(indexPath));
+    }
+
+    private Tree createPath(String path){
+        Tree base = root.getTree("/");
+        for (String e : PathUtils.elements(path)){
+            base = base.hasChild(e) ? base.getChild(e) : base.addChild(e);
+        }
+        return base;
+    }
+}

Propchange: jackrabbit/oak/trunk/oak-lucene/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/property/SynchronousPropertyIndexTest.java
------------------------------------------------------------------------------
    svn:eol-style = native

Added: jackrabbit/oak/trunk/oak-lucene/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/property/UniquenessConstraintValidatorTest.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-lucene/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/property/UniquenessConstraintValidatorTest.java?rev=1810633&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-lucene/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/property/UniquenessConstraintValidatorTest.java (added)
+++ jackrabbit/oak/trunk/oak-lucene/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/property/UniquenessConstraintValidatorTest.java Tue Oct  3 05:06:57 2017
@@ -0,0 +1,140 @@
+/*
+ * 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.index.lucene.property;
+
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.plugins.index.lucene.IndexDefinition;
+import org.apache.jackrabbit.oak.plugins.index.lucene.PropertyDefinition;
+import org.apache.jackrabbit.oak.plugins.index.lucene.PropertyUpdateCallback;
+import org.apache.jackrabbit.oak.plugins.index.lucene.util.IndexDefinitionBuilder;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.apache.jackrabbit.oak.spi.state.NodeState;
+import org.junit.Test;
+
+import static java.util.Collections.singletonList;
+import static org.apache.jackrabbit.oak.InitialContent.INITIAL_CONTENT;
+import static org.apache.jackrabbit.oak.api.CommitFailedException.CONSTRAINT;
+import static org.apache.jackrabbit.oak.plugins.memory.EmptyNodeState.EMPTY_NODE;
+import static org.apache.jackrabbit.oak.plugins.memory.PropertyStates.createProperty;
+import static org.hamcrest.Matchers.containsString;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.fail;
+
+public class UniquenessConstraintValidatorTest {
+    private NodeState root = INITIAL_CONTENT;
+    private NodeBuilder builder = EMPTY_NODE.builder();
+    private IndexDefinitionBuilder defnb = new IndexDefinitionBuilder();
+    private String indexPath  = "/oak:index/foo";
+
+    @Test
+    public void singleUniqueProperty() throws Exception{
+        defnb.indexRule("nt:base").property("foo").unique();
+
+        PropertyUpdateCallback callback = newCallback();
+
+        callback.propertyUpdated("/a", "foo", pd("foo"),
+                null, createProperty("foo", "bar"));
+        callback.propertyUpdated("/b", "foo", pd("foo"),
+                null, createProperty("foo", "bar"));
+
+        try {
+            callback.done();
+            fail();
+        } catch (CommitFailedException e) {
+            assertEquals(CONSTRAINT, e.getType());
+            assertEquals(30, e.getCode());
+
+            assertThat(e.getMessage(), containsString(indexPath));
+            assertThat(e.getMessage(), containsString("/a"));
+            assertThat(e.getMessage(), containsString("/b"));
+            assertThat(e.getMessage(), containsString("foo"));
+        }
+    }
+
+    @Test
+    public void multipleUniqueProperties() throws Exception{
+        defnb.indexRule("nt:base").property("foo").unique();
+        defnb.indexRule("nt:base").property("foo2").unique();
+
+        PropertyUpdateCallback callback = newCallback();
+
+        propertyUpdated(callback, "/a", "foo", "bar");
+        propertyUpdated(callback, "/a", "foo2", "bar");
+
+        //As properties are different this should pass
+        callback.done();
+    }
+
+    @Test(expected = CommitFailedException.class)
+    public void firstStore_PreExist() throws Exception{
+        defnb.indexRule("nt:base").property("foo").unique();
+
+        PropertyUpdateCallback callback = newCallback();
+        propertyUpdated(callback, "/a", "foo", "bar");
+
+        builder = builder.getNodeState().builder();
+
+        callback = newCallback();
+        propertyUpdated(callback, "/b", "foo", "bar");
+        callback.done();
+    }
+    
+    @Test
+    public void secondStore_SamePath() throws Exception{
+        defnb.indexRule("nt:base").property("foo").unique();
+
+        PropertyIndexUpdateCallback callback = newCallback();
+        propertyUpdated(callback, "/a", "foo", "bar");
+
+        callback.getUniquenessConstraintValidator()
+                .setSecondStore((propertyRelativePath, value) -> singletonList("/a"));
+
+        //Should work as paths for unique property are same
+        callback.done();
+    }
+
+    @Test(expected = CommitFailedException.class)
+    public void secondStore_DiffPath() throws Exception{
+        defnb.indexRule("nt:base").property("foo").unique();
+
+        PropertyIndexUpdateCallback callback = newCallback();
+        propertyUpdated(callback, "/a", "foo", "bar");
+
+        callback.getUniquenessConstraintValidator()
+                .setSecondStore((propertyRelativePath, value) -> singletonList("/b"));
+
+        callback.done();
+    }
+
+    private void propertyUpdated(PropertyUpdateCallback callback, String nodePath, String propertyName, String value){
+        callback.propertyUpdated(nodePath, propertyName, pd(propertyName),
+                null, createProperty(propertyName, value));
+    }
+
+    private PropertyIndexUpdateCallback newCallback(){
+        return new PropertyIndexUpdateCallback(indexPath, builder);
+    }
+
+    private PropertyDefinition pd(String propName){
+        IndexDefinition defn = new IndexDefinition(root, defnb.build(), indexPath);
+        return defn.getApplicableIndexingRule("nt:base").getConfig(propName);
+    }
+}
\ No newline at end of file

Propchange: jackrabbit/oak/trunk/oak-lucene/src/test/java/org/apache/jackrabbit/oak/plugins/index/lucene/property/UniquenessConstraintValidatorTest.java
------------------------------------------------------------------------------
    svn:eol-style = native