001/*
002 *  Licensed to the Apache Software Foundation (ASF) under one
003 *  or more contributor license agreements.  See the NOTICE file
004 *  distributed with this work for additional information
005 *  regarding copyright ownership.  The ASF licenses this file
006 *  to you under the Apache License, Version 2.0 (the
007 *  "License"); you may not use this file except in compliance
008 *  with the License.  You may obtain a copy of the License at
009 * 
010 *    http://www.apache.org/licenses/LICENSE-2.0
011 * 
012 *  Unless required by applicable law or agreed to in writing,
013 *  software distributed under the License is distributed on an
014 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 *  KIND, either express or implied.  See the License for the
016 *  specific language governing permissions and limitations
017 *  under the License.
018 * 
019 */
020package org.apache.directory.server.xdbm.search.cursor;
021
022
023import java.io.IOException;
024
025import org.apache.directory.api.ldap.model.constants.Loggers;
026import org.apache.directory.api.ldap.model.cursor.Cursor;
027import org.apache.directory.api.ldap.model.cursor.CursorException;
028import org.apache.directory.api.ldap.model.cursor.InvalidCursorPositionException;
029import org.apache.directory.api.ldap.model.exception.LdapException;
030import org.apache.directory.api.ldap.model.schema.PrepareString;
031import org.apache.directory.server.core.api.partition.PartitionTxn;
032import org.apache.directory.server.i18n.I18n;
033import org.apache.directory.server.xdbm.AbstractIndexCursor;
034import org.apache.directory.server.xdbm.Index;
035import org.apache.directory.server.xdbm.IndexEntry;
036import org.apache.directory.server.xdbm.IndexNotFoundException;
037import org.apache.directory.server.xdbm.Store;
038import org.apache.directory.server.xdbm.search.evaluator.SubstringEvaluator;
039import org.slf4j.Logger;
040import org.slf4j.LoggerFactory;
041
042
043/**
044 * A Cursor traversing candidates matching a Substring assertion expression.
045 *
046 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
047 */
048public class SubstringCursor extends AbstractIndexCursor<String>
049{
050    /** A dedicated log for cursors */
051    private static final Logger LOG_CURSOR = LoggerFactory.getLogger( Loggers.CURSOR_LOG.getName() );
052
053    /** Speedup for logs */
054    private static final boolean IS_DEBUG = LOG_CURSOR.isDebugEnabled();
055
056    private static final String UNSUPPORTED_MSG = I18n.err( I18n.ERR_725 );
057    private final boolean hasIndex;
058    private final Cursor<IndexEntry<String, String>> wrapped;
059    private final SubstringEvaluator evaluator;
060    private final IndexEntry<String, String> indexEntry = new IndexEntry<>();
061
062
063    /**
064     * Creates a new instance of an SubstringCursor
065     * 
066     * @param partitionTxn The transaction to use
067     * @param store The store
068     * @param substringEvaluator The SubstringEvaluator
069     * @throws LdapException If the creation failed
070     * @throws IndexNotFoundException If the index was not found
071     */
072    @SuppressWarnings("unchecked")
073    public SubstringCursor( PartitionTxn partitionTxn, Store store, final SubstringEvaluator substringEvaluator )
074        throws LdapException, IndexNotFoundException
075    {
076        if ( IS_DEBUG )
077        {
078            LOG_CURSOR.debug( "Creating SubstringCursor {}", this );
079        }
080
081        evaluator = substringEvaluator;
082        this.partitionTxn = partitionTxn;
083        hasIndex = store.hasIndexOn( evaluator.getExpression().getAttributeType() );
084
085        if ( hasIndex )
086        {
087            wrapped = ( ( Index<String, String> ) store.getIndex( evaluator.getExpression().getAttributeType() ) )
088                .forwardCursor( partitionTxn );
089        }
090        else
091        {
092            /*
093             * There is no index on the attribute here.  We have no choice but
094             * to perform a full table scan but need to leverage an index for the
095             * wrapped Cursor.  We know that all entries are listed under
096             * the ndn index and so this will enumerate over all entries.  The
097             * substringEvaluator is used in an assertion to constrain the
098             * result set to only those entries matching the pattern.  The
099             * substringEvaluator handles all the details of normalization and
100             * knows to use it, when it itself detects the lack of an index on
101             * the node's attribute.
102             */
103            wrapped = new AllEntriesCursor( partitionTxn, store );
104        }
105    }
106
107
108    /**
109     * {@inheritDoc}
110     */
111    protected String getUnsupportedMessage()
112    {
113        return UNSUPPORTED_MSG;
114    }
115
116
117    /**
118     * {@inheritDoc}
119     */
120    public void beforeFirst() throws LdapException, CursorException
121    {
122        checkNotClosed();
123        
124        if ( evaluator.getExpression().getInitial() != null && hasIndex )
125        {
126            IndexEntry<String, String> beforeFirstIndexEntry = new IndexEntry<>();
127            String normalizedKey = evaluator.getExpression().getAttributeType().getEquality().getNormalizer().normalize( 
128                evaluator.getExpression().getInitial(), PrepareString.AssertionType.SUBSTRING_INITIAL );
129            beforeFirstIndexEntry.setKey( normalizedKey );
130            wrapped.before( beforeFirstIndexEntry );
131        }
132        else
133        {
134            wrapped.beforeFirst();
135        }
136
137        clear();
138    }
139
140
141    private void clear()
142    {
143        setAvailable( false );
144        indexEntry.setEntry( null );
145        indexEntry.setId( null );
146        indexEntry.setKey( null );
147    }
148
149
150    /**
151     * {@inheritDoc}
152     */
153    public void afterLast() throws LdapException, CursorException
154    {
155        checkNotClosed();
156
157        // to keep the cursor always *after* the last matched tuple
158        // This fixes an issue if the last matched tuple is also the last record present in the
159        // index. In this case the wrapped cursor is positioning on the last tuple instead of positioning after that
160        wrapped.afterLast();
161        clear();
162    }
163
164
165    /**
166     * {@inheritDoc}
167     */
168    public boolean first() throws LdapException, CursorException
169    {
170        beforeFirst();
171        return next();
172    }
173
174
175    private boolean evaluateCandidate( PartitionTxn partitionTxn, IndexEntry<String, String> indexEntry ) throws LdapException
176    {
177        if ( hasIndex )
178        {
179            String key = indexEntry.getKey();
180            return evaluator.getPattern().matcher( key ).matches();
181        }
182        else
183        {
184            return evaluator.evaluate( partitionTxn, indexEntry );
185        }
186    }
187
188
189    /**
190     * {@inheritDoc}
191     */
192    public boolean last() throws LdapException, CursorException
193    {
194        afterLast();
195
196        return previous();
197    }
198
199
200    /**
201     * {@inheritDoc}
202     */
203    public boolean previous() throws LdapException, CursorException
204    {
205        while ( wrapped.previous() )
206        {
207            checkNotClosed();
208            IndexEntry<String, String> entry = wrapped.get();
209
210            if ( evaluateCandidate( partitionTxn, entry ) )
211            {
212                setAvailable( true );
213                this.indexEntry.setId( entry.getId() );
214                this.indexEntry.setKey( entry.getKey() );
215                this.indexEntry.setEntry( entry.getEntry() );
216                return true;
217            }
218        }
219
220        clear();
221        return false;
222    }
223
224
225    /**
226     * {@inheritDoc}
227     */
228    public boolean next() throws LdapException, CursorException
229    {
230        while ( wrapped.next() )
231        {
232            checkNotClosed();
233            IndexEntry<String, String> entry = wrapped.get();
234
235            if ( evaluateCandidate( partitionTxn, entry ) )
236            {
237                setAvailable( true );
238                this.indexEntry.setId( entry.getId() );
239                this.indexEntry.setKey( entry.getKey() );
240                this.indexEntry.setEntry( entry.getEntry() );
241
242                return true;
243            }
244        }
245
246        clear();
247        return false;
248    }
249
250
251    /**
252     * {@inheritDoc}
253     */
254    public IndexEntry<String, String> get() throws CursorException
255    {
256        checkNotClosed();
257
258        if ( available() )
259        {
260            return indexEntry;
261        }
262
263        throw new InvalidCursorPositionException( I18n.err( I18n.ERR_708 ) );
264    }
265
266
267    /**
268     * {@inheritDoc}
269     */
270    @Override
271    public void close() throws IOException
272    {
273        if ( IS_DEBUG )
274        {
275            LOG_CURSOR.debug( "Closing SubstringCursor {}", this );
276        }
277
278        super.close();
279        wrapped.close();
280        clear();
281    }
282
283
284    /**
285     * {@inheritDoc}
286     */
287    @Override
288    public void close( Exception cause ) throws IOException
289    {
290        if ( IS_DEBUG )
291        {
292            LOG_CURSOR.debug( "Closing SubstringCursor {}", this );
293        }
294
295        super.close( cause );
296        wrapped.close( cause );
297        clear();
298    }
299
300
301    /**
302     * @see Object#toString()
303     */
304    @Override
305    public String toString( String tabs )
306    {
307        StringBuilder sb = new StringBuilder();
308
309        sb.append( tabs ).append( "SubstringCursor (" );
310
311        if ( available() )
312        {
313            sb.append( "available)" );
314        }
315        else
316        {
317            sb.append( "absent)" );
318        }
319
320        sb.append( "#index<" ).append( hasIndex ).append( "> :\n" );
321
322        sb.append( tabs + "  >>" ).append( evaluator ).append( '\n' );
323
324        sb.append( wrapped.toString( tabs + "    " ) );
325
326        return sb.toString();
327    }
328
329
330    /**
331     * @see Object#toString()
332     */
333    public String toString()
334    {
335        return toString( "" );
336    }
337}