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