blob: b0053be0c5fd468c81f8e17affe495d8523bef40 [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.directory.server.xdbm.search.impl;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
import org.apache.directory.api.ldap.model.cursor.Cursor;
import org.apache.directory.api.ldap.model.entry.Entry;
import org.apache.directory.api.ldap.model.entry.Value;
import org.apache.directory.api.ldap.model.filter.AndNode;
import org.apache.directory.api.ldap.model.filter.ApproximateNode;
import org.apache.directory.api.ldap.model.filter.EqualityNode;
import org.apache.directory.api.ldap.model.filter.ExprNode;
import org.apache.directory.api.ldap.model.filter.GreaterEqNode;
import org.apache.directory.api.ldap.model.filter.LessEqNode;
import org.apache.directory.api.ldap.model.filter.NotNode;
import org.apache.directory.api.ldap.model.filter.OrNode;
import org.apache.directory.api.ldap.model.filter.PresenceNode;
import org.apache.directory.api.ldap.model.filter.ScopeNode;
import org.apache.directory.api.ldap.model.filter.SubstringNode;
import org.apache.directory.api.ldap.model.message.SearchScope;
import org.apache.directory.api.ldap.model.name.Dn;
import org.apache.directory.api.ldap.model.name.Rdn;
import org.apache.directory.api.ldap.model.schema.AttributeType;
import org.apache.directory.api.ldap.model.schema.MatchingRule;
import org.apache.directory.api.ldap.model.schema.Normalizer;
import org.apache.directory.api.ldap.model.schema.normalizers.NoOpNormalizer;
import org.apache.directory.api.util.exception.NotImplementedException;
import org.apache.directory.server.core.api.partition.Partition;
import org.apache.directory.server.i18n.I18n;
import org.apache.directory.server.xdbm.Index;
import org.apache.directory.server.xdbm.IndexEntry;
import org.apache.directory.server.xdbm.ParentIdAndRdn;
import org.apache.directory.server.xdbm.SingletonIndexCursor;
import org.apache.directory.server.xdbm.Store;
import org.apache.directory.server.xdbm.search.PartitionSearchResult;
import org.apache.directory.server.xdbm.search.cursor.ApproximateCursor;
import org.apache.directory.server.xdbm.search.cursor.ChildrenCursor;
import org.apache.directory.server.xdbm.search.cursor.DescendantCursor;
import org.apache.directory.server.xdbm.search.evaluator.ApproximateEvaluator;
/**
* Builds Cursors over candidates that satisfy a filter expression.
*
* @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
*/
public class CursorBuilder
{
/** The database used by this builder */
private Store db = null;
/** Evaluator dependency on a EvaluatorBuilder */
private EvaluatorBuilder evaluatorBuilder;
/**
* Creates an expression tree enumerator.
*
* @param db database used by this enumerator
* @param evaluatorBuilder the evaluator builder
*/
public CursorBuilder( Store db, EvaluatorBuilder evaluatorBuilder )
{
this.db = db;
this.evaluatorBuilder = evaluatorBuilder;
}
public <T> long build( ExprNode node, PartitionSearchResult searchResult ) throws Exception
{
Object count = node.get( "count" );
if ( ( count != null ) && ( ( Long ) count ) == 0L )
{
return 0;
}
switch ( node.getAssertionType() )
{
/* ---------- LEAF NODE HANDLING ---------- */
case APPROXIMATE:
return computeApproximate( ( ApproximateNode<T> ) node, searchResult );
case EQUALITY:
return computeEquality( ( EqualityNode<T> ) node, searchResult );
case GREATEREQ:
return computeGreaterEq( ( GreaterEqNode<T> ) node, searchResult );
case LESSEQ:
return computeLessEq( ( LessEqNode<T> ) node, searchResult );
case PRESENCE:
return computePresence( ( PresenceNode ) node, searchResult );
case SCOPE:
if ( ( ( ScopeNode ) node ).getScope() == SearchScope.ONELEVEL )
{
return computeOneLevelScope( ( ScopeNode ) node, searchResult );
}
else
{
return computeSubLevelScope( ( ScopeNode ) node, searchResult );
}
case SUBSTRING:
return computeSubstring( ( SubstringNode ) node, searchResult );
/* ---------- LOGICAL OPERATORS ---------- */
case AND:
return computeAnd( ( AndNode ) node, searchResult );
case NOT:
// Always return infinite, except if the resulting eva
return computeNot( ( NotNode ) node, searchResult );
case OR:
return computeOr( ( OrNode ) node, searchResult );
/* ---------- NOT IMPLEMENTED ---------- */
case ASSERTION:
case EXTENSIBLE:
throw new NotImplementedException();
default:
throw new IllegalStateException( I18n.err( I18n.ERR_260, node.getAssertionType() ) );
}
}
/**
* Computes the set of candidates for an Approximate filter. We will feed the set only if
* we have an index for the AT.
*/
private <T> long computeApproximate( ApproximateNode<T> node, PartitionSearchResult searchResult )
throws Exception
{
ApproximateCursor<T> cursor = new ApproximateCursor<T>( db,
( ApproximateEvaluator<T> ) evaluatorBuilder
.build( node ) );
int nbResults = 0;
Set<String> uuidSet = searchResult.getCandidateSet();
while ( cursor.next() )
{
IndexEntry<T, String> indexEntry = cursor.get();
String uuid = indexEntry.getId();
if ( !uuidSet.contains( uuid ) )
{
uuidSet.add( uuid );
nbResults++;
}
}
cursor.close();
return nbResults;
}
/**
* Computes the set of candidates for an Equality filter. We will feed the set only if
* we have an index for the AT.
*/
private <T> long computeEquality( EqualityNode<T> node, PartitionSearchResult searchResult )
throws Exception
{
Set<String> thisCandidates = ( Set<String> ) node.get( DefaultOptimizer.CANDIDATES_ANNOTATION_KEY );
if ( thisCandidates != null )
{
Set<String> candidates = searchResult.getCandidateSet();
for ( String candidate : thisCandidates )
{
candidates.add( candidate );
}
return thisCandidates.size();
}
AttributeType attributeType = node.getAttributeType();
Value<T> value = node.getValue();
int nbResults = 0;
// Fetch all the UUIDs if we have an index
if ( db.hasIndexOn( attributeType ) )
{
// Get the cursor using the index
Index<T, String> userIndex = ( Index<T, String> ) db.getIndex( attributeType );
Cursor<IndexEntry<T, String>> userIdxCursor = userIndex.forwardCursor( value.getValue() );
Set<String> uuidSet = searchResult.getCandidateSet();
// And loop on it
while ( userIdxCursor.next() )
{
IndexEntry<T, String> indexEntry = userIdxCursor.get();
String uuid = indexEntry.getId();
if ( !uuidSet.contains( uuid ) )
{
// The UUID is not present in the Set, we add it
uuidSet.add( uuid );
nbResults++;
}
}
userIdxCursor.close();
}
else
{
// No index, we will have to do a full scan
return Long.MAX_VALUE;
}
return nbResults;
}
/**
* Computes the set of candidates for an GreateEq filter. We will feed the set only if
* we have an index for the AT.
*/
private <T> long computeGreaterEq( GreaterEqNode<T> node, PartitionSearchResult searchResult )
throws Exception
{
AttributeType attributeType = node.getAttributeType();
Value<T> value = node.getValue();
int nbResults = 0;
// Fetch all the UUIDs if we have an index
if ( db.hasIndexOn( attributeType ) )
{
// Get the cursor using the index
Index<T, String> userIndex = ( Index<T, String> ) db.getIndex( attributeType );
Cursor<IndexEntry<T, String>> userIdxCursor = userIndex.forwardCursor();
// Position the index on the element we should start from
IndexEntry<T, String> indexEntry = new IndexEntry<T, String>();
indexEntry.setKey( value.getValue() );
userIdxCursor.before( indexEntry );
Set<String> uuidSet = searchResult.getCandidateSet();
// And loop on it
while ( userIdxCursor.next() )
{
indexEntry = userIdxCursor.get();
String uuid = indexEntry.getId();
if ( !uuidSet.contains( uuid ) )
{
// The UUID is not present in the Set, we add it
uuidSet.add( uuid );
nbResults++;
}
}
userIdxCursor.close();
}
else
{
// No index, we will have to do a full scan
return Long.MAX_VALUE;
}
return nbResults;
}
/**
* Computes the set of candidates for an LessEq filter. We will feed the set only if
* we have an index for the AT.
*/
private <T> long computeLessEq( LessEqNode<T> node, PartitionSearchResult searchResult )
throws Exception
{
AttributeType attributeType = node.getAttributeType();
Value<T> value = node.getValue();
int nbResults = 0;
// Fetch all the UUIDs if we have an index
if ( db.hasIndexOn( attributeType ) )
{
// Get the cursor using the index
Index<T, String> userIndex = ( Index<T, String> ) db.getIndex( attributeType );
Cursor<IndexEntry<T, String>> userIdxCursor = userIndex.forwardCursor();
// Position the index on the element we should start from
IndexEntry<T, String> indexEntry = new IndexEntry<T, String>();
indexEntry.setKey( value.getValue() );
userIdxCursor.after( indexEntry );
Set<String> uuidSet = searchResult.getCandidateSet();
// And loop on it
while ( userIdxCursor.previous() )
{
indexEntry = userIdxCursor.get();
String uuid = indexEntry.getId();
if ( !uuidSet.contains( uuid ) )
{
// The UUID is not present in the Set, we add it
uuidSet.add( uuid );
nbResults++;
}
}
userIdxCursor.close();
}
else
{
// No index, we will have to do a full scan
return Long.MAX_VALUE;
}
return nbResults;
}
/**
* Computes the set of candidates for a Presence filter. We will feed the set only if
* we have an index for the AT.
*/
private <T> long computePresence( PresenceNode node, PartitionSearchResult searchResult )
throws Exception
{
AttributeType attributeType = node.getAttributeType();
int nbResults = 0;
// Fetch all the UUIDs if we have an index
if ( db.hasIndexOn( attributeType ) )
{
// Get the cursor using the index
Cursor<IndexEntry<String, String>> presenceCursor = db.getPresenceIndex().forwardCursor(
attributeType.getOid() );
// Position the index on the element we should start from
IndexEntry<String, String> indexEntry = new IndexEntry<String, String>();
Set<String> uuidSet = searchResult.getCandidateSet();
// And loop on it
while ( presenceCursor.next() )
{
indexEntry = presenceCursor.get();
String uuid = indexEntry.getId();
if ( !uuidSet.contains( uuid ) )
{
// The UUID is not present in the Set, we add it
uuidSet.add( uuid );
nbResults++;
}
}
presenceCursor.close();
}
else
{
// No index, we will have to do a full scan
return Long.MAX_VALUE;
}
return nbResults;
}
/**
* Computes the set of candidates for a OneLevelScope filter. We will feed the set only if
* we have an index for the AT.
*/
private long computeOneLevelScope( ScopeNode node, PartitionSearchResult searchResult )
throws Exception
{
int nbResults = 0;
// We use the RdnIndex to get all the entries from a starting point
// and below up to the number of children
Cursor<IndexEntry<ParentIdAndRdn, String>> rdnCursor = db.getRdnIndex().forwardCursor();
IndexEntry<ParentIdAndRdn, String> startingPos = new IndexEntry<ParentIdAndRdn, String>();
startingPos.setKey( new ParentIdAndRdn( node.getBaseId(), ( Rdn[] ) null ) );
rdnCursor.before( startingPos );
Cursor<IndexEntry<String, String>> scopeCursor = new ChildrenCursor( db, node.getBaseId(), rdnCursor );
Set<String> candidateSet = searchResult.getCandidateSet();
// Fetch all the UUIDs if we have an index
// And loop on it
while ( scopeCursor.next() )
{
IndexEntry<String, String> indexEntry = scopeCursor.get();
String uuid = indexEntry.getId();
// If the entry is an alias, and we asked for it to be dereferenced,
// we will dereference the alias
if ( searchResult.isDerefAlways() || searchResult.isDerefInSearching() )
{
Dn aliasedDn = db.getAliasIndex().reverseLookup( uuid );
if ( aliasedDn != null )
{
aliasedDn.apply( evaluatorBuilder.getSchemaManager() );
String aliasedId = db.getEntryId( aliasedDn );
// This is an alias. Add it to the set of candidates to process, if it's not already
// present in the candidate set
if ( !candidateSet.contains( aliasedId ) )
{
candidateSet.add( aliasedId );
nbResults++;
}
}
else
{
// This is not an alias
if ( !candidateSet.contains( uuid ) )
{
// The UUID is not present in the Set, we add it
candidateSet.add( uuid );
nbResults++;
}
}
}
else
{
if ( !candidateSet.contains( uuid ) )
{
// The UUID is not present in the Set, we add it
candidateSet.add( uuid );
nbResults++;
}
}
}
scopeCursor.close();
return nbResults;
}
/**
* Computes the set of candidates for a SubLevelScope filter. We will feed the set only if
* we have an index for the AT.
*/
private long computeSubLevelScope( ScopeNode node, PartitionSearchResult searchResult )
throws Exception
{
// If we are searching from the partition DN, better get out.
String contextEntryId = db.getEntryId( ( ( Partition ) db ).getSuffixDn() );
if ( node.getBaseId() == contextEntryId )
{
return Long.MAX_VALUE;
}
int nbResults = 0;
// We use the RdnIndex to get all the entries from a starting point
// and below up to the number of descendant
String baseId = node.getBaseId();
ParentIdAndRdn parentIdAndRdn = db.getRdnIndex().reverseLookup( baseId );
IndexEntry<ParentIdAndRdn, String> startingPos = new IndexEntry<ParentIdAndRdn, String>();
startingPos.setKey( parentIdAndRdn );
startingPos.setId( baseId );
Cursor<IndexEntry<ParentIdAndRdn, String>> rdnCursor = new SingletonIndexCursor<ParentIdAndRdn>(
startingPos );
String parentId = parentIdAndRdn.getParentId();
Cursor<IndexEntry<String, String>> scopeCursor = new DescendantCursor( db, baseId, parentId, rdnCursor );
Set<String> candidateSet = searchResult.getCandidateSet();
// Fetch all the UUIDs if we have an index
// And loop on it
while ( scopeCursor.next() )
{
IndexEntry<String, String> indexEntry = scopeCursor.get();
String uuid = indexEntry.getId();
// If the entry is an alias, and we asked for it to be dereferenced,
// we will dereference the alias
if ( searchResult.isDerefAlways() || searchResult.isDerefInSearching() )
{
Dn aliasedDn = db.getAliasIndex().reverseLookup( uuid );
if ( aliasedDn != null )
{
aliasedDn.apply( evaluatorBuilder.getSchemaManager() );
String aliasedId = db.getEntryId( aliasedDn );
// This is an alias. Add it to the set of candidates to process, if it's not already
// present in the candidate set
if ( !candidateSet.contains( aliasedId ) )
{
candidateSet.add( aliasedId );
nbResults++;
ScopeNode newScopeNode = new ScopeNode(
node.getDerefAliases(),
aliasedDn,
aliasedId,
node.getScope() );
nbResults += computeSubLevelScope( newScopeNode, searchResult );
}
}
else
{
// This is not an alias
if ( !candidateSet.contains( uuid ) )
{
// The UUID is not present in the Set, we add it
candidateSet.add( uuid );
nbResults++;
}
}
}
else
{
if ( !candidateSet.contains( uuid ) )
{
// The UUID is not present in the Set, we add it
candidateSet.add( uuid );
nbResults++;
}
}
}
scopeCursor.close();
return nbResults;
}
/**
* Computes the set of candidates for an Substring filter. We will feed the set only if
* we have an index for the AT.
*/
private long computeSubstring( SubstringNode node, PartitionSearchResult searchResult )
throws Exception
{
AttributeType attributeType = node.getAttributeType();
// Fetch all the UUIDs if we have an index
if ( db.hasIndexOn( attributeType ) )
{
Index<String, String> userIndex = ( ( Index<String, String> ) db.getIndex( attributeType ) );
Cursor<IndexEntry<String, String>> cursor = userIndex.forwardCursor();
// Position the index on the element we should start from
IndexEntry<String, String> indexEntry = new IndexEntry<String, String>();
String initial = node.getInitial();
boolean fullIndexScan = false;
if ( initial == null )
{
fullIndexScan = true;
cursor.beforeFirst();
}
else
{
indexEntry.setKey( initial );
cursor.before( indexEntry );
}
int nbResults = 0;
MatchingRule rule = attributeType.getSubstring();
if ( rule == null )
{
rule = attributeType.getEquality();
}
Normalizer normalizer;
Pattern regexp;
if ( rule != null )
{
normalizer = rule.getNormalizer();
}
else
{
normalizer = new NoOpNormalizer( attributeType.getSyntaxOid() );
}
// compile the regular expression to search for a matching attribute
// if the attributeType is humanReadable
if ( attributeType.getSyntax().isHumanReadable() )
{
regexp = node.getRegex( normalizer );
}
else
{
regexp = null;
}
Set<String> uuidSet = searchResult.getCandidateSet();
// And loop on it
while ( cursor.next() )
{
indexEntry = cursor.get();
String key = indexEntry.getKey();
boolean matched = regexp.matcher( key ).matches();
if ( !fullIndexScan & !matched )
{
cursor.close();
return nbResults;
}
if ( !matched )
{
continue;
}
String uuid = indexEntry.getId();
if ( !uuidSet.contains( uuid ) )
{
// The UUID is not present in the Set, we add it
uuidSet.add( uuid );
nbResults++;
}
}
cursor.close();
return nbResults;
}
else
{
// No index, we will have to do a full scan
return Long.MAX_VALUE;
}
}
/**
* Creates a OrCursor over a disjunction expression branch node.
*
* @param node the disjunction expression branch node
* @return Cursor over candidates satisfying disjunction expression
* @throws Exception on db access failures
*/
private <T> long computeOr( OrNode node, PartitionSearchResult searchResult ) throws Exception
{
List<ExprNode> children = node.getChildren();
long nbOrResults = 0;
// Recursively create Cursors and Evaluators for each child expression node
for ( ExprNode child : children )
{
Object count = child.get( "count" );
if ( ( count != null ) && ( ( Long ) count == 0L ) )
{
long countLong = ( Long ) count;
if ( countLong == 0 )
{
// We can skip the cursor, it will not return any candidate
continue;
}
else if ( countLong == Long.MAX_VALUE )
{
// We can stop here, we will anyway do a full scan
return countLong;
}
}
long nbResults = build( child, searchResult );
if ( nbResults == Long.MAX_VALUE )
{
// We can stop here, we will anyway do a full scan
return nbResults;
}
else
{
nbOrResults += nbResults;
}
}
return nbOrResults;
}
/**
* Creates an AndCursor over a conjunction expression branch node.
*
* @param node a conjunction expression branch node
* @return Cursor over the conjunction expression
* @throws Exception on db access failures
*/
private long computeAnd( AndNode node, PartitionSearchResult searchResult ) throws Exception
{
int minIndex = 0;
long minValue = Long.MAX_VALUE;
long value = Long.MAX_VALUE;
/*
* We scan the child nodes of a branch node searching for the child
* expression node with the smallest scan count. This is the child
* we will use for iteration
*/
final List<ExprNode> children = node.getChildren();
for ( int i = 0; i < children.size(); i++ )
{
ExprNode child = children.get( i );
Object count = child.get( "count" );
if ( count == null )
{
continue;
}
value = ( Long ) count;
if ( value == 0L )
{
// No need to go any further : we won't have matching candidates anyway
return 0L;
}
if ( value < minValue )
{
minValue = value;
minIndex = i;
}
}
// Once found we return the number of candidates for this child
ExprNode minChild = children.get( minIndex );
long nbResults = build( minChild, searchResult );
return nbResults;
}
/**
* Creates an AndCursor over a conjunction expression branch node.
*
* @param node a conjunction expression branch node
* @return Cursor over the conjunction expression
* @throws Exception on db access failures
*/
private long computeNot( NotNode node, PartitionSearchResult searchResult ) throws Exception
{
final List<ExprNode> children = node.getChildren();
ExprNode child = children.get( 0 );
Object count = child.get( "count" );
if ( count == null )
{
return Long.MAX_VALUE;
}
long value = ( Long ) count;
if ( value == Long.MAX_VALUE )
{
// No need to go any further : we won't have matching candidates anyway
return 0L;
}
return Long.MAX_VALUE;
}
}