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}