    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 jakarta.servlet.http.HttpServletRequest;
    047import jakarta.servlet.http.HttpServletResponse;
    048import java.io.IOException;
    049import java.util.ArrayList;
    050import java.util.Collection;
    051import java.util.Collections;
    052import java.util.Comparator;
    053import java.util.HashMap;
    054import java.util.Iterator;
    055import java.util.List;
    056import java.util.Locale;
    057import java.util.Map;
    058import java.util.Properties;
    059import java.util.Set;
    060import org.apache.wiki.api.plugin.Plugin;
    061import org.apache.wiki.plugin.PluginManager;
    062import static org.apache.wiki.search.SearchManager.PLUGIN_SEARCH;
    063
    064
    065/**
    066 *  Manages searching the Wiki.
    067 *
    068 *  @since 2.2.21.
    069 */
    070public class DefaultSearchManager extends BasePageFilter implements SearchManager {
    071
    072    private static final Logger LOG = LogManager.getLogger( DefaultSearchManager.class );
    073
    074    private SearchProvider m_searchProvider;
    075
    076    /**
    077     *  Creates a new SearchManager.
    078     *
    079     *  @param engine The Engine that owns this SearchManager.
    080     *  @param properties The list of Properties.
    081     *  @throws FilterException If it cannot be instantiated.
    082     */
    083    public DefaultSearchManager( final Engine engine, final Properties properties ) throws FilterException {
    084        initialize( engine, properties );
    085        WikiEventManager.addWikiEventListener( m_engine.getManager( PageManager.class ), this );
    086
    087        // TODO: Replace with custom annotations. See JSPWIKI-566
    088        WikiAjaxDispatcherServlet.registerServlet( JSON_SEARCH, new JSONSearch() );
    089        WikiAjaxDispatcherServlet.registerServlet( PLUGIN_SEARCH, new PluginSearch() );
    090    }
    091
    092    /**
    093     *  Provides a JSON AJAX API to the JSPWiki Plugin discovery mechanism,
    094     *  primarily used for [{}] based auto complete
    095     */
    096    public class PluginSearch implements WikiAjaxServlet {
    097
    098        public static final String AJAX_ACTION_PLUGINS = "plugins";
    099        public static final int DEFAULT_MAX_RESULTS = 20;
    100        public int maxResults = DEFAULT_MAX_RESULTS;
    101
    102        /** {@inheritDoc} */
    103        @Override
    104        public String getServletMapping() {
    105            return PLUGIN_SEARCH;
    106        }
    107        public static class SimpleSnipData {
    108            public String displayName;
    109            public String snip;
    110        }
    111
    112        /** {@inheritDoc} */
    113        @Override
    114        public void service( final HttpServletRequest req,
    115                             final HttpServletResponse resp,
    116                             final String actionName,
    117                             final List< String > params ) throws IOException {
    118            String result = "[]";
    119            resp.setContentType("application/json");
    120            if( actionName != null && StringUtils.isNotBlank( actionName ) ) {
    121                if( actionName.equals( AJAX_ACTION_PLUGINS ) ) {
    122                    LOG.debug( "Calling getPlugins() START" );
    123                    PluginManager mgr = m_engine.getManager(PluginManager.class);
    124                    final List< Plugin > plugins = mgr.getDiscoveredPlugins();
    125                    List< SimpleSnipData > callResults = new ArrayList<>();
    126
    127                    Locale locale = req.getLocale();
    128                    if (locale == null) {
    129                        locale = Locale.getDefault();
    130                    }
    131                    final Locale sorter = locale;
    132                    Collections.sort(plugins, new Comparator<Plugin>() {
    133                        @Override
    134                        public int compare(Plugin o1, Plugin o2) {
    135                            return o1.getDisplayName(sorter).compareTo(o2.getDisplayName(sorter));
    136                        }
    137                    });
    138                    for (Plugin p : plugins) {
    139                        try {
    140                            SimpleSnipData data = new SimpleSnipData();
    141                            data.snip = p.getSnipExample();
    142                            data.displayName = p.getDisplayName(locale);
    143                            callResults.add(data);
    144                        } catch (Throwable t) {
    145                            LOG.warn("failed to get plugin informatiom from " + p.getClass().getCanonicalName(), t);
    146                        }
    147                    }
    148                    LOG.debug("Calling getSuggestions() DONE. " + callResults.size() );
    149                    result = AjaxUtil.toJson( callResults );
    150                }
    151            }
    152            LOG.debug( "result=" + result );
    153            resp.getWriter().write( result );
    154        }
    155    }
    156
    157    /**
    158     *  Provides a JSON AJAX API to the JSPWiki Search Engine.
    159     */
    160    public class JSONSearch implements WikiAjaxServlet {
    161
    162        public static final String AJAX_ACTION_SUGGESTIONS = "suggestions";
    163        public static final String AJAX_ACTION_PAGES = "pages";
    164        public static final int DEFAULT_MAX_RESULTS = 20;
    165        public int maxResults = DEFAULT_MAX_RESULTS;
    166
    167        /** {@inheritDoc} */
    168        @Override
    169        public String getServletMapping() {
    170            return JSON_SEARCH;
    171        }
    172
    173        /** {@inheritDoc} */
    174        @Override
    175        public void service( final HttpServletRequest req,
    176                             final HttpServletResponse resp,
    177                             final String actionName,
    178                             final List< String > params ) throws IOException {
    179            String result = "[]";
    180            resp.setContentType("application/json");
    181            if( StringUtils.isNotBlank( actionName ) ) {
    182                if( params.isEmpty() ) {
    183                    return;
    184                }
    185                final String itemId = params.get( 0 );
    186                LOG.debug( "itemId=" + itemId );
    187                if( params.size() > 1 ) {
    188                    final String maxResultsParam = params.get( 1 );
    189                    LOG.debug( "maxResultsParam=" + maxResultsParam );
    190                    if( StringUtils.isNotBlank( maxResultsParam ) && StringUtils.isNumeric( maxResultsParam ) ) {
    191                        maxResults = Integer.parseInt( maxResultsParam );
    192                    }
    193                }
    194
    195                if( actionName.equals( AJAX_ACTION_SUGGESTIONS ) ) {
    196                    LOG.debug( "Calling getSuggestions() START" );
    197                    final List< String > callResults = getSuggestions( itemId, maxResults );
    198                    LOG.debug( "Calling getSuggestions() DONE. " + callResults.size() );
    199                    result = AjaxUtil.toJson( callResults );
    200                } else if( actionName.equals( AJAX_ACTION_PAGES ) ) {
    201                    LOG.debug("Calling findPages() START");
    202                    final Context wikiContext = Wiki.context().create( m_engine, req, ContextEnum.PAGE_VIEW.getRequestContext() );
    203                    final List< Map< String, Object > > callResults = findPages( itemId, maxResults, wikiContext );
    204                    LOG.debug( "Calling findPages() DONE. " + callResults.size() );
    205                    result = AjaxUtil.toJson( callResults );
    206                }
    207            }
    208            LOG.debug( "result=" + result );
    209            resp.getWriter().write( result );
    210        }
    211
    212        /**
    213         *  Provides a list of suggestions to use for a page name. Currently, the algorithm just looks into the value parameter,
    214         *  and returns all page names from that.
    215         *
    216         *  @param wikiName the page name
    217         *  @param maxLength maximum number of suggestions
    218         *  @return the suggestions
    219         */
    220        public List< String > getSuggestions( String wikiName, final int maxLength ) {
    221            final StopWatch sw = new StopWatch();
    222            sw.start();
    223            final List< String > list = new ArrayList<>( maxLength );
    224            if( !wikiName.isEmpty() ) {
    225                // split pagename and attachment filename
    226                String filename = "";
    227                final int pos = wikiName.indexOf("/");
    228                if( pos >= 0 ) {
    229                    filename = wikiName.substring( pos ).toLowerCase();
    230                    wikiName = wikiName.substring( 0, pos );
    231                }
    232
    233                final String cleanWikiName = MarkupParser.cleanLink(wikiName).toLowerCase() + filename;
    234                final String oldStyleName = MarkupParser.wikifyLink(wikiName).toLowerCase() + filename;
    235                final Set< String > allPages = m_engine.getManager( ReferenceManager.class ).findCreated();
    236
    237                int counter = 0;
    238                for( final Iterator< String > i = allPages.iterator(); i.hasNext() && counter < maxLength; ) {
    239                    final String p = i.next();
    240                    final String pp = p.toLowerCase();
    241                    if( pp.startsWith( cleanWikiName) || pp.startsWith( oldStyleName ) ) {
    242                        list.add( p );
    243                        counter++;
    244                    }
    245                }
    246            }
    247
    248            sw.stop();
    249            LOG.debug( "Suggestion request for {} done in {}", wikiName, sw );
    250            return list;
    251        }
    252
    253        /**
    254         *  Performs a full search of pages.
    255         *
    256         *  @param searchString The query string
    257         *  @param maxLength How many hits to return
    258         *  @return the pages found
    259         */
    260        public List< Map< String, Object > > findPages( final String searchString, final int maxLength, final Context wikiContext ) {
    261            final StopWatch sw = new StopWatch();
    262            sw.start();
    263
    264            final List< Map< String, Object > > list = new ArrayList<>( maxLength );
    265            if( !searchString.isEmpty() ) {
    266                try {
    267                    final Collection< SearchResult > c;
    268                    if( m_searchProvider instanceof LuceneSearchProvider ) {
    269                        c = ( ( LuceneSearchProvider )m_searchProvider ).findPages( searchString, 0, wikiContext );
    270                    } else {
    271                        c = m_searchProvider.findPages( searchString, wikiContext );
    272                    }
    273
    274                    int count = 0;
    275                    for( final Iterator< SearchResult > i = c.iterator(); i.hasNext() && count < maxLength; count++ ) {
    276                        final SearchResult sr = i.next();
    277                        final HashMap< String, Object > hm = new HashMap<>();
    278                        hm.put( "page", sr.getPage().getName() );
    279                        hm.put( "score", sr.getScore() );
    280                        list.add( hm );
    281                    }
    282                } catch( final Exception e ) {
    283                    LOG.info( "AJAX search failed; ", e );
    284                }
    285            }
    286
    287            sw.stop();
    288            LOG.debug( "AJAX search complete in {}", sw );
    289            return list;
    290        }
    291    }
    292
    293
    294    /** {@inheritDoc} */
    295    @Override
    296    public void initialize( final Engine engine, final Properties properties ) throws FilterException {
    297        m_engine = engine;
    298        loadSearchProvider(properties);
    299
    300        try {
    301            m_searchProvider.initialize( engine, properties );
    302        } catch( final NoRequiredPropertyException | IOException e ) {
    303            LOG.error( e.getMessage(), e );
    304        }
    305    }
    306
    307    private void loadSearchProvider( final Properties properties ) {
    308        // See if we're using Lucene, and if so, ensure that its index directory is up-to-date.
    309        final String providerClassName = TextUtil.getStringProperty( properties, PROP_SEARCHPROVIDER, DEFAULT_SEARCHPROVIDER );
    310
    311        try {
    312            m_searchProvider = ClassUtil.buildInstance( "org.apache.wiki.search", providerClassName );
    313        } catch( final ReflectiveOperationException e ) {
    314            LOG.warn( "Failed loading SearchProvider, will use BasicSearchProvider.", e );
    315        }
    316
    317        if( null == m_searchProvider ) {
    318            m_searchProvider = new BasicSearchProvider();
    319        }
    320        LOG.debug( "Loaded search provider {}", m_searchProvider );
    321    }
    322
    323    /** {@inheritDoc} */
    324    @Override
    325    public SearchProvider getSearchEngine()
    326    {
    327        return m_searchProvider;
    328    }
    329
    330    /** {@inheritDoc} */
    331    @Override
    332    public void actionPerformed( final WikiEvent event ) {
    333        if( event instanceof WikiPageEvent ) {
    334            final String pageName = ( ( WikiPageEvent ) event ).getPageName();
    335            if( event.getType() == WikiPageEvent.PAGE_DELETE_REQUEST ) {
    336                final Page p = m_engine.getManager( PageManager.class ).getPage( pageName );
    337                if( p != null ) {
    338                    pageRemoved( p );
    339                }
    340            }
    341            if( event.getType() == WikiPageEvent.PAGE_REINDEX ) {
    342                final Page p = m_engine.getManager( PageManager.class ).getPage( pageName );
    343                if( p != null ) {
    344                    reindexPage( p );
    345                }
    346            }
    347        }
    348    }
    349
    350}