// 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: GuideModel.java,v 1.31 2008/02/28 15:59:49 spyromus Exp $ // package com.salas.bb.core; import com.salas.bb.domain.FeedClassifier; import com.salas.bb.domain.FeedsSortOrder; import com.salas.bb.domain.IFeed; import com.salas.bb.domain.IGuide; import com.salas.bb.domain.prefs.UserPreferences; import com.salas.bb.utils.Constants; import com.salas.bb.utils.Sort; import javax.swing.*; /** * This list indirectly forwards various requests to the * currently selected Channel Guide. There is only ONE ChannelGuideModel which simply * forward the requests to the underlying ChannelGuide models which is currently * selected. * * <h2>Sorting</h2> * * <p>When sorting is enabled model calculates the intermediate scores for each feed * and orders them using these scores. The scores are calculated basing on data of feeds * and selected first/second sort classes * ({@link #calculateScore(com.salas.bb.domain.IFeed, int, int)}). Later when sorting is * required the scores are compared as regular integers and position of feed items in the * list is evaluated.</p> */ public class GuideModel extends AbstractListModel { private final boolean nonVisual; private ScoresCalculator scoreCalculator; private JList listComponent; private IFeed[] feeds; private int[] channelsClasses; private int[] channelsScores; private int[] filteredChannels; private int filteredChannelsCount; private int[] sortedChannels; private int sortedChannelsCount; // Settings private int scoreThreshold; private boolean sortingEnabled; private int primarySort; private boolean primarySortRev; private int secondarySort; private boolean secondarySortRev; private IGuide currentGuide; private FeedScoresComparator feedScoresComparator; private IFeed selectedFeed; /** * Creates model and initializes it with calculator of feeds scores. * * @param aCalculator calculator of feeds scores. * @param aFeedDDM feed display mode manager instance to control visibility. */ public GuideModel(ScoresCalculator aCalculator, FeedDisplayModeManager aFeedDDM) { this(aCalculator, true, aFeedDDM); } /** * Creates model and initializes it with calculator of feeds scores. * * @param aCalculator calculator of feeds scores. * @param visual TRUE if this model will have visual components attached. * @param aFeedDMM feed display mode manager instance to control visibility. */ public GuideModel(ScoresCalculator aCalculator, boolean visual, FeedDisplayModeManager aFeedDMM) { scoreThreshold = UserPreferences.DEFAULT_GOOD_CHANNEL_STARZ - 1; sortingEnabled = UserPreferences.DEFAULT_SORTING_ENABLED; primarySort = UserPreferences.DEFAULT_SORT_BY_CLASS_1; primarySortRev = UserPreferences.DEFAULT_REVERSED_SORT_BY_CLASS_1; secondarySort = UserPreferences.DEFAULT_SORT_BY_CLASS_2; secondarySortRev = UserPreferences.DEFAULT_REVERSED_SORT_BY_CLASS_2; scoreCalculator = aCalculator; nonVisual = !visual; currentGuide = null; selectedFeed = null; setFeeds(Constants.EMPTY_FEEDS_LIST, true); } /** * Registers component which is using this model. This component will be questioned for * selection when the model will be preparing updates. * * @param aListComponent component. */ public void setListComponent(JList aListComponent) { listComponent = aListComponent; } /** * Sets the guide to display. * * @param aGuide guide to display. */ public void setGuide(IGuide aGuide) { IFeed[] feedsList; if (aGuide == null) { feedsList = Constants.EMPTY_FEEDS_LIST; } else { feedsList = aGuide.getFeeds(); } synchronized (this) { boolean newGuide = currentGuide != aGuide; currentGuide = aGuide; setFeeds(feedsList, newGuide); } } /** * Returns current guide this model has been initialized with. * * @return the guide. */ public IGuide getCurrentGuide() { return currentGuide; } private void setFeeds(final IFeed[] feedsList, final boolean newGuide) { // EDT !!! // TODO: The problem here is that if we aren't under EDT there will be // TODO: a period when new channels list do not corresponds to what we have // TODO: on the screen. feeds = feedsList; rebuild0(newGuide, true); } /** * Recalculates classes and scores. */ private synchronized void recalculate() { channelsClasses = calculateClasses(feeds); channelsScores = calculateScores(feeds, channelsClasses); } /** * Refilter and resort current channels list. * * @param doFiltering TRUE to refilter feeds. */ public synchronized void rebuild(boolean doFiltering) { rebuild0(false, doFiltering); } private void rebuild0(boolean newGuide, boolean doFiltering) { // EDT!!! if (doFiltering) recalculate(); final int currentViewIndex = newGuide ? -1 : getSelectedChannelIndex(); final int oldSortedCount = sortedChannelsCount; final int currentDataIndex = currentViewIndex == -1 ? -1 : sortedChannels[currentViewIndex]; if (newGuide || filteredChannels.length < feeds.length) { filteredChannels = new int[feeds.length]; sortedChannels = new int[feeds.length]; doFiltering = true; } if (newGuide || doFiltering) { filteredChannelsCount = filterFeeds(feeds, channelsScores.length, channelsClasses, filteredChannels); } sortedChannels = copyFeedsList(filteredChannels, filteredChannelsCount, sortingEnabled, channelsScores); sortedChannelsCount = filteredChannelsCount; final int newViewIndex = currentDataIndex == -1 ? -1 : dataToView(sortedChannels, sortedChannelsCount, currentDataIndex); if (!nonVisual) { fireChanges(oldSortedCount, sortedChannelsCount, currentViewIndex, newViewIndex); } } /** * Returns view index of currently selected item in the list. * * @return view index. */ private int getSelectedChannelIndex() { return listComponent == null ? -1 : listComponent.getSelectedIndex(); } /** * Sets the starz threshold. * * @param aScoreThreshold threshold. */ public synchronized void setScoreThreshold(int aScoreThreshold) { if (scoreThreshold != aScoreThreshold) { scoreThreshold = aScoreThreshold; rebuild(true); } } /** * Gets the number of channels in the currently selected * ChannelGuide. We do this simply by asking the model what the currently selected * ChannelGuide is. * * @return number of channels in list. */ public synchronized int getSize() { return sortedChannelsCount; } /** * Called to return the ChannelGuide entry that can be found at the * specified index. Note that because this overrides the corresponding method in * AbstractListModel, it has to return Object. Caller has to know to cast. * * @param index index of element. * * @return element. */ public synchronized Object getElementAt(final int index) { return feeds[viewToData(index)]; } /** * Returns <code>true</code> if specified feed is in current model list. * * @param feed feed to check. * * @return <code>true</code> if present in list. */ public synchronized boolean isPresent(IFeed feed) { return indexOf(feed) != -1; } /** * Called when feed information chaged and we need to redraw the view. * * @param feed feed with changed info. */ public synchronized void contentsChangedAt(final IFeed feed) { if (feed == null) return; // If view index is available then we need to repaint corresponding cell if (!nonVisual) { int viewIndex = indexOf(feed); if (viewIndex > -1) { fireContentsChanged(this, viewIndex, viewIndex); } } // TODO Potential concurrency problem !!! // Run this code in another event to let the changed feed cell repaint itself SwingUtilities.invokeLater(new Runnable() { public void run() { final int viewIndex = indexOf(feed); final int dataIndex = viewIndex > -1 ? viewToData(viewIndex) : dataIndexOf(feed); // If feed belongs to this guide then we need to recalculate its // class and score and rebuild the list. if (dataIndex != -1) { IFeed channel = feeds[dataIndex]; int newClass = FeedClassifier.classify(channel); int newScore = calculateScore(channel, newClass, -1); int oldClass = channelsClasses[dataIndex]; boolean doRebuild = false; boolean doFiltering = false; if (oldClass != newClass || channelsScores[dataIndex] != newScore) { channelsClasses[dataIndex] = newClass; channelsScores[dataIndex] = newScore; doRebuild = true; doFiltering = true; } // If this channel became visible add it to the list of filtered channels boolean shouldBeDisplayed = shouldBeDisplayed(channel, newClass); if (viewIndex == -1 && shouldBeDisplayed) { filteredChannels[filteredChannelsCount++] = dataIndex; doRebuild = true; doFiltering = false; } else if (viewIndex != -1 && !shouldBeDisplayed) { doRebuild = true; doFiltering = true; } if (doRebuild) rebuild(doFiltering); } } }); } /** * Called when some channels added to the guide being in the model. * * @param indexFrom index of starting channel. * @param indexTo index of ending channel. */ public synchronized void feedsAdded(final int indexFrom, final int indexTo) { // Add another slots for new feeds int size = filteredChannels.length + indexTo - indexFrom + 1; int[] newFiltered = new int[size]; int[] newSorted = new int[size]; System.arraycopy(filteredChannels, 0, newFiltered, 0, filteredChannels.length); System.arraycopy(sortedChannels, 0, newSorted, 0, filteredChannels.length); filteredChannels = newFiltered; sortedChannels = newSorted; setGuide(currentGuide); } /** * Recalculates classes and scores of all items, filters and sorts the list. */ public synchronized void fullRebuild() { setGuide(currentGuide); } /** * Returns index of specified feed in current list. * * @param feed feed to check. * * @return index of feed or <code>-1</code>. */ public synchronized int indexOf(IFeed feed) { int viewIndex = -1; if (feed != null) { for (int i = 0; viewIndex == -1 && i < sortedChannelsCount; i++) { if (feeds[viewToData(i)] == feed) viewIndex = i; } } return viewIndex; } // Converts data index into view index using the specified list and looking into // defined first elements. private int dataToView(int[] list, int length, int dataIndex) { int viewIndex = -1; for (int i = 0; viewIndex == -1 && i < length; i++) { if (list[i] == dataIndex) viewIndex = i; } return viewIndex; } // Returns index of feed in original data (-1 if feed isn't present) private synchronized int dataIndexOf(IFeed channel) { int index = -1; for (int i = 0; index == -1 && i < feeds.length; i++) { if (feeds[i] == channel) index = i; } return index; } /** * Converts view index into data. * * @param viewIndex view index. * * @return data index. */ public synchronized int viewToData(int viewIndex) { return sortedChannels[viewIndex]; } /** * Enables / disables sorting of the list. * * @param enabled TRUE to enable sorting. */ public void setSortingEnabled(boolean enabled) { if (sortingEnabled != enabled) { sortingEnabled = enabled; rebuild(false); } } /** * Sets primary sorting order. * * @param order sort order. * * @see FeedsSortOrder */ public void setPrimarySortOrder(int order) { if (primarySort != order) { primarySort = order; fullRebuild(); } } /** * Sets secondary sorting order. * * @param order sort order. * * @see FeedsSortOrder */ public void setSecondarySortOrder(int order) { if (secondarySort != order) { secondarySort = order; fullRebuild(); } } /** * Sets direction of primary sorting. * * @param reversed <code>TRUE</code> for reversed direction. */ public void setPrimarySortOrderDirection(boolean reversed) { if (primarySortRev != reversed) { primarySortRev = reversed; fullRebuild(); } } /** * Sets direction of secondary sorting. * * @param reversed <code>TRUE</code> for reversed direction. */ public void setSecondarySortOrderDirection(boolean reversed) { if (secondarySortRev != reversed) { secondarySortRev = reversed; fullRebuild(); } } /** * Called to notify the model that filter has changed values and the model needs * to be recalculated. This method is used instead of listeners approach because * we can have multiple changes of filter and we don't need the model to be repaired * for all of them -- just for the last in the series. */ public void filterChanged() { rebuild(true); } /** * Sets all options and doesn't make any updates to the model. Useful to setup everything * before setting new guide. * * @param aScoreThreshold new score threshold. * @param aPrSortOrder primary sorting order. * @param aPrSortOrderReversed primary sorting order reverse flag. * @param aScSortOrder secondary sorting order. * @param aScSortOrderReversed secondary sorting order reverse flag. * @param aSortingEnabled sorting enableness flag. */ public void setOptions(int aScoreThreshold, int aPrSortOrder, boolean aPrSortOrderReversed, int aScSortOrder, boolean aScSortOrderReversed, boolean aSortingEnabled) { scoreThreshold = aScoreThreshold; primarySort = aPrSortOrder; primarySortRev = aPrSortOrderReversed; secondarySort = aScSortOrder; secondarySortRev = aScSortOrderReversed; sortingEnabled = aSortingEnabled; } // --------------------------------------------------------------------------------------------- // Calculations // --------------------------------------------------------------------------------------------- /** * Filters list of feeds using their classes and puts in the resulting buffer. * * @param aFeeds feeds to filter. * @param aFeedsCount number of feeds to filter. * @param aFeedsClasses classes of feeds. * @param aFilteredFeedsList buffer to fill with filtered feeds list. * * @return number of feeds were put in the buffer. */ private int filterFeeds(IFeed[] aFeeds, int aFeedsCount, int[] aFeedsClasses, int[] aFilteredFeedsList) { int index = 0; for (int i = 0; i < aFeedsCount; i++) { IFeed feed = aFeeds[i]; int feedsClass = aFeedsClasses[i]; if (shouldBeDisplayed(feed, feedsClass)) aFilteredFeedsList[index++] = i; } return index; } /** * Returns TRUE if feed should be displayed. * * @param aFeed feed object. * @param aFeedClasses feed's classes (mask). * * @return TRUE if feed should be displayed. */ private boolean shouldBeDisplayed(IFeed aFeed, int aFeedClasses) { // Show if // * currently being processed, or // * selected (showBeVisible == true), or // * should be visible according to filter settings, or // * is not initialized (just added) while valid and not disabled return shouldBeVisible(aFeed) || aFeed.isVisible(); } /** * Returns <code>TRUE</code> if given feed should be visible. * * @param aFeed feed to check. * * @return <code>TRUE</code> if given feed should be visible. */ private boolean shouldBeVisible(IFeed aFeed) { return aFeed == selectedFeed; } /** * Makes a copy of source feeds list. Sorts it if required using feeds scores. * * @param source source list. * @param sourceLength number of items to copy. * @param sort TRUE to sort. * @param feedsScores feeds' scores to use for sorting. * * @return resulting feeds list. */ private int[] copyFeedsList(int[] source, int sourceLength, boolean sort, final int[] feedsScores) { int[] dest; if (sort) { FeedScoresComparator comp = getFeedScoresComparator(); synchronized (comp) { comp.initialize(feedsScores); dest = Sort.sort(source, 0, sourceLength, comp); } } else { dest = new int[source.length]; System.arraycopy(source, 0, dest, 0, sourceLength); } return dest; } /** * Returns feed scores comparator. * * @return comparator. */ private synchronized FeedScoresComparator getFeedScoresComparator() { if (feedScoresComparator == null) feedScoresComparator = new FeedScoresComparator(); return feedScoresComparator; } /** * Calculates classes for the feeds. * * @param aFeeds list of feeds. * * @return list of corresponding classes. */ private int[] calculateClasses(IFeed[] aFeeds) { int[] classes = new int[aFeeds.length]; for (int i = 0; i < aFeeds.length; i++) classes[i] = aFeeds[i].getClassesMask(); return classes; } /** * Calculates scores for all feeds in the list with corresponding classes. * * @param aFeeds list of feeds. * @param aFeedsClasses list of corresponding classes. * * @return list of scores for the feeds. */ private int[] calculateScores(IFeed[] aFeeds, int[] aFeedsClasses) { int[] scores = new int[aFeeds.length]; // Calculate minimum visits across the feeds if it's not provided // if one of sorting orders is by visits count int minVisits = 0; if (primarySort == FeedsSortOrder.VISITS || secondarySort == FeedsSortOrder.VISITS) { minVisits = getMininumVisits(); } for (int i = 0; i < aFeeds.length; i++) { scores[i] = calculateScore(aFeeds[i], aFeedsClasses[i], minVisits); } return scores; } /** * Calculates score of the single feed. * * @param feed feed (for score calculation). * @param feedClass class of feed. * @param minVisits minimum visits. * * @return final score ready for sorting. */ int calculateScore(IFeed feed, int feedClass, int minVisits) { int score = 0; // Calculate minimum visits across the feeds if it's not provided // if one of sorting orders is by visits count if (minVisits == -1 && (primarySort == FeedsSortOrder.VISITS || secondarySort == FeedsSortOrder.VISITS)) { minVisits = getMininumVisits(); } // Shift it bits for primary and secondary sorting orders score = shiftInSortMark(score, feedClass, feed, primarySort, primarySortRev, minVisits); if (primarySort != secondarySort) { score = shiftInSortMark(score, feedClass, feed, secondarySort, secondarySortRev, minVisits); } // Order feeds by their rating additionally score = (score << 3) | (4 - getFeedRating(feed)); // Shift in alphabetical order if only it was not among primary or secondary sorting orders if (primarySort != FeedsSortOrder.ALPHABETICAL && secondarySort != FeedsSortOrder.ALPHABETICAL) { score = (score << 10) | (getFeedAlphaOrder(feed) & 0x7ff); } return score; } /** * Evaluates new bits for given sort order for given feed and its set of classes. After * the bits are ready it adds these bits to the <code>score</code> by pushing them from the * low end by shifting existing value to the left for necessary amount of bits. Depending * on <code>sortOrder</code> this method can request alphabetical order of feed and number * of keywords values. * * @param score original score to update with new bits. * @param feedClass class of the feed. * @param feed target feed. * @param sortOrder order of sorting. * @param reverseSort <code>TRUE</code> to do reverse sorting. * @param minVisits minimum number of visits across all feeds in this guide. * * @return updated score. */ int shiftInSortMark(int score, int feedClass, IFeed feed, int sortOrder, boolean reverseSort, int minVisits) { int value; int shift; switch (sortOrder) { case FeedsSortOrder.ALPHABETICAL: value = getFeedAlphaOrder(feed) & 0x3ff; if (reverseSort) value = 1023 - value; shift = 10; break; case FeedsSortOrder.RATING: int feedRating = getFeedRating(feed); value = feedRating == -1 ? 0 : feedRating & 7; if (!reverseSort) value = 7 - value; shift = 3; break; case FeedsSortOrder.VISITS: value = getFeedVisits(feed, minVisits) & 0xff; if (!reverseSort) value = 255 - value; shift = 8; break; default: value = (feedClass & sortOrder) != 0 ? 1 : 0; if (reverseSort) value = 1 - value; shift = 1; break; } return (score << shift) | value; } /** * Returns compressed number of visits. * * @param feed feed to check. * @param minVisits minimum number of visits across all feeds in the guide. * * @return visits number. */ private int getFeedVisits(IFeed feed, int minVisits) { return Math.min(255, feed.getViews() - minVisits); } /** * Finds the minimum number of visits among feeds in this guide. * * @return minimum number of visits. */ int getMininumVisits() { int min = Integer.MAX_VALUE; for (IFeed feed : feeds) min = Math.min(feed.getViews(), min); return min; } /** * Returns rating of the feed for further math. * * @param feed feed. * * @return rating in range [0;4]. */ int getFeedRating(IFeed feed) { return scoreCalculator.calcFinalScore(feed); } /** * Returns alphabetical order index of the feed within the currently selected guide. * * @param feed feed. * * @return order index in range [0;1023]. */ synchronized int getFeedAlphaOrder(IFeed feed) { return currentGuide.alphaIndexOf(feed); } /** * Makes sure that the specified feed is visible. * * @param aFeed feed to be visible. */ public void ensureVisibilityOf(IFeed aFeed) { if (selectedFeed != aFeed) { selectedFeed = aFeed; if (selectedFeed == null) { if (getSize() > 0) fullRebuild(); } else if (selectedFeed.belongsTo(currentGuide) && indexOf(selectedFeed) == -1) { fullRebuild(); } } } /** Simple comparator which is using feeds scores table to compare elements by indeces. */ private static class FeedScoresComparator implements Sort.IValueComparator { private int[] feedsScores; public int compare(int dataIndex1, int dataIndex2) { int s1 = feedsScores[dataIndex1]; int s2 = feedsScores[dataIndex2]; return s1 == s2 ? 0 : s1 > s2 ? 1 : -1; } public void initialize(int[] aFeedsScores) { feedsScores = aFeedsScores; } } // --------------------------------------------------------------------------------------------- // Events // --------------------------------------------------------------------------------------------- /** * Intelligent firing of changes in the list. It preserves the item that was selected before * the change in list structure to save the selection. If the list has no previously selected * element any mode then more rude, but faster processing applied. * * @param oldLength number of first elements in list to look at. * @param newLength number of first elements in new list to look at. * @param currentViewIndex index in <code>oldList</code> which was selected before the change. * @param newViewIndex index in <code>newList</code> which should be selected after change. */ private void fireChanges(int oldLength, int newLength, int currentViewIndex, int newViewIndex) { // Save time on empty calls if (oldLength == 0 && newLength == 0) return; // If previously selected item is in new list the apply full (intelligent) notification // scheme. if (newViewIndex != -1) { // Full scheme int changeStart = 0; int changeEnd = newLength - 1; int diffInPositions = newViewIndex - currentViewIndex; if (diffInPositions < 0) { fireIntervalRemoved(this, 0, -diffInPositions - 1); } else if (diffInPositions > 0) { fireIntervalAdded(this, 0, diffInPositions - 1); changeStart = diffInPositions; } oldLength += diffInPositions; int diffInLength = newLength - oldLength; if (diffInLength < 0) { fireIntervalRemoved(this, newLength, oldLength - 1); } else if (diffInLength > 0) { fireIntervalAdded(this, oldLength, newLength - 1); changeEnd = oldLength - 1; } fireContentsChanged(this, changeStart, changeEnd); // At this step we will have the list repainted and it's time to make focused index // be the same as selected index. if (listComponent != null && newViewIndex != oldLength) { listComponent.setSelectedIndex(newViewIndex); } } else { // Simplified scheme // I understand that calling list component from model isn't good, // but it's the easiest way to clear selection. Alternatively we can // fire event that items removed and then add them back. if (listComponent != null) listComponent.clearSelection(); fireChanges(0, oldLength, newLength); } } /** * Fires <code>intervalAdded</code> or <code>intervalRemoved</code> event with * possible <code>contentsChanged</code> event to cover whole list of changed * items from old to new list size. * * @param offset offset to start events from. * @param aOldSize old list size. * @param aNewSize new list size. */ private void fireChanges(int offset, int aOldSize, int aNewSize) { int o = aOldSize <= aNewSize ? aOldSize : aNewSize; int d = aNewSize - aOldSize; if (d > 0) { fireIntervalAdded(this, offset + o, offset + o + d - 1); } else if (d < 0) { fireIntervalRemoved(this, offset + o, offset + o - d - 1); } if (o > 0) fireContentsChanged(this, offset, offset + o - 1); } // --------------------------------------------------------------------------------------------- // Making sure that all events are fired from EDT // --------------------------------------------------------------------------------------------- /** * Fires change in contents range. * * @param source source of event. * @param index0 start of range (including). * @param index1 end of range (excluding). */ public void fireContentsChanged(Object source, int index0, int index1) { synchronized (this) { index0 = Math.min(index0, sortedChannelsCount); index1 = Math.min(index1, sortedChannelsCount); } super.fireContentsChanged(source, index0, index1); } protected void fireIntervalAdded(Object source, int index0, int index1) { super.fireIntervalAdded(source, index0, index1); } protected void fireIntervalRemoved(Object source, int index0, int index1) { super.fireIntervalRemoved(source, index0, index1); } }