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}