/*
|
* CDDL HEADER START
|
*
|
* The contents of this file are subject to the terms of the
|
* Common Development and Distribution License, Version 1.0 only
|
* (the "License"). You may not use this file except in compliance
|
* with the License.
|
*
|
* You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
|
* or http://forgerock.org/license/CDDLv1.0.html.
|
* See the License for the specific language governing permissions
|
* and limitations under the License.
|
*
|
* When distributing Covered Code, include this CDDL HEADER in each
|
* file and include the License file at legal-notices/CDDLv1_0.txt.
|
* If applicable, add the following below this CDDL HEADER, with the
|
* fields enclosed by brackets "[]" replaced with your own identifying
|
* information:
|
* Portions Copyright [yyyy] [name of copyright owner]
|
*
|
* CDDL HEADER END
|
*
|
*
|
* Copyright 2006-2010 Sun Microsystems, Inc.
|
* Portions Copyright 2011-2016 ForgeRock AS
|
* Portions Copyright 2014 Manuel Gaupp
|
*/
|
package org.opends.server.backends.pluggable;
|
|
import static org.opends.messages.BackendMessages.*;
|
import static org.opends.server.backends.pluggable.EntryIDSet.*;
|
import static org.opends.server.util.StaticUtils.*;
|
|
import java.io.Closeable;
|
import java.util.*;
|
|
import org.forgerock.i18n.LocalizableMessage;
|
import org.forgerock.i18n.LocalizableMessageBuilder;
|
import org.forgerock.i18n.slf4j.LocalizedLogger;
|
import org.forgerock.opendj.config.server.ConfigChangeResult;
|
import org.forgerock.opendj.config.server.ConfigException;
|
import org.forgerock.opendj.ldap.Assertion;
|
import org.forgerock.opendj.ldap.ByteSequence;
|
import org.forgerock.opendj.ldap.ByteString;
|
import org.forgerock.opendj.ldap.DecodeException;
|
import org.forgerock.opendj.ldap.schema.MatchingRule;
|
import org.forgerock.opendj.ldap.schema.Schema;
|
import org.forgerock.opendj.ldap.spi.IndexQueryFactory;
|
import org.forgerock.opendj.ldap.spi.Indexer;
|
import org.forgerock.opendj.ldap.spi.IndexingOptions;
|
import org.opends.server.admin.server.ConfigurationChangeListener;
|
import org.opends.server.admin.std.meta.BackendIndexCfgDefn.IndexType;
|
import org.opends.server.admin.std.server.BackendIndexCfg;
|
import org.opends.server.backends.pluggable.spi.StorageRuntimeException;
|
import org.opends.server.backends.pluggable.spi.TreeName;
|
import org.opends.server.backends.pluggable.spi.WriteOperation;
|
import org.opends.server.backends.pluggable.spi.WriteableTransaction;
|
import org.opends.server.core.DirectoryServer;
|
import org.forgerock.opendj.ldap.schema.AttributeType;
|
import org.opends.server.types.*;
|
import org.opends.server.util.StaticUtils;
|
|
/**
|
* Class representing an attribute index.
|
* We have a separate tree for each type of indexing, which makes it easy
|
* to tell which attribute indexes are configured. The different types of
|
* indexing are equality, presence, substrings and ordering. The keys in the
|
* ordering index are ordered by setting the btree comparator to the ordering
|
* matching rule comparator.
|
* Note that the values in the equality index are normalized by the equality
|
* matching rule, whereas the values in the ordering index are normalized
|
* by the ordering matching rule. If these could be guaranteed to be identical
|
* then we would not need a separate ordering index.
|
*/
|
class AttributeIndex implements ConfigurationChangeListener<BackendIndexCfg>, Closeable
|
{
|
private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
|
|
/** Type of the index filter. */
|
enum IndexFilterType
|
{
|
/** Equality. */
|
EQUALITY(IndexType.EQUALITY),
|
/** Presence. */
|
PRESENCE(IndexType.PRESENCE),
|
/** Ordering. */
|
GREATER_OR_EQUAL(IndexType.ORDERING),
|
/** Ordering. */
|
LESS_OR_EQUAL(IndexType.ORDERING),
|
/** Substring. */
|
SUBSTRING(IndexType.SUBSTRING),
|
/** Approximate. */
|
APPROXIMATE(IndexType.APPROXIMATE);
|
|
private final IndexType indexType;
|
|
IndexFilterType(IndexType indexType)
|
{
|
this.indexType = indexType;
|
}
|
|
@Override
|
public String toString()
|
{
|
return indexType.toString();
|
}
|
}
|
|
/** This class implements an attribute indexer for matching rules in a Backend. */
|
static final class MatchingRuleIndex extends DefaultIndex
|
{
|
private final AttributeType attributeType;
|
private final Indexer indexer;
|
|
private MatchingRuleIndex(EntryContainer entryContainer, AttributeType attributeType, State state, Indexer indexer,
|
int indexEntryLimit)
|
{
|
super(getIndexName(entryContainer, attributeType, indexer.getIndexID()), state, indexEntryLimit, entryContainer);
|
this.attributeType = attributeType;
|
this.indexer = indexer;
|
}
|
|
Set<ByteString> indexEntry(Entry entry)
|
{
|
final Set<ByteString> keys = new HashSet<>();
|
indexEntry(entry, keys);
|
return keys;
|
}
|
|
private void modifyEntry(Entry oldEntry, Entry newEntry, Map<ByteString, Boolean> modifiedKeys)
|
{
|
for (ByteString key : indexEntry(oldEntry))
|
{
|
modifiedKeys.put(key, false);
|
}
|
|
for (ByteString key : indexEntry(newEntry))
|
{
|
final Boolean needsAdding = modifiedKeys.get(key);
|
if (needsAdding == null)
|
{
|
// This value has been added.
|
modifiedKeys.put(key, true);
|
}
|
else if (!needsAdding)
|
{
|
// This value has not been added or removed.
|
modifiedKeys.remove(key);
|
}
|
}
|
}
|
|
void indexEntry(Entry entry, Set<ByteString> keys)
|
{
|
for (Attribute attr : entry.getAttribute(attributeType))
|
{
|
if (!attr.isVirtual())
|
{
|
for (ByteString value : attr)
|
{
|
try
|
{
|
indexer.createKeys(Schema.getDefaultSchema(), value, keys);
|
|
/*
|
* Optimization for presence: return immediately after first value since all values
|
* have the same key.
|
*/
|
if (indexer == PRESENCE_INDEXER)
|
{
|
return;
|
}
|
}
|
catch (DecodeException e)
|
{
|
logger.traceException(e);
|
}
|
}
|
}
|
}
|
}
|
|
@Override
|
public String keyToString(ByteString key)
|
{
|
return indexer.keyToHumanReadableString(key);
|
}
|
|
@Override
|
public ByteString generateKey(String key)
|
{
|
try
|
{
|
SortedSet<ByteString> keys = new TreeSet<>();
|
indexer.createKeys(Schema.getDefaultSchema(), ByteString.valueOfUtf8(key), keys);
|
return keys.first();
|
}
|
catch (DecodeException e)
|
{
|
return super.generateKey(key);
|
}
|
}
|
}
|
|
/** The key bytes used for the presence index as a {@link ByteString}. */
|
static final ByteString PRESENCE_KEY = ByteString.valueOfUtf8("+");
|
|
/** A special indexer for generating presence indexes. */
|
private static final Indexer PRESENCE_INDEXER = new Indexer()
|
{
|
@Override
|
public void createKeys(Schema schema, ByteSequence value, Collection<ByteString> keys) throws DecodeException
|
{
|
keys.add(PRESENCE_KEY);
|
}
|
|
@Override
|
public String keyToHumanReadableString(ByteSequence key)
|
{
|
return "PRESENCE";
|
}
|
|
@Override
|
public String getIndexID()
|
{
|
return IndexType.PRESENCE.toString();
|
}
|
};
|
|
/*
|
* FIXME Matthew Swift: Once the matching rules have been migrated we should
|
* revisit this class. All of the evaluateXXX methods should go (the Matcher
|
* class in the SDK could implement the logic, I hope).
|
*/
|
|
/** The entryContainer in which this attribute index resides. */
|
private final EntryContainer entryContainer;
|
|
/** The attribute index configuration. */
|
private BackendIndexCfg config;
|
|
/** The mapping from names to indexes. */
|
private Map<String, MatchingRuleIndex> indexIdToIndexes;
|
private IndexingOptions indexingOptions;
|
private final State state;
|
|
AttributeIndex(BackendIndexCfg config, State state, EntryContainer entryContainer) throws ConfigException
|
{
|
this.entryContainer = entryContainer;
|
this.config = config;
|
this.state = state;
|
this.indexingOptions = new IndexingOptionsImpl(config.getSubstringLength());
|
this.indexIdToIndexes = Collections.unmodifiableMap(buildIndexes(entryContainer, state, config));
|
}
|
|
private static Map<String, MatchingRuleIndex> buildIndexes(EntryContainer entryContainer, State state,
|
BackendIndexCfg config) throws ConfigException
|
{
|
final AttributeType attributeType = config.getAttribute();
|
final int indexEntryLimit = config.getIndexEntryLimit();
|
final IndexingOptions indexingOptions = new IndexingOptionsImpl(config.getSubstringLength());
|
|
Collection<Indexer> indexers = new ArrayList<>();
|
for(IndexType indexType : config.getIndexType()) {
|
switch (indexType)
|
{
|
case PRESENCE:
|
indexers.add(PRESENCE_INDEXER);
|
break;
|
case EXTENSIBLE:
|
indexers.addAll(
|
getExtensibleIndexers(config.getAttribute(), config.getIndexExtensibleMatchingRule(), indexingOptions));
|
break;
|
case APPROXIMATE:
|
case EQUALITY:
|
case ORDERING:
|
case SUBSTRING:
|
MatchingRule rule = getMatchingRule(indexType, attributeType);
|
throwIfNoMatchingRule(attributeType, indexType, rule);
|
indexers.addAll(rule.createIndexers(indexingOptions));
|
break;
|
default:
|
throw new ConfigException(ERR_CONFIG_INDEX_TYPE_NEEDS_MATCHING_RULE.get(attributeType, indexType));
|
}
|
}
|
return buildIndexesForIndexers(entryContainer, attributeType, state, indexEntryLimit, indexers);
|
}
|
|
private static void throwIfNoMatchingRule(AttributeType attributeType, IndexType indexType, MatchingRule rule)
|
throws ConfigException
|
{
|
if (rule == null)
|
{
|
throw new ConfigException(ERR_CONFIG_INDEX_TYPE_NEEDS_MATCHING_RULE.get(attributeType, indexType));
|
}
|
}
|
|
private static Map<String, MatchingRuleIndex> buildIndexesForIndexers(EntryContainer entryContainer,
|
AttributeType attributeType, State state, int indexEntryLimit, Collection<? extends Indexer> indexers)
|
{
|
final Map<String, MatchingRuleIndex> indexes = new HashMap<>();
|
for (Indexer indexer : indexers)
|
{
|
final String indexID = indexer.getIndexID();
|
if (!indexes.containsKey(indexID))
|
{
|
indexes.put(indexID, new MatchingRuleIndex(entryContainer, attributeType, state, indexer, indexEntryLimit));
|
}
|
}
|
return indexes;
|
}
|
|
private static Collection<Indexer> getExtensibleIndexers(AttributeType attributeType, Set<String> extensibleRules,
|
IndexingOptions options) throws ConfigException
|
{
|
IndexType indexType = IndexType.EXTENSIBLE;
|
if (extensibleRules == null || extensibleRules.isEmpty())
|
{
|
throw new ConfigException(ERR_CONFIG_INDEX_TYPE_NEEDS_MATCHING_RULE.get(attributeType, indexType));
|
}
|
|
final Collection<Indexer> indexers = new ArrayList<>();
|
for (final String ruleName : extensibleRules)
|
{
|
final MatchingRule rule = DirectoryServer.getMatchingRule(toLowerCase(ruleName));
|
throwIfNoMatchingRule(attributeType, indexType, rule);
|
indexers.addAll(rule.createIndexers(options));
|
}
|
|
return indexers;
|
}
|
|
private static TreeName getIndexName(EntryContainer entryContainer, AttributeType attrType, String indexID)
|
{
|
return new TreeName(entryContainer.getTreePrefix(), attrType.getNameOrOID() + "." + indexID);
|
}
|
|
/**
|
* Open the attribute index.
|
*
|
* @param txn a non null transaction
|
* @param createOnDemand true if the tree should be created if it does not exist
|
* @throws StorageRuntimeException if an error occurs while opening the index
|
*/
|
void open(WriteableTransaction txn, boolean createOnDemand) throws StorageRuntimeException
|
{
|
for (Index index : indexIdToIndexes.values())
|
{
|
index.open(txn, createOnDemand);
|
}
|
config.addChangeListener(this);
|
}
|
|
@Override
|
public void close()
|
{
|
config.removeChangeListener(this);
|
}
|
|
/**
|
* Get the attribute type of this attribute index.
|
* @return The attribute type of this attribute index.
|
*/
|
AttributeType getAttributeType()
|
{
|
return config.getAttribute();
|
}
|
|
/**
|
* Return the indexing options of this AttributeIndex.
|
*
|
* @return the indexing options of this AttributeIndex.
|
*/
|
IndexingOptions getIndexingOptions()
|
{
|
return indexingOptions;
|
}
|
|
/**
|
* Returns {@code true} if this attribute index supports the provided index type.
|
*
|
* @param indexType
|
* The index type.
|
* @return {@code true} if this attribute index supports the provided index type.
|
*/
|
boolean isIndexed(org.opends.server.types.IndexType indexType)
|
{
|
Set<IndexType> indexTypes = config.getIndexType();
|
switch (indexType)
|
{
|
case PRESENCE:
|
return indexTypes.contains(IndexType.PRESENCE);
|
|
case EQUALITY:
|
return indexTypes.contains(IndexType.EQUALITY);
|
|
case SUBSTRING:
|
case SUBINITIAL:
|
case SUBANY:
|
case SUBFINAL:
|
return indexTypes.contains(IndexType.SUBSTRING);
|
|
case GREATER_OR_EQUAL:
|
case LESS_OR_EQUAL:
|
return indexTypes.contains(IndexType.ORDERING);
|
|
case APPROXIMATE:
|
return indexTypes.contains(IndexType.APPROXIMATE);
|
|
default:
|
return false;
|
}
|
}
|
|
/**
|
* Update the attribute index for a new entry.
|
*
|
* @param buffer The index buffer to use to store the added keys
|
* @param entryID The entry ID.
|
* @param entry The contents of the new entry.
|
* @throws StorageRuntimeException If an error occurs in the storage.
|
* @throws DirectoryException If a Directory Server error occurs.
|
*/
|
void addEntry(IndexBuffer buffer, EntryID entryID, Entry entry) throws StorageRuntimeException, DirectoryException
|
{
|
for (MatchingRuleIndex index : indexIdToIndexes.values())
|
{
|
for (ByteString key : index.indexEntry(entry))
|
{
|
buffer.put(index, key, entryID);
|
}
|
}
|
}
|
|
/**
|
* Update the attribute index for a deleted entry.
|
*
|
* @param buffer The index buffer to use to store the deleted keys
|
* @param entryID The entry ID
|
* @param entry The contents of the deleted entry.
|
* @throws StorageRuntimeException If an error occurs in the storage.
|
* @throws DirectoryException If a Directory Server error occurs.
|
*/
|
void removeEntry(IndexBuffer buffer, EntryID entryID, Entry entry) throws StorageRuntimeException, DirectoryException
|
{
|
for (MatchingRuleIndex index : indexIdToIndexes.values())
|
{
|
for (ByteString key : index.indexEntry(entry))
|
{
|
buffer.remove(index, key, entryID);
|
}
|
}
|
}
|
|
/**
|
* Update the index to reflect a sequence of modifications in a Modify operation.
|
*
|
* @param buffer The index buffer used to buffer up the index changes.
|
* @param entryID The ID of the entry that was modified.
|
* @param oldEntry The entry before the modifications were applied.
|
* @param newEntry The entry after the modifications were applied.
|
* @throws StorageRuntimeException If an error occurs during an operation on a
|
* storage.
|
*/
|
void modifyEntry(IndexBuffer buffer, EntryID entryID, Entry oldEntry, Entry newEntry) throws StorageRuntimeException
|
{
|
for (MatchingRuleIndex index : indexIdToIndexes.values())
|
{
|
TreeMap<ByteString, Boolean> modifiedKeys = new TreeMap<>();
|
index.modifyEntry(oldEntry, newEntry, modifiedKeys);
|
for (Map.Entry<ByteString, Boolean> modifiedKey : modifiedKeys.entrySet())
|
{
|
if (modifiedKey.getValue())
|
{
|
buffer.put(index, modifiedKey.getKey(), entryID);
|
}
|
else
|
{
|
buffer.remove(index, modifiedKey.getKey(), entryID);
|
}
|
}
|
}
|
}
|
|
/**
|
* Retrieve the entry IDs that might match the provided assertion.
|
*
|
* @param indexQuery
|
* The query used to retrieve entries.
|
* @param indexName
|
* The name of index used to retrieve entries.
|
* @param filter
|
* The filter on entries.
|
* @param debugBuffer
|
* If not null, a diagnostic string will be written which will help
|
* determine how the indexes contributed to this search.
|
* @param monitor
|
* The backend monitor provider that will keep index
|
* filter usage statistics.
|
* @return The candidate entry IDs that might contain the filter assertion value.
|
*/
|
private static EntryIDSet evaluateIndexQuery(IndexQuery indexQuery, String indexName, SearchFilter filter,
|
StringBuilder debugBuffer, BackendMonitor monitor)
|
{
|
// FIXME equivalent code exists in evaluateExtensibleFilter()
|
LocalizableMessageBuilder debugMessage = monitor.isFilterUseEnabled() ? new LocalizableMessageBuilder() : null;
|
StringBuilder indexNameOut = debugBuffer == null ? null : new StringBuilder();
|
EntryIDSet results = indexQuery.evaluate(debugMessage, indexNameOut);
|
|
if (debugBuffer != null)
|
{
|
appendDebugIndexInformation(debugBuffer, filter.getAttributeType(), indexName);
|
appendDebugUnindexedInformation(debugBuffer, filter.getAttributeType(), indexNameOut);
|
}
|
|
updateStats(monitor, filter, results, debugMessage);
|
return results;
|
}
|
|
private static void updateStats(BackendMonitor monitor, SearchFilter filter, EntryIDSet idSet,
|
LocalizableMessageBuilder debugMessage)
|
{
|
if (monitor.isFilterUseEnabled())
|
{
|
if (idSet.isDefined())
|
{
|
monitor.updateStats(filter, idSet.size());
|
}
|
else
|
{
|
monitor.updateStats(filter, debugMessage.toMessage());
|
}
|
}
|
}
|
|
/**
|
* Appends additional traces to {@code debugsearchindex} when a filter successfully used
|
* an auxiliary index type during index query.
|
*
|
* @param debugBuffer the current debugsearchindex buffer
|
* @param indexName the name of the index type
|
*/
|
private static void appendDebugUnindexedInformation(StringBuilder debugBuffer, AttributeType attrType,
|
StringBuilder indexName)
|
{
|
if (indexName.length() > 0)
|
{
|
debugBuffer.append(newUndefinedSet());
|
appendDebugIndexInformation(debugBuffer, attrType, indexName);
|
}
|
}
|
|
private static void appendDebugIndexInformation(StringBuilder debugBuffer, AttributeType attrType,
|
CharSequence indexName)
|
{
|
String attrNameOrOID = attrType.getNameOrOID();
|
debugBuffer.append("[INDEX:").append(attrNameOrOID).append(".").append(indexName).append("]");
|
}
|
|
private static void appendDebugIndexesInformation(StringBuilder debugBuffer, AttributeType attrType,
|
Collection<? extends Indexer> indexers)
|
{
|
final String attrNameOrOID = attrType.getNameOrOID();
|
debugBuffer.append("[INDEX:");
|
boolean isFirst = true;
|
for (Indexer indexer : indexers)
|
{
|
if (isFirst)
|
{
|
isFirst = false;
|
}
|
else
|
{
|
debugBuffer.append(" ");
|
}
|
debugBuffer.append(attrNameOrOID).append(".").append(indexer.getIndexID());
|
}
|
debugBuffer.append("]");
|
}
|
|
/**
|
* Retrieve the entry IDs that might match two filters that restrict a value
|
* to both a lower bound and an upper bound.
|
*
|
* @param indexQueryFactory
|
* The index query factory to use for the evaluation
|
* @param filter1
|
* The first filter, that is either a less-or-equal filter or a
|
* greater-or-equal filter.
|
* @param filter2
|
* The second filter, that is either a less-or-equal filter or a
|
* greater-or-equal filter. It must not be of the same type than the
|
* first filter.
|
* @param debugBuffer
|
* If not null, a diagnostic string will be written which will help
|
* determine how the indexes contributed to this search.
|
* @param monitor
|
* The backend monitor provider that will keep index
|
* filter usage statistics.
|
* @return The candidate entry IDs that might contain match both filters.
|
*/
|
static EntryIDSet evaluateBoundedRange(IndexQueryFactory<IndexQuery> indexQueryFactory,
|
SearchFilter filter1, SearchFilter filter2, StringBuilder debugBuffer, BackendMonitor monitor)
|
{
|
// TODO : this implementation is not optimal
|
// as it implies two separate evaluations instead of a single one, thus defeating the purpose of
|
// the optimization done in IndexFilter#evaluateLogicalAndFilter method.
|
// One solution could be to implement a boundedRangeAssertion that combine the two operations in one.
|
// Such an optimization can only work for attributes declared as SINGLE-VALUE, though, since multiple
|
// values may match both filters with values outside the range. See OPENDJ-2194.
|
StringBuilder tmpBuff1 = debugBuffer != null ? new StringBuilder() : null;
|
StringBuilder tmpBuff2 = debugBuffer != null ? new StringBuilder() : null;
|
EntryIDSet results1 = evaluate(indexQueryFactory, filter1, tmpBuff1, monitor);
|
EntryIDSet results2 = evaluate(indexQueryFactory, filter2, tmpBuff2, monitor);
|
if (debugBuffer != null)
|
{
|
debugBuffer
|
.append(filter1).append(tmpBuff1).append(results1)
|
.append(filter2).append(tmpBuff2).append(results2);
|
}
|
results1.retainAll(results2);
|
return results1;
|
}
|
|
private static EntryIDSet evaluate(IndexQueryFactory<IndexQuery> indexQueryFactory, SearchFilter filter,
|
StringBuilder debugBuffer, BackendMonitor monitor)
|
{
|
boolean isLessOrEqual = filter.getFilterType() == FilterType.LESS_OR_EQUAL;
|
IndexFilterType indexFilterType = isLessOrEqual ? IndexFilterType.LESS_OR_EQUAL : IndexFilterType.GREATER_OR_EQUAL;
|
return evaluateFilter(indexQueryFactory, indexFilterType, filter, debugBuffer, monitor);
|
}
|
|
/**
|
* Retrieve the entry IDs that might match a filter.
|
*
|
* @param indexQueryFactory the index query factory to use for the evaluation
|
* @param indexFilterType the index type filter
|
* @param filter The filter.
|
* @param debugBuffer If not null, a diagnostic string will be written
|
* which will help determine how the indexes contributed
|
* to this search.
|
* @param monitor The backend monitor provider that will keep
|
* index filter usage statistics.
|
* @return The candidate entry IDs that might contain a value
|
* that matches the filter type.
|
*/
|
static EntryIDSet evaluateFilter(IndexQueryFactory<IndexQuery> indexQueryFactory, IndexFilterType indexFilterType,
|
SearchFilter filter, StringBuilder debugBuffer, BackendMonitor monitor)
|
{
|
try
|
{
|
final IndexQuery indexQuery = getIndexQuery(indexQueryFactory, indexFilterType, filter);
|
return evaluateIndexQuery(indexQuery, indexFilterType.toString(), filter, debugBuffer, monitor);
|
}
|
catch (DecodeException e)
|
{
|
logger.traceException(e);
|
return newUndefinedSet();
|
}
|
}
|
|
private static IndexQuery getIndexQuery(IndexQueryFactory<IndexQuery> indexQueryFactory,
|
IndexFilterType indexFilterType, SearchFilter filter) throws DecodeException
|
{
|
MatchingRule rule;
|
switch (indexFilterType)
|
{
|
case EQUALITY:
|
rule = filter.getAttributeType().getEqualityMatchingRule();
|
if (rule != null) {
|
Assertion assertion = rule.getAssertion(filter.getAssertionValue());
|
return assertion.createIndexQuery(indexQueryFactory);
|
}
|
break;
|
|
case PRESENCE:
|
return indexQueryFactory.createMatchAllQuery();
|
|
case GREATER_OR_EQUAL:
|
rule = filter.getAttributeType().getOrderingMatchingRule();
|
if (rule != null) {
|
Assertion assertion = rule.getGreaterOrEqualAssertion(filter.getAssertionValue());
|
return assertion.createIndexQuery(indexQueryFactory);
|
}
|
break;
|
|
case LESS_OR_EQUAL:
|
rule = filter.getAttributeType().getOrderingMatchingRule();
|
if (rule != null) {
|
Assertion assertion = rule.getLessOrEqualAssertion(filter.getAssertionValue());
|
return assertion.createIndexQuery(indexQueryFactory);
|
}
|
break;
|
|
case SUBSTRING:
|
rule = filter.getAttributeType().getSubstringMatchingRule();
|
if (rule != null) {
|
Assertion assertion = rule.getSubstringAssertion(filter.getSubInitialElement(),
|
filter.getSubAnyElements(),
|
filter.getSubFinalElement());
|
return assertion.createIndexQuery(indexQueryFactory);
|
}
|
break;
|
|
case APPROXIMATE:
|
rule = filter.getAttributeType().getApproximateMatchingRule();
|
if (rule != null) {
|
Assertion assertion = rule.getAssertion(filter.getAssertionValue());
|
return assertion.createIndexQuery(indexQueryFactory);
|
}
|
break;
|
|
default:
|
break;
|
}
|
|
// The filter is undefined.
|
return indexQueryFactory.createMatchAllQuery();
|
}
|
|
/**
|
* Get a string representation of this object.
|
* @return return A string representation of this object.
|
*/
|
@Override
|
public String toString()
|
{
|
return getName();
|
}
|
|
@Override
|
public synchronized boolean isConfigurationChangeAcceptable(
|
BackendIndexCfg cfg, List<LocalizableMessage> unacceptableReasons)
|
{
|
return isIndexAcceptable(cfg, IndexType.EQUALITY, unacceptableReasons)
|
&& isIndexAcceptable(cfg, IndexType.SUBSTRING, unacceptableReasons)
|
&& isIndexAcceptable(cfg, IndexType.ORDERING, unacceptableReasons)
|
&& isIndexAcceptable(cfg, IndexType.APPROXIMATE, unacceptableReasons)
|
&& isExtensibleIndexAcceptable(cfg, unacceptableReasons);
|
}
|
|
private boolean isExtensibleIndexAcceptable(BackendIndexCfg cfg, List<LocalizableMessage> unacceptableReasons)
|
{
|
IndexType indexType = IndexType.EXTENSIBLE;
|
AttributeType attrType = cfg.getAttribute();
|
if (cfg.getIndexType().contains(indexType))
|
{
|
Set<String> newRules = cfg.getIndexExtensibleMatchingRule();
|
if (newRules == null || newRules.isEmpty())
|
{
|
unacceptableReasons.add(ERR_CONFIG_INDEX_TYPE_NEEDS_MATCHING_RULE.get(attrType, indexType));
|
return false;
|
}
|
}
|
return true;
|
}
|
|
private static boolean isIndexAcceptable(BackendIndexCfg cfg, IndexType indexType,
|
List<LocalizableMessage> unacceptableReasons)
|
{
|
final AttributeType attrType = cfg.getAttribute();
|
if (cfg.getIndexType().contains(indexType) && getMatchingRule(indexType, attrType) == null)
|
{
|
unacceptableReasons.add(ERR_CONFIG_INDEX_TYPE_NEEDS_MATCHING_RULE.get(attrType, indexType));
|
return false;
|
}
|
return true;
|
}
|
|
static MatchingRule getMatchingRule(IndexType indexType, AttributeType attrType)
|
{
|
switch (indexType)
|
{
|
case APPROXIMATE:
|
return attrType.getApproximateMatchingRule();
|
case EQUALITY:
|
return attrType.getEqualityMatchingRule();
|
case ORDERING:
|
return attrType.getOrderingMatchingRule();
|
case SUBSTRING:
|
return attrType.getSubstringMatchingRule();
|
default:
|
throw new IllegalArgumentException("Not implemented for index type " + indexType);
|
}
|
}
|
|
@Override
|
public synchronized ConfigChangeResult applyConfigurationChange(final BackendIndexCfg newConfiguration)
|
{
|
final ConfigChangeResult ccr = new ConfigChangeResult();
|
final IndexingOptions newIndexingOptions = new IndexingOptionsImpl(newConfiguration.getSubstringLength());
|
try
|
{
|
final Map<String, MatchingRuleIndex> newIndexIdToIndexes = buildIndexes(entryContainer, state, newConfiguration);
|
|
final Map<String, MatchingRuleIndex> removedIndexes = new HashMap<>(indexIdToIndexes);
|
removedIndexes.keySet().removeAll(newIndexIdToIndexes.keySet());
|
|
final Map<String, MatchingRuleIndex> addedIndexes = new HashMap<>(newIndexIdToIndexes);
|
addedIndexes.keySet().removeAll(indexIdToIndexes.keySet());
|
|
final Map<String, MatchingRuleIndex> updatedIndexes = new HashMap<>(indexIdToIndexes);
|
updatedIndexes.keySet().retainAll(newIndexIdToIndexes.keySet());
|
|
// Replace instances of Index created by buildIndexes() with the one already opened and present in the actual
|
// indexIdToIndexes
|
newIndexIdToIndexes.putAll(updatedIndexes);
|
|
// Open added indexes *before* adding them to indexIdToIndexes
|
entryContainer.getRootContainer().getStorage().write(new WriteOperation()
|
{
|
@Override
|
public void run(WriteableTransaction txn) throws Exception
|
{
|
for (MatchingRuleIndex addedIndex : addedIndexes.values())
|
{
|
createIndex(txn, addedIndex, ccr);
|
}
|
}
|
});
|
|
config = newConfiguration;
|
indexingOptions = newIndexingOptions;
|
indexIdToIndexes = Collections.unmodifiableMap(newIndexIdToIndexes);
|
|
// We get exclusive lock to ensure that no query is actually using the indexes that will be deleted.
|
entryContainer.lock();
|
try
|
{
|
entryContainer.getRootContainer().getStorage().write(new WriteOperation()
|
{
|
@Override
|
public void run(WriteableTransaction txn) throws Exception
|
{
|
for (MatchingRuleIndex removedIndex : removedIndexes.values())
|
{
|
deleteIndex(txn, entryContainer, removedIndex);
|
}
|
}
|
});
|
}
|
finally
|
{
|
entryContainer.unlock();
|
}
|
|
for (Index updatedIndex : updatedIndexes.values())
|
{
|
updateIndex(updatedIndex, newConfiguration.getIndexEntryLimit(), ccr);
|
}
|
}
|
catch (Exception e)
|
{
|
ccr.setResultCode(DirectoryServer.getServerErrorResultCode());
|
ccr.addMessage(LocalizableMessage.raw(StaticUtils.stackTraceToSingleLineString(e)));
|
}
|
|
return ccr;
|
}
|
|
private static void createIndex(WriteableTransaction txn, MatchingRuleIndex index, ConfigChangeResult ccr)
|
{
|
index.open(txn, true);
|
if (!index.isTrusted())
|
{
|
ccr.setAdminActionRequired(true);
|
ccr.addMessage(NOTE_INDEX_ADD_REQUIRES_REBUILD.get(index.getName()));
|
}
|
}
|
|
private static void updateIndex(Index updatedIndex, int newIndexEntryLimit, ConfigChangeResult ccr)
|
{
|
if (updatedIndex.setIndexEntryLimit(newIndexEntryLimit))
|
{
|
// This index can still be used since index size limit doesn't impact validity of the results.
|
ccr.setAdminActionRequired(true);
|
ccr.addMessage(NOTE_CONFIG_INDEX_ENTRY_LIMIT_REQUIRES_REBUILD.get(updatedIndex.getName()));
|
}
|
}
|
|
private static void deleteIndex(WriteableTransaction txn, EntryContainer entryContainer, Index index)
|
{
|
entryContainer.exclusiveLock.lock();
|
try
|
{
|
entryContainer.deleteTree(txn, index);
|
}
|
finally
|
{
|
entryContainer.exclusiveLock.unlock();
|
}
|
}
|
|
/**
|
* Return true iff this index is trusted.
|
* @return the trusted state of this index
|
*/
|
boolean isTrusted()
|
{
|
for (Index index : indexIdToIndexes.values())
|
{
|
if (!index.isTrusted())
|
{
|
return false;
|
}
|
}
|
return true;
|
}
|
|
/**
|
* Get the tree name prefix for indexes in this attribute index.
|
*
|
* @return tree name for this container.
|
*/
|
String getName()
|
{
|
return entryContainer.getTreePrefix()
|
+ "_"
|
+ config.getAttribute().getNameOrOID();
|
}
|
|
Map<String, MatchingRuleIndex> getNameToIndexes()
|
{
|
return indexIdToIndexes;
|
}
|
|
/**
|
* Retrieve the entry IDs that might match an extensible filter.
|
*
|
* @param indexQueryFactory the index query factory to use for the evaluation
|
* @param filter The extensible filter.
|
* @param debugBuffer If not null, a diagnostic string will be written
|
* which will help determine how the indexes contributed
|
* to this search.
|
* @param monitor The backend monitor provider that will keep
|
* index filter usage statistics.
|
* @return The candidate entry IDs that might contain the filter assertion value.
|
*/
|
EntryIDSet evaluateExtensibleFilter(IndexQueryFactory<IndexQuery> indexQueryFactory,
|
SearchFilter filter, StringBuilder debugBuffer, BackendMonitor monitor)
|
{
|
//Get the Matching Rule OID of the filter.
|
String matchRuleOID = filter.getMatchingRuleID();
|
/*
|
* Use the default equality index in two conditions:
|
* 1. There is no matching rule provided
|
* 2. The matching rule specified is actually the default equality.
|
*/
|
MatchingRule eqRule = config.getAttribute().getEqualityMatchingRule();
|
if (matchRuleOID == null
|
|| matchRuleOID.equals(eqRule.getOID())
|
|| matchRuleOID.equalsIgnoreCase(eqRule.getNameOrOID()))
|
{
|
//No matching rule is defined; use the default equality matching rule.
|
return evaluateFilter(indexQueryFactory, IndexFilterType.EQUALITY, filter, debugBuffer, monitor);
|
}
|
|
MatchingRule rule = DirectoryServer.getMatchingRule(matchRuleOID);
|
if (!ruleHasAtLeastOneIndex(rule))
|
{
|
if (monitor.isFilterUseEnabled())
|
{
|
monitor.updateStats(filter,
|
INFO_INDEX_FILTER_MATCHING_RULE_NOT_INDEXED.get(matchRuleOID, config.getAttribute().getNameOrOID()));
|
}
|
return IndexQueryFactoryImpl.createNullIndexQuery().evaluate(null, null);
|
}
|
|
try
|
{
|
// FIXME equivalent code exists in evaluateIndexQuery()
|
final IndexQuery indexQuery = rule.getAssertion(filter.getAssertionValue()).createIndexQuery(indexQueryFactory);
|
LocalizableMessageBuilder debugMessage = monitor.isFilterUseEnabled() ? new LocalizableMessageBuilder() : null;
|
StringBuilder indexNameOut = debugBuffer == null ? null : new StringBuilder();
|
EntryIDSet results = indexQuery.evaluate(debugMessage, indexNameOut);
|
|
if (debugBuffer != null)
|
{
|
appendDebugIndexesInformation(debugBuffer, filter.getAttributeType(), rule.createIndexers(indexingOptions));
|
appendDebugUnindexedInformation(debugBuffer, filter.getAttributeType(), indexNameOut);
|
}
|
|
updateStats(monitor, filter, results, debugMessage);
|
return results;
|
}
|
catch (DecodeException e)
|
{
|
logger.traceException(e);
|
return IndexQueryFactoryImpl.createNullIndexQuery().evaluate(null, null);
|
}
|
}
|
|
private boolean ruleHasAtLeastOneIndex(MatchingRule rule)
|
{
|
for (Indexer indexer : rule.createIndexers(indexingOptions))
|
{
|
if (indexIdToIndexes.containsKey(indexer.getIndexID()))
|
{
|
return true;
|
}
|
}
|
return false;
|
}
|
|
/** Indexing options implementation. */
|
private static final class IndexingOptionsImpl implements IndexingOptions
|
{
|
/** The length of substring keys used in substring indexes. */
|
private int substringKeySize;
|
|
private IndexingOptionsImpl(int substringKeySize)
|
{
|
this.substringKeySize = substringKeySize;
|
}
|
|
@Override
|
public int substringKeySize()
|
{
|
return substringKeySize;
|
}
|
}
|
|
void closeAndDelete(WriteableTransaction txn)
|
{
|
close();
|
for (Index index : indexIdToIndexes.values())
|
{
|
index.delete(txn);
|
state.deleteRecord(txn, index.getName());
|
}
|
}
|
}
|