    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 */
    019package org.apache.wiki.search;
    020
    021import org.apache.commons.lang3.StringUtils;
    022import org.apache.logging.log4j.LogManager;
    023import org.apache.logging.log4j.Logger;
    024import org.apache.lucene.analysis.Analyzer;
    025import org.apache.lucene.analysis.TokenStream;
    026import org.apache.lucene.analysis.classic.ClassicAnalyzer;
    027import org.apache.lucene.document.Document;
    028import org.apache.lucene.document.Field;
    029import org.apache.lucene.document.StringField;
    030import org.apache.lucene.document.TextField;
    031import org.apache.lucene.index.DirectoryReader;
    032import org.apache.lucene.index.IndexReader;
    033import org.apache.lucene.index.IndexWriter;
    034import org.apache.lucene.index.IndexWriterConfig;
    035import org.apache.lucene.index.IndexWriterConfig.OpenMode;
    036import org.apache.lucene.index.StoredFields;
    037import org.apache.lucene.index.Term;
    038import org.apache.lucene.queryparser.classic.MultiFieldQueryParser;
    039import org.apache.lucene.queryparser.classic.ParseException;
    040import org.apache.lucene.queryparser.classic.QueryParser;
    041import org.apache.lucene.search.IndexSearcher;
    042import org.apache.lucene.search.Query;
    043import org.apache.lucene.search.ScoreDoc;
    044import org.apache.lucene.search.TermQuery;
    045import org.apache.lucene.search.TopDocs;
    046import org.apache.lucene.search.highlight.Highlighter;
    047import org.apache.lucene.search.highlight.InvalidTokenOffsetsException;
    048import org.apache.lucene.search.highlight.QueryScorer;
    049import org.apache.lucene.search.highlight.SimpleHTMLEncoder;
    050import org.apache.lucene.search.highlight.SimpleHTMLFormatter;
    051import org.apache.lucene.store.Directory;
    052import org.apache.lucene.store.NIOFSDirectory;
    053import org.apache.wiki.InternalWikiException;
    054import org.apache.wiki.WatchDog;
    055import org.apache.wiki.WikiBackgroundThread;
    056import org.apache.wiki.api.core.Attachment;
    057import org.apache.wiki.api.core.Context;
    058import org.apache.wiki.api.core.Engine;
    059import org.apache.wiki.api.core.Page;
    060import org.apache.wiki.api.exceptions.NoRequiredPropertyException;
    061import org.apache.wiki.api.exceptions.ProviderException;
    062import org.apache.wiki.api.providers.PageProvider;
    063import org.apache.wiki.api.providers.WikiProvider;
    064import org.apache.wiki.api.search.SearchResult;
    065import org.apache.wiki.api.spi.Wiki;
    066import org.apache.wiki.attachment.AttachmentManager;
    067import org.apache.wiki.auth.AuthorizationManager;
    068import org.apache.wiki.auth.permissions.PagePermission;
    069import org.apache.wiki.pages.PageManager;
    070import org.apache.wiki.util.ClassUtil;
    071import org.apache.wiki.util.FileUtil;
    072import org.apache.wiki.util.TextUtil;
    073
    074import java.io.File;
    075import java.io.IOException;
    076import java.io.InputStream;
    077import java.io.InputStreamReader;
    078import java.io.StringReader;
    079import java.io.StringWriter;
    080import java.util.ArrayList;
    081import java.util.Arrays;
    082import java.util.Collection;
    083import java.util.Collections;
    084import java.util.Date;
    085import java.util.List;
    086import java.util.Properties;
    087import java.util.concurrent.Executor;
    088import java.util.concurrent.Executors;
    089import java.util.stream.Collectors;
    090
    091
    092/**
    093 * Interface for the search providers that handle searching the Wiki
    094 *
    095 * @since 2.2.21.
    096 */
    097public class LuceneSearchProvider implements SearchProvider {
    098
    099    protected static final Logger LOG = LogManager.getLogger( LuceneSearchProvider.class );
    100
    101    private Engine m_engine;
    102    private Executor searchExecutor;
    103
    104    // Lucene properties.
    105
    106    /** Which analyzer to use.  Default is StandardAnalyzer. */
    107    public static final String PROP_LUCENE_ANALYZER      = "jspwiki.lucene.analyzer";
    108    private static final String PROP_LUCENE_INDEXDELAY   = "jspwiki.lucene.indexdelay";
    109    private static final String PROP_LUCENE_INITIALDELAY = "jspwiki.lucene.initialdelay";
    110
    111    private String m_analyzerClass = ClassicAnalyzer.class.getName();
    112
    113    private static final String LUCENE_DIR = "lucene";
    114
    115    /** These attachment file suffixes will be indexed. */
    116    public static final String[] SEARCHABLE_FILE_SUFFIXES = new String[] { ".txt", ".ini", ".xml", ".html", "htm", ".mm", ".htm",
    117                                                                           ".xhtml", ".java", ".c", ".cpp", ".php", ".asm", ".sh",
    118                                                                           ".properties", ".kml", ".gpx", ".loc", ".md", ".xml" };
    119
    120    protected static final String LUCENE_ID            = "id";
    121    protected static final String LUCENE_PAGE_CONTENTS = "contents";
    122    protected static final String LUCENE_AUTHOR        = "author";
    123    protected static final String LUCENE_ATTACHMENTS   = "attachment";
    124    protected static final String LUCENE_PAGE_NAME     = "name";
    125    protected static final String LUCENE_PAGE_KEYWORDS = "keywords";
    126
    127    private String m_luceneDirectory;
    128    protected final List< Object[] > m_updates = Collections.synchronizedList( new ArrayList<>() );
    129
    130    /** Maximum number of fragments from search matches. */
    131    private static final int MAX_FRAGMENTS = 3;
    132
    133    /** The maximum number of hits to return from searches. */
    134    public static final int MAX_SEARCH_HITS = 99_999;
    135
    136    private static final String PUNCTUATION_TO_SPACES = StringUtils.repeat( " ", TextUtil.PUNCTUATION_CHARS_ALLOWED.length() );
    137
    138    /** {@inheritDoc} */
    139    @Override
    140    public void initialize( final Engine engine, final Properties props ) throws NoRequiredPropertyException, IOException {
    141        m_engine = engine;
    142        searchExecutor = Executors.newCachedThreadPool();
    143
    144        m_luceneDirectory = engine.getWorkDir() + File.separator + LUCENE_DIR;
    145
    146        final int initialDelay = TextUtil.getIntegerProperty( props, PROP_LUCENE_INITIALDELAY, LuceneUpdater.INITIAL_DELAY );
    147        final int indexDelay   = TextUtil.getIntegerProperty( props, PROP_LUCENE_INDEXDELAY, LuceneUpdater.INDEX_DELAY );
    148
    149        m_analyzerClass = TextUtil.getStringProperty( props, PROP_LUCENE_ANALYZER, m_analyzerClass );
    150        // FIXME: Just to be simple for now, we will do full reindex only if no files are in lucene directory.
    151
    152        final File dir = new File( m_luceneDirectory );
    153        LOG.info( "Lucene enabled, cache will be in: {}", dir.getAbsolutePath() );
    154        try {
    155            if( !dir.exists() ) {
    156                dir.mkdirs();
    157            }
    158
    159            if( !dir.exists() || !dir.canWrite() || !dir.canRead() ) {
    160                LOG.error( "Cannot write to Lucene directory, disabling Lucene: {}", dir.getAbsolutePath() );
    161                throw new IOException( "Invalid Lucene directory." );
    162            }
    163
    164            final String[] filelist = dir.list();
    165            if( filelist == null ) {
    166                throw new IOException( "Invalid Lucene directory: cannot produce listing: " + dir.getAbsolutePath() );
    167            }
    168        } catch( final IOException e ) {
    169            LOG.error( "Problem while creating Lucene index - not using Lucene.", e );
    170        }
    171
    172        // Start the Lucene update thread, which waits first for a little while before starting to go through
    173        // the Lucene "pages that need updating".
    174        final LuceneUpdater updater = new LuceneUpdater( m_engine, this, initialDelay, indexDelay );
    175        updater.start();
    176    }
    177
    178    /**
    179     * Returns the handling engine.
    180     *
    181     * @return Current Engine
    182     */
    183    protected Engine getEngine() {
    184        return m_engine;
    185    }
    186
    187    /**
    188     * Performs a full Lucene reindex, if necessary.
    189     *
    190     * @throws IOException If there's a problem during indexing
    191     */
    192    protected void doFullLuceneReindex() throws IOException {
    193        final File dir = new File( m_luceneDirectory );
    194        final String[] filelist = dir.list();
    195        if( filelist == null ) {
    196            throw new IOException( "Invalid Lucene directory: cannot produce listing: " + dir.getAbsolutePath() );
    197        }
    198
    199        try {
    200            if( filelist.length == 0 ) {
    201                //
    202                //  No files? Reindex!
    203                //
    204                final Date start = new Date();
    205
    206                LOG.info( "Starting Lucene reindexing, this can take a couple of minutes..." );
    207
    208                final Directory luceneDir = new NIOFSDirectory( dir.toPath() );
    209                try( final IndexWriter writer = getIndexWriter( luceneDir ) ) {
    210                    long pagesIndexed = 0L;
    211                    final Collection< Page > allPages = m_engine.getManager( PageManager.class ).getAllPages();
    212                    for( final Page page : allPages ) {
    213                        try {
    214                            final String text = m_engine.getManager( PageManager.class ).getPageText( page.getName(), WikiProvider.LATEST_VERSION );
    215                            luceneIndexPage( page, text, writer );
    216                            pagesIndexed++;
    217                        } catch( final IOException e ) {
    218                            LOG.warn( "Unable to index page {}, continuing to next ", page.getName(), e );
    219                        }
    220                    }
    221                    LOG.info( "Indexed {} pages", pagesIndexed );
    222
    223                    long attachmentsIndexed = 0L;
    224                    final Collection< Attachment > allAttachments = m_engine.getManager( AttachmentManager.class ).getAllAttachments();
    225                    for( final Attachment att : allAttachments ) {
    226                        try {
    227                            final String text = getAttachmentContent( att.getName(), WikiProvider.LATEST_VERSION );
    228                            luceneIndexPage( att, text, writer );
    229                            attachmentsIndexed++;
    230                        } catch( final IOException e ) {
    231                            LOG.warn( "Unable to index attachment {}, continuing to next", att.getName(), e );
    232                        }
    233                    }
    234                    LOG.info( "Indexed {} attachments", attachmentsIndexed );
    235                }
    236
    237                final Date end = new Date();
    238                LOG.info( "Full Lucene index finished in {} milliseconds.", end.getTime() - start.getTime() );
    239            } else {
    240                LOG.info( "Files found in Lucene directory, not reindexing." );
    241            }
    242        } catch( final IOException e ) {
    243            LOG.error( "Problem while creating Lucene index - not using Lucene.", e );
    244        } catch( final ProviderException e ) {
    245            LOG.error( "Problem reading pages while creating Lucene index (JSPWiki won't start.)", e );
    246            throw new IllegalArgumentException( "unable to create Lucene index" );
    247        } catch( final Exception e ) {
    248            LOG.error( "Unable to start lucene", e );
    249        }
    250
    251    }
    252
    253    /**
    254     * Fetches the attachment content from the repository.
    255     * Content is flat text that can be used for indexing/searching or display
    256     *
    257     * @param attachmentName Name of the attachment.
    258     * @param version        The version of the attachment.
    259     * @return the content of the Attachment as a String.
    260     */
    261    protected String getAttachmentContent( final String attachmentName, final int version ) {
    262        final AttachmentManager mgr = m_engine.getManager( AttachmentManager.class );
    263        try {
    264            final Attachment att = mgr.getAttachmentInfo( attachmentName, version );
    265            //FIXME: Find out why sometimes att is null
    266            if( att != null ) {
    267                return getAttachmentContent( att );
    268            }
    269        } catch( final ProviderException e ) {
    270            LOG.error( "Attachment cannot be loaded", e );
    271        }
    272        return null;
    273    }
    274
    275    /**
    276     * @param att Attachment to get content for. Filename extension is used to determine the type of the attachment.
    277     * @return String representing the content of the file.
    278     * FIXME This is a very simple implementation of some text-based attachment, mainly used for testing.
    279     * This should be replaced /moved to Attachment search providers or some other 'pluggable' way to search attachments
    280     */
    281    protected String getAttachmentContent( final Attachment att ) {
    282        final AttachmentManager mgr = m_engine.getManager( AttachmentManager.class );
    283        //FIXME: Add attachment plugin structure
    284
    285        final String filename = att.getFileName();
    286
    287        boolean searchSuffix = Arrays.stream(SEARCHABLE_FILE_SUFFIXES).anyMatch(filename::endsWith);
    288
    289        String out = filename;
    290        if( searchSuffix ) {
    291            try( final InputStream attStream = mgr.getAttachmentStream( att ); final StringWriter sout = new StringWriter() ) {
    292                FileUtil.copyContents( new InputStreamReader( attStream ), sout );
    293                out = out + " " + sout;
    294            } catch( final ProviderException | IOException e ) {
    295                LOG.error( "Attachment cannot be loaded", e );
    296            }
    297        }
    298
    299        return out;
    300    }
    301
    302    /**
    303     * Updates the lucene index for a single page.
    304     *
    305     * @param page The WikiPage to check
    306     * @param text The page text to index.
    307     */
    308    protected synchronized void updateLuceneIndex( final Page page, final String text ) {
    309        LOG.debug( "Updating Lucene index for page '{}'...", page.getName() );
    310        pageRemoved( page );
    311
    312        // Now add back the new version.
    313        try( final Directory luceneDir = new NIOFSDirectory( new File( m_luceneDirectory ).toPath() );
    314             final IndexWriter writer = getIndexWriter( luceneDir ) ) {
    315            luceneIndexPage( page, text, writer );
    316        } catch( final IOException e ) {
    317            LOG.error( "Unable to update page '{}' from Lucene index", page.getName(), e );
    318            // reindexPage( page );
    319        } catch( final Exception e ) {
    320            LOG.error( "Unexpected Lucene exception - please check configuration!", e );
    321            // reindexPage( page );
    322        }
    323
    324        LOG.debug( "Done updating Lucene index for page '{}'.", page.getName() );
    325    }
    326
    327    private Analyzer getLuceneAnalyzer() throws ProviderException {
    328        try {
    329            return ClassUtil.buildInstance( m_analyzerClass );
    330        } catch( final Exception e ) {
    331            final String msg = "Could not get LuceneAnalyzer class " + m_analyzerClass + ", reason: ";
    332            LOG.error( msg, e );
    333            throw new ProviderException( msg + e );
    334        }
    335    }
    336
    337    /**
    338     * Indexes page using the given IndexWriter.
    339     *
    340     * @param page   WikiPage
    341     * @param text   Page text to index
    342     * @param writer The Lucene IndexWriter to use for indexing
    343     * @return the created index Document
    344     * @throws IOException If there's an indexing problem
    345     */
    346    protected Document luceneIndexPage( final Page page, final String text, final IndexWriter writer ) throws IOException {
    347        LOG.debug( "Indexing {}...", page.getName() );
    348
    349        // make a new, empty document
    350        final Document doc = new Document();
    351        if( text == null ) {
    352            return doc;
    353        }
    354
    355        final String indexedText = text.replace( "__", " " ); // be nice to Language Analyzers - cfr. JSPWIKI-893
    356
    357        // Raw name is the keyword we'll use to refer to this document for updates.
    358        Field field = new Field( LUCENE_ID, page.getName(), StringField.TYPE_STORED );
    359        doc.add( field );
    360
    361        // Body text.  It is stored in the doc for search contexts.
    362        field = new Field( LUCENE_PAGE_CONTENTS, indexedText, TextField.TYPE_STORED );
    363        doc.add( field );
    364
    365        // Allow searching by page name. Both beautified and raw
    366        final String unTokenizedTitle = StringUtils.replaceChars( page.getName(), TextUtil.PUNCTUATION_CHARS_ALLOWED, PUNCTUATION_TO_SPACES );
    367        field = new Field( LUCENE_PAGE_NAME, TextUtil.beautifyString( page.getName() ) + " " + unTokenizedTitle, TextField.TYPE_STORED );
    368        doc.add( field );
    369
    370        // Allow searching by authorname
    371        if( page.getAuthor() != null ) {
    372            field = new Field( LUCENE_AUTHOR, page.getAuthor(), TextField.TYPE_STORED );
    373            doc.add( field );
    374        }
    375
    376        // Now add the names of the attachments of this page
    377        try {
    378            final List< Attachment > attachments = m_engine.getManager( AttachmentManager.class ).listAttachments( page );
    379            final String attachmentNames = attachments.stream().map(att -> att.getName() + ";").collect(Collectors.joining());
    380
    381            field = new Field( LUCENE_ATTACHMENTS, attachmentNames, TextField.TYPE_STORED );
    382            doc.add( field );
    383
    384        } catch( final ProviderException e ) {
    385            // Unable to read attachments
    386            LOG.error( "Failed to get attachments for page", e );
    387        }
    388
    389        // also index page keywords, if available
    390        if( page.getAttribute( "keywords" ) != null ) {
    391            field = new Field( LUCENE_PAGE_KEYWORDS, page.getAttribute( "keywords" ).toString(), TextField.TYPE_STORED );
    392            doc.add( field );
    393        }
    394        synchronized( writer ) {
    395            writer.addDocument( doc );
    396        }
    397
    398        return doc;
    399    }
    400
    401    /**
    402     * {@inheritDoc}
    403     */
    404    @Override
    405    public synchronized void pageRemoved( final Page page ) {
    406        try( final Directory luceneDir = new NIOFSDirectory( new File( m_luceneDirectory ).toPath() );
    407             final IndexWriter writer = getIndexWriter( luceneDir ) ) {
    408            final Query query = new TermQuery( new Term( LUCENE_ID, page.getName() ) );
    409            writer.deleteDocuments( query );
    410        } catch( final Exception e ) {
    411            LOG.error( "Unable to remove page '{}' from Lucene index", page.getName(), e );
    412        }
    413    }
    414
    415    IndexWriter getIndexWriter( final Directory luceneDir ) throws IOException, ProviderException {
    416        final IndexWriterConfig writerConfig = new IndexWriterConfig( getLuceneAnalyzer() );
    417        writerConfig.setOpenMode( OpenMode.CREATE_OR_APPEND );
    418        return new IndexWriter( luceneDir, writerConfig );
    419    }
    420
    421    /**
    422     * Adds a page-text pair to the lucene update queue.  Safe to call always
    423     *
    424     * @param page WikiPage to add to the update queue.
    425     */
    426    @Override
    427    public void reindexPage( final Page page ) {
    428        if( page != null ) {
    429            final String text;
    430
    431            // TODO: Think if this was better done in the thread itself?
    432            if( page instanceof Attachment ) {
    433                text = getAttachmentContent( ( Attachment ) page );
    434            } else {
    435                text = m_engine.getManager( PageManager.class ).getPureText( page );
    436            }
    437
    438            if( text != null ) {
    439                // Add work item to m_updates queue.
    440                final Object[] pair = new Object[ 2 ];
    441                pair[ 0 ] = page;
    442                pair[ 1 ] = text;
    443                m_updates.add( pair );
    444                LOG.debug( "Scheduling page {} for index update", page.getName() );
    445            }
    446        }
    447    }
    448
    449    /** {@inheritDoc} */
    450    @Override
    451    public Collection< SearchResult > findPages( final String query, final Context wikiContext ) throws ProviderException {
    452        return findPages( query, FLAG_CONTEXTS, wikiContext );
    453    }
    454
    455    /** Create contexts also. Generating contexts can be expensive, so they're not on by default. */
    456    public static final int FLAG_CONTEXTS = 0x01;
    457
    458    /**
    459     * Searches pages using a particular combination of flags.
    460     *
    461     * @param query The query to perform in Lucene query language
    462     * @param flags A set of flags
    463     * @return A Collection of SearchResult instances
    464     * @throws ProviderException if there is a problem with the backend
    465     */
    466    public Collection< SearchResult > findPages( final String query, final int flags, final Context wikiContext ) throws ProviderException {
    467        ArrayList< SearchResult > list = null;
    468        Highlighter highlighter = null;
    469
    470        try( final Directory luceneDir = new NIOFSDirectory( new File( m_luceneDirectory ).toPath() );
    471             final IndexReader reader = DirectoryReader.open( luceneDir ) ) {
    472            final String[] queryfields = { LUCENE_PAGE_CONTENTS, LUCENE_PAGE_NAME, LUCENE_AUTHOR, LUCENE_ATTACHMENTS, LUCENE_PAGE_KEYWORDS };
    473            final QueryParser qp = new MultiFieldQueryParser( queryfields, getLuceneAnalyzer() );
    474            final Query luceneQuery = qp.parse( query );
    475            final IndexSearcher searcher = new IndexSearcher( reader, searchExecutor );
    476
    477            if( ( flags & FLAG_CONTEXTS ) != 0 ) {
    478                highlighter = new Highlighter( new SimpleHTMLFormatter( "<span class=\"searchmatch\">", "</span>" ),
    479                                               new SimpleHTMLEncoder(),
    480                                               new QueryScorer( luceneQuery ) );
    481            }
    482
    483            final AuthorizationManager mgr = m_engine.getManager( AuthorizationManager.class );
    484            final TopDocs hits = searcher.search( luceneQuery, MAX_SEARCH_HITS );
    485            final StoredFields storedFields = reader.storedFields();
    486
    487            list = new ArrayList<>( hits.scoreDocs.length );
    488            for( final ScoreDoc hit : hits.scoreDocs ) {
    489                final Document doc = storedFields.document( hit.doc );
    490                final String pageName = doc.get( LUCENE_ID );
    491                final Page page = m_engine.getManager( PageManager.class ).getPage( pageName, PageProvider.LATEST_VERSION );
    492
    493                if( page != null ) {
    494                    final PagePermission pp = new PagePermission( page, PagePermission.VIEW_ACTION );
    495                    if( mgr.checkPermission( wikiContext.getWikiSession(), pp ) ) {
    496                        final int score = ( int ) ( hit.score * 100 );
    497
    498                        // Get highlighted search contexts
    499                        final String text = doc.get( LUCENE_PAGE_CONTENTS );
    500
    501                        String[] fragments = new String[ 0 ];
    502                        if( text != null && highlighter != null ) {
    503                            final TokenStream tokenStream = getLuceneAnalyzer().tokenStream( LUCENE_PAGE_CONTENTS, new StringReader( text ) );
    504                            fragments = highlighter.getBestFragments( tokenStream, text, MAX_FRAGMENTS );
    505                        }
    506
    507                        final SearchResult result = new SearchResultImpl( page, score, fragments );
    508                        list.add( result );
    509                    }
    510                } else {
    511                    LOG.error( "Lucene found a result page '{}' that could not be loaded, removing from Lucene cache",  pageName );
    512                    pageRemoved( Wiki.contents().page( m_engine, pageName ) );
    513                }
    514            }
    515        } catch( final IOException e ) {
    516            LOG.error( "Failed during lucene search", e );
    517        } catch( final ParseException e ) {
    518            LOG.error( "Broken query; cannot parse query: {}", query, e );
    519            throw new ProviderException( "You have entered a query Lucene cannot process [" + query + "]: " + e.getMessage() );
    520        } catch( final InvalidTokenOffsetsException e ) {
    521            LOG.error( "Tokens are incompatible with provided text ", e );
    522        }
    523
    524        return list;
    525    }
    526
    527    /** {@inheritDoc} */
    528    @Override
    529    public String getProviderInfo() {
    530        return "LuceneSearchProvider";
    531    }
    532
    533    /**
    534     * Updater thread that updates Lucene indexes.
    535     */
    536    private static final class LuceneUpdater extends WikiBackgroundThread {
    537        static final int INDEX_DELAY    = 5;
    538        static final int INITIAL_DELAY = 60;
    539        private final LuceneSearchProvider m_provider;
    540
    541        private final int m_initialDelay;
    542
    543        private WatchDog m_watchdog;
    544
    545        private LuceneUpdater( final Engine engine, final LuceneSearchProvider provider, final int initialDelay, final int indexDelay ) {
    546            super( engine, indexDelay );
    547            m_provider = provider;
    548            m_initialDelay = initialDelay;
    549            setName( "JSPWiki Lucene Indexer" );
    550        }
    551
    552        @Override
    553        public void startupTask() throws Exception {
    554            m_watchdog = WatchDog.getCurrentWatchDog( getEngine() );
    555
    556            // Sleep initially...
    557            try {
    558                Thread.sleep( m_initialDelay * 1000L );
    559            } catch( final InterruptedException e ) {
    560                throw new InternalWikiException( "Interrupted while waiting to start.", e );
    561            }
    562
    563            m_watchdog.enterState( "Full reindex" );
    564            // Reindex everything
    565            m_provider.doFullLuceneReindex();
    566            m_watchdog.exitState();
    567        }
    568
    569        @Override
    570        public void backgroundTask() {
    571            m_watchdog.enterState( "Emptying index queue", 60 );
    572
    573            synchronized( m_provider.m_updates ) {
    574                while(!m_provider.m_updates.isEmpty()) {
    575                    final Object[] pair = m_provider.m_updates.remove( 0 );
    576                    final Page page = ( Page )pair[ 0 ];
    577                    final String text = ( String )pair[ 1 ];
    578                    m_provider.updateLuceneIndex( page, text );
    579                }
    580            }
    581
    582            m_watchdog.exitState();
    583        }
    584
    585    }
    586
    587    // FIXME: This class is dumb; needs to have a better implementation
    588    private static class SearchResultImpl implements SearchResult {
    589
    590        private final Page m_page;
    591        private final int m_score;
    592        private final String[] m_contexts;
    593
    594        public SearchResultImpl( final Page page, final int score, final String[] contexts ) {
    595            m_page = page;
    596            m_score = score;
    597            m_contexts = contexts != null ? contexts.clone() : null;
    598        }
    599
    600        @Override
    601        public Page getPage() {
    602            return m_page;
    603        }
    604
    605        /* (non-Javadoc)
    606         * @see org.apache.wiki.SearchResult#getScore()
    607         */
    608        @Override
    609        public int getScore() {
    610            return m_score;
    611        }
    612
    613
    614        @Override
    615        public String[] getContexts() {
    616            return m_contexts;
    617        }
    618    }
    619
    620}