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    }