// BlogBridge -- RSS feed reader, manager, and web based service // Copyright (C) 2002-2006 by R. Pito Salas // // This program is free software; you can redistribute it and/or modify it under // the terms of the GNU General Public License as published by the Free Software Foundation; // either version 2 of the License, or (at your option) any later version. // // This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; // without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. // See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with this program; // if not, write to the Free Software Foundation, Inc., 59 Temple Place, // Suite 330, Boston, MA 02111-1307 USA // // Contact: R. Pito Salas // mailto:pitosalas@users.sourceforge.net // More information: about BlogBridge // http://www.blogbridge.com // http://sourceforge.net/projects/blogbridge // // $Id: SearchEngine.java,v 1.23 2008/12/25 07:50:45 spyromus Exp $ // package com.salas.bb.search; import EDU.oswego.cs.dl.util.concurrent.Executor; import EDU.oswego.cs.dl.util.concurrent.ThreadedExecutor; import com.salas.bb.domain.*; import com.salas.bb.utils.StringUtils; import com.salas.bb.utils.swinghtml.TextProcessor; import java.util.ArrayList; import java.util.List; import java.util.regex.Pattern; /** * Search engine which takes the domain and search string as input and returns * the list of matching things. */ public class SearchEngine { /** Search engine thread. */ private static final Executor executor; /** Result object tis search engine communicates with the outer world through. */ private final SearchResult result; /** The set of guides this engine runs over. */ private GuidesSet guidesSet; static { executor = new ThreadedExecutor(); } /** * Creates search engine. */ public SearchEngine() { result = new SearchResult(); } /** * Registers the set to supervise. * * @param aGuidesSet set. */ public void setGuidesSet(GuidesSet aGuidesSet) { guidesSet = aGuidesSet; } /** * Returns the result of a search which will be updated by this search engine. * It can be a good idea to add listener to watch the changes right after you * get the object. * * @return result object. */ public ISearchResult getResult() { return result; } /** * Sets the search text and starts search immediately, while updating the * results of the search asynchronously. * * @param text new text. * @param pinnedArticlesOnly <code>TRUE</code> to match only pinned articles. */ public void setSearchText(final String text, final boolean pinnedArticlesOnly) { // EDT !!! result.removeAll(); if (guidesSet != null && StringUtils.isNotEmpty(text)) { Runnable task = new Runnable() { public void run() { doSearch(text.trim(), pinnedArticlesOnly); } }; try { executor.execute(task); } catch (InterruptedException e) { task.run(); } } else finished(); } /** * Performs search for a given text and updates the result. * * @param text text. * @param pinnedArticlesOnly <code>TRUE</code> to match only pinned articles. */ private void doSearch(String text, boolean pinnedArticlesOnly) { try { SearchMatcher matcher = createMatcher(text, pinnedArticlesOnly); // Check guides int guidesCnt = guidesSet.getGuidesCount(); for (int g = 0; g < guidesCnt; g++) { IGuide guide = guidesSet.getGuideAt(g); if (matcher.matches(guide)) addItem(guide); } // Check feeds FeedsList feedsList = guidesSet.getFeedsList(); for (int f = 0; f < feedsList.getFeedsCount(); f++) { IFeed feed = feedsList.getFeedAt(f); if (matcher.matches(feed)) addItem(feed); } // Check articles for (int f = 0; f < feedsList.getFeedsCount(); f++) { IFeed feed = feedsList.getFeedAt(f); if (feed instanceof DataFeed) { // If this feed is data feed (contains articles) IArticle[] articles = feed.getArticles(); for (IArticle article : articles) { if (matcher.matches(article)) addItem(article); } } } } finally { finished(); } } /** * Reports finish of search. */ private void finished() { result.fireFinished(); } /** * Adds item to the cache and submits if the last submission was more * than <code>NEW_ITEMS_DELAY</code> milis ago. * * @param item item. */ private void addItem(final Object item) { result.addItem(item); } // --------------------------------------------------------------------------------------------- // Matchers // --------------------------------------------------------------------------------------------- /** * Creates matcher for pattern. * * @param pattern pattern. * @param aPinnedArticlesOnly <code>TRUE</code> to match only pinned articles. * * @return matcher. */ static SearchMatcher createMatcher(String pattern, boolean aPinnedArticlesOnly) { SearchMatcher matcher; if (isComplexSeachPattern(pattern)) { // regex matcher is required matcher = new RegexMatcher(pattern, aPinnedArticlesOnly); } else { matcher = new SimpleMatcher(pattern, aPinnedArticlesOnly); } return matcher; } /** * Returns <code>TRUE</code> if the search pattern has complex format. * * @param pattern pattern text. * * @return <code>TRUE</code> if the search pattern has complex format. */ public static boolean isComplexSeachPattern(String pattern) { return pattern.indexOf('"') != -1 || pattern.indexOf('*') != -1 || pattern.indexOf('+') != -1; } /** * Matcher interface for all types of matchers. */ static abstract class SearchMatcher { private final boolean pinnedArticlesOnly; /** * Creates the matcher. * * @param pinnedArticlesOnly <code>TRUE</code> if pinned articles only match. */ protected SearchMatcher(boolean pinnedArticlesOnly) { this.pinnedArticlesOnly = pinnedArticlesOnly; } /** * Returns <code>TRUE</code> if a guide matches. * * @param guide guide. * * @return result. */ public boolean matches(IGuide guide) { return !pinnedArticlesOnly && matches(guide.getTitle().toLowerCase()); } /** * Returns <code>TRUE</code> if a feed matches. * * @param feed feed. * * @return result. */ public boolean matches(IFeed feed) { return !pinnedArticlesOnly && matches(feed.getTitle().toLowerCase()); } /** * Returns <code>TRUE</code> if an article matches. * * @param article guide. * * @return result. */ public boolean matches(IArticle article) { if (pinnedArticlesOnly && !article.isPinned()) return false; String title = article.getTitle().toLowerCase(); boolean matches = matches(title); if (!matches) { matches = matches(TextProcessor.toPlainText(article.getPlainText())); } return matches; } /** * Returns <code>TRUE</code> if the text matches. * * @param text text. * * @return result. */ protected abstract boolean matches(String text); } /** * Simply checks if the pattern is in the text. */ private static class SimpleMatcher extends SearchMatcher { private final String pattern; /** * Creates matcher. * * @param aPattern pattern. * @param aPinnedArticlesOnly <code>TRUE</code> to match only pinned articles. */ public SimpleMatcher(String aPattern, boolean aPinnedArticlesOnly) { super(aPinnedArticlesOnly); pattern = aPattern; } /** * Returns <code>TRUE</code> if the text matches. * * @param text text. * * @return result. */ protected boolean matches(String text) { return text.indexOf(pattern) != -1; } } /** * Checks if the pattern is in the text. */ private static class RegexMatcher extends SearchMatcher { private final Pattern pattern; /** * Creates matcher for a given pattern. * * @param aPattern pattern. * @param aPinnedArticlesOnly <code>TRUE</code> to match only pinned articles. */ public RegexMatcher(String aPattern, boolean aPinnedArticlesOnly) { super(aPinnedArticlesOnly); String regex = StringUtils.keywordsToPattern(aPattern.toLowerCase().trim()); pattern = regex == null ? null : Pattern.compile(regex, Pattern.CASE_INSENSITIVE); } /** * Returns <code>TRUE</code> if the text matches. * * @param text text. * * @return result. */ protected boolean matches(String text) { return pattern != null && pattern.matcher(text).find(); } } // --------------------------------------------------------------------------------------------- // Result interaction // --------------------------------------------------------------------------------------------- /** * Search result. */ private static class SearchResult implements ISearchResult { private final List<ResultItem> items = new ArrayList<ResultItem>(); private final List<ISearchResultListener> listeners = new ArrayList<ISearchResultListener>(); /** * Returns the number of result items. * * @return items. */ public int getItemsCount() { return items.size(); } /** * Returns the result item at a given index. * * @param index index. * * @return item. */ public ResultItem getItem(int index) { return items.get(index); } /** * Subscribes the listener to changes notifications. * * @param l listener. */ public void addChangesListener(ISearchResultListener l) { if (!listeners.contains(l)) listeners.add(l); } /** * Unsubscribes the listener from changes notifications. * * @param l listener. */ public void removeChangesListener(ISearchResultListener l) { listeners.remove(l); } /** * Removes all items. */ public void removeAll() { items.clear(); fireItemsRemoved(); } /** * Adds new item if it's not there yet. * * @param item item. */ public void addItem(Object item) { ResultItem it = new ResultItem(item); boolean present = items.contains(it); if (!present) { items.add(it); fireItemAdded(it); } } /** * Fires item addition event. * * @param item item. */ private void fireItemAdded(ResultItem item) { int index = items.indexOf(item); for (ISearchResultListener listener : listeners) { listener.itemAdded(this, item, index); } } /** * Fires items removal event. */ private void fireItemsRemoved() { for (ISearchResultListener listener : listeners) { listener.itemsRemoved(this); } } /** * Fires search finish event. */ public void fireFinished() { for (ISearchResultListener listener : listeners) { listener.finished(this); } } } }