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