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