| /* |
| Licensed to the Apache Software Foundation (ASF) under one |
| or more contributor license agreements. See the NOTICE file |
| distributed with this work for additional information |
| regarding copyright ownership. The ASF licenses this file |
| to you under the Apache License, Version 2.0 (the |
| "License"); you may not use this file except in compliance |
| with the License. You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, |
| software distributed under the License is distributed on an |
| "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| KIND, either express or implied. See the License for the |
| specific language governing permissions and limitations |
| under the License. |
| */ |
| package org.apache.wiki.search; |
| |
| import org.apache.commons.lang3.StringUtils; |
| import org.apache.commons.lang3.time.StopWatch; |
| import org.apache.log4j.Logger; |
| import org.apache.wiki.WikiContext; |
| import org.apache.wiki.WikiEngine; |
| import org.apache.wiki.WikiPage; |
| import org.apache.wiki.ajax.AjaxUtil; |
| import org.apache.wiki.ajax.WikiAjaxDispatcherServlet; |
| import org.apache.wiki.ajax.WikiAjaxServlet; |
| import org.apache.wiki.api.exceptions.FilterException; |
| import org.apache.wiki.api.exceptions.NoRequiredPropertyException; |
| import org.apache.wiki.api.exceptions.ProviderException; |
| import org.apache.wiki.api.filters.BasicPageFilter; |
| import org.apache.wiki.event.WikiEvent; |
| import org.apache.wiki.event.WikiEventListener; |
| import org.apache.wiki.event.WikiEventUtils; |
| import org.apache.wiki.event.WikiPageEvent; |
| import org.apache.wiki.modules.InternalModule; |
| import org.apache.wiki.parser.MarkupParser; |
| import org.apache.wiki.util.ClassUtil; |
| |
| import javax.servlet.ServletException; |
| import javax.servlet.http.HttpServletRequest; |
| import javax.servlet.http.HttpServletResponse; |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Properties; |
| import java.util.Set; |
| |
| /** |
| * Manages searching the Wiki. |
| * |
| * @since 2.2.21. |
| */ |
| public class SearchManager extends BasicPageFilter implements InternalModule, WikiEventListener { |
| |
| private static final Logger log = Logger.getLogger(SearchManager.class); |
| |
| private static final String DEFAULT_SEARCHPROVIDER = "org.apache.wiki.search.LuceneSearchProvider"; |
| |
| /** Property name for setting the search provider. Value is <tt>{@value}</tt>. */ |
| public static final String PROP_SEARCHPROVIDER = "jspwiki.searchProvider"; |
| |
| private SearchProvider m_searchProvider; |
| |
| /** |
| * The name of the JSON object that manages search. |
| */ |
| public static final String JSON_SEARCH = "search"; |
| |
| /** |
| * Creates a new SearchManager. |
| * |
| * @param engine The WikiEngine that owns this SearchManager. |
| * @param properties The list of Properties. |
| * @throws FilterException If it cannot be instantiated. |
| */ |
| public SearchManager( WikiEngine engine, Properties properties ) |
| throws FilterException |
| { |
| initialize( engine, properties ); |
| |
| WikiEventUtils.addWikiEventListener(m_engine.getPageManager(), |
| WikiPageEvent.PAGE_DELETE_REQUEST, this); |
| |
| //TODO: Replace with custom annotations. See JSPWIKI-566 |
| WikiAjaxDispatcherServlet.registerServlet( JSON_SEARCH, new JSONSearch() ); |
| } |
| |
| /** |
| * Provides a JSON RPC API to the JSPWiki Search Engine. |
| */ |
| public class JSONSearch implements WikiAjaxServlet |
| { |
| public static final String AJAX_ACTION_SUGGESTIONS = "suggestions"; |
| public static final String AJAX_ACTION_PAGES = "pages"; |
| public static final int DEFAULT_MAX_RESULTS = 20; |
| public int maxResults = DEFAULT_MAX_RESULTS; |
| |
| @Override |
| public String getServletMapping() { |
| return JSON_SEARCH; |
| } |
| |
| @Override |
| public void service(HttpServletRequest req, HttpServletResponse resp, String actionName, List<String> params) |
| throws ServletException, IOException { |
| String result = ""; |
| if (StringUtils.isNotBlank(actionName)) { |
| if (params.size()<1) { |
| return; |
| } |
| String itemId = params.get(0); |
| log.debug("itemId="+itemId); |
| if (params.size()>1) { |
| String maxResultsParam = params.get(1); |
| log.debug("maxResultsParam="+maxResultsParam); |
| if (StringUtils.isNotBlank(maxResultsParam) && StringUtils.isNumeric(maxResultsParam)) { |
| maxResults = Integer.parseInt(maxResultsParam); |
| } |
| } |
| |
| if (actionName.equals(AJAX_ACTION_SUGGESTIONS)) { |
| List<String> callResults = new ArrayList<>(); |
| log.debug("Calling getSuggestions() START"); |
| callResults = getSuggestions(itemId, maxResults); |
| log.debug("Calling getSuggestions() DONE. "+callResults.size()); |
| result = AjaxUtil.toJson(callResults); |
| } else if (actionName.equals(AJAX_ACTION_PAGES)) { |
| List<Map<String,Object>> callResults = new ArrayList<>(); |
| log.debug("Calling findPages() START"); |
| WikiContext wikiContext = m_engine.createContext(req, WikiContext.VIEW); |
| if (wikiContext == null) { |
| throw new ServletException("Could not create a WikiContext from the request "+req); |
| } |
| callResults = findPages(itemId, maxResults, wikiContext); |
| log.debug("Calling findPages() DONE. "+callResults.size()); |
| result = AjaxUtil.toJson(callResults); |
| } |
| } |
| log.debug("result="+result); |
| resp.getWriter().write(result); |
| } |
| |
| /** |
| * Provides a list of suggestions to use for a page name. |
| * Currently the algorithm just looks into the value parameter, |
| * and returns all page names from that. |
| * |
| * @param wikiName the page name |
| * @param maxLength maximum number of suggestions |
| * @return the suggestions |
| */ |
| public List<String> getSuggestions( String wikiName, int maxLength ) |
| { |
| StopWatch sw = new StopWatch(); |
| sw.start(); |
| List<String> list = new ArrayList<>(maxLength); |
| |
| if( wikiName.length() > 0 ) |
| { |
| |
| // split pagename and attachment filename |
| String filename = ""; |
| int pos = wikiName.indexOf("/"); |
| if( pos >= 0 ) |
| { |
| filename = wikiName.substring( pos ).toLowerCase(); |
| wikiName = wikiName.substring( 0, pos ); |
| } |
| |
| String cleanWikiName = MarkupParser.cleanLink(wikiName).toLowerCase() + filename; |
| |
| String oldStyleName = MarkupParser.wikifyLink(wikiName).toLowerCase() + filename; |
| |
| Set< String > allPages = m_engine.getReferenceManager().findCreated(); |
| |
| int counter = 0; |
| for( Iterator< String > i = allPages.iterator(); i.hasNext() && counter < maxLength; ) |
| { |
| String p = i.next(); |
| String pp = p.toLowerCase(); |
| if( pp.startsWith( cleanWikiName) || pp.startsWith( oldStyleName ) ) |
| { |
| list.add( p ); |
| counter++; |
| } |
| } |
| } |
| |
| sw.stop(); |
| if( log.isDebugEnabled() ) log.debug("Suggestion request for "+wikiName+" done in "+sw); |
| return list; |
| } |
| |
| /** |
| * Performs a full search of pages. |
| * |
| * @param searchString The query string |
| * @param maxLength How many hits to return |
| * @return the pages found |
| */ |
| public List<Map<String,Object>> findPages( String searchString, int maxLength, WikiContext wikiContext ) |
| { |
| StopWatch sw = new StopWatch(); |
| sw.start(); |
| |
| List<Map<String,Object>> list = new ArrayList<>(maxLength); |
| |
| if( searchString.length() > 0 ) { |
| try { |
| Collection< SearchResult > c; |
| |
| if( m_searchProvider instanceof LuceneSearchProvider ) { |
| c = ((LuceneSearchProvider)m_searchProvider).findPages( searchString, 0, wikiContext ); |
| } else { |
| c = m_searchProvider.findPages( searchString, wikiContext ); |
| } |
| |
| int count = 0; |
| for( Iterator< SearchResult > i = c.iterator(); i.hasNext() && count < maxLength; count++ ) |
| { |
| SearchResult sr = i.next(); |
| HashMap<String,Object> hm = new HashMap<>(); |
| hm.put( "page", sr.getPage().getName() ); |
| hm.put( "score", sr.getScore() ); |
| list.add( hm ); |
| } |
| } catch(Exception e) { |
| log.info("AJAX search failed; ",e); |
| } |
| } |
| |
| sw.stop(); |
| if( log.isDebugEnabled() ) log.debug("AJAX search complete in "+sw); |
| return list; |
| } |
| } |
| |
| |
| /** |
| * This particular method starts off indexing and all sorts of various activities, |
| * so you need to run this last, after things are done. |
| * |
| * @param engine the wiki engine |
| * @param properties the properties used to initialize the wiki engine |
| * @throws FilterException if the search provider failed to initialize |
| */ |
| @Override |
| public void initialize(WikiEngine engine, Properties properties) |
| throws FilterException |
| { |
| m_engine = engine; |
| |
| loadSearchProvider(properties); |
| |
| try |
| { |
| m_searchProvider.initialize(engine, properties); |
| } |
| catch (NoRequiredPropertyException e) |
| { |
| log.error( e.getMessage(), e ); |
| } |
| catch (IOException e) |
| { |
| log.error( e.getMessage(), e ); |
| } |
| } |
| |
| private void loadSearchProvider( final Properties properties ) { |
| // |
| // See if we're using Lucene, and if so, ensure that its index directory is up to date. |
| // |
| final String providerClassName = properties.getProperty( PROP_SEARCHPROVIDER, DEFAULT_SEARCHPROVIDER ); |
| |
| try { |
| Class<?> providerClass = ClassUtil.findClass( "org.apache.wiki.search", providerClassName ); |
| m_searchProvider = (SearchProvider)providerClass.newInstance(); |
| } catch( ClassNotFoundException | InstantiationException | IllegalAccessException e ) { |
| log.warn("Failed loading SearchProvider, will use BasicSearchProvider.", e); |
| } |
| |
| if( null == m_searchProvider ) |
| { |
| // FIXME: Make a static with the default search provider |
| m_searchProvider = new BasicSearchProvider(); |
| } |
| log.debug("Loaded search provider " + m_searchProvider); |
| } |
| |
| /** |
| * Returns the SearchProvider used. |
| * |
| * @return The current SearchProvider. |
| */ |
| public SearchProvider getSearchEngine() |
| { |
| return m_searchProvider; |
| } |
| |
| /** |
| * Sends a search to the current search provider. The query is is whatever native format |
| * the query engine wants to use. |
| * |
| * @param query The query. Null is safe, and is interpreted as an empty query. |
| * @param wikiContext the context within which to run the search |
| * @return A collection of WikiPages that matched. |
| * @throws ProviderException If the provider fails and a search cannot be completed. |
| * @throws IOException If something else goes wrong. |
| */ |
| public Collection< SearchResult > findPages( String query, final WikiContext wikiContext ) throws ProviderException, IOException { |
| if( query == null ) { |
| query = ""; |
| } |
| return m_searchProvider.findPages( query, wikiContext ); |
| } |
| |
| /** |
| * Removes the page from the search cache (if any). |
| * @param page The page to remove |
| */ |
| public void pageRemoved( final WikiPage page ) |
| { |
| m_searchProvider.pageRemoved(page); |
| } |
| |
| /** |
| * Reindexes the page. |
| * |
| * @param wikiContext {@inheritDoc} |
| * @param content {@inheritDoc} |
| */ |
| @Override |
| public void postSave( final WikiContext wikiContext, final String content ) { |
| // Makes sure that we're indexing the latest version of this page. |
| final WikiPage p = m_engine.getPageManager().getPage( wikiContext.getPage().getName() ); |
| reindexPage( p ); |
| } |
| |
| /** |
| * Forces the reindex of the given page. |
| * |
| * @param page The page. |
| */ |
| public void reindexPage( final WikiPage page ) |
| { |
| m_searchProvider.reindexPage( page ); |
| } |
| |
| /** |
| * If the page has been deleted, removes it from the index. |
| * |
| * @param event {@inheritDoc} |
| */ |
| @Override |
| public void actionPerformed( final WikiEvent event ) { |
| if( event instanceof WikiPageEvent && event.getType() == WikiPageEvent.PAGE_DELETE_REQUEST ) { |
| final String pageName = ( ( WikiPageEvent ) event ).getPageName(); |
| |
| final WikiPage p = m_engine.getPageManager().getPage( pageName ); |
| if( p != null ) { |
| pageRemoved( p ); |
| } |
| } |
| } |
| |
| } |