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}