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