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