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