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