blob: 951dff1b4de47d8b5523e3fa4d5d0c1a028d7072 [file] [log] [blame]
/*
* 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());
}
}
}