Author: rombert
Date: Tue Sep  5 15:01:28 2017
New Revision: 1807361

URL: http://svn.apache.org/viewvc?rev=1807361&view=rev
Log:
OAK-6506 - Ensure unique property indexes are consistent when mounting
NodeStores

Add a custom UniqueIndexNodeStoreChecker which validates consistency of
unique indexes from mounts.

Added:
    
jackrabbit/oak/trunk/oak-store-composite/src/main/java/org/apache/jackrabbit/oak/composite/checks/UniqueIndexNodeStoreChecker.java
    
jackrabbit/oak/trunk/oak-store-composite/src/test/java/org/apache/jackrabbit/oak/composite/checks/UniqueIndexNodeStoreCheckerTest.java
Modified:
    
jackrabbit/oak/trunk/oak-it/src/test/java/org/apache/jackrabbit/oak/composite/CompositeNodeStoreBuilderTest.java
    
jackrabbit/oak/trunk/oak-store-composite/src/main/java/org/apache/jackrabbit/oak/composite/checks/ErrorHolder.java
    
jackrabbit/oak/trunk/oak-store-composite/src/main/java/org/apache/jackrabbit/oak/composite/checks/MountedNodeStoreChecker.java
    
jackrabbit/oak/trunk/oak-store-composite/src/main/java/org/apache/jackrabbit/oak/composite/checks/NodeStoreChecksService.java
    
jackrabbit/oak/trunk/oak-store-composite/src/main/java/org/apache/jackrabbit/oak/composite/checks/NodeTypeMountedNodeStoreChecker.java
    
jackrabbit/oak/trunk/oak-store-composite/src/test/java/org/apache/jackrabbit/oak/composite/checks/NodeStoreChecksServiceTest.java
    
jackrabbit/oak/trunk/oak-store-composite/src/test/java/org/apache/jackrabbit/oak/composite/checks/NodeTypeMountedNodeStoreCheckerTest.java

Modified: 
jackrabbit/oak/trunk/oak-it/src/test/java/org/apache/jackrabbit/oak/composite/CompositeNodeStoreBuilderTest.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-it/src/test/java/org/apache/jackrabbit/oak/composite/CompositeNodeStoreBuilderTest.java?rev=1807361&r1=1807360&r2=1807361&view=diff
==============================================================================
--- 
jackrabbit/oak/trunk/oak-it/src/test/java/org/apache/jackrabbit/oak/composite/CompositeNodeStoreBuilderTest.java
 (original)
+++ 
jackrabbit/oak/trunk/oak-it/src/test/java/org/apache/jackrabbit/oak/composite/CompositeNodeStoreBuilderTest.java
 Tue Sep  5 15:01:28 2017
@@ -115,7 +115,7 @@ public class CompositeNodeStoreBuilderTe
 
         new CompositeNodeStore.Builder(mip, root)
             .addMount("readOnly", mount)
-            .with(new NodeStoreChecksService(Collections.singletonList(new 
NodeTypeMountedNodeStoreChecker(JcrConstants.MIX_VERSIONABLE, "test error"))))
+            .with(new NodeStoreChecksService(mip, 
Collections.singletonList(new 
NodeTypeMountedNodeStoreChecker(JcrConstants.MIX_VERSIONABLE, "test error"))))
             .build();        
         
     }

Modified: 
jackrabbit/oak/trunk/oak-store-composite/src/main/java/org/apache/jackrabbit/oak/composite/checks/ErrorHolder.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-store-composite/src/main/java/org/apache/jackrabbit/oak/composite/checks/ErrorHolder.java?rev=1807361&r1=1807360&r2=1807361&view=diff
==============================================================================
--- 
jackrabbit/oak/trunk/oak-store-composite/src/main/java/org/apache/jackrabbit/oak/composite/checks/ErrorHolder.java
 (original)
+++ 
jackrabbit/oak/trunk/oak-store-composite/src/main/java/org/apache/jackrabbit/oak/composite/checks/ErrorHolder.java
 Tue Sep  5 15:01:28 2017
@@ -34,6 +34,15 @@ class ErrorHolder {
         }
     }
     
