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