blob: 393f99fd8e2d943f956b1490f462d251bbcba42d [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.plugins.index.property.strategy;
import static org.apache.jackrabbit.oak.plugins.index.IndexConstants.ENTRY_COUNT_PROPERTY_NAME;
import static org.apache.jackrabbit.oak.plugins.index.IndexConstants.INDEX_CONTENT_NODE_NAME;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Set;
import java.util.function.Consumer;
import com.google.common.base.Supplier;
import org.apache.jackrabbit.oak.api.PropertyState;
import org.apache.jackrabbit.oak.api.Type;
import org.apache.jackrabbit.oak.plugins.memory.MultiStringPropertyState;
import org.apache.jackrabbit.oak.spi.query.Filter;
import org.apache.jackrabbit.oak.spi.state.ChildNodeEntry;
import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
import org.apache.jackrabbit.oak.spi.state.NodeState;
import org.apache.jackrabbit.oak.plugins.index.counter.ApproximateCounter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* An IndexStoreStrategy implementation that saves the unique node in a single property.<br>
* This should reduce the number of nodes in the repository, and speed up access.<br>
* <br>
* For example for a node that is under {@code /test/node}, the index
* structure will be {@code /oak:index/index/@key}:
*/
public class UniqueEntryStoreStrategy implements IndexStoreStrategy {
static final Logger LOG = LoggerFactory.getLogger(UniqueEntryStoreStrategy.class);
private static final Consumer<NodeBuilder> NOOP = (nb) -> {};
private final String indexName;
private final Consumer<NodeBuilder> insertCallback;
public UniqueEntryStoreStrategy() {
this(INDEX_CONTENT_NODE_NAME);
}
public UniqueEntryStoreStrategy(String indexName) {
this(indexName, NOOP);
}
public UniqueEntryStoreStrategy(String indexName, @NotNull Consumer<NodeBuilder> insertCallback) {
this.indexName = indexName;
this.insertCallback = insertCallback;
}
@Override
public void update(
Supplier<NodeBuilder> index, String path,
@Nullable final String indexName,
@Nullable final NodeBuilder indexMeta,
Set<String> beforeKeys, Set<String> afterKeys) {
for (String key : beforeKeys) {
remove(index.get(), key, path);
}
for (String key : afterKeys) {
insert(index.get(), key, path);
}
}
private static void remove(NodeBuilder index, String key, String value) {
ApproximateCounter.adjustCountSync(index, -1);
NodeBuilder builder = index.getChildNode(key);
if (builder.exists()) {
// there could be (temporarily) multiple entries
// we need to remove the right one
PropertyState s = builder.getProperty("entry");
if (s.count() == 1) {
builder.remove();
} else {
ArrayList<String> list = new ArrayList<String>();
for (int i = 0; i < s.count(); i++) {
String r = s.getValue(Type.STRING, i);
if (!r.equals(value)) {
list.add(r);
}
}
PropertyState s2 = MultiStringPropertyState.stringProperty("entry", list);
builder.setProperty(s2);
}
}
}
private void insert(NodeBuilder index, String key, String value) {
ApproximateCounter.adjustCountSync(index, 1);
NodeBuilder k = index.child(key);
ArrayList<String> list = new ArrayList<String>();
list.add(value);
if (k.hasProperty("entry")) {
// duplicate key (to detect duplicate entries)
// this is just set temporarily,
// while trying to add a duplicate entry
PropertyState s = k.getProperty("entry");
for (int i = 0; i < s.count(); i++) {
String r = s.getValue(Type.STRING, i);
if (!list.contains(r)) {
list.add(r);
}
}
}
PropertyState s2 = MultiStringPropertyState.stringProperty("entry", list);
k.setProperty(s2);
insertCallback.accept(k);
}
@Override
public Iterable<String> query(final Filter filter, final String indexName,
final NodeState indexMeta, final Iterable<String> values) {
return query0(filter, indexName, indexMeta, values, new HitProducer<String>() {
@Override
public String produce(NodeState indexHit, String pathName) {
PropertyState s = indexHit.getProperty("entry");
if (s.count() <= 1) {
return s.getValue(Type.STRING, 0);
} else {
StringBuilder buff = new StringBuilder();
for (int i = 0; i < s.count(); i++) {
if (i > 0) {
buff.append(", ");
}
buff.append(s.getValue(Type.STRING, i));
}
return buff.toString();
}
}
});
}
/**
* Search for a given set of values, returning {@linkplain IndexEntry} results
*
* @param filter the filter (can optionally be used for optimized query execution)
* @param indexName the name of the index (for logging)
* @param indexMeta the index metadata node (may not be null)
* @param values values to look for (null to check for property existence)
* @return an iterator of index entries
*
* @throws UnsupportedOperationException if the operation is not supported
*/
public Iterable<IndexEntry> queryEntries(Filter filter, String indexName, NodeState indexMeta,
Iterable<String> values) {
return query0(filter, indexName, indexMeta, values, new HitProducer<IndexEntry>() {
@Override
public IndexEntry produce(NodeState indexHit, String pathName) {
PropertyState s = indexHit.getProperty("entry");
return new IndexEntry(s.getValue(Type.STRING, 0), pathName);
}
});
}
private <T> Iterable<T> query0(Filter filter, String indexName, NodeState indexMeta,
Iterable<String> values, HitProducer<T> prod) {
final NodeState index = indexMeta.getChildNode(getIndexNodeName());
return new Iterable<T>() {
@Override
public Iterator<T> iterator() {
if (values == null) {
return new Iterator<T>() {
Iterator<? extends ChildNodeEntry> it = index.getChildNodeEntries().iterator();
@Override
public boolean hasNext() {
return it.hasNext();
}
@Override
public T next() {
ChildNodeEntry indexEntry = it.next();
return prod.produce(indexEntry.getNodeState(), indexEntry.getName());
}
@Override
public void remove() {
it.remove();
}
};
}
ArrayList<T> list = new ArrayList<>();
for (String p : values) {
NodeState key = index.getChildNode(p);
if (key.exists()) {
// we have an entry for this value, so use it
list.add(prod.produce(key, p));
}
}
return list.iterator();
}
};
}
@Override
public boolean exists(Supplier<NodeBuilder> index, String key) {
return index.get().hasChildNode(key);
}
@Override
public long count(NodeState root, NodeState indexMeta, Set<String> values, int max) {
NodeState index = indexMeta.getChildNode(getIndexNodeName());
long count = 0;
if (values == null) {
PropertyState ec = indexMeta.getProperty(ENTRY_COUNT_PROPERTY_NAME);
if (ec != null) {
count = ec.getValue(Type.LONG);
if (count >= 0) {
return count;
}
}
if (count == 0) {
long approxCount = ApproximateCounter.getCountSync(index);
if (approxCount != -1) {
return approxCount;
}
}
count = 1 + index.getChildNodeCount(max);
// "is not null" queries typically read more data
count *= 10;
} else if (values.size() == 1) {
NodeState k = index.getChildNode(values.iterator().next());
if (k.exists()) {
count = k.getProperty("entry").count();
} else {
count = 0;
}
} else {
count = values.size();
}
return count;
}
@Override
public long count(final Filter filter, NodeState root, NodeState indexMeta, Set<String> values, int max) {
return count(root, indexMeta, values, max);
}
@Override
public String getIndexNodeName() {
return indexName;
}
/**
* Creates a specific type of "hit" to return from the query methods
*
* <p>Use primarily to reduce duplication when the query algorithms execute mostly the same steps but return different objects.</p>
*
* @param <T> The type of Hit to produce
*/
private interface HitProducer<T> {
/**
* Invoked when a matching index entry is found
*
* @param indexHit the index node
* @param propertyValue the value of the property
* @return the value produced for the specific "hit"
*/
T produce(NodeState indexHit, String propertyValue);
}
}