+    public void report(MountedNodeStore firstNS, String firstPath, 
MountedNodeStore secondNS, String secondPath, String value, String error) {
+        errors.add(String.format("For NodeStore mount %s, path %s, and 
NodeStore mount %s, path %s, encountered the following clash for value %s: 
'%s'", 
+                firstNS.getMount().getName(), firstPath, 
secondNS.getMount().getName(), secondPath, value, error));
+        if ( errors.size() == FAIL_IMMEDIATELY_THRESHOLD ) { 
+            end();
+        }
+        
+    }
+    
     public void end() {
         if ( errors.isEmpty() ) {
             return;

Modified: 
jackrabbit/oak/trunk/oak-store-composite/src/main/java/org/apache/jackrabbit/oak/composite/checks/MountedNodeStoreChecker.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-store-composite/src/main/java/org/apache/jackrabbit/oak/composite/checks/MountedNodeStoreChecker.java?rev=1807361&r1=1807360&r2=1807361&view=diff
==============================================================================
--- 
jackrabbit/oak/trunk/oak-store-composite/src/main/java/org/apache/jackrabbit/oak/composite/checks/MountedNodeStoreChecker.java
 (original)
+++ 
jackrabbit/oak/trunk/oak-store-composite/src/main/java/org/apache/jackrabbit/oak/composite/checks/MountedNodeStoreChecker.java
 Tue Sep  5 15:01:28 2017
@@ -21,6 +21,7 @@ package org.apache.jackrabbit.oak.compos
 
 import org.apache.jackrabbit.oak.api.Tree;
 import org.apache.jackrabbit.oak.composite.MountedNodeStore;
+import org.apache.jackrabbit.oak.spi.mount.MountInfoProvider;
 import org.apache.jackrabbit.oak.spi.state.NodeStore;
 
 /**
@@ -33,8 +34,8 @@ import org.apache.jackrabbit.oak.spi.sta
  */
 public interface MountedNodeStoreChecker<T> {
     
-    public T createContext(NodeStore globalStore);
+    public T createContext(NodeStore globalStore, MountInfoProvider mip);
     
-    void check(MountedNodeStore mountedStore, Tree tree, ErrorHolder 
errorHolder, T context);
+    boolean check(MountedNodeStore mountedStore, Tree tree, ErrorHolder 
errorHolder, T context);
 
 }

Modified: 
jackrabbit/oak/trunk/oak-store-composite/src/main/java/org/apache/jackrabbit/oak/composite/checks/NodeStoreChecksService.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-store-composite/src/main/java/org/apache/jackrabbit/oak/composite/checks/NodeStoreChecksService.java?rev=1807361&r1=1807360&r2=1807361&view=diff
==============================================================================
--- 
jackrabbit/oak/trunk/oak-store-composite/src/main/java/org/apache/jackrabbit/oak/composite/checks/NodeStoreChecksService.java
 (original)
+++ 
jackrabbit/oak/trunk/oak-store-composite/src/main/java/org/apache/jackrabbit/oak/composite/checks/NodeStoreChecksService.java
 Tue Sep  5 15:01:28 2017
@@ -27,6 +27,7 @@ import org.apache.jackrabbit.oak.api.Tre
 import org.apache.jackrabbit.oak.composite.MountedNodeStore;
 import org.apache.jackrabbit.oak.plugins.tree.TreeFactory;
 import org.apache.jackrabbit.oak.spi.mount.Mount;
+import org.apache.jackrabbit.oak.spi.mount.MountInfoProvider;
 import org.apache.jackrabbit.oak.spi.state.NodeStore;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -42,6 +43,9 @@ public class NodeStoreChecksService impl
             unbind = "unbindChecker",
             referenceInterface = MountedNodeStoreChecker.class)
     private List<MountedNodeStoreChecker<?>> checkers = new 
CopyOnWriteArrayList<>();
+    
+    @Reference
+    private MountInfoProvider mip;
 
     // used by SCR
     public NodeStoreChecksService() {
@@ -49,8 +53,9 @@ public class NodeStoreChecksService impl
     }
     
     // visible for testing
-    public NodeStoreChecksService(List<MountedNodeStoreChecker<?>> checkers) {
+    public NodeStoreChecksService(MountInfoProvider mip, 
List<MountedNodeStoreChecker<?>> checkers) {
         this.checkers = checkers;
+        this.mip = mip;
     }
 
     @Override
@@ -72,7 +77,7 @@ public class NodeStoreChecksService impl
     private <T> void check(MountedNodeStore mountedStore, ErrorHolder 
errorHolder, NodeStore globalStore,
             MountedNodeStoreChecker<T> c) {
         
-        T context = c.createContext(globalStore);
+        T context = c.createContext(globalStore, mip);
         Tree mountRoot = 
TreeFactory.createReadOnlyTree(mountedStore.getNodeStore().getRoot());
         
         visit(mountRoot, mountedStore,  errorHolder, context, c);
@@ -89,11 +94,12 @@ public class NodeStoreChecksService impl
         boolean mounted = mount.isMounted(tree.getPath());
         
         
+        boolean keepGoing = true;
         if ( mounted ) {
-            c.check(mountedStore, tree, errorHolder, context);
+            keepGoing = c.check(mountedStore, tree, errorHolder, context);
         }
 
-        if ( mounted || under ) {
+        if ( ( mounted || under ) && keepGoing ) {
             tree.getChildren().forEach( child -> visit(child, mountedStore, 
errorHolder, context, c));
         }
     }    

Modified: 
jackrabbit/oak/trunk/oak-store-composite/src/main/java/org/apache/jackrabbit/oak/composite/checks/NodeTypeMountedNodeStoreChecker.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-store-composite/src/main/java/org/apache/jackrabbit/oak/composite/checks/NodeTypeMountedNodeStoreChecker.java?rev=1807361&r1=1807360&r2=1807361&view=diff
==============================================================================
--- 
jackrabbit/oak/trunk/oak-store-composite/src/main/java/org/apache/jackrabbit/oak/composite/checks/NodeTypeMountedNodeStoreChecker.java
 (original)
+++ 
jackrabbit/oak/trunk/oak-store-composite/src/main/java/org/apache/jackrabbit/oak/composite/checks/NodeTypeMountedNodeStoreChecker.java
 Tue Sep  5 15:01:28 2017
@@ -31,6 +31,7 @@ import org.apache.jackrabbit.oak.composi
 import org.apache.jackrabbit.oak.namepath.NamePathMapper;
 import org.apache.jackrabbit.oak.plugins.nodetype.ReadOnlyNodeTypeManager;
 import org.apache.jackrabbit.oak.plugins.tree.RootFactory;
+import org.apache.jackrabbit.oak.spi.mount.MountInfoProvider;
 import org.apache.jackrabbit.oak.spi.state.NodeStore;
 import org.osgi.service.component.ComponentContext;
 import org.slf4j.Logger;
@@ -78,7 +79,7 @@ public class NodeTypeMountedNodeStoreChe
     }
 
     @Override
-    public Context createContext(NodeStore globalStore) {
+    public Context createContext(NodeStore globalStore, MountInfoProvider mip) 
{
         
         Root globalRoot = 
RootFactory.createReadOnlyRoot(globalStore.getRoot());
         ReadOnlyNodeTypeManager typeManager = 
ReadOnlyNodeTypeManager.getInstance(globalRoot, NamePathMapper.DEFAULT);
@@ -87,12 +88,14 @@ public class NodeTypeMountedNodeStoreChe
     }
 
     @Override
-    public void check(MountedNodeStore mountedStore, Tree tree, ErrorHolder 
errorHolder, Context context) {
+    public boolean check(MountedNodeStore mountedStore, Tree tree, ErrorHolder 
errorHolder, Context context) {
         
         if ( context.getTypeManager().isNodeType(tree, invalidNodeType) &&
                 !isExcluded(mountedStore, tree, context) ) {
             errorHolder.report(mountedStore, tree.getPath(), errorLabel);
         }
+        
+        return true;
     }
 
     private boolean isExcluded(MountedNodeStore mountedStore, Tree tree, 
Context context) {

Added: 
jackrabbit/oak/trunk/oak-store-composite/src/main/java/org/apache/jackrabbit/oak/composite/checks/UniqueIndexNodeStoreChecker.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-store-composite/src/main/java/org/apache/jackrabbit/oak/composite/checks/UniqueIndexNodeStoreChecker.java?rev=1807361&view=auto
==============================================================================
--- 
jackrabbit/oak/trunk/oak-store-composite/src/main/java/org/apache/jackrabbit/oak/composite/checks/UniqueIndexNodeStoreChecker.java
 (added)
+++ 
jackrabbit/oak/trunk/oak-store-composite/src/main/java/org/apache/jackrabbit/oak/composite/checks/UniqueIndexNodeStoreChecker.java
 Tue Sep  5 15:01:28 2017
@@ -0,0 +1,293 @@
+/*
+ * 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.composite.checks;
+
+import static 
org.apache.jackrabbit.oak.plugins.index.IndexConstants.INDEX_CONTENT_NODE_NAME;
+import static 
org.apache.jackrabbit.oak.plugins.index.IndexConstants.INDEX_DEFINITIONS_NAME;
+import static 
org.apache.jackrabbit.oak.plugins.index.IndexConstants.UNIQUE_PROPERTY_NAME;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Optional;
+import java.util.Set;
+import java.util.TreeSet;
+
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Service;
+import org.apache.jackrabbit.oak.api.Tree;
+import org.apache.jackrabbit.oak.composite.MountedNodeStore;
+import org.apache.jackrabbit.oak.plugins.index.property.Multiplexers;
+import org.apache.jackrabbit.oak.plugins.index.property.strategy.IndexEntry;
+import 
org.apache.jackrabbit.oak.plugins.index.property.strategy.UniqueEntryStoreStrategy;
+import org.apache.jackrabbit.oak.spi.mount.Mount;
+import org.apache.jackrabbit.oak.spi.mount.MountInfoProvider;
+import org.apache.jackrabbit.oak.spi.query.Filter;
+import org.apache.jackrabbit.oak.spi.state.ChildNodeEntry;
+import org.apache.jackrabbit.oak.spi.state.NodeState;
+import org.apache.jackrabbit.oak.spi.state.NodeStore;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.collect.Maps;
+
+/**
+ * Checker that ensures the consistency of unique entries in the various mounts
+ * 
+ * <p>For all unique indexes, it checks that the uniqueness constraint holds 
when
+ * taking into account the combined index from all the mounts, including the 
global one.</p>
+ * 
+ * <p>Being a one-off check, it does not strictly implement the {@link 
#check(MountedNodeStore, Tree, ErrorHolder, Context)}
+ * contract in terms of navigating the specified tree, but instead accesses 
the index definitions node directly
+ * on first access and skips all subsequent executions.</p>
+ *
+ */
+@Component
+@Service(MountedNodeStoreChecker.class)
+public class UniqueIndexNodeStoreChecker implements 
MountedNodeStoreChecker<UniqueIndexNodeStoreChecker.Context> {
+    
+    private static final Logger LOG = 
LoggerFactory.getLogger(UniqueIndexNodeStoreChecker.class);
+    
+    @Override
+    public Context createContext(NodeStore globalStore, MountInfoProvider mip) 
{
+
+        Context ctx = new Context(mip);
+        
+        // read definitions from oak:index, and pick all unique indexes
+        NodeState indexDefs = 
globalStore.getRoot().getChildNode(INDEX_DEFINITIONS_NAME);
+        for ( ChildNodeEntry indexDef : indexDefs.getChildNodeEntries() ) {
+            if ( indexDef.getNodeState().hasProperty(UNIQUE_PROPERTY_NAME) &&
+                    indexDef.getNodeState().getBoolean(UNIQUE_PROPERTY_NAME) ) 
{
+                ctx.add(indexDef, mip.getDefaultMount(), indexDefs);
+                ctx.track(new MountedNodeStore(mip.getDefaultMount() , 
globalStore));
+            }
+        }
+        
+        return ctx;
+    }
+    
+    @Override
+    public boolean check(MountedNodeStore mountedStore, Tree tree, ErrorHolder 
errorHolder, Context context) {
+        context.track(mountedStore);
+        
+        // gather index definitions owned by this mount
+        NodeState indexDefs = 
mountedStore.getNodeStore().getRoot().getChildNode(INDEX_DEFINITIONS_NAME);
+        
+        for ( ChildNodeEntry indexDef : indexDefs.getChildNodeEntries() ) {
+            if ( indexDef.getNodeState().hasProperty(UNIQUE_PROPERTY_NAME) &&
+                    indexDef.getNodeState().getBoolean(UNIQUE_PROPERTY_NAME) ) 
{
+                
+                String mountIndexDefName = 
Multiplexers.getNodeForMount(mountedStore.getMount(), INDEX_CONTENT_NODE_NAME);
+                
+                NodeState mountIndexDef = 
indexDef.getNodeState().getChildNode(mountIndexDefName);
+                
+                if ( mountIndexDef.exists() ) {
+                    context.add(indexDef, mountedStore.getMount(), indexDefs);
+                }
+            }
+        }
+
+        // execute checks
+        context.runChecks(context, errorHolder);
+        
+        return false;
+    }
+    
+    static class Context {
+        private final MountInfoProvider mip;
+        private final Map<String, IndexCombination> combinations = new 
HashMap<>();
+        private final Map<String, MountedNodeStore> mountedNodeStoresByName = 
Maps.newHashMap();
+        
+        Context(MountInfoProvider mip) {
+            this.mip = mip;
+        }
+
+        public void track(MountedNodeStore mountedNodeStore) {
+            mountedNodeStoresByName.put(mountedNodeStore.getMount().getName(), 
mountedNodeStore);
+        }
+
+        public void add(ChildNodeEntry rootIndexDef, Mount mount, NodeState 
indexDef) {
+
+            IndexCombination combination = 
combinations.get(rootIndexDef.getName());
+            if ( combination == null ) {
+                combination = new IndexCombination(rootIndexDef);
+                combinations.put(rootIndexDef.getName(), combination);
+            }
+            
+            combination.addEntry(mount, indexDef);
+        }
+        
+        public MountInfoProvider getMountInfoProvider() {
+            
+            return mip;
+        }
+        
+        public void runChecks(Context context, ErrorHolder errorHolder) {
+            for ( IndexCombination combination: combinations.values() ) {
+                combination.runCheck(context, errorHolder);
+            }
+        }
+    }
+    
+    static class IndexCombination {
+        private final ChildNodeEntry rootIndexDef;
+        private final Map<Mount, NodeState> indexEntries = Maps.newHashMap();
+        private final List<Mount[]> checked = new ArrayList<>();
+        // bounded as the ErrorHolder has a reasonable limit of entries before 
it fails immediately
+        private final Set<String> reportedConflictingValues = new HashSet<>();
+        
+        IndexCombination(ChildNodeEntry rootIndexDef) {
+            this.rootIndexDef = rootIndexDef;
+        }
+        
+        public void addEntry(Mount mount, NodeState indexDef) {
+            
+            if ( !indexEntries.containsKey(mount) )
+                indexEntries.put(mount, indexDef);
+        }
+        
+        public void runCheck(Context context, ErrorHolder errorHolder) {
+            
+            for ( Map.Entry<Mount, NodeState> indexEntry : 
indexEntries.entrySet() ) {
+                for ( Map.Entry<Mount, NodeState> indexEntry2 : 
indexEntries.entrySet() ) {
+                    
+                    // same entry, skip
+                    if ( indexEntry.getKey().equals(indexEntry2.getKey()) ) {
+                        continue;
+                    }
+                    
+                    // ensure we don't check twice
+                    if ( wasChecked(indexEntry.getKey(), indexEntry2.getKey() 
)) {
+                        continue;
+                    }
+                    
+                    check(indexEntry, indexEntry2, context, errorHolder);
+                    
+                    recordChecked(indexEntry.getKey(), indexEntry2.getKey());
+                }
+            }
+        }
+
+        private boolean wasChecked(Mount first, Mount second) {
+
+            for ( Mount[] checkedEntry : checked ) {
+                if ( ( checkedEntry[0].equals(first) && 
checkedEntry[1].equals(second) ) ||
+                        ( checkedEntry[1].equals(first) && 
checkedEntry[0].equals(second))) {
+                    return true;
+                }
+            }
+            
+            return false;
+        }
+        
+        private void recordChecked(Mount first, Mount second) {
+            
+            checked.add(new Mount[] { first, second });
+        }
+        
+        private void check(Entry<Mount, NodeState> indexEntry, Entry<Mount, 
NodeState> indexEntry2, Context ctx, ErrorHolder errorHolder) {
+
+            String indexName = rootIndexDef.getName();
+            
+            // optimisation: sort strategies to ensuer that we query all the 
entries from the mount and check them
+            // against the default. this way we'll run a significantly smaller 
number of checks. The assumption is
+            // that the non-default mount will always hold a larger number of 
entries
+            TreeSet<StrategyWrapper> wrappers = new TreeSet<>();
+            wrappers.add(getWrapper(indexEntry, indexName, ctx));
+            wrappers.add(getWrapper(indexEntry2, indexName, ctx));
+            
+            StrategyWrapper wrapper = wrappers.first();
+            StrategyWrapper wrapper2 = wrappers.last();
+            
+            LOG.info("Checking index definitions for {} between mounts {} and 
{}", indexName, wrapper.mount.getName(), wrapper2.mount.getName());
+            
+            for ( IndexEntry hit : wrapper.queryAll() ) {
+                Optional<IndexEntry> result = 
wrapper2.queryOne(hit.getPropertyValue());
+                if ( result.isPresent() ) {
+                    IndexEntry hit2 = result.get();
+                    if ( 
reportedConflictingValues.add(hit.getPropertyValue())) {
+                        errorHolder.report(wrapper.nodeStore, hit.getPath(), 
wrapper2.nodeStore, hit2.getPath(), 
+                                hit.getPropertyValue(), "duplicate unique 
index entry");
+                    }
+                }
+            }
+        }
+
+        private StrategyWrapper getWrapper(Entry<Mount, NodeState> indexEntry, 
String indexName, Context ctx) {
+
+            NodeState indexNode = indexEntry.getValue();
+            Mount mount = indexEntry.getKey();
+            MountedNodeStore mountedNodeStore = 
ctx.mountedNodeStoresByName.get(mount.getName());
+            
+            UniqueEntryStoreStrategy strategy = mount.isDefault() ? new 
UniqueEntryStoreStrategy() : 
+                new 
UniqueEntryStoreStrategy(Multiplexers.getNodeForMount(mount, 
INDEX_CONTENT_NODE_NAME));
+            
+            return new StrategyWrapper(strategy, 
indexNode.getChildNode(indexName), indexName, mountedNodeStore);
+        }
+    }
+    
+    private static class StrategyWrapper implements 
Comparable<StrategyWrapper> {
+        
+        private static final int PRIORITY_DEFAULT = 0;
+        private static final int PRIORITY_MOUNT = 100;
+        
+        private final UniqueEntryStoreStrategy strategy;
+        private final int priority;
+        private final NodeState indexNode;
+        private final Mount mount;
+        private final MountedNodeStore nodeStore;
+        private final String indexName;
+        
+        private StrategyWrapper(UniqueEntryStoreStrategy strategy, NodeState 
indexNode, String indexName, MountedNodeStore nodeStore) {
+            this.strategy = strategy;
+            this.mount = nodeStore.getMount();
+            this.indexNode = indexNode;
+            this.nodeStore = nodeStore;
+            this.indexName = indexName;
+            this.priority = mount.isDefault() ? PRIORITY_MOUNT : 
PRIORITY_DEFAULT;            
+        }
+
+        @Override
+        public int compareTo(StrategyWrapper o) {
+            int prioCmp = Integer.compare(priority, o.priority);
+            if ( prioCmp != 0 ) {
+                return prioCmp;
+            }
+            return mount.getName().compareTo(o.mount.getName());
+        }
+        
+        public Iterable<IndexEntry> queryAll() {
+            return strategy.queryEntries(Filter.EMPTY_FILTER, indexName, 
indexNode, null);
+        }
+        
+        public Optional<IndexEntry> queryOne(String value) {
+            
+            Iterable<IndexEntry> results = 
strategy.queryEntries(Filter.EMPTY_FILTER, indexName, indexNode, 
Collections.singleton(value));
+            if ( !results.iterator().hasNext() ) {
+                return Optional.empty();
+            }
+            
+            return Optional.of(results.iterator().next());
+        }
+    }
+}

Modified: 
jackrabbit/oak/trunk/oak-store-composite/src/test/java/org/apache/jackrabbit/oak/composite/checks/NodeStoreChecksServiceTest.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-store-composite/src/test/java/org/apache/jackrabbit/oak/composite/checks/NodeStoreChecksServiceTest.java?rev=1807361&r1=1807360&r2=1807361&view=diff
==============================================================================
--- 
jackrabbit/oak/trunk/oak-store-composite/src/test/java/org/apache/jackrabbit/oak/composite/checks/NodeStoreChecksServiceTest.java
 (original)
+++ 
jackrabbit/oak/trunk/oak-store-composite/src/test/java/org/apache/jackrabbit/oak/composite/checks/NodeStoreChecksServiceTest.java
 Tue Sep  5 15:01:28 2017
@@ -17,6 +17,7 @@
 package org.apache.jackrabbit.oak.composite.checks;
 
 import static com.google.common.base.Preconditions.checkNotNull;
+import static 
org.apache.jackrabbit.oak.spi.mount.Mounts.defaultMountInfoProvider;
 
 import java.util.Arrays;
 
@@ -70,7 +71,7 @@ public class NodeStoreChecksServiceTest
     @Test(expected = IllegalRepositoryStateException.class)
     public void failOnNodeCoveredByMount() {
 
-        NodeStoreChecksService checks = new 
NodeStoreChecksService(Arrays.asList(new FailOnTreeNameChecker("third")));
+        NodeStoreChecksService checks = new 
NodeStoreChecksService(defaultMountInfoProvider(), Arrays.asList(new 
FailOnTreeNameChecker("third")));
         
         checks.check(globalStore, new MountedNodeStore(mount, mountedStore));
     }
@@ -78,7 +79,7 @@ public class NodeStoreChecksServiceTest
     @Test
     public void doNotFailOnNodeNotCoveredByMount() {
         
-        NodeStoreChecksService checks = new 
NodeStoreChecksService(Arrays.asList(new FailOnTreeNameChecker("not-covered")));
+        NodeStoreChecksService checks = new 
NodeStoreChecksService(defaultMountInfoProvider(), Arrays.asList(new 
FailOnTreeNameChecker("not-covered")));
         
         checks.check(globalStore, new MountedNodeStore(mount, mountedStore));
     }
@@ -92,14 +93,16 @@ public class NodeStoreChecksServiceTest
         }
 
         @Override
-        public Void createContext(NodeStore globalStore) {
+        public Void createContext(NodeStore globalStore, MountInfoProvider 
mip) {
             return null;
         }
 
         @Override
-        public void check(MountedNodeStore mountedStore, Tree tree, 
ErrorHolder errorHolder, Void context) {
+        public boolean check(MountedNodeStore mountedStore, Tree tree, 
ErrorHolder errorHolder, Void context) {
             if ( name.equals(tree.getName()))
                 errorHolder.report(mountedStore, tree.getPath(), "test 
failure");
+            
+            return true;
         }
         
     }

Modified: 
jackrabbit/oak/trunk/oak-store-composite/src/test/java/org/apache/jackrabbit/oak/composite/checks/NodeTypeMountedNodeStoreCheckerTest.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-store-composite/src/test/java/org/apache/jackrabbit/oak/composite/checks/NodeTypeMountedNodeStoreCheckerTest.java?rev=1807361&r1=1807360&r2=1807361&view=diff
==============================================================================
--- 
jackrabbit/oak/trunk/oak-store-composite/src/test/java/org/apache/jackrabbit/oak/composite/checks/NodeTypeMountedNodeStoreCheckerTest.java
 (original)
+++ 
jackrabbit/oak/trunk/oak-store-composite/src/test/java/org/apache/jackrabbit/oak/composite/checks/NodeTypeMountedNodeStoreCheckerTest.java
 Tue Sep  5 15:01:28 2017
@@ -58,7 +58,7 @@ public class NodeTypeMountedNodeStoreChe
                 .build();
         
         NodeTypeMountedNodeStoreChecker checker = new 
NodeTypeMountedNodeStoreChecker(JcrConstants.MIX_REFERENCEABLE, "test error");
-        Context context = checker.createContext(root);
+        Context context = checker.createContext(root, mip);
         ErrorHolder errorHolder = new ErrorHolder();
         
         checker.check(new MountedNodeStore(mip.getMountByName("first"), 
mount), TreeFactory.createReadOnlyTree(mount.getRoot()).getChild("first"), 
errorHolder, context);
@@ -88,7 +88,7 @@ public class NodeTypeMountedNodeStoreChe
         
         NodeTypeMountedNodeStoreChecker checker = new 
NodeTypeMountedNodeStoreChecker(JcrConstants.MIX_REFERENCEABLE, "test error", 
                 JcrConstants.NT_RESOURCE);
-        Context context = checker.createContext(root);
+        Context context = checker.createContext(root, mip);
         ErrorHolder errorHolder = new ErrorHolder();
         
         checker.check(new MountedNodeStore(mip.getMountByName("first"), 
mount), TreeFactory.createReadOnlyTree(mount.getRoot()).getChild("first"), 
errorHolder, context);

Added: 
jackrabbit/oak/trunk/oak-store-composite/src/test/java/org/apache/jackrabbit/oak/composite/checks/UniqueIndexNodeStoreCheckerTest.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-store-composite/src/test/java/org/apache/jackrabbit/oak/composite/checks/UniqueIndexNodeStoreCheckerTest.java?rev=1807361&view=auto
==============================================================================
--- 
jackrabbit/oak/trunk/oak-store-composite/src/test/java/org/apache/jackrabbit/oak/composite/checks/UniqueIndexNodeStoreCheckerTest.java
 (added)
+++ 
jackrabbit/oak/trunk/oak-store-composite/src/test/java/org/apache/jackrabbit/oak/composite/checks/UniqueIndexNodeStoreCheckerTest.java
 Tue Sep  5 15:01:28 2017
@@ -0,0 +1,177 @@
+/*
+ * 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.composite.checks;
+
+import static 
org.apache.jackrabbit.oak.plugins.index.IndexConstants.INDEX_DEFINITIONS_NAME;
+import static 
org.apache.jackrabbit.oak.plugins.index.IndexUtils.createIndexDefinition;
+
+import java.util.function.Consumer;
+
+import org.apache.jackrabbit.oak.api.CommitFailedException;
+import org.apache.jackrabbit.oak.api.IllegalRepositoryStateException;
+import org.apache.jackrabbit.oak.composite.MountedNodeStore;
+import 
org.apache.jackrabbit.oak.composite.checks.UniqueIndexNodeStoreChecker.Context;
+import org.apache.jackrabbit.oak.plugins.index.IndexUpdateProvider;
+import 
org.apache.jackrabbit.oak.plugins.index.property.PropertyIndexEditorProvider;
+import org.apache.jackrabbit.oak.plugins.memory.MemoryNodeStore;
+import org.apache.jackrabbit.oak.plugins.tree.TreeFactory;
+import org.apache.jackrabbit.oak.spi.commit.CommitInfo;
+import org.apache.jackrabbit.oak.spi.commit.EditorHook;
+import org.apache.jackrabbit.oak.spi.mount.MountInfoProvider;
+import org.apache.jackrabbit.oak.spi.mount.Mounts;
+import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
+import org.apache.jackrabbit.oak.spi.state.NodeStore;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import com.google.common.collect.ImmutableSet;
+
+public class UniqueIndexNodeStoreCheckerTest {
+
+    @Rule
+    public final ExpectedException exception = ExpectedException.none();
+    
+    private MountInfoProvider mip;
+    
+    @Before
+    public void prepare() {
+        mip = Mounts.newBuilder().
+                readOnlyMount("libs", "/libs", "/libs2").
+                readOnlyMount("apps", "/apps", "/apps2").
+                build();
+    }
+    
+    @Test
+    public void uuidConflict_twoStores() throws Exception {
+        
+        MemoryNodeStore globalStore = new MemoryNodeStore();
+        MemoryNodeStore mountedStore = new MemoryNodeStore();
+        
+        populateStore(globalStore, b  -> b.child("first").setProperty("foo", 
"bar"));
+        populateStore(mountedStore, b -> 
b.child("libs").child("first").setProperty("foo", "bar"));
+        
+        UniqueIndexNodeStoreChecker checker = new 
UniqueIndexNodeStoreChecker();
+        Context ctx = checker.createContext(globalStore, mip);
+        
+        exception.expect(IllegalRepositoryStateException.class);
+        exception.expectMessage("1 errors were found");
+        exception.expectMessage("clash for value bar: 'duplicate unique index 
entry'");
+        
+        ErrorHolder error = new ErrorHolder();
+        checker.check(new MountedNodeStore(mip.getMountByName("libs"), 
mountedStore), TreeFactory.createReadOnlyTree(mountedStore.getRoot()), error, 
ctx);
+        error.end();
+    }
+    
+    @Test
+    public void uuidConflict_threeStores() throws Exception {
+        
+        MemoryNodeStore globalStore = new MemoryNodeStore();
+        MemoryNodeStore mountedStore = new MemoryNodeStore();
+        MemoryNodeStore mountedStore2 = new MemoryNodeStore();
+        
+        populateStore(globalStore, b  -> b.child("first").setProperty("foo", 
"bar"));
+        populateStore(globalStore, b  -> b.child("second").setProperty("foo", 
"baz"));
+        populateStore(mountedStore, b -> 
b.child("libs").child("first").setProperty("foo", "bar"));
+        populateStore(mountedStore2, b -> 
b.child("apps").child("first").setProperty("foo", "baz"));
+        
+        UniqueIndexNodeStoreChecker checker = new 
UniqueIndexNodeStoreChecker();
+        Context ctx = checker.createContext(globalStore, mip);
+        
+        exception.expect(IllegalRepositoryStateException.class);
+        exception.expectMessage("2 errors were found");
+        exception.expectMessage("clash for value bar: 'duplicate unique index 
entry'");
+        exception.expectMessage("clash for value baz: 'duplicate unique index 
entry'");
+        
+        ErrorHolder error = new ErrorHolder();
+        checker.check(new MountedNodeStore(mip.getMountByName("libs"), 
mountedStore), TreeFactory.createReadOnlyTree(mountedStore.getRoot()), error, 
ctx);
+        checker.check(new MountedNodeStore(mip.getMountByName("apps"), 
mountedStore2), TreeFactory.createReadOnlyTree(mountedStore.getRoot()), error, 
ctx);
+        error.end();
+    }
+    
+    @Test
+    public void noConflict() throws Exception {
+
+        MemoryNodeStore globalStore = new MemoryNodeStore();
+        MemoryNodeStore mountedStore = new MemoryNodeStore();
+        
+        populateStore(globalStore, b  -> b.child("first").setProperty("foo", 
"baz"));
+        populateStore(mountedStore, b -> 
b.child("libs").child("first").setProperty("foo", "bar"));
+        
+        UniqueIndexNodeStoreChecker checker = new 
UniqueIndexNodeStoreChecker();
+        Context ctx = checker.createContext(globalStore, mip);
+        
+        ErrorHolder error = new ErrorHolder();
+        checker.check(new MountedNodeStore(mip.getMountByName("libs"), 
mountedStore), TreeFactory.createReadOnlyTree(mountedStore.getRoot()), error, 
ctx);
+        error.end();
+    }
+    
+    /**
+     * Tests that if a mount has an index clash but the path does not belong 
to the mount no error is reported
+     */
+    @Test
+    public void noConflict_mountHasDuplicateOutsideOfPath() throws Exception {
+        
+        MemoryNodeStore globalStore = new MemoryNodeStore();
+        MemoryNodeStore mountedStore = new MemoryNodeStore();
+        
+        populateStore(globalStore, b  -> b.child("first").setProperty("foo", 
"bar"));
+        populateStore(mountedStore, b -> b.child("second").setProperty("foo", 
"bar"));
+        
+        UniqueIndexNodeStoreChecker checker = new 
UniqueIndexNodeStoreChecker();
+        Context ctx = checker.createContext(globalStore, mip);
+        
+        ErrorHolder error = new ErrorHolder();
+        checker.check(new MountedNodeStore(mip.getMountByName("libs"), 
mountedStore), TreeFactory.createReadOnlyTree(mountedStore.getRoot()), error, 
ctx);
+        error.end();
+    }
+    
+    /**
+     * Tests that if a mount has an index clash but the path does not belog to 
the global mount no error is reported  
+     * 
+     */
+    @Test
+    public void noConflict_globalMountHasDuplicateOutsideOfPath() throws 
Exception {
+        
+        MemoryNodeStore globalStore = new MemoryNodeStore();
+        MemoryNodeStore mountedStore = new MemoryNodeStore();
+        
+        populateStore(globalStore, b  -> 
b.child("libs").child("first").setProperty("foo", "bar"));
+        populateStore(mountedStore, b -> 
b.child("libs").child("second").setProperty("foo", "bar"));
+        
+        UniqueIndexNodeStoreChecker checker = new 
UniqueIndexNodeStoreChecker();
+        Context ctx = checker.createContext(globalStore, mip);
+        
+        ErrorHolder error = new ErrorHolder();
+        checker.check(new MountedNodeStore(mip.getMountByName("libs"), 
mountedStore), TreeFactory.createReadOnlyTree(mountedStore.getRoot()), error, 
ctx);
+        error.end();
+    }
+    
+    private void populateStore(NodeStore ns, Consumer<NodeBuilder> action) 
throws CommitFailedException {
+        
+        NodeBuilder builder = ns.getRoot().builder();
+        NodeBuilder index = 
createIndexDefinition(builder.child(INDEX_DEFINITIONS_NAME), "foo",
+                true, true, ImmutableSet.of("foo"), null);
+        index.setProperty("entryCount", -1);   
+        
+        action.accept(builder);
+        
+        ns.merge(builder,new EditorHook(new IndexUpdateProvider(
+                new PropertyIndexEditorProvider().with(mip))), 
CommitInfo.EMPTY);
+    }
+}


Reply via email to