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