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