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