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