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