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.isEmpty() ) {
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            LOG.debug( "Suggestion request for {} done in {}", wikiName, sw );
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.isEmpty() ) {
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            LOG.debug( "AJAX search complete in {}", sw );
216            return list;
217        }
218    }
219
220
221    /** {@inheritDoc} */
222    @Override
223    public void initialize( final Engine engine, final Properties properties ) throws FilterException {
224        m_engine = engine;
225        loadSearchProvider(properties);
226
227        try {
228            m_searchProvider.initialize( engine, properties );
229        } catch( final NoRequiredPropertyException | IOException e ) {
230            LOG.error( e.getMessage(), e );
231        }
232    }
233
234    private void loadSearchProvider( final Properties properties ) {
235        // See if we're using Lucene, and if so, ensure that its index directory is up-to-date.
236        final String providerClassName = TextUtil.getStringProperty( properties, PROP_SEARCHPROVIDER, DEFAULT_SEARCHPROVIDER );
237
238        try {
239            m_searchProvider = ClassUtil.buildInstance( "org.apache.wiki.search", providerClassName );
240        } catch( final ReflectiveOperationException e ) {
241            LOG.warn( "Failed loading SearchProvider, will use BasicSearchProvider.", e );
242        }
243
244        if( null == m_searchProvider ) {
245            m_searchProvider = new BasicSearchProvider();
246        }
247        LOG.debug( "Loaded search provider {}", m_searchProvider );
248    }
249
250    /** {@inheritDoc} */
251    @Override
252    public SearchProvider getSearchEngine()
253    {
254        return m_searchProvider;
255    }
256
257    /** {@inheritDoc} */
258    @Override
259    public void actionPerformed( final WikiEvent event ) {
260        if( event instanceof WikiPageEvent ) {
261            final String pageName = ( ( WikiPageEvent ) event ).getPageName();
262            if( event.getType() == WikiPageEvent.PAGE_DELETE_REQUEST ) {
263                final Page p = m_engine.getManager( PageManager.class ).getPage( pageName );
264                if( p != null ) {
265                    pageRemoved( p );
266                }
267            }
268            if( event.getType() == WikiPageEvent.PAGE_REINDEX ) {
269                final Page p = m_engine.getManager( PageManager.class ).getPage( pageName );
270                if( p != null ) {
271                    reindexPage( p );
272                }
273            }
274        }
275    }
276
277}