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