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<>(); 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<>(); 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<>(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< String > allPages = m_engine.getReferenceManager().findCreated(); 189 190 int counter = 0; 191 for( Iterator< String > i = allPages.iterator(); i.hasNext() && counter < maxLength; ) 192 { 193 String p = 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<>(maxLength); 221 222 if( searchString.length() > 0 ) { 223 try { 224 Collection< SearchResult > c; 225 226 if( m_searchProvider instanceof LuceneSearchProvider ) { 227 c = ((LuceneSearchProvider)m_searchProvider).findPages( searchString, 0, wikiContext ); 228 } else { 229 c = m_searchProvider.findPages( searchString, wikiContext ); 230 } 231 232 int count = 0; 233 for( Iterator< SearchResult > i = c.iterator(); i.hasNext() && count < maxLength; count++ ) 234 { 235 SearchResult sr = i.next(); 236 HashMap<String,Object> hm = new HashMap<>(); 237 hm.put( "page", sr.getPage().getName() ); 238 hm.put( "score", sr.getScore() ); 239 list.add( hm ); 240 } 241 } catch(Exception e) { 242 log.info("AJAX search failed; ",e); 243 } 244 } 245 246 sw.stop(); 247 if( log.isDebugEnabled() ) log.debug("AJAX search complete in "+sw); 248 return list; 249 } 250 } 251 252 253 /** 254 * This particular method starts off indexing and all sorts of various activities, 255 * so you need to run this last, after things are done. 256 * 257 * @param engine the wiki engine 258 * @param properties the properties used to initialize the wiki engine 259 * @throws FilterException if the search provider failed to initialize 260 */ 261 @Override 262 public void initialize(WikiEngine engine, Properties properties) 263 throws FilterException 264 { 265 m_engine = engine; 266 267 loadSearchProvider(properties); 268 269 try 270 { 271 m_searchProvider.initialize(engine, properties); 272 } 273 catch (NoRequiredPropertyException e) 274 { 275 log.error( e.getMessage(), e ); 276 } 277 catch (IOException e) 278 { 279 log.error( e.getMessage(), e ); 280 } 281 } 282 283 private void loadSearchProvider(Properties properties) { 284 // 285 // See if we're using Lucene, and if so, ensure that its 286 // index directory is up to date. 287 // 288 String useLucene = properties.getProperty(PROP_USE_LUCENE); 289 290 // FIXME: Obsolete, remove, or change logic to first load searchProvder? 291 // If the old jspwiki.useLucene property is set we use that instead of the searchProvider class. 292 if( useLucene != null ) 293 { 294 log.info( PROP_USE_LUCENE+" is deprecated; please use "+PROP_SEARCHPROVIDER+"=<your search provider> instead." ); 295 if( TextUtil.isPositive( useLucene ) ) 296 { 297 m_searchProvider = new LuceneSearchProvider(); 298 } 299 else 300 { 301 m_searchProvider = new BasicSearchProvider(); 302 } 303 log.debug("useLucene was set, loading search provider " + m_searchProvider); 304 return; 305 } 306 307 String providerClassName = properties.getProperty( PROP_SEARCHPROVIDER, DEFAULT_SEARCHPROVIDER ); 308 309 try { 310 Class<?> providerClass = ClassUtil.findClass( "org.apache.wiki.search", providerClassName ); 311 m_searchProvider = (SearchProvider)providerClass.newInstance(); 312 } catch( ClassNotFoundException | InstantiationException | IllegalAccessException e ) { 313 log.warn("Failed loading SearchProvider, will use BasicSearchProvider.", e); 314 } 315 316 if( null == m_searchProvider ) 317 { 318 // FIXME: Make a static with the default search provider 319 m_searchProvider = new BasicSearchProvider(); 320 } 321 log.debug("Loaded search provider " + m_searchProvider); 322 } 323 324 /** 325 * Returns the SearchProvider used. 326 * 327 * @return The current SearchProvider. 328 */ 329 public SearchProvider getSearchEngine() 330 { 331 return m_searchProvider; 332 } 333 334 /** 335 * Sends a search to the current search provider. The query is is whatever native format 336 * the query engine wants to use. 337 * 338 * @param query The query. Null is safe, and is interpreted as an empty query. 339 * @param wikiContext the context within which to run the search 340 * @return A collection of WikiPages that matched. 341 * @throws ProviderException If the provider fails and a search cannot be completed. 342 * @throws IOException If something else goes wrong. 343 */ 344 public Collection< SearchResult > findPages( String query, WikiContext wikiContext ) 345 throws ProviderException, IOException 346 { 347 if( query == null ) query = ""; 348 return m_searchProvider.findPages( query, wikiContext ); 349 } 350 351 /** 352 * Removes the page from the search cache (if any). 353 * @param page The page to remove 354 */ 355 public void pageRemoved(WikiPage page) 356 { 357 m_searchProvider.pageRemoved(page); 358 } 359 360 /** 361 * Reindexes the page. 362 * 363 * @param wikiContext {@inheritDoc} 364 * @param content {@inheritDoc} 365 */ 366 @Override 367 public void postSave( WikiContext wikiContext, String content ) 368 { 369 // 370 // Makes sure that we're indexing the latest version of this 371 // page. 372 // 373 WikiPage p = m_engine.getPage( wikiContext.getPage().getName() ); 374 reindexPage( p ); 375 } 376 377 /** 378 * Forces the reindex of the given page. 379 * 380 * @param page The page. 381 */ 382 public void reindexPage(WikiPage page) 383 { 384 m_searchProvider.reindexPage(page); 385 } 386 387 /** 388 * If the page has been deleted, removes it from the index. 389 * 390 * @param event {@inheritDoc} 391 */ 392 @Override 393 public void actionPerformed(WikiEvent event) 394 { 395 if( (event instanceof WikiPageEvent) && (event.getType() == WikiPageEvent.PAGE_DELETE_REQUEST) ) 396 { 397 String pageName = ((WikiPageEvent) event).getPageName(); 398 399 WikiPage p = m_engine.getPage( pageName ); 400 if( p != null ) 401 { 402 pageRemoved( p ); 403 } 404 } 405 } 406 407}