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