/* ********************************************************************** ** ** Copyright notice ** ** ** ** (c) 2005-2009 RSSOwl Development Team ** ** http://www.rssowl.org/ ** ** ** ** All rights reserved ** ** ** ** This program and the accompanying materials are made available under ** ** the terms of the Eclipse Public License v1.0 which accompanies this ** ** distribution, and is available at: ** ** http://www.rssowl.org/legal/epl-v10.html ** ** ** ** A copy is found in the file epl-v10.html and important notices to the ** ** license from the team is found in the textfile LICENSE.txt distributed ** ** in this package. ** ** ** ** This copyright notice MUST APPEAR in all copies of the file! ** ** ** ** Contributors: ** ** RSSOwl Development Team - initial API and implementation ** ** ** ** ********************************************************************** */ package org.rssowl.ui.internal.editors.feed; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.jface.viewers.ITreeContentProvider; import org.eclipse.jface.viewers.Viewer; import org.eclipse.jface.viewers.ViewerComparator; import org.eclipse.swt.widgets.Tree; import org.eclipse.swt.widgets.TreeItem; import org.rssowl.core.Owl; import org.rssowl.core.internal.persist.LongArrayList; import org.rssowl.core.internal.persist.SearchMark; import org.rssowl.core.internal.persist.pref.DefaultPreferences; import org.rssowl.core.persist.IBookMark; import org.rssowl.core.persist.IEntity; import org.rssowl.core.persist.IFolder; import org.rssowl.core.persist.IFolderChild; import org.rssowl.core.persist.IMark; import org.rssowl.core.persist.IModelFactory; import org.rssowl.core.persist.INews; import org.rssowl.core.persist.INews.State; import org.rssowl.core.persist.INewsBin; import org.rssowl.core.persist.INewsMark; import org.rssowl.core.persist.ISearchCondition; import org.rssowl.core.persist.ISearchField; import org.rssowl.core.persist.ISearchMark; import org.rssowl.core.persist.SearchSpecifier; import org.rssowl.core.persist.dao.DynamicDAO; import org.rssowl.core.persist.dao.INewsDAO; import org.rssowl.core.persist.event.NewsAdapter; import org.rssowl.core.persist.event.NewsEvent; import org.rssowl.core.persist.event.NewsListener; import org.rssowl.core.persist.event.SearchMarkAdapter; import org.rssowl.core.persist.event.SearchMarkEvent; import org.rssowl.core.persist.pref.IPreferenceScope; import org.rssowl.core.persist.reference.BookMarkReference; import org.rssowl.core.persist.reference.FeedLinkReference; import org.rssowl.core.persist.reference.ModelReference; import org.rssowl.core.persist.reference.NewsBinReference; import org.rssowl.core.persist.reference.NewsReference; import org.rssowl.core.persist.reference.SearchMarkReference; import org.rssowl.core.persist.service.IModelSearch; import org.rssowl.core.persist.service.PersistenceException; import org.rssowl.core.util.CoreUtils; import org.rssowl.core.util.DateUtils; import org.rssowl.core.util.Pair; import org.rssowl.core.util.SearchHit; import org.rssowl.core.util.Triple; import org.rssowl.ui.internal.Controller; import org.rssowl.ui.internal.EntityGroup; import org.rssowl.ui.internal.EntityGroupItem; import org.rssowl.ui.internal.FolderNewsMark; import org.rssowl.ui.internal.FolderNewsMark.FolderNewsMarkReference; import org.rssowl.ui.internal.OwlUI; import org.rssowl.ui.internal.editors.feed.NewsFilter.Type; import org.rssowl.ui.internal.util.JobRunner; import org.rssowl.ui.internal.util.ModelUtils; import org.rssowl.ui.internal.util.UIBackgroundJob; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; /** * @author bpasero */ @SuppressWarnings("restriction") public class NewsContentProvider implements ITreeContentProvider { /* The maximum number of items returned from a FolderNewsMark */ static final int MAX_FOLDER_ELEMENTS = 500; /* The maximum number of items that will get resolved from a FolderNewsMark */ private static final int MAX_RESOLVED_FOLDER_ELEMENTS = 5000; /* The maximum number of items in a NewsMark before scoping the results as specified by the filter */ private static final int NEWSMARK_SCOPE_SEARCH_LIMIT = 200; /* The maximum number of items in a Bookmark before scoping the results as specified by the filter */ private static final int BOOKMARK_SCOPE_SEARCH_LIMIT = 500; /* System Property to override the limits above */ private static final String NO_FOLDER_LIMIT_PROPERTY = "noFolderLimit"; //$NON-NLS-1$ private final NewsBrowserViewer fBrowserViewer; private final NewsTableViewer fTableViewer; private final NewsGrouping fGrouping; private final NewsFilter fFilter; private NewsListener fNewsListener; private SearchMarkAdapter fSearchMarkListener; private INewsMark fInput; private final FeedView fFeedView; private final AtomicBoolean fDisposed = new AtomicBoolean(false); private final INewsDAO fNewsDao; private final IModelFactory fFactory; private final IModelSearch fSearch; private final boolean fNoFolderLimit; /* Cache displayed News */ private final Map<Long, INews> fCachedNews; /* Enumeration of possible news event types */ private static enum NewsEventType { PERSISTED, UPDATED, REMOVED, RESTORED } /** * @param tableViewer * @param browserViewer * @param feedView */ public NewsContentProvider(NewsTableViewer tableViewer, NewsBrowserViewer browserViewer, FeedView feedView) { fTableViewer = tableViewer; fBrowserViewer = browserViewer; fFeedView = feedView; fGrouping = feedView.getGrouper(); fFilter = feedView.getFilter(); fCachedNews = new HashMap<Long, INews>(); fNewsDao = DynamicDAO.getDAO(INewsDAO.class); fFactory = Owl.getModelFactory(); fSearch = Owl.getPersistenceService().getModelSearch(); fNoFolderLimit = System.getProperty(NO_FOLDER_LIMIT_PROPERTY) != null; } /* * @see org.eclipse.jface.viewers.IStructuredContentProvider#getElements(java.lang.Object) */ public Object[] getElements(Object inputElement) { List<Object> elements = new ArrayList<Object>(); /* Wrap into Object Array */ if (!(inputElement instanceof Object[])) inputElement = new Object[] { inputElement }; /* Foreach Object */ Object[] objects = (Object[]) inputElement; for (Object object : objects) { /* This is a News */ if (object instanceof INews && ((INews) object).isVisible()) { elements.add(object); } /* This is a NewsReference */ else if (object instanceof NewsReference) { NewsReference newsRef = (NewsReference) object; INews news = obtainFromCache(newsRef); if (news != null) elements.add(news); } /* This is a FeedReference */ else if (object instanceof FeedLinkReference) { synchronized (NewsContentProvider.this) { Collection<INews> news = fCachedNews.values(); if (news != null) { if (fGrouping.getType() == NewsGrouping.Type.NO_GROUPING) elements.addAll(news); else elements.addAll(fGrouping.group(news)); } } } /* This is a class that implements IMark */ else if (object instanceof ModelReference) { Class<? extends IEntity> entityClass = ((ModelReference) object).getEntityClass(); if (IMark.class.isAssignableFrom(entityClass) || IFolder.class.isAssignableFrom(entityClass)) { //Suppoer FolderNewsMark too synchronized (NewsContentProvider.this) { Collection<INews> news = fCachedNews.values(); if (news != null) { if (fGrouping.getType() == NewsGrouping.Type.NO_GROUPING) elements.addAll(news); else elements.addAll(fGrouping.group(news)); } } } } /* This is a EntityGroup */ else if (object instanceof EntityGroup) { EntityGroup group = (EntityGroup) object; List<EntityGroupItem> items = group.getItems(); for (EntityGroupItem item : items) { if (((INews) item.getEntity()).isVisible()) elements.add(item.getEntity()); } } } return elements.toArray(); } /* * @see org.eclipse.jface.viewers.ITreeContentProvider#getChildren(java.lang.Object) */ public Object[] getChildren(Object parentElement) { List<Object> children = new ArrayList<Object>(); /* Handle EntityGroup */ if (parentElement instanceof EntityGroup) { List<EntityGroupItem> items = ((EntityGroup) parentElement).getItems(); for (EntityGroupItem item : items) children.add(item.getEntity()); } return children.toArray(); } /* * @see org.eclipse.jface.viewers.ITreeContentProvider#getParent(java.lang.Object) */ public Object getParent(Object element) { /* Handle Grouping specially */ if (fGrouping.isActive() && element instanceof INews) { Collection<EntityGroup> groups = fGrouping.group(Collections.singletonList((INews) element)); if (groups.size() == 1) return groups.iterator().next(); } return null; } /* * @see org.eclipse.jface.viewers.ITreeContentProvider#hasChildren(java.lang.Object) */ public boolean hasChildren(Object element) { return element instanceof EntityGroup; } /* * @see org.eclipse.jface.viewers.IContentProvider#dispose() */ public synchronized void dispose() { fDisposed.set(true); unregisterListeners(); fCachedNews.clear(); } /* * @see org.eclipse.jface.viewers.IContentProvider#inputChanged(org.eclipse.jface.viewers.Viewer, * java.lang.Object, java.lang.Object) */ public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { /* Ignore - Input changes are handled via refreshCache(Object input) */ } boolean isGroupingEnabled() { return fGrouping.getType() != NewsGrouping.Type.NO_GROUPING; } boolean isGroupingByFeed() { return fGrouping.getType() == NewsGrouping.Type.GROUP_BY_FEED; } boolean isGroupingByStickyness() { return fGrouping.getType() == NewsGrouping.Type.GROUP_BY_STICKY; } boolean isGroupingByLabel() { return fGrouping.getType() == NewsGrouping.Type.GROUP_BY_LABEL; } boolean isGroupingByState() { return fGrouping.getType() == NewsGrouping.Type.GROUP_BY_STATE; } synchronized void refreshCache(IProgressMonitor monitor, INewsMark input) throws PersistenceException { refreshCache(monitor, input, null); } @SuppressWarnings("unchecked") synchronized void refreshCache(IProgressMonitor monitor, INewsMark input, NewsComparator comparer) throws PersistenceException { /* If input is identical, keep the cache during this method to speed up lookup of already resolved items */ Map<Long, INews> cacheCopy = null; if (fInput != null && fInput.equals(input)) cacheCopy = new HashMap(fCachedNews); /* Update Input */ fInput = input; /* Register Listeners if not yet done */ if (fNewsListener == null) registerListeners(); /* Clear old Data */ fCachedNews.clear(); /* Check if ContentProvider was already disposed or RSSOwl shutting down */ if (canceled(monitor)) return; /* Obtain the News */ List<INews> resolvedNews = new ArrayList<INews>(); /* Check if ContentProvider was already disposed or RSSOwl shutting down */ if (canceled(monitor)) return; /* Determine Set of News States based on the filter */ Type filter = fFilter.getType(); Set<State> states; if (filter == Type.SHOW_NEW) states = EnumSet.of(INews.State.NEW); else if (filter == Type.SHOW_UNREAD) states = EnumSet.of(INews.State.NEW, INews.State.UNREAD, INews.State.UPDATED); else states = INews.State.getVisible(); /* Handle Folder, Newsbin and Saved Search or bookmark under certain circumstances */ boolean needToFilter = true; if (input.isGetNewsRefsEfficient() || (input instanceof IBookMark && shouldResolveBookMarkWithSearch((IBookMark) input, filter))) { Triple<Boolean, Boolean, List<NewsReference>> result = getNewsRefsFromInput(input, fFilter, states, monitor); needToFilter = !result.getFirst(); List<NewsReference> newsReferences = result.getThird(); for (NewsReference newsRef : newsReferences) { /* Check if ContentProvider was already disposed or RSSOwl shutting down */ if (canceled(monitor)) return; INews resolvedNewsItem = null; /* Ask the local cache first */ if (cacheCopy != null) resolvedNewsItem = cacheCopy.get(newsRef.getId()); /* Otherwise resolve from DB */ if (resolvedNewsItem == null) resolvedNewsItem = fNewsDao.load(newsRef.getId()); /* Add if visible */ if (resolvedNewsItem != null && resolvedNewsItem.isVisible()) resolvedNews.add(resolvedNewsItem); /* News is null from a search, potential index issue - report it */ else if (result.getSecond()) //TRUE if search was involved CoreUtils.reportIndexIssue(); /* Never resolve more than MAX_RESOLVED_FOLDER_ELEMENTS for a folder */ if (input instanceof FolderNewsMark && !fNoFolderLimit && resolvedNews.size() > MAX_RESOLVED_FOLDER_ELEMENTS) break; } /* Special treat folders and limit them by size */ if (input instanceof FolderNewsMark) resolvedNews = limitFolderNewsMark(resolvedNews, comparer != null ? comparer : fFeedView.getComparator()); } /* Resolve directly by state (check for news counts as optimization) */ else if (shouldResolve(input, filter)) { resolvedNews.addAll(input.getNews(states)); } /* Filter Elements as needed */ if (needToFilter && isFilteredByOtherThanState()) filterElements(resolvedNews); /* Check if ContentProvider was already disposed or RSSOwl shutting down */ if (canceled(monitor)) return; /* Add into Cache */ for (INews news : resolvedNews) { fCachedNews.put(news.getId(), news); } } private boolean shouldResolveBookMarkWithSearch(IBookMark input, NewsFilter.Type filter) { /* Return if input is not a bookmark or not filtering at all */ if (filter == Type.SHOW_ALL) return false; /* Return if filter can already quickly be handled from bookmark itself */ if (filter == Type.SHOW_NEW || filter == Type.SHOW_UNREAD) return false; /* Check for number of new, unread and updated news */ if (input.getNewsCount(EnumSet.of(INews.State.NEW, INews.State.UNREAD, INews.State.UPDATED)) > BOOKMARK_SCOPE_SEARCH_LIMIT) return true; /* Return if bookmark retention is setup, assuming that the number of elements is limited already */ if (!hasRetentionLimit(input)) return true; return false; } private boolean hasRetentionLimit(IBookMark bookmark) { IPreferenceScope preferences = Owl.getPreferenceService().getEntityScope(bookmark); /* High Retention: Read News Deleted */ if (preferences.getBoolean(DefaultPreferences.DEL_READ_NEWS_STATE)) return true; /* Medium Retention: Aged News Deleted */ if (preferences.getBoolean(DefaultPreferences.DEL_NEWS_BY_AGE_STATE)) return true; /* Low-High Retention: News Deleted by Count (Depends on actual count) */ if (preferences.getBoolean(DefaultPreferences.DEL_NEWS_BY_COUNT_STATE) && preferences.getInteger(DefaultPreferences.DEL_NEWS_BY_COUNT_VALUE) <= BOOKMARK_SCOPE_SEARCH_LIMIT) return true; return false; } private boolean shouldResolve(INewsMark input, NewsFilter.Type filter) { /* Check for NEW News in Input */ if (filter == Type.SHOW_NEW && input.getNewsCount(EnumSet.of(INews.State.NEW)) == 0) return false; /* Check for UNREAD News in Input */ else if (filter == Type.SHOW_UNREAD && input.getNewsCount(EnumSet.of(INews.State.NEW, INews.State.UNREAD, INews.State.UPDATED)) == 0) return false; /* Check for Sticky News in Bookmark */ else if (filter == Type.SHOW_STICKY && input instanceof IBookMark && ((IBookMark) input).getStickyNewsCount() == 0) return false; /* Check for Recent Date or 5 Days Age */ else if ((filter == Type.SHOW_RECENT || filter == Type.SHOW_LAST_5_DAYS) && input instanceof IBookMark) { Date mostRecentNewsDate = ((IBookMark) input).getMostRecentNewsDate(); if (mostRecentNewsDate != null) { //Date can be null e.g. when having more than 1 Bookmark for the same Feed (known issue) if (filter == Type.SHOW_RECENT && (mostRecentNewsDate.getTime() < (DateUtils.getToday().getTimeInMillis() - DateUtils.DAY))) return false; else if (filter == Type.SHOW_LAST_5_DAYS && (mostRecentNewsDate.getTime() < (DateUtils.getToday().getTimeInMillis() - 5 * DateUtils.DAY))) return false; } } return true; } private Triple<Boolean /* Filtered */, Boolean /* Searched */, List<NewsReference>> getNewsRefsFromInput(INewsMark input, NewsFilter newsFilter, Set<State> states, IProgressMonitor monitor) { Type filter = newsFilter.getType(); /* * Optimization: If input is a saved search or bin with many results and the news filter is set to any condition that * is not scoped by news state, we get the results from a search to potentially get less results and so need less memory. */ if (input instanceof ISearchMark || input instanceof INewsBin) { if (isFilteredByOtherThanState() && input.getNewsCount(states) > NEWSMARK_SCOPE_SEARCH_LIMIT) { ISearchCondition filterCondition = ModelUtils.getConditionForFilter(filter); List<SearchHit<NewsReference>> result = null; /* Inject into Saved Search */ if (input instanceof ISearchMark) { ISearchMark searchMark = (ISearchMark) input; result = fSearch.searchNews(searchMark.getSearchConditions(), filterCondition, searchMark.matchAllConditions()); } /* Location search for News Bin */ else { INewsBin newsBin = (INewsBin) input; ISearchField locationField = fFactory.createSearchField(INews.LOCATION, INews.class.getName()); ISearchCondition locationCondition = fFactory.createSearchCondition(locationField, SearchSpecifier.IS, ModelUtils.toPrimitive(Collections.singleton((IFolderChild) newsBin))); result = fSearch.searchNews(Arrays.asList(locationCondition, filterCondition), true); } /* Fill Newsreferences from Search Results */ List<NewsReference> newsRefs = new ArrayList<NewsReference>(result.size()); for (SearchHit<NewsReference> item : result) { newsRefs.add(item.getResult()); } return Triple.create(true, true, newsRefs); } } /* Resolve items from bookmark through searching inside */ else if (input instanceof IBookMark) { IBookMark bookmark = (IBookMark) input; /* Return early if bookmark should not be resolved at all */ if (!shouldResolve(bookmark, filter)) return Triple.create(true, false, Collections.<NewsReference> emptyList()); ISearchCondition filterCondition = ModelUtils.getConditionForFilter(filter); ISearchField locationField = fFactory.createSearchField(INews.LOCATION, INews.class.getName()); ISearchCondition locationCondition = fFactory.createSearchCondition(locationField, SearchSpecifier.IS, ModelUtils.toPrimitive(Collections.singleton((IFolderChild) bookmark))); List<SearchHit<NewsReference>> result = fSearch.searchNews(Arrays.asList(locationCondition, filterCondition), true); /* Fill Newsreferences from Search Results */ List<NewsReference> newsRefs = new ArrayList<NewsReference>(result.size()); for (SearchHit<NewsReference> item : result) { newsRefs.add(item.getResult()); } return Triple.create(true, true, newsRefs); } /* Resolve Folder News Mark and pass in current filter */ else if (input instanceof FolderNewsMark) { ((FolderNewsMark) input).resolve(filter, monitor); List<NewsReference> references = input.getNewsRefs(states); /* Optimization: If the folder has lots of elements and a text filter is set, limit result by this pattern */ if (!fNoFolderLimit && input.getNewsCount(states) > MAX_FOLDER_ELEMENTS && newsFilter.isPatternSet()) { Iterator<NewsReference> iterator = references.iterator(); while (iterator.hasNext()) { if (!newsFilter.isTextPatternMatch(iterator.next().getId())) iterator.remove(); } } /* Optimization: If the folder contains more than MAX_RESOLVED_FOLDER_ELEMENTS, put the most recent at the beginning */ if (!fNoFolderLimit && references.size() > MAX_RESOLVED_FOLDER_ELEMENTS) sortDescendingById(references); return Triple.create(true, true, references); } /* Return news refs by state */ return Triple.create(false, false, input.getNewsRefs(states)); } private void sortDescendingById(List<NewsReference> references) { Collections.sort(references, new Comparator<NewsReference>() { public int compare(NewsReference o1, NewsReference o2) { return o1.getId() > o2.getId() ? -1 : 1; } }); } private synchronized Triple<Boolean /* Was Empty */, Collection<NewsEvent>, Collection<INews>> addToCache(Collection<NewsEvent> events, Collection<INews> addedNews) { boolean wasEmpty = fCachedNews.isEmpty(); Collection<NewsEvent> visibleEvents = new ArrayList<NewsEvent>(); Collection<INews> visibleNews = new ArrayList<INews>(); /* Check if ContentProvider was already disposed or RSSOwl shutting down */ if (canceled()) return Triple.create(wasEmpty, visibleEvents, visibleNews); /* Use the filter (if set) to determine elements to cache */ if (isFilteredByState() || isFilteredByOtherThanState()) { /* Quickly Map from News to Event */ Map<INews, NewsEvent> mapNewsToEvent = new HashMap<INews, NewsEvent>(events.size()); for (NewsEvent event : events) { mapNewsToEvent.put(event.getEntity(), event); } /* Filter the Added News */ visibleNews.addAll(addedNews); filterElements(visibleNews); /* Also indicate related events for visible news */ for (INews news : visibleNews) visibleEvents.add(mapNewsToEvent.get(news)); } /* Not relevant for bookmarks, just add all */ else { visibleEvents = events; visibleNews = addedNews; } /* Add to Cache */ for (INews news : visibleNews) { fCachedNews.put(news.getId(), news); } /* * Since the folder news mark is bound to the lifecycle of the feedview, * make sure that the contents are updated properly from here. */ if (fInput instanceof FolderNewsMark) ((FolderNewsMark) fInput).add(visibleNews); return Triple.create(wasEmpty, visibleEvents, visibleNews); } private synchronized Pair<List<NewsEvent>, List<INews>> updateCache(Set<NewsEvent> events, List<INews> updatedNews) { List<NewsEvent> visibleEvents = new ArrayList<NewsEvent>(events.size()); List<INews> visibleNews = new ArrayList<INews>(updatedNews.size()); /* Check if ContentProvider was already disposed or RSSOwl shutting down */ if (canceled()) return Pair.create(visibleEvents, visibleNews); for (NewsEvent event : events) { if (event.getEntity().getId() != null && fCachedNews.containsKey(event.getEntity().getId())) { visibleEvents.add(event); visibleNews.add(event.getEntity()); } } return Pair.create(visibleEvents, visibleNews); } private synchronized Pair<List<NewsEvent>, List<INews>> removeFromCache(Set<NewsEvent> events, List<INews> deletedNews) { List<NewsEvent> visibleEvents = new ArrayList<NewsEvent>(events.size()); List<INews> visibleNews = new ArrayList<INews>(deletedNews.size()); /* Check if ContentProvider was already disposed or RSSOwl shutting down */ if (canceled()) return Pair.create(visibleEvents, visibleNews); /* Remove from Cache and keep track of contained items */ for (NewsEvent event : events) { if (event.getEntity().getId() != null && fCachedNews.remove(event.getEntity().getId()) != null) { visibleEvents.add(event); visibleNews.add(event.getEntity()); } } /* * Since the folder news mark is bound to the lifecycle of the feedview, * make sure that the contents are updated properly from here. */ if (fInput instanceof FolderNewsMark) ((FolderNewsMark) fInput).remove(deletedNews); return Pair.create(visibleEvents, visibleNews); } private synchronized Pair<List<INews>, Boolean> newsChangedFromSearch(IProgressMonitor monitor, List<SearchMarkEvent> eventsRelatedToInput, boolean onlyHandleAddedNews) { /* Check if ContentProvider was already disposed or RSSOwl shutting down */ if (canceled(monitor)) return Pair.create(Collections.<INews> emptyList(), false); boolean needToFilter = true; boolean wasEmpty = fCachedNews.isEmpty(); List<INews> addedNews = new ArrayList<INews>(); /* Update Saved Search from Events */ if (fInput instanceof ISearchMark) { /* Update cache alltogether based on search results */ if (!onlyHandleAddedNews) { refreshCache(monitor, fInput); addedNews.addAll(fCachedNews.values()); needToFilter = false; } /* Only show the added news */ else { Set<Long> newsIds = extractNewsIds(eventsRelatedToInput); for (Long newsId : newsIds) { /* Skip already cached news */ if (hasCachedNews(newsId)) continue; /* Resolve News */ INews news = fNewsDao.load(newsId); if (news != null && news.isVisible()) addedNews.add(news); /* Check if ContentProvider was already disposed or RSSOwl shutting down */ if (canceled(monitor)) return Pair.create(Collections.<INews> emptyList(), false); } } } /* Update Folder News Mark from Events (we only add news, never remove) */ else if (fInput instanceof FolderNewsMark) { FolderNewsMark folderNewsMark = (FolderNewsMark) fInput; Set<Long> newsIds = extractNewsIds(eventsRelatedToInput); for (Long newsId : newsIds) { /* Skip already cached news */ if (hasCachedNews(newsId) || folderNewsMark.containsNews(newsId)) continue; /* Resolve News */ INews news = fNewsDao.load(newsId); if (news != null && news.isVisible()) addedNews.add(news); /* Check if ContentProvider was already disposed or RSSOwl shutting down */ if (canceled(monitor)) return Pair.create(Collections.<INews> emptyList(), false); } } /* Check if ContentProvider was already disposed or RSSOwl shutting down */ if (canceled(monitor)) return Pair.create(Collections.<INews> emptyList(), false); /* Optimization: Only consider those news that pass the filter when news are added (or in general for Folder News Mark) */ if (needToFilter && isFilteredByOtherThanState()) filterElements(addedNews); /* Add to Cache */ for (INews news : addedNews) { fCachedNews.put(news.getId(), news); } /* Add to Folder if necessary */ if (fInput instanceof FolderNewsMark) ((FolderNewsMark) fInput).add(addedNews); return Pair.create(addedNews, wasEmpty); } private boolean isFilteredByState() { return fFilter.getType() == Type.SHOW_NEW || fFilter.getType() == Type.SHOW_UNREAD; } private boolean isFilteredByOtherThanState() { return fFilter.getType() == Type.SHOW_STICKY || fFilter.getType() == Type.SHOW_LABELED || fFilter.getType() == Type.SHOW_RECENT || fFilter.getType() == Type.SHOW_LAST_5_DAYS; } private void filterElements(Collection<INews> elements) { Iterator<INews> iterator = elements.iterator(); while (iterator.hasNext()) { if (!fFilter.select(iterator.next(), true)) iterator.remove(); } } private Set<Long> extractNewsIds(List<SearchMarkEvent> events) { Set<Long> set = new HashSet<Long>(); for (SearchMarkEvent event : events) { LongArrayList[] newsIds = ((SearchMark) event.getEntity()).internalGetNewsContainer().internalGetNewsIds(); for (int i = 0; i < newsIds.length; i++) { /* Ignore hidden/deleted and states that are filtered */ if (i == INews.State.HIDDEN.ordinal() || i == INews.State.DELETED.ordinal()) continue; else if (fFilter.getType() == Type.SHOW_NEW && i != INews.State.NEW.ordinal()) continue; else if (fFilter.getType() == Type.SHOW_UNREAD && i == INews.State.READ.ordinal()) continue; long[] elements = newsIds[i].getElements(); for (long element : elements) { if (element > 0) set.add(element); } } } return set; } private List<INews> limitFolderNewsMark(List<INews> resolvedNews, NewsComparator comparer) { /* Return if no capping is required at all */ if (fNoFolderLimit || resolvedNews.size() <= MAX_FOLDER_ELEMENTS) return resolvedNews; /* First add those news that are Labeled or Sticky if this group mode is active */ List<INews> priorityItems = Collections.emptyList(); if (isGroupingByLabel() || isGroupingByStickyness()) { priorityItems = new ArrayList<INews>(); for (INews news : resolvedNews) { if (isGroupingByLabel() && !news.getLabels().isEmpty() || isGroupingByStickyness() && news.isFlagged()) priorityItems.add(news); } } /* Check if Labeled/Sticky News already at limit size and return then */ if (priorityItems.size() >= MAX_FOLDER_ELEMENTS) return priorityItems; /* Need to sort now to pick the top N remaining elements */ Object[] elements = resolvedNews.toArray(); comparer.sort(null, elements); /* Pick top N remaining Elements */ int limit = MAX_FOLDER_ELEMENTS - priorityItems.size(); List<INews> limitedResult = new ArrayList<INews>(Math.min(elements.length, MAX_FOLDER_ELEMENTS)); for (int i = 0, c = 0; i < elements.length && c < limit; i++) { INews news = (INews) elements[i]; if (!priorityItems.contains(news)) { limitedResult.add(news); c++; } } /* Fill in priority items if any */ limitedResult.addAll(priorityItems); return limitedResult; } synchronized INewsMark getInput() { return fInput; } synchronized Collection<INews> getCachedNewsCopy() { return new ArrayList<INews>(fCachedNews.values()); } synchronized boolean hasCachedNews() { return !fCachedNews.isEmpty(); } synchronized boolean hasCachedNews(INews news) { return news.getId() != null && hasCachedNews(news.getId()); } private synchronized boolean hasCachedNews(long newsId) { return fCachedNews.containsKey(newsId); } private synchronized INews obtainFromCache(NewsReference ref) { return obtainFromCache(ref.getId()); } synchronized INews obtainFromCache(long newsId) { return fCachedNews.get(newsId); } private void registerListeners() { /* Saved Search Listener */ fSearchMarkListener = new SearchMarkAdapter() { @Override public void newsChanged(Set<SearchMarkEvent> events) { final List<SearchMarkEvent> eventsRelatedToInput = new ArrayList<SearchMarkEvent>(1); /* Check if ContentProvider was already disposed or RSSOwl shutting down */ if (canceled()) return; /* Find those events that are related to the current input */ for (SearchMarkEvent event : events) { ISearchMark searchMark = event.getEntity(); if (fInput.equals(searchMark)) { eventsRelatedToInput.add(event); break; //Can only be one search mark per feed view } else if (fInput instanceof FolderNewsMark && ((FolderNewsMark) fInput).isRelatedTo(searchMark)) { eventsRelatedToInput.add(event); } } /* Check if ContentProvider was already disposed or RSSOwl shutting down */ if (canceled()) return; /* Properly update given searches are related to input */ if (!eventsRelatedToInput.isEmpty()) { JobRunner.runInUIThread(fFeedView.getEditorControl(), new Runnable() { public void run() { final boolean onlyHandleAddedNews = fFeedView.isVisible(); JobRunner.runUIUpdater(new UIBackgroundJob(fFeedView.getEditorControl()) { private List<INews> fAddedNews; private boolean fWasEmpty; @Override protected void runInBackground(IProgressMonitor monitor) { if (canceled(monitor)) return; Pair<List<INews>, Boolean> result = newsChangedFromSearch(monitor, eventsRelatedToInput, onlyHandleAddedNews); fAddedNews = result.getFirst(); fWasEmpty = result.getSecond(); } @Override protected void runInUI(IProgressMonitor monitor) { if (canceled(monitor)) return; /* Check if we need to Refresh at all */ if (onlyHandleAddedNews && (fAddedNews == null || fAddedNews.size() == 0)) return; /* Refresh only Table Viewer if not using Newspaper Mode in Browser */ if (!browserShowsCollection()) fFeedView.refreshTableViewer(true, true); //TODO Seems some JFace caching problem here (redraw=true) /* Browser shows Newspaper Mode: Only refresh under certain circumstances */ else { if (canDoBrowserRefresh(fWasEmpty)) fFeedView.refreshBrowserViewer(); else fFeedView.getNewsBrowserControl().setInfoBarVisible(true); } } }); } }); /* Done */ return; } } }; DynamicDAO.addEntityListener(ISearchMark.class, fSearchMarkListener); /* News Listener */ fNewsListener = new NewsAdapter() { /* News got Added */ @Override public void entitiesAdded(final Set<NewsEvent> events) { JobRunner.runInUIThread(fFeedView.getEditorControl(), new Runnable() { public void run() { Set<NewsEvent> addedNews = null; /* Check if ContentProvider was already disposed or RSSOwl shutting down */ if (canceled()) return; /* Filter News which are from a different Feed than displayed */ for (NewsEvent event : events) { if (event.getEntity().isVisible() && isInputRelatedTo(event, NewsEventType.PERSISTED)) { if (addedNews == null) addedNews = new HashSet<NewsEvent>(); addedNews.add(event); } /* Return on Shutdown or disposal */ if (canceled()) return; } /* Event not interesting for us or we are disposed */ if (addedNews == null || addedNews.size() == 0) return; /* Handle */ boolean refresh = handleAddedNews(addedNews); if (refresh) { if (!browserShowsCollection()) fFeedView.refreshTableViewer(true, false); else fFeedView.refresh(true, false); } } }); } /* News got Updated */ @Override public void entitiesUpdated(final Set<NewsEvent> events) { JobRunner.runInUIThread(fFeedView.getEditorControl(), new Runnable() { public void run() { Set<NewsEvent> restoredNews = null; Set<NewsEvent> updatedNews = null; Set<NewsEvent> deletedNews = null; /* Check if ContentProvider was already disposed or RSSOwl shutting down */ if (canceled()) return; /* Filter News which are from a different Feed than displayed */ for (NewsEvent event : events) { boolean isRestored = gotRestored(event, fFilter.getType()); INews news = event.getEntity(); /* Return on Shutdown or disposal */ if (canceled()) return; /* Check if input relates to news events */ if (isInputRelatedTo(event, isRestored ? NewsEventType.RESTORED : NewsEventType.UPDATED)) { /* News got Deleted */ if (!news.isVisible()) { if (deletedNews == null) deletedNews = new HashSet<NewsEvent>(); deletedNews.add(event); } /* News got Restored */ else if (isRestored) { if (restoredNews == null) restoredNews = new HashSet<NewsEvent>(); restoredNews.add(event); } /* News got Updated */ else { if (updatedNews == null) updatedNews = new HashSet<NewsEvent>(); updatedNews.add(event); } } } /* Return on Shutdown or disposal */ if (canceled()) return; boolean refresh = false; boolean updateSelectionFromDelete = false; /* Handle Restored News */ if (restoredNews != null && !restoredNews.isEmpty()) refresh = handleAddedNews(restoredNews); /* Handle Updated News */ if (updatedNews != null && !updatedNews.isEmpty()) refresh = handleUpdatedNews(updatedNews); /* Handle Deleted News */ if (deletedNews != null && !deletedNews.isEmpty()) { refresh = handleDeletedNews(deletedNews); updateSelectionFromDelete = refresh; } /* Check if ContentProvider was already disposed or RSSOwl shutting down */ if (canceled()) return; /* Refresh and update selection due to deletion */ if (updateSelectionFromDelete) { fTableViewer.updateSelectionAfterDelete(new Runnable() { public void run() { refreshViewers(events, NewsEventType.REMOVED); } }); } /* Normal refresh w/o deletion */ else if (refresh) refreshViewers(events, NewsEventType.UPDATED); } }); } /* News got Deleted */ @Override public void entitiesDeleted(final Set<NewsEvent> events) { JobRunner.runInUIThread(fFeedView.getEditorControl(), new Runnable() { public void run() { Set<NewsEvent> deletedNews = null; /* Check if ContentProvider was already disposed or RSSOwl shutting down */ if (canceled()) return; /* Filter News which are from a different Feed than displayed */ for (NewsEvent event : events) { INews news = event.getEntity(); if ((news.isVisible() || news.getParentId() != 0) && isInputRelatedTo(event, NewsEventType.REMOVED)) { if (deletedNews == null) deletedNews = new HashSet<NewsEvent>(); deletedNews.add(event); } /* Return on Shutdown or disposal */ if (canceled()) return; } /* Event not interesting for us or we are disposed */ if (deletedNews == null || deletedNews.size() == 0) return; /* Handle Deleted News */ boolean refresh = handleDeletedNews(deletedNews); /* Check if ContentProvider was already disposed or RSSOwl shutting down */ if (canceled()) return; /* Handle Refresh */ if (refresh) { if (!browserShowsCollection()) fFeedView.refreshTableViewer(true, false); else fFeedView.refresh(true, false); } } }); } }; DynamicDAO.addEntityListener(INews.class, fNewsListener); } private boolean gotRestored(NewsEvent event, NewsFilter.Type filter) { INews news = event.getEntity(); INews old = event.getOldNews(); /* Quickly check common conditions under which the news can not be a restored one */ if (news == null || old == null || !news.isVisible() || hasCachedNews(news)) return false; INews.State newState = news.getState(); INews.State oldState = old.getState(); /* Restored: Deletion was undone */ if (oldState == INews.State.HIDDEN || oldState == INews.State.DELETED) return true; /* Check if new state matches filter now */ switch (filter) { case SHOW_NEW: return newState == INews.State.NEW && oldState != INews.State.NEW; case SHOW_UNREAD: return newState != INews.State.READ && oldState == INews.State.READ; case SHOW_STICKY: return CoreUtils.isStickyStateChange(Collections.singleton(event), true); case SHOW_LABELED: return CoreUtils.isLabelChange(Collections.singleton(event), true); } return false; } private void refreshViewers(final Set<NewsEvent> events, NewsEventType type) { /* Return on Shutdown or disposal */ if (canceled()) return; /* * Optimization: The Browser is likely only showing a single news and thus * there is no need to refresh the entire content but rather use the update * instead. */ if (!browserShowsCollection()) { List<INews> items = new ArrayList<INews>(events.size()); for (NewsEvent event : events) { items.add(event.getEntity()); } /* Update Browser Viewer */ if (fFeedView.isBrowserViewerVisible() && contains(fBrowserViewer.getInput(), items)) { /* Update */ if (type == NewsEventType.UPDATED) { Set<NewsEvent> newsToUpdate = events; /* * Optimization: If more than a single news is to update, check * if the Browser only shows a single news to avoid a full refresh. */ if (events.size() > 1) { NewsEvent event = findShowingEventFromBrowser(events); if (event != null) newsToUpdate = Collections.singleton(event); } fBrowserViewer.update(newsToUpdate); } /* Remove */ else if (type == NewsEventType.REMOVED) fBrowserViewer.remove(items.toArray()); } /* Check if ContentProvider was already disposed or RSSOwl shutting down */ if (canceled()) return; /* Refresh Table Viewer */ fFeedView.refreshTableViewer(true, true); } /* Browser is showing Collection, thereby perform a refresh */ else fFeedView.refresh(true, true); } private boolean handleAddedNews(Set<NewsEvent> events) { /* * Input can be NULL if this listener was called before NewsTableControl.setPartInput() * has been called (can happen if the viewer has thousands of items to load) */ if (fFeedView.isTableViewerVisible() && fTableViewer.getInput() == null) return false; /* Receive added News */ List<INews> addedNews = new ArrayList<INews>(events.size()); for (NewsEvent event : events) { addedNews.add(event.getEntity()); } /* Add to Cache */ Triple<Boolean, Collection<NewsEvent>, Collection<INews>> result = addToCache(events, addedNews); boolean wasEmpty = result.getFirst(); Collection<NewsEvent> visibleEvents = result.getSecond(); Collection<INews> visibleNews = result.getThird(); /* Return early if a refresh is required anyways */ if (fGrouping.needsRefresh(visibleEvents, false)) { /* Avoid a refresh when user is reading a filled newspaper view at the moment */ if (!browserShowsCollection() || canDoBrowserRefresh(wasEmpty, visibleEvents)) return true; } /* Return on Shutdown or disposal */ if (canceled()) return false; /* Add to Viewers */ addToViewers(visibleNews, visibleEvents, wasEmpty); return false; } /* Add a List of News to Table and Browser Viewers */ private void addToViewers(Collection<INews> addedNews, Collection<NewsEvent> events, boolean wasEmpty) { /* Return on Shutdown or disposal */ if (canceled()) return; /* Return early if nothing to do */ if (addedNews.isEmpty()) return; /* Add to Table-Viewer if Visible (keep top item and selection stable) */ if (fFeedView.isTableViewerVisible()) { Tree tree = fTableViewer.getTree(); TreeItem topItem = tree.getTopItem(); int indexOfTopItem = 0; if (topItem != null) indexOfTopItem = tree.indexOf(topItem); tree.setRedraw(false); try { fTableViewer.add(fTableViewer.getInput(), addedNews.toArray()); if (topItem != null && indexOfTopItem != 0) tree.setTopItem(topItem); } finally { tree.setRedraw(true); } } /* Add to Browser-Viewer if showing entire Feed */ else if (browserShowsCollection()) { /* Feedview is active and user reads news, thereby only show info about added news */ if (!canDoBrowserRefresh(wasEmpty, events)) fFeedView.getNewsBrowserControl().setInfoBarVisible(true); /* Otherwise refresh the browser viewer to show added news */ else fBrowserViewer.add(fBrowserViewer.getInput(), addedNews.toArray()); } } /* Some conditions under which a browser refresh is tolerated */ @SuppressWarnings("unchecked") private boolean canDoBrowserRefresh(boolean wasEmpty) { return canDoBrowserRefresh(wasEmpty, Collections.EMPTY_SET); } /* Some conditions under which a browser refresh is tolerated */ private boolean canDoBrowserRefresh(boolean wasEmpty, Collection<NewsEvent> events) { return (wasEmpty || !fFeedView.isVisible() || OwlUI.isMinimized() || CoreUtils.gotRestored(events)); } /* Browser shows collection if maximized */ private boolean browserShowsCollection() { Object input = fBrowserViewer.getInput(); return (input instanceof BookMarkReference || input instanceof NewsBinReference || input instanceof SearchMarkReference || input instanceof FolderNewsMarkReference); } private boolean handleUpdatedNews(Set<NewsEvent> events) { /* Receive updated News */ List<INews> updatedNews = new ArrayList<INews>(events.size()); for (NewsEvent event : events) { updatedNews.add(event.getEntity()); } /* Update Cache */ Pair<List<NewsEvent>, List<INews>> result = updateCache(events, updatedNews); final List<NewsEvent> visibleEvents = result.getFirst(); List<INews> visibleNews = result.getSecond(); /* Return if news was not part of cache at all (e.g. limited Folder News Mark) */ if (visibleNews.isEmpty()) return false; /* Return on Shutdown or disposal */ if (canceled()) return false; /* Return early if refresh is required anyways for Grouper */ if (fGrouping.needsRefresh(visibleEvents, true)) return true; /* Return early if refresh is required anyways for Sorter */ if (fFeedView.isTableViewerVisible()) { //Only makes sense if Browser not maximized ViewerComparator sorter = fTableViewer.getComparator(); if (sorter instanceof NewsComparator && ((NewsComparator) sorter).needsRefresh(visibleEvents)) return true; } /* Update in Table-Viewer */ if (fFeedView.isTableViewerVisible()) fTableViewer.update(visibleNews.toArray(), null); /* Update in Browser-Viewer */ if (fFeedView.isBrowserViewerVisible() && contains(fBrowserViewer.getInput(), visibleNews)) { Collection<NewsEvent> newsToUpdate = visibleEvents; /* * Optimization: If more than a single news is to update, check * if the Browser only shows a single news to avoid a full refresh. */ if (visibleEvents.size() > 1) { NewsEvent event = findShowingEventFromBrowser(visibleEvents); if (event != null) newsToUpdate = Collections.singleton(event); } fBrowserViewer.update(newsToUpdate); } return false; } private boolean handleDeletedNews(Set<NewsEvent> events) { /* Receive deleted News */ List<INews> deletedNews = new ArrayList<INews>(events.size()); for (NewsEvent event : events) { deletedNews.add(event.getEntity()); } /* Remove from Cache */ Pair<List<NewsEvent>, List<INews>> result = removeFromCache(events, deletedNews); List<NewsEvent> visibleEvents = result.getFirst(); List<INews> visibleNews = result.getSecond(); /* Return if news was not part of cache at all (e.g. limited Folder News Mark) */ if (visibleNews.isEmpty()) return false; /* Return on Shutdown or disposal */ if (canceled()) return false; /* Only refresh if grouping requires this from table viewer */ if (isGroupingEnabled() && fFeedView.isTableViewerVisible() && fGrouping.needsRefresh(visibleEvents, false)) return true; /* Otherwise: Remove from Table-Viewer */ if (fFeedView.isTableViewerVisible()) fTableViewer.remove(visibleNews.toArray()); /* And: Remove from Browser-Viewer */ if (fFeedView.isBrowserViewerVisible() && contains(fBrowserViewer.getInput(), visibleNews)) fBrowserViewer.remove(visibleNews.toArray()); return false; } private void unregisterListeners() { DynamicDAO.removeEntityListener(INews.class, fNewsListener); DynamicDAO.removeEntityListener(ISearchMark.class, fSearchMarkListener); } private boolean isInputRelatedTo(NewsEvent event, NewsEventType type) { INews news = event.getEntity(); /* Check if BookMark references the News' Feed and is not a copy */ if (fInput instanceof IBookMark) { /* Return early if news is from bin */ if (news.getParentId() != 0) return false; /* Perform fast HashMap lookup first */ if (hasCachedNews(news)) return true; /* Otherwise compare by feed link */ IBookMark bookmark = (IBookMark) fInput; if (bookmark.getFeedLinkReference().equals(news.getFeedReference())) return true; } /* Check if Saved Search contains the given News */ else if (type != NewsEventType.PERSISTED && fInput instanceof ISearchMark) { return hasCachedNews(news) || fInput.containsNews(news); } /* Update / Remove: Check if News points to this Bin */ else if (fInput instanceof INewsBin) { return news.getParentId() == fInput.getId(); } /* In Memory Folder News Mark (aggregated news) */ else if (fInput instanceof FolderNewsMark) { /* Perform fast HashMap lookup first */ if (hasCachedNews(news)) return true; /* Ask FolderNewsMark directly */ return ((FolderNewsMark) fInput).isRelatedTo(news); } return false; } private boolean contains(Object input, List<INews> list) { /* Can only belong to this Feed since filtered before already */ if (input instanceof BookMarkReference || input instanceof NewsBinReference || input instanceof SearchMarkReference || input instanceof FolderNewsMarkReference) return true; /* News */ else if (input instanceof INews) return list.contains(input); /* Entity Group */ else if (input instanceof EntityGroup) { List<EntityGroupItem> items = ((EntityGroup) input).getItems(); for (EntityGroupItem item : items) { if (list.contains(item.getEntity())) return true; } } /* Other Input */ else if (input instanceof Object[]) { Object inputNews[] = (Object[]) input; for (Object inputNewsItem : inputNews) { if (list.contains(inputNewsItem)) return true; } } return false; } private NewsEvent findShowingEventFromBrowser(Collection<NewsEvent> events) { Object input = fBrowserViewer.getInput(); if (input instanceof INews) { INews news = (INews) input; for (NewsEvent event : events) { if (news.equals(event.getEntity())) return event; } } return null; } private boolean canceled() { return canceled(null); } private boolean canceled(IProgressMonitor monitor) { return fDisposed.get() || Controller.getDefault().isShuttingDown() || (monitor != null && monitor.isCanceled()); } }