// 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: SearchFeed.java,v 1.55 2007/11/09 16:24:19 spyromus Exp $ // package com.salas.bb.domain; import com.salas.bb.domain.query.articles.Query; import com.salas.bb.domain.utils.ArticleDateComparator; import com.salas.bb.utils.i18n.Strings; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** * Search feed is a special kind of feeds which is using predefined query to to find * the articles of interest. */ public class SearchFeed extends AbstractFeed { private static final int MAXIMUM_ARTICLES_LIMIT = Integer.MAX_VALUE; private static final ArticleDateComparator articleDateComparator; /** Articles limit property. */ public static final String PROP_ARTICLES_LIMIT = "articlesLimit"; /** Name of query property. */ public static final String PROP_QUERY = "query"; public static final String PROP_DEDUP_ENABLED = "dedupEnabled"; public static final String PROP_DEDUP_FROM = "dedupFrom"; public static final String PROP_DEDUP_TO = "dedupTo"; /** Fake property which is fired if the dedup properties were updated. */ public static final String PROP_DEDUP_UPDATED = "dedupUpdated"; private final List<IArticle> articles; private String baseTitle; private int articlesLimit; private Query query; /** TRUE when deduplication functionality is enabled. */ private boolean dedupEnabled; /** The first word index to look for the match. */ private int dedupFrom; /** The last word index to look for the match. */ private int dedupTo; private SearchFeed.ArticlesListener articlesListener; static { articleDateComparator = new NoDupArticleDateComparator(); } /** * Creates search feed. */ public SearchFeed() { // The LinkedList was replaced by the ArrayList because of EDT locks and // performance issues articles = new ArrayList<IArticle>(); articlesListener = new ArticlesListener(); super.setCustomViewModeEnabled(true); dedupEnabled = false; dedupFrom = 0; dedupTo = 0; } /** * Returns the Article at the specified index. * * @param index index of article in channel. * * @return article object. */ public synchronized IArticle getArticleAt(int index) { return articles.get(index); } /** * Returns number of articles in channel. * * @return number of articles. */ public synchronized int getArticlesCount() { return Math.min(articles.size(), articlesLimit); } /** * Returns number of articles this feed owns. * * @return number of articles. */ public int getOwnArticlesCount() { // Search feed has no own articles return 0; } /** * Returns the list of all articles which are currently in the feed. * * @return all articles at this moment. */ public IArticle[] getArticles() { int visibleArticlesCount; IArticle[] fullList; synchronized (this) { visibleArticlesCount = getArticlesCount(); fullList = articles.toArray(new IArticle[articles.size()]); } // Leave only visible articles in the list. IArticle[] cropped = new IArticle[visibleArticlesCount]; System.arraycopy(fullList, 0, cropped, 0, visibleArticlesCount); return cropped; } /** * Returns title of feed. * * @return title. */ public String getTitle() { return baseTitle; } /** * Gets the base title. * * @return base title. */ public String getBaseTitle() { return baseTitle; } /** * Sets the title of feed. * * @param aTitle feed title. */ public void setBaseTitle(String aTitle) { String oldTitle = getTitle(); baseTitle = aTitle; firePropertyChanged(IFeed.PROP_TITLE, oldTitle, getTitle()); } /** * Returns articles limit. * * @return articles limit. */ public int getArticlesLimit() { return articlesLimit; } /** * Sets the limit of articles in this feed. If the limit is bigger than * <code>MAXIMUM_ARTICLES_LIMIT</code> then it's assigned to it. * * @param anArticlesLimit limit. * * @throws IllegalArgumentException if limit is negative. * * @see #MAXIMUM_ARTICLES_LIMIT */ public synchronized void setArticlesLimit(int anArticlesLimit) { if (anArticlesLimit < 0) throw new IllegalArgumentException(Strings.error("limit.should.be.non.negative")); if (anArticlesLimit > MAXIMUM_ARTICLES_LIMIT) anArticlesLimit = MAXIMUM_ARTICLES_LIMIT; int oldLimit = articlesLimit; articlesLimit = anArticlesLimit; if (isVisible(oldLimit)) { int end = Math.min(articles.size(), articlesLimit); for (int i = oldLimit; i < end; i++) { fireArticleAdded(getArticleAt(i)); } } else { int size = articles.size(); for (int i = size - 1; i >= articlesLimit; i--) { fireArticleRemoved(getArticleAt(i)); } } firePropertyChanged(PROP_ARTICLES_LIMIT, new Integer(oldLimit), new Integer(articlesLimit)); } /** * Returns query. * * @return query. */ public Query getQuery() { return query; } /** * Sets the new query. * * @param aQuery query to set. */ public void setQuery(Query aQuery) { if (query == null || !query.equals(aQuery)) { Query oldQuery = query; query = aQuery; firePropertyChanged(PROP_QUERY, oldQuery, query, true, false); } } /** * Returns simple match key, which can be used to detect similarity of feeds. For example, it's * XML URL for the direct feeds, query type + parameter for the query feeds, serialized search * criteria for the search feeds. * * @return match key. */ public String getMatchKey() { return "SF" + (query == null ? null : query.serializeToString()); } /** * Checks the article agains the query and adds it if the query returned success. * The article may be not added if its insertion index is going to be out of limit. * * @param anArticle article to check. * * @throws NullPointerException if the article isn't specified. */ public synchronized void addArticleIfMatching(IArticle anArticle) { if (anArticle == null) throw new NullPointerException(Strings.error("unspecified.article")); if (query != null && query.match(anArticle) && !isDuplicate(anArticle)) { // Collections.binarySearch can be used to find the index of potential // insertion of new item in the collection basing on its natural order // or order, reported by some comparator. int articleIndex = Collections.binarySearch(articles, anArticle, articleDateComparator); if (articleIndex < 0) { int insertionIndex = -articleIndex - 1; addArticle(anArticle, insertionIndex); } } } /** * Checks if an article is duplicate of some other article. * * @param anArticle article to check. * * @return <code>TRUE</code> if it is. */ private boolean isDuplicate(IArticle anArticle) { return dedupEnabled && isDuplicate(anArticle, dedupFrom, dedupTo, articles); } private void addArticle(IArticle anArticle, int insertionIndex) { int unread = getUnreadArticlesCount(); articles.add(insertionIndex, anArticle); anArticle.addListener(articlesListener); if (isVisible(insertionIndex)) { fireArticleAdded(anArticle); if (articles.size() > articlesLimit) removeArticle(getArticleAt(articlesLimit)); } int newUnread = getUnreadArticlesCount(); if (unread != newUnread) firePropertyChanged(PROP_UNREAD_ARTICLES_COUNT, new Integer(unread), new Integer(getUnreadArticlesCount())); } private boolean isVisible(int index) { return index < articlesLimit; } /** * Removes article from the feed. * * @param anArticle article. */ public synchronized void removeArticle(IArticle anArticle) { int unread = getUnreadArticlesCount(); if (articles.remove(anArticle)) { anArticle.removeListener(articlesListener); fireArticleRemoved(anArticle); int newUnread = getUnreadArticlesCount(); if (unread != newUnread) firePropertyChanged(PROP_UNREAD_ARTICLES_COUNT, new Integer(unread), new Integer(newUnread)); } } /** * Reviews all articles from given feed and removes if they are no longer matching. * * @param feed feed to which article should belong to be reviewed or <code>NULL</code> for any. */ public synchronized void reviewArticlesTakenFrom(IFeed feed) { int count = articles.size(); for (int i = 0; i < count; i++) { int index = count - i - 1; IArticle article = getArticleAt(index); if (feed == null || article.getFeed() == feed) reviewArticle(article); } } private void reviewArticle(IArticle aArticle) { if (!query.match(aArticle) || isDuplicate(aArticle)) { int index = articles.indexOf(aArticle); removeArticle(aArticle); // This piece of code makes another article from the matching appear in the list // instead of deleted one. if (isVisible(index) && articles.size() >= articlesLimit) { fireArticleAdded(getArticleAt(articlesLimit - 1)); } } } /** * Removes all listener registrations. */ public void unregisterListeners() { for (IArticle article : articles) article.removeListener(articlesListener); } /** * Listens for changes in all articles, this feed is referring to. */ private class ArticlesListener implements IArticleListener { /** * Invoked when the property of the article has been changed. * * @param article article. * @param property property of the article. * @param oldValue old property value. * @param newValue new property value. */ public void propertyChanged(IArticle article, String property, Object oldValue, Object newValue) { if (!AbstractArticle.PROP_ID.equals(property)) { synchronized (SearchFeed.this) { reviewArticle(article); } } if (IArticle.PROP_READ.equals(property)) { boolean readNow = (Boolean)newValue; int unread = getUnreadArticlesCount(); firePropertyChanged(PROP_UNREAD_ARTICLES_COUNT, new Integer(readNow ? unread + 1 : unread - 1), new Integer(unread)); } } } /** * This comparator takes the dates order in account, but if the dates are the same * it checks the hash codes. */ private static class NoDupArticleDateComparator extends ArticleDateComparator { public NoDupArticleDateComparator() { super(true); } @Override public int compare(IArticle o1, IArticle o2) { int result = super.compare(o1, o2); if (result == 0) { result = new Integer(o1.hashCode()).compareTo(o2.hashCode()); } return result; } } // ------------------------------------------------------------------------ // Duplicates Checking // ------------------------------------------------------------------------ /** * Returns <code>TRUE</code> if remove duplicates is enabled. * * @return <code>TRUE</code> if remove duplicates is enabled. */ public boolean isDedupEnabled() { return dedupEnabled; } /** * Sets remove duplicates flag. * * @param flag <code>TRUE</code> if remove duplicates is enabled. */ public void setDedupEnabled(boolean flag) { boolean old = dedupEnabled; if (old == flag) return; dedupEnabled = flag; firePropertyChanged(PROP_DEDUP_ENABLED, old, flag, true, false); } /** * Returns the first word to look for duplicates. * * @return word number. */ public int getDedupFrom() { return dedupFrom; } /** * Sets the first word to look for duplicates. * * @param word number */ public void setDedupFrom(int word) { int old = dedupFrom; dedupFrom = word; firePropertyChanged(PROP_DEDUP_FROM, old, word, true, false); } /** * Returns the last word to look for duplicates. * * @return word number. */ public int getDedupTo() { return dedupTo; } /** * Sets the last word to look for duplicates. * * @param word number */ public void setDedupTo(int word) { int old = dedupTo; dedupTo = word; firePropertyChanged(PROP_DEDUP_TO, old, word, true, false); } /** * Updates all dedup properties at once and fires an additional event * if any of them have changed. * * @param enabled <code>TRUE</code> to enable. * @param from the index of the first word. * @param to the index of the last word. */ public void setDedupProperties(boolean enabled, int from, int to) { setDedupProperties(enabled, from, to, true); } /** * Updates all dedup properties at once and fires an additional event * if any of them have changed. * * @param enabled <code>TRUE</code> to enable. * @param from the index of the first word. * @param to the index of the last word. * @param fireEvent <code>TRUE</code> to fire deduplication event. We may not need it * during the batch update, to save a trouble of rescanning feeds * more than once. */ public void setDedupProperties(boolean enabled, int from, int to, boolean fireEvent) { boolean oldEnabled = isDedupEnabled(); int oldFrom = getDedupFrom(); int oldTo = getDedupTo(); setDedupEnabled(enabled); setDedupFrom(from); setDedupTo(to); if (fireEvent && (oldEnabled != isDedupEnabled() || oldFrom != getDedupFrom() || oldTo != getDedupTo())) { firePropertyChanged(PROP_DEDUP_UPDATED, false, true); } } }