    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}