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