// 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: GuidesSet.java,v 1.43 2007/10/04 09:55:08 spyromus Exp $ // package com.salas.bb.domain; import com.salas.bb.domain.events.FeedRemovedEvent; import com.salas.bb.domain.query.articles.Query; import com.salas.bb.domain.querytypes.QueryType; import com.salas.bb.utils.IdentityList; import com.salas.bb.utils.StringComparator; import com.salas.bb.utils.i18n.Strings; import java.net.URL; import java.text.MessageFormat; import java.util.*; import java.util.concurrent.CopyOnWriteArrayList; /** * Set of guides. The guides are presented in an order. Each time the guide is added, removed * or moved the event is fired to notify the listeners. * * This implementation is completely thread-safe. */ public class GuidesSet { private final FeedsList feedsList; private final List<IGuide> guides; private final List<IGuidesSetListener> listeners; private final GuidesListener guidesListener; /** * Creates empty guides set. */ public GuidesSet() { feedsList = new FeedsList(); guides = new IdentityList<IGuide>(); listeners = new CopyOnWriteArrayList<IGuidesSetListener>(); guidesListener = new GuidesListener(); } /** * Returns feeds list. * * @return feeds list. */ public FeedsList getFeedsList() { return feedsList; } /** * Adds guide to the set. * * @param guide guide to add. * * @throws NullPointerException if guide isn't specified. */ public synchronized void add(IGuide guide) { add(-1, guide); } /** * Adds guide to the set. * * @param index index of the guide to add at (shifts everything down). * @param guide guide to add. * * @throws NullPointerException if guide isn't specified. */ public synchronized void add(int index, IGuide guide) { add(index, guide, true); } /** * Adds guide to the set. * * @param index index of the guide to add at (shifts everything down). * @param guide guide to add. * @param lastInBatch <code>TRUE</code> if it's the last guide in the batch. * * @throws NullPointerException if guide isn't specified. */ public synchronized void add(int index, IGuide guide, boolean lastInBatch) { if (guide == null) throw new NullPointerException(Strings.error("unspecified.guide")); if (!guides.contains(guide)) { if (index > -1) guides.add(index, guide); else guides.add(guide); guide.addListener(guidesListener); // move all feeds from the guide to the feeds list int count = guide.getFeedsCount(); for (int i = 0; i < count; i++) { feedsList.add(guide.getFeedAt(i)); } fireGuideAdded(guide, lastInBatch); } } /** * Removes guide from the set. * * @param guide guide to remove. * * @return index of the removed guide in the list. * * @throws NullPointerException if guide isn't specified. */ public synchronized int remove(IGuide guide) { if (guide == null) throw new NullPointerException(Strings.error("unspecified.guide")); int index = guides.indexOf(guide); if (index != -1) { guide.removeChildren(); guides.remove(guide); guide.removeListener(guidesListener); fireGuideRemoved(guide, index); } return index; } /** * Returns guide at specified position. * * @param index index of guide. * * @return guide. * * @throws IndexOutOfBoundsException if index is out of guides list index range. */ public synchronized IGuide getGuideAt(int index) { return guides.get(index); } /** * Returns number of guides in the set. * * @return number of guides. */ public synchronized int getGuidesCount() { return guides.size(); } /** * Returns index of guide in the list. * * @param guide guide. * * @return index of guide or (-1) if not in list. * * @throws NullPointerException if guide isn't specified. */ public synchronized int indexOf(IGuide guide) { if (guide == null) throw new NullPointerException(Strings.error("unspecified.guide")); return guides.indexOf(guide); } /** * Returns the list of standard guides which are currently in the set except * the specified guide. If guide is not specified then all standard guides will * be returned. * * @param guide the guide to not to return. * * @return list of guides. */ public synchronized StandardGuide[] getStandardGuides(StandardGuide guide) { ArrayList<StandardGuide> list = new ArrayList<StandardGuide>(getGuidesCount()); for (IGuide iGuide : guides) { if (iGuide instanceof StandardGuide && iGuide != guide) list.add((StandardGuide)iGuide); } return list.toArray(new StandardGuide[list.size()]); } /** * Returns the titles of all guides in this set. The set contains no duplicates. * * @return titles of all guides. */ public synchronized Set<String> getGuidesTitles() { Set<String> titles = new HashSet<String>(getGuidesCount()); for (IGuide guide : guides) titles.add(guide.getTitle()); return titles; } /** * Returns the list of all used icon keys. * * @return list of used icons. */ public synchronized Set<String> getGuidesIconKeys() { Set<String> keys = new HashSet<String>(); for (IGuide guide : guides) { String iconKey = guide.getIconKey(); if (iconKey != null) keys.add(iconKey); } return keys; } /** * Marks all guides as read or unread depending on parameter. * * @param read TRUE to mark as read. */ public synchronized void setRead(boolean read) { for (IGuide guide : guides) guide.setRead(read); } // --------------------------------------------------------------------------------------------- /** * Returns static direct feed by its XML URL. * * @param xmlUrl URL. * * @return feed or <code>NULL</code>. */ public DirectFeed findDirectFeed(URL xmlUrl) { return feedsList.findDirectFeed(xmlUrl); } /** * Returns feed similar to that specified. * * @param feed feed. * * @return feed or <code>NULL</code> if not found. */ public IFeed findFeed(IFeed feed) { IFeed result; if (feed instanceof DirectFeed) { result = findDirectFeed(((DirectFeed)feed).getXmlURL()); } else if (feed instanceof SearchFeed) { result = findSearchFeed(((SearchFeed)feed).getQuery()); } else { QueryFeed qfeed = (QueryFeed)feed; result = findQueryFeed(qfeed.getQueryType(), qfeed.getParameter()); } return result; } /** * Returns query feed by its attributes. * * @param type type of the query. * @param parameter query parameter. * * @return feed or <code>NULL</code>. */ public QueryFeed findQueryFeed(QueryType type, String parameter) { return feedsList.findQueryFeed(type, parameter); } /** * Returns search feed by its query. * * @param query query. * * @return feed or <code>NULL</code>. */ public SearchFeed findSearchFeed(Query query) { return feedsList.findSearchFeed(query); } /** * Returns the list of guides having the given title. * * @param title title of the guides. * * @return collection of the guides. * * @throws NullPointerException if title isn't specified. */ public synchronized Collection<IGuide> findGuidesByTitle(String title) { if (title == null) throw new NullPointerException(Strings.error("unspecified.title")); List<IGuide> guidesCol = new ArrayList<IGuide>(); for (IGuide guide : guides) { if (guide.getTitle().equals(title)) guidesCol.add(guide); } return guidesCol; } /** * Moves the guide to a new position. * * @param guide guide. * @param position new position. * * @throws IllegalArgumentException if guide does not belong to the set. * @throws IndexOutOfBoundsException if index felt out of bounds of guides list. * @throws NullPointerException if guide isn't specified. */ public synchronized void relocateGuide(IGuide guide, int position) { if (guide == null) throw new NullPointerException(Strings.error("unspecified.guide")); // Verify that guide belongs to this set int oldIndex = guides.indexOf(guide); if (oldIndex == -1) throw new IllegalArgumentException(Strings.error("guide.does.not.belong.to.the.set")); // Verify new position int maxPos = guides.size() - 1; if (position < 0 || position > maxPos) throw new IndexOutOfBoundsException(MessageFormat.format( Strings.error("new.guide.position.is.out.of.range"), maxPos)); // Move if (oldIndex != position) { if (guides.remove(guide)) { guides.add(position, guide); fireGuideMoved(guide, oldIndex, position); } } } /** * Adds listener. * * @param listener listener. * * @throws NullPointerException if listener isn't specified. */ public void addListener(IGuidesSetListener listener) { if (listener == null) throw new NullPointerException(Strings.error("unspecified.listener")); if (!listeners.contains(listener)) listeners.add(listener); } /** * Removes listener. * * @param listener listener. * * @throws NullPointerException if listener isn't specified. */ public void removeListener(IGuidesSetListener listener) { if (listener == null) throw new NullPointerException(Strings.error("unspecified.listener")); listeners.remove(listener); } /** * Fires <code>guideAdded</code> event. * * @param guide guide which has been added. * @param lastInBatch <code>TRUE</code> if it's the last guide in the batch. */ private void fireGuideAdded(IGuide guide, boolean lastInBatch) { for (IGuidesSetListener listener : listeners) listener.guideAdded(this, guide, lastInBatch); } /** * Fires <code>guideRemoved</code> event. * * @param guide guide which has been removed. * @param index old guide index. */ private void fireGuideRemoved(IGuide guide, int index) { for (IGuidesSetListener listener : listeners) listener.guideRemoved(this, guide, index); } /** * Fires <code>guideMoved</code> event. * * @param guide guide which has been removed. * @param oldIndex old guide index. * @param newIndex new guide index. */ private void fireGuideMoved(IGuide guide, int oldIndex, int newIndex) { for (IGuidesSetListener listener : listeners) listener.guideMoved(this, guide, oldIndex, newIndex); } /** * Removes all guides from set one-by-one. */ public synchronized void clear() { int count = getGuidesCount(); for (int i = count - 1; i >= 0; i--) { remove(getGuideAt(i)); } } /** * Performs cleaning of all guides. */ public synchronized void clean() { for (IGuide guide : guides) guide.clean(); } /** * Counts all feeds in all guides. * * @return number of feeds. */ public synchronized int countFeeds() { int count = 0; int guidesCount = getGuidesCount(); for (int i = 0; i < guidesCount; i++) count += getGuideAt(i).getFeedsCount(); return count; } /** * Returns collection of unique XML URL's from all URL-having feeds. * * @return collection of XML URL's. */ public Collection<URL> getFeedsXmlURLs() { int count = feedsList.getFeedsCount(); Set<URL> urls = new TreeSet<URL>(new StringComparator<URL>()); for (int i = 0; i < count; i++) { IFeed feed = feedsList.getFeedAt(i); if (feed instanceof NetworkFeed) { URL xmlURL = ((NetworkFeed)feed).getXmlURL(); if (xmlURL != null) urls.add(xmlURL); } } return urls; } /** * Looks for a guide with the given publishing title. Case-insensitive. * * @param publishingTitle title to look for. * * @return the guide or <code>NULL</code> if not found. */ public IGuide getGuideByPublishingTitle(String publishingTitle) { IGuide guide = null; for (int i = 0; guide == null && i < guides.size(); i++) { IGuide iguide = guides.get(i); String title = iguide.getPublishingTitle(); if (title != null && title.equalsIgnoreCase(publishingTitle)) guide = iguide; } return guide; } /** * Returns list of feeds from all guides. Please note that only standard * guides (non-virtual) are taken in account. * * @return list of all feeds. */ public List<IFeed> getFeeds() { // TODO !!! review List<IFeed> feedsList = new ArrayList<IFeed>(); StandardGuide[] allGuides = getStandardGuides(null); for (StandardGuide guide : allGuides) { IFeed[] feeds = guide.getFeeds(); for (IFeed feed : feeds) if (!feedsList.contains(feed)) feedsList.add(feed); } return feedsList; } /** * Returns the list of guides. * * @return guides. */ public synchronized IGuide[] getGuides() { return guides.toArray(new IGuide[guides.size()]); } /** * Updates the times of all objects in all guides with current time. * * @param syncOut <code>TRUE</code> if it was sync out. */ private void onSyncCompletion(boolean syncOut) { long time = System.currentTimeMillis(); StandardGuide[] allGuides = getStandardGuides(null); for (StandardGuide guide : allGuides) guide.onSyncCompletion(time, syncOut); } /** * Invoked on sync-out completion. */ public void onSyncOutCompletion() { onSyncCompletion(true); } /** * Invoked on sync-in completion. */ public void onSyncInCompletion() { onSyncCompletion(false); } /** * Counts all published guides. * * @return number of published guides. */ public synchronized int countPublishedGuides() { int cnt = 0; for (IGuide guide : guides) { if (guide.isPublishingEnabled()) cnt++; } return cnt; } /** * Invalidates feed visibility caches of all feeds in this set. */ public void invalidateFeedVisibilityCaches() { List<IFeed> feeds = feedsList.getFeeds(); for (IFeed feed : feeds) { feed.invalidateVisibilityCache(); } } /** * Replaces one feed in guides and reading lists with the other. * * @param feed feed. * @param replacement replacement. */ public static void replaceFeed(IFeed feed, IFeed replacement) { if (feed instanceof DirectFeed && replacement instanceof DirectFeed) { DirectFeed dfeeds = (DirectFeed)feed; DirectFeed dreplacement = (DirectFeed)replacement; ReadingList[] lists = dfeeds.getReadingLists(); for (ReadingList list : lists) { list.remove(dfeeds); list.add(dreplacement); } } IGuide[] parentGuides = feed.getParentGuides(); for (IGuide parentGuide : parentGuides) { if (parentGuide.remove(feed)) parentGuide.add(replacement); } } /** * Finds a guide with a given ID. * * @param id guide id. * * @return guide or <code>NULL</code> if not found. */ public StandardGuide findGuideByID(Long id) { if (id != null) { for (IGuide g : guides) { if (g.getID() == id && (g instanceof StandardGuide)) return (StandardGuide)g; } } return null; } /** * Finds a feed by ID. * * @param id feed ID. * * @return feed or <code>NULL</code> if not found. */ public IFeed findFeedByID(long id) { List<IFeed> feeds = getFeedsList().getFeeds(); for (IFeed feed : feeds) { if (id == feed.getID()) return feed; } return null; } /** * Listens to guides. */ private class GuidesListener extends GuideAdapter { /** * Invoked when new feed has been added to the guide. * * @param guide parent guide. * @param feed added feed. */ public void feedAdded(IGuide guide, IFeed feed) { // Note that it's not feedLinkAdded because we need to record feeds // from reading lists as well feedsList.add(feed); } /** * Invoked when the feed has been removed from the guide. * * @param event feed removal event. */ public void feedRemoved(FeedRemovedEvent event) { IFeed feed = event.getFeed(); if (!feed.isDynamic() && feed.getParentGuides().length == 0) feedsList.remove(feed); } } }