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