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 }