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.IOException; 022import java.util.ArrayList; 023import java.util.Collection; 024import java.util.HashMap; 025import java.util.Iterator; 026import java.util.List; 027import java.util.Map; 028import java.util.Properties; 029import java.util.Set; 030 031import javax.servlet.ServletException; 032import javax.servlet.http.HttpServletRequest; 033import javax.servlet.http.HttpServletResponse; 034 035import org.apache.commons.lang.StringUtils; 036import org.apache.commons.lang.time.StopWatch; 037import org.apache.log4j.Logger; 038import org.apache.wiki.WikiContext; 039import org.apache.wiki.WikiEngine; 040import org.apache.wiki.WikiPage; 041import org.apache.wiki.ajax.AjaxUtil; 042import org.apache.wiki.ajax.WikiAjaxDispatcherServlet; 043import org.apache.wiki.ajax.WikiAjaxServlet; 044import org.apache.wiki.api.exceptions.FilterException; 045import org.apache.wiki.api.exceptions.NoRequiredPropertyException; 046import org.apache.wiki.api.exceptions.ProviderException; 047import org.apache.wiki.api.filters.BasicPageFilter; 048import org.apache.wiki.event.WikiEvent; 049import org.apache.wiki.event.WikiEventListener; 050import org.apache.wiki.event.WikiEventUtils; 051import org.apache.wiki.event.WikiPageEvent; 052import org.apache.wiki.modules.InternalModule; 053import org.apache.wiki.parser.MarkupParser; 054import org.apache.wiki.tags.WikiTagBase; 055import org.apache.wiki.util.ClassUtil; 056import org.apache.wiki.util.TextUtil; 057 058/** 059 * Manages searching the Wiki. 060 * 061 * @since 2.2.21. 062 */ 063public class SearchManager extends BasicPageFilter implements InternalModule, WikiEventListener { 064 065 private static final Logger log = Logger.getLogger(SearchManager.class); 066 067 private static final String DEFAULT_SEARCHPROVIDER = "org.apache.wiki.search.LuceneSearchProvider"; 068 069 /** Old option, now deprecated. */ 070 private static final String PROP_USE_LUCENE = "jspwiki.useLucene"; 071 072 /** 073 * Property name for setting the search provider. Value is <tt>{@value}</tt>. 074 */ 075 public static final String PROP_SEARCHPROVIDER = "jspwiki.searchProvider"; 076 077 private SearchProvider m_searchProvider; 078 079 /** 080 * The name of the JSON object that manages search. 081 */ 082 public static final String JSON_SEARCH = "search"; 083 084 /** 085 * Creates a new SearchManager. 086 * 087 * @param engine The WikiEngine that owns this SearchManager. 088 * @param properties The list of Properties. 089 * @throws FilterException If it cannot be instantiated. 090 */ 091 public SearchManager( WikiEngine engine, Properties properties ) 092 throws FilterException 093 { 094 initialize( engine, properties ); 095 096 WikiEventUtils.addWikiEventListener(m_engine.getPageManager(), 097 WikiPageEvent.PAGE_DELETE_REQUEST, this); 098 099 //TODO: Replace with custom annotations. See JSPWIKI-566 100 WikiAjaxDispatcherServlet.registerServlet( JSON_SEARCH, new JSONSearch() ); 101 } 102 103 /** 104 * Provides a JSON RPC API to the JSPWiki Search Engine. 105 */ 106 public class JSONSearch implements WikiAjaxServlet 107 { 108 public static final String AJAX_ACTION_SUGGESTIONS = "suggestions"; 109 public static final String AJAX_ACTION_PAGES = "pages"; 110 public static final int DEFAULT_MAX_RESULTS = 20; 111 public int maxResults = DEFAULT_MAX_RESULTS; 112 113 @Override 114 public String getServletMapping() { 115 return JSON_SEARCH; 116 } 117 118 @Override 119 public void service(HttpServletRequest req, HttpServletResponse resp, String actionName, List<String> params) 120 throws ServletException, IOException { 121 String result = ""; 122 if (StringUtils.isNotBlank(actionName)) { 123 if (params.size()<1) { 124 return; 125 } 126 String itemId = params.get(0); 127 log.debug("itemId="+itemId); 128 if (params.size()>1) { 129 String maxResultsParam = params.get(1); 130 log.debug("maxResultsParam="+maxResultsParam); 131 if (StringUtils.isNotBlank(maxResultsParam) && StringUtils.isNumeric(maxResultsParam)) { 132 maxResults = Integer.parseInt(maxResultsParam); 133 } 134 } 135 136 if (actionName.equals(AJAX_ACTION_SUGGESTIONS)) { 137 List<String> callResults = new ArrayList<String>(); 138 log.debug("Calling getSuggestions() START"); 139 callResults = getSuggestions(itemId, maxResults); 140 log.debug("Calling getSuggestions() DONE. "+callResults.size()); 141 result = AjaxUtil.toJson(callResults); 142 } else if (actionName.equals(AJAX_ACTION_PAGES)) { 143 List<Map<String,Object>> callResults = new ArrayList<Map<String,Object>>(); 144 log.debug("Calling findPages() START"); 145 WikiContext wikiContext = m_engine.createContext(req, WikiContext.VIEW); 146 if (wikiContext == null) { 147 throw new ServletException("Could not create a WikiContext from the request "+req); 148 } 149 callResults = findPages(itemId, maxResults, wikiContext); 150 log.debug("Calling findPages() DONE. "+callResults.size()); 151 result = AjaxUtil.toJson(callResults); 152 } 153 } 154 log.debug("result="+result); 155 resp.getWriter().write(result); 156 } 157 158 /** 159 * Provides a list of suggestions to use for a page name. 160 * Currently the algorithm just looks into the value parameter, 161 * and returns all page names from that. 162 * 163 * @param wikiName the page name 164 * @param maxLength maximum number of suggestions 165 * @return the suggestions 166 */ 167 public List<String> getSuggestions( String wikiName, int maxLength ) 168 { 169 StopWatch sw = new StopWatch(); 170 sw.start(); 171 List<String> list = new ArrayList<String>(maxLength); 172 173 if( wikiName.length() > 0 ) 174 { 175 176 // split pagename and attachment filename 177 String filename = ""; 178 int pos = wikiName.indexOf("/"); 179 if( pos >= 0 ) 180 { 181 filename = wikiName.substring( pos ).toLowerCase(); 182 wikiName = wikiName.substring( 0, pos ); 183 } 184 185 String cleanWikiName = MarkupParser.cleanLink(wikiName).toLowerCase() + filename; 186 187 String oldStyleName = MarkupParser.wikifyLink(wikiName).toLowerCase() + filename; 188 189 Set allPages = m_engine.getReferenceManager().findCreated(); 190 191 int counter = 0; 192 for( Iterator i = allPages.iterator(); i.hasNext() && counter < maxLength; ) 193 { 194 String p = (String) i.next(); 195 String pp = p.toLowerCase(); 196 if( pp.startsWith( cleanWikiName) || pp.startsWith( oldStyleName ) ) 197 { 198 list.add( p ); 199 counter++; 200 } 201 } 202 } 203 204 sw.stop(); 205 if( log.isDebugEnabled() ) log.debug("Suggestion request for "+wikiName+" done in "+sw); 206 return list; 207 } 208 209 /** 210 * Performs a full search of pages. 211 * 212 * @param searchString The query string 213 * @param maxLength How many hits to return 214 * @return the pages found 215 */ 216 public List<Map<String,Object>> findPages( String searchString, int maxLength, WikiContext wikiContext ) 217 { 218 StopWatch sw = new StopWatch(); 219 sw.start(); 220 221 List<Map<String,Object>> list = new ArrayList<Map<String,Object>>(maxLength); 222 223 if( searchString.length() > 0 ) 224 { 225 try 226 { 227 Collection c; 228 229 if( m_searchProvider instanceof LuceneSearchProvider ) { 230 c = ((LuceneSearchProvider)m_searchProvider).findPages( searchString, 0, wikiContext ); 231 } else { 232 c = m_searchProvider.findPages( searchString, wikiContext ); 233 } 234 235 int count = 0; 236 for( Iterator i = c.iterator(); i.hasNext() && count < maxLength; count++ ) 237 { 238 SearchResult sr = (SearchResult)i.next(); 239 HashMap<String,Object> hm = new HashMap<String,Object>(); 240 hm.put( "page", sr.getPage().getName() ); 241 hm.put( "score", sr.getScore() ); 242 list.add( hm ); 243 } 244 } 245 catch(Exception e) 246 { 247 log.info("AJAX search failed; ",e); 248 } 249 } 250 251 sw.stop(); 252 if( log.isDebugEnabled() ) log.debug("AJAX search complete in "+sw); 253 return list; 254 } 255 } 256 257 258 /** 259 * This particular method starts off indexing and all sorts of various activities, 260 * so you need to run this last, after things are done. 261 * 262 * @param engine the wiki engine 263 * @param properties the properties used to initialize the wiki engine 264 * @throws FilterException if the search provider failed to initialize 265 */ 266 public void initialize(WikiEngine engine, Properties properties) 267 throws FilterException 268 { 269 m_engine = engine; 270 271 loadSearchProvider(properties); 272 273 try 274 { 275 m_searchProvider.initialize(engine, properties); 276 } 277 catch (NoRequiredPropertyException e) 278 { 279 log.error( e.getMessage(), e ); 280 } 281 catch (IOException e) 282 { 283 log.error( e.getMessage(), e ); 284 } 285 } 286 287 private void loadSearchProvider(Properties properties) 288 { 289 // 290 // See if we're using Lucene, and if so, ensure that its 291 // index directory is up to date. 292 // 293 String useLucene = properties.getProperty(PROP_USE_LUCENE); 294 295 // FIXME: Obsolete, remove, or change logic to first load searchProvder? 296 // If the old jspwiki.useLucene property is set we use that instead of the searchProvider class. 297 if( useLucene != null ) 298 { 299 log.info( PROP_USE_LUCENE+" is deprecated; please use "+PROP_SEARCHPROVIDER+"=<your search provider> instead." ); 300 if( TextUtil.isPositive( useLucene ) ) 301 { 302 m_searchProvider = new LuceneSearchProvider(); 303 } 304 else 305 { 306 m_searchProvider = new BasicSearchProvider(); 307 } 308 log.debug("useLucene was set, loading search provider " + m_searchProvider); 309 return; 310 } 311 312 String providerClassName = properties.getProperty( PROP_SEARCHPROVIDER, 313 DEFAULT_SEARCHPROVIDER ); 314 315 try 316 { 317 Class providerClass = ClassUtil.findClass( "org.apache.wiki.search", providerClassName ); 318 m_searchProvider = (SearchProvider)providerClass.newInstance(); 319 } 320 catch( ClassNotFoundException e ) 321 { 322 log.warn("Failed loading SearchProvider, will use BasicSearchProvider.", e); 323 } 324 catch( InstantiationException e ) 325 { 326 log.warn("Failed loading SearchProvider, will use BasicSearchProvider.", e); 327 } 328 catch( IllegalAccessException e ) 329 { 330 log.warn("Failed loading SearchProvider, will use BasicSearchProvider.", e); 331 } 332 333 if( null == m_searchProvider ) 334 { 335 // FIXME: Make a static with the default search provider 336 m_searchProvider = new BasicSearchProvider(); 337 } 338 log.debug("Loaded search provider " + m_searchProvider); 339 } 340 341 /** 342 * Returns the SearchProvider used. 343 * 344 * @return The current SearchProvider. 345 */ 346 public SearchProvider getSearchEngine() 347 { 348 return m_searchProvider; 349 } 350 351 /** 352 * Sends a search to the current search provider. The query is is whatever native format 353 * the query engine wants to use. 354 * 355 * @param query The query. Null is safe, and is interpreted as an empty query. 356 * @param wikiContext the context within which to run the search 357 * @return A collection of WikiPages that matched. 358 * @throws ProviderException If the provider fails and a search cannot be completed. 359 * @throws IOException If something else goes wrong. 360 */ 361 public Collection findPages( String query, WikiContext wikiContext ) 362 throws ProviderException, IOException 363 { 364 if( query == null ) query = ""; 365 Collection c = m_searchProvider.findPages( query, wikiContext ); 366 367 return c; 368 } 369 370 /** 371 * Removes the page from the search cache (if any). 372 * @param page The page to remove 373 */ 374 public void pageRemoved(WikiPage page) 375 { 376 m_searchProvider.pageRemoved(page); 377 } 378 379 /** 380 * Reindexes the page. 381 * 382 * @param wikiContext {@inheritDoc} 383 * @param content {@inheritDoc} 384 */ 385 @Override 386 public void postSave( WikiContext wikiContext, String content ) 387 { 388 // 389 // Makes sure that we're indexing the latest version of this 390 // page. 391 // 392 WikiPage p = m_engine.getPage( wikiContext.getPage().getName() ); 393 reindexPage( p ); 394 } 395 396 /** 397 * Forces the reindex of the given page. 398 * 399 * @param page The page. 400 */ 401 public void reindexPage(WikiPage page) 402 { 403 m_searchProvider.reindexPage(page); 404 } 405 406 /** 407 * If the page has been deleted, removes it from the index. 408 * 409 * @param event {@inheritDoc} 410 */ 411 public void actionPerformed(WikiEvent event) 412 { 413 if( (event instanceof WikiPageEvent) && (event.getType() == WikiPageEvent.PAGE_DELETE_REQUEST) ) 414 { 415 String pageName = ((WikiPageEvent) event).getPageName(); 416 417 WikiPage p = m_engine.getPage( pageName ); 418 if( p != null ) 419 { 420 pageRemoved( p ); 421 } 422 } 423 } 424 425}