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.ajax.AjaxUtil;
025import org.apache.wiki.ajax.WikiAjaxDispatcherServlet;
026import org.apache.wiki.ajax.WikiAjaxServlet;
027import org.apache.wiki.api.core.Context;
028import org.apache.wiki.api.core.ContextEnum;
029import org.apache.wiki.api.core.Engine;
030import org.apache.wiki.api.core.Page;
031import org.apache.wiki.api.exceptions.FilterException;
032import org.apache.wiki.api.exceptions.NoRequiredPropertyException;
033import org.apache.wiki.api.filters.BasePageFilter;
034import org.apache.wiki.api.search.SearchResult;
035import org.apache.wiki.api.spi.Wiki;
036import org.apache.wiki.event.WikiEvent;
037import org.apache.wiki.event.WikiEventManager;
038import org.apache.wiki.event.WikiPageEvent;
039import org.apache.wiki.pages.PageManager;
040import org.apache.wiki.parser.MarkupParser;
041import org.apache.wiki.references.ReferenceManager;
042import org.apache.wiki.util.ClassUtil;
043
044import javax.servlet.http.HttpServletRequest;
045import javax.servlet.http.HttpServletResponse;
046import java.io.IOException;
047import java.util.ArrayList;
048import java.util.Collection;
049import java.util.HashMap;
050import java.util.Iterator;
051import java.util.List;
052import java.util.Map;
053import java.util.Properties;
054import java.util.Set;
055
056
057/**
058 *  Manages searching the Wiki.
059 *
060 *  @since 2.2.21.
061 */
062public class DefaultSearchManager extends BasePageFilter implements SearchManager {
063
064    private static final Logger log = Logger.getLogger( DefaultSearchManager.class );
065
066    private SearchProvider m_searchProvider;
067
068    /**
069     *  Creates a new SearchManager.
070     *
071     *  @param engine The Engine that owns this SearchManager.
072     *  @param properties The list of Properties.
073     *  @throws FilterException If it cannot be instantiated.
074     */
075    public DefaultSearchManager( final Engine engine, final Properties properties ) throws FilterException {
076        initialize( engine, properties );
077        WikiEventManager.addWikiEventListener( m_engine.getManager( PageManager.class ), this );
078
079        // TODO: Replace with custom annotations. See JSPWIKI-566
080        WikiAjaxDispatcherServlet.registerServlet( JSON_SEARCH, new JSONSearch() );
081    }
082
083    /**
084     *  Provides a JSON AJAX API to the JSPWiki Search Engine.
085     */
086    public class JSONSearch implements WikiAjaxServlet {
087
088        public static final String AJAX_ACTION_SUGGESTIONS = "suggestions";
089        public static final String AJAX_ACTION_PAGES = "pages";
090        public static final int DEFAULT_MAX_RESULTS = 20;
091        public int maxResults = DEFAULT_MAX_RESULTS;
092
093        /** {@inheritDoc} */
094        @Override
095        public String getServletMapping() {
096            return JSON_SEARCH;
097        }
098
099        /** {@inheritDoc} */
100        @Override
101        public void service( final HttpServletRequest req,
102                             final HttpServletResponse resp,
103                             final String actionName,
104                             final List< String > params ) throws IOException {
105            String result = "";
106            if( StringUtils.isNotBlank( actionName ) ) {
107                if( params.size() < 1 ) {
108                    return;
109                }
110                final String itemId = params.get( 0 );
111                log.debug( "itemId=" + itemId );
112                if( params.size() > 1 ) {
113                    final String maxResultsParam = params.get( 1 );
114                    log.debug( "maxResultsParam=" + maxResultsParam );
115                    if( StringUtils.isNotBlank( maxResultsParam ) && StringUtils.isNumeric( maxResultsParam ) ) {
116                        maxResults = Integer.parseInt( maxResultsParam );
117                    }
118                }
119
120                if( actionName.equals( AJAX_ACTION_SUGGESTIONS ) ) {
121                    log.debug( "Calling getSuggestions() START" );
122                    final List< String > callResults = getSuggestions( itemId, maxResults );
123                    log.debug( "Calling getSuggestions() DONE. " + callResults.size() );
124                    result = AjaxUtil.toJson( callResults );
125                } else if( actionName.equals( AJAX_ACTION_PAGES ) ) {
126                    log.debug("Calling findPages() START");
127                    final Context wikiContext = Wiki.context().create( m_engine, req, ContextEnum.PAGE_VIEW.getRequestContext() );
128                    final List< Map< String, Object > > callResults = findPages( itemId, maxResults, wikiContext );
129                    log.debug( "Calling findPages() DONE. " + callResults.size() );
130                    result = AjaxUtil.toJson( callResults );
131                }
132            }
133            log.debug( "result=" + result );
134            resp.getWriter().write( result );
135        }
136
137        /**
138         *  Provides a list of suggestions to use for a page name. Currently the algorithm just looks into the value parameter,
139         *  and returns all page names from that.
140         *
141         *  @param wikiName the page name
142         *  @param maxLength maximum number of suggestions
143         *  @return the suggestions
144         */
145        public List< String > getSuggestions( String wikiName, final int maxLength ) {
146            final StopWatch sw = new StopWatch();
147            sw.start();
148            final List< String > list = new ArrayList<>( maxLength );
149            if( wikiName.length() > 0 ) {
150                // split pagename and attachment filename
151                String filename = "";
152                final int pos = wikiName.indexOf("/");
153                if( pos >= 0 ) {
154                    filename = wikiName.substring( pos ).toLowerCase();
155                    wikiName = wikiName.substring( 0, pos );
156                }
157
158                final String cleanWikiName = MarkupParser.cleanLink(wikiName).toLowerCase() + filename;
159                final String oldStyleName = MarkupParser.wikifyLink(wikiName).toLowerCase() + filename;
160                final Set< String > allPages = m_engine.getManager( ReferenceManager.class ).findCreated();
161
162                int counter = 0;
163                for( final Iterator< String > i = allPages.iterator(); i.hasNext() && counter < maxLength; ) {
164                    final String p = i.next();
165                    final String pp = p.toLowerCase();
166                    if( pp.startsWith( cleanWikiName) || pp.startsWith( oldStyleName ) ) {
167                        list.add( p );
168                        counter++;
169                    }
170                }
171            }
172
173            sw.stop();
174            if( log.isDebugEnabled() ) {
175                log.debug( "Suggestion request for " + wikiName + " done in " + sw );
176            }
177            return list;
178        }
179
180        /**
181         *  Performs a full search of pages.
182         *
183         *  @param searchString The query string
184         *  @param maxLength How many hits to return
185         *  @return the pages found
186         */
187        public List< Map< String, Object > > findPages( final String searchString, final int maxLength, final Context wikiContext ) {
188            final StopWatch sw = new StopWatch();
189            sw.start();
190
191            final List< Map< String, Object > > list = new ArrayList<>( maxLength );
192            if( searchString.length() > 0 ) {
193                try {
194                    final Collection< SearchResult > c;
195                    if( m_searchProvider instanceof LuceneSearchProvider ) {
196                        c = ( ( LuceneSearchProvider )m_searchProvider ).findPages( searchString, 0, wikiContext );
197                    } else {
198                        c = m_searchProvider.findPages( searchString, wikiContext );
199                    }
200
201                    int count = 0;
202                    for( final Iterator< SearchResult > i = c.iterator(); i.hasNext() && count < maxLength; count++ ) {
203                        final SearchResult sr = i.next();
204                        final HashMap< String, Object > hm = new HashMap<>();
205                        hm.put( "page", sr.getPage().getName() );
206                        hm.put( "score", sr.getScore() );
207                        list.add( hm );
208                    }
209                } catch( final Exception e ) {
210                    log.info( "AJAX search failed; ", e );
211                }
212            }
213
214            sw.stop();
215            if( log.isDebugEnabled() ) {
216                log.debug( "AJAX search complete in " + sw );
217            }
218            return list;
219        }
220    }
221
222
223    /** {@inheritDoc} */
224    @Override
225    public void initialize( final Engine engine, final Properties properties ) throws FilterException {
226        m_engine = engine;
227        loadSearchProvider(properties);
228
229        try {
230            m_searchProvider.initialize( engine, properties );
231        } catch( final NoRequiredPropertyException | IOException e ) {
232            log.error( e.getMessage(), e );
233        }
234    }
235
236    private void loadSearchProvider( final Properties properties ) {
237        // See if we're using Lucene, and if so, ensure that its index directory is up to date.
238        final String providerClassName = properties.getProperty( PROP_SEARCHPROVIDER, DEFAULT_SEARCHPROVIDER );
239
240        try {
241            final Class<?> providerClass = ClassUtil.findClass( "org.apache.wiki.search", providerClassName );
242            m_searchProvider = ( SearchProvider )providerClass.newInstance();
243        } catch( final ClassNotFoundException | InstantiationException | IllegalAccessException e ) {
244            log.warn("Failed loading SearchProvider, will use BasicSearchProvider.", e);
245        }
246
247        if( null == m_searchProvider ) {
248            // FIXME: Make a static with the default search provider
249            m_searchProvider = new BasicSearchProvider();
250        }
251        log.debug("Loaded search provider " + m_searchProvider);
252    }
253
254    /** {@inheritDoc} */
255    @Override
256    public SearchProvider getSearchEngine()
257    {
258        return m_searchProvider;
259    }
260
261    /** {@inheritDoc} */
262    @Override
263    public void actionPerformed( final WikiEvent event ) {
264        if( event instanceof WikiPageEvent ) {
265            final String pageName = ( ( WikiPageEvent ) event ).getPageName();
266            if( event.getType() == WikiPageEvent.PAGE_DELETE_REQUEST ) {
267                final Page p = m_engine.getManager( PageManager.class ).getPage( pageName );
268                if( p != null ) {
269                    pageRemoved( p );
270                }
271            }
272            if( event.getType() == WikiPageEvent.PAGE_REINDEX ) {
273                final Page p = m_engine.getManager( PageManager.class ).getPage( pageName );
274                if( p != null ) {
275                    reindexPage( p );
276                }
277            }
278        }
279    }
280
281}