/* ********************************************************************** **
** 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.views.explorer;
import org.eclipse.core.runtime.Assert;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.jface.viewers.Viewer;
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.INews;
import org.rssowl.core.persist.INewsBin;
import org.rssowl.core.persist.ISearchMark;
import org.rssowl.core.persist.dao.DynamicDAO;
import org.rssowl.core.persist.dao.IBookMarkDAO;
import org.rssowl.core.persist.dao.INewsBinDAO;
import org.rssowl.core.persist.event.BookMarkEvent;
import org.rssowl.core.persist.event.BookMarkListener;
import org.rssowl.core.persist.event.FolderEvent;
import org.rssowl.core.persist.event.FolderListener;
import org.rssowl.core.persist.event.MarkEvent;
import org.rssowl.core.persist.event.NewsAdapter;
import org.rssowl.core.persist.event.NewsBinEvent;
import org.rssowl.core.persist.event.NewsBinListener;
import org.rssowl.core.persist.event.NewsEvent;
import org.rssowl.core.persist.event.NewsListener;
import org.rssowl.core.persist.event.SearchMarkEvent;
import org.rssowl.core.persist.event.SearchMarkListener;
import org.rssowl.core.persist.reference.FeedLinkReference;
import org.rssowl.core.persist.service.PersistenceException;
import org.rssowl.core.util.CoreUtils;
import org.rssowl.ui.internal.Controller;
import org.rssowl.ui.internal.EntityGroup;
import org.rssowl.ui.internal.util.JobRunner;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;
/**
* @author bpasero
*/
public class BookMarkContentProvider implements ITreeContentProvider {
/* Delay in ms before updating Selection on Events */
private static final int SELECTION_DELAY = 20;
/* Listener */
private FolderListener fFolderListener;
private BookMarkListener fBookMarkListener;
private NewsBinListener fNewsBinListener;
private SearchMarkListener fSearchMarkListener;
private NewsListener fNewsListener;
/* Viewer Related */
private IFolder fInput;
private TreeViewer fViewer;
private BookMarkFilter fBookmarkFilter;
private BookMarkGrouping fBookmarkGrouping;
/* Misc. */
private IBookMarkDAO fBookMarkDAO = DynamicDAO.getDAO(IBookMarkDAO.class);
/*
* @see org.eclipse.jface.viewers.IStructuredContentProvider#getElements(java.lang.Object)
*/
public Object[] getElements(Object inputElement) {
if (inputElement instanceof IFolder) {
IFolder rootFolder = (IFolder) inputElement;
/* No Grouping */
if (!fBookmarkGrouping.isActive()) {
Collection<IFolderChild> elements = rootFolder.getChildren();
/* Return Children */
return elements.toArray();
}
/* Grouping Enabled */
List<IMark> marks = new ArrayList<IMark>();
getAllMarks(rootFolder, marks);
return fBookmarkGrouping.group(marks);
}
return new Object[0];
}
/*
* @see org.eclipse.jface.viewers.ITreeContentProvider#getChildren(java.lang.Object)
*/
public Object[] getChildren(Object parentElement) {
/* Return Children of Folder */
if (parentElement instanceof IFolder) {
IFolder parent = (IFolder) parentElement;
Collection<IFolderChild> children = parent.getChildren();
return children.toArray();
}
/* Return Children of Group */
else if (parentElement instanceof EntityGroup) {
List<IEntity> children = ((EntityGroup) parentElement).getEntities();
return children.toArray();
}
return new Object[0];
}
/*
* @see org.eclipse.jface.viewers.ITreeContentProvider#getParent(java.lang.Object)
*/
public Object getParent(Object element) {
/* Handle Grouping specially */
if (fBookmarkGrouping.isActive() && element instanceof IEntity) {
IEntity entity = (IEntity) element;
EntityGroup[] groups = fBookmarkGrouping.group(Collections.singletonList(entity));
if (groups.length == 1)
return groups[0];
}
/* Grouping not enabled */
else {
/* Parent Folder of Folder */
if (element instanceof IFolder) {
IFolder folder = (IFolder) element;
return folder.getParent();
}
/* Parent Folder of Mark */
else if (element instanceof IMark) {
IMark mark = (IMark) element;
return mark.getParent();
}
}
return null;
}
/*
* @see org.eclipse.jface.viewers.ITreeContentProvider#hasChildren(java.lang.Object)
*/
public boolean hasChildren(Object element) {
if (element instanceof IFolder) {
IFolder folder = (IFolder) element;
return folder.getChildren().size() > 0;
}
else if (element instanceof EntityGroup)
return ((EntityGroup) element).size() > 0;
return false;
}
/*
* @see org.eclipse.jface.viewers.IContentProvider#dispose()
*/
public void dispose() {
unregisterListeners();
}
/*
* @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) {
Assert.isTrue(newInput instanceof IFolder || newInput == null);
fViewer = (TreeViewer) viewer;
fInput = (IFolder) newInput;
/* Register Listeners if Input is available */
if (newInput != null && oldInput == null)
registerListeners();
/* If new Input is NULL, unregister Listeners */
else if (newInput == null && oldInput != null)
unregisterListeners();
}
/* The ContentProvider needs to know about this Filter */
void setBookmarkFilter(BookMarkFilter bookmarkFilter) {
fBookmarkFilter = bookmarkFilter;
}
/* The ContentProvider needs to know about this Grouping */
void setBookmarkGrouping(BookMarkGrouping bookmarkGrouping) {
fBookmarkGrouping = bookmarkGrouping;
}
private void registerListeners() {
/* Folder Listener */
fFolderListener = new FolderListener() {
/* Folders got updated */
public void entitiesUpdated(final Set<FolderEvent> events) {
JobRunner.runInUIThread(fViewer.getControl(), new Runnable() {
public void run() {
Set<IFolder> updatedFolders = null;
Map<IFolder, IFolder> reparentedFolders = null;
/* Retrieve Updated Folders */
for (FolderEvent event : events) {
if (event.isRoot()) {
/* Folder got reparented */
if (event.getOldParent() != null) {
if (reparentedFolders == null)
reparentedFolders = new HashMap<IFolder, IFolder>();
reparentedFolders.put(event.getEntity(), event.getOldParent());
}
/* Normal Update */
else {
if (updatedFolders == null)
updatedFolders = new HashSet<IFolder>();
updatedFolders.add(event.getEntity());
}
}
}
/* Event not interesting for us or we are disposed */
if (updatedFolders == null && reparentedFolders == null)
return;
/* Ask Filter */
if (fBookmarkFilter.needsRefresh(IFolder.class, events))
fViewer.refresh(false);
/* Ask Group */
else if (fBookmarkGrouping.needsRefresh(IFolder.class))
fViewer.refresh(false);
/* Handle reparented Folders */
else if (reparentedFolders != null) {
Set<Entry<IFolder, IFolder>> entries = reparentedFolders.entrySet();
Set<IFolder> parentsToUpdate = new HashSet<IFolder>();
List<Object> expandedElements = new ArrayList<Object>(Arrays.asList(fViewer.getExpandedElements()));
try {
fViewer.getControl().getParent().setRedraw(false);
for (Entry<IFolder, IFolder> entry : entries) {
IFolder reparentedFolder = entry.getKey();
IFolder oldParent = entry.getValue();
/* Reparent while keeping the Selection / Expansion */
ISelection selection = fViewer.getSelection();
boolean expand = expandedElements.contains(reparentedFolder);
fViewer.remove(oldParent, new Object[] { reparentedFolder });
fViewer.refresh(reparentedFolder.getParent(), false);
fViewer.setSelection(selection);
if (expand)
fViewer.setExpandedState(reparentedFolder, expand);
/* Remember to update parents */
parentsToUpdate.add(oldParent);
parentsToUpdate.add(reparentedFolder.getParent());
}
} finally {
fViewer.getControl().getParent().setRedraw(true);
}
/* Update old Parents of Reparented Bookmarks */
for (IFolder folder : parentsToUpdate)
updateFolderAndParents(folder);
}
/* Handle Updated Folders */
if (updatedFolders != null) {
for (IFolder folder : updatedFolders) {
if (fInput.equals(folder))
fViewer.refresh(fInput);
else
fViewer.refresh(folder);
}
}
}
});
}
/* Folders got deleted */
public void entitiesDeleted(final Set<FolderEvent> events) {
JobRunner.runInUIThread(fViewer.getControl(), new Runnable() {
public void run() {
/* Retrieve Removed Folders */
Set<IFolder> removedFolders = null;
for (FolderEvent event : events) {
if (event.isRoot() && event.getEntity().getParent() != null) {
if (removedFolders == null)
removedFolders = new HashSet<IFolder>();
removedFolders.add(event.getEntity());
}
}
/* Event not interesting for us or we are disposed */
if (removedFolders == null || removedFolders.size() == 0)
return;
/* Ask Filter */
if (fBookmarkFilter.needsRefresh(IFolder.class, events))
fViewer.refresh(false);
/* Ask Group */
else if (fBookmarkGrouping.needsRefresh(IFolder.class))
fViewer.refresh(false);
/* React normally then */
else
fViewer.remove(removedFolders.toArray());
/* Update Read-State counters on Parents */
if (!fBookmarkGrouping.isActive()) {
for (FolderEvent event : events) {
IFolder eventParent = event.getEntity().getParent();
if (eventParent != null && eventParent.getParent() != null)
updateFolderAndParents(eventParent);
}
}
}
});
}
/* Folders got added */
public void entitiesAdded(final Set<FolderEvent> events) {
JobRunner.runInUIThread(SELECTION_DELAY, fViewer.getControl(), new Runnable() {
public void run() {
/* Reveal and Select added Folders */
final List<IFolder> addedFolders = new ArrayList<IFolder>();
for (FolderEvent folderEvent : events) {
IFolder addedFolder = folderEvent.getEntity();
if (addedFolder.getParent() != null)
addedFolders.add(addedFolder);
}
if (addedFolders.size() == 1)
fViewer.setSelection(new StructuredSelection(addedFolders), true);
}
});
}
};
/* BookMark Listener */
fBookMarkListener = new BookMarkListener() {
/* BookMarks got Updated */
public void entitiesUpdated(final Set<BookMarkEvent> events) {
onMarksUpdated(events);
}
/* BookMarks got Deleted */
public void entitiesDeleted(final Set<BookMarkEvent> events) {
onMarksRemoved(events);
}
/* BookMarks got Added */
public void entitiesAdded(Set<BookMarkEvent> events) {
onMarksAdded(events);
}
};
/* SearchMark Listener */
fSearchMarkListener = new SearchMarkListener() {
/* SearchMarks got Updated */
public void entitiesUpdated(final Set<SearchMarkEvent> events) {
onMarksUpdated(events);
}
/* SearchMarks got Deleted */
public void entitiesDeleted(final Set<SearchMarkEvent> events) {
onMarksRemoved(events);
}
/* SearchMarks got Added */
public void entitiesAdded(Set<SearchMarkEvent> events) {
onMarksAdded(events);
}
/* SearchMark result changed */
public void newsChanged(final Set<SearchMarkEvent> events) {
JobRunner.runInUIThread(fViewer.getControl(), new Runnable() {
public void run() {
/* Ask Filter for a refresh */
if (fBookmarkFilter.needsRefresh(ISearchMark.class, events, true))
fViewer.refresh(false);
/* Update SearchMarks */
Set<ISearchMark> updatedSearchMarks = new HashSet<ISearchMark>(events.size());
for (SearchMarkEvent event : events) {
updatedSearchMarks.add(event.getEntity());
}
fViewer.update(updatedSearchMarks.toArray(), null);
/* Update Parents */
if (!fBookmarkGrouping.isActive()) {
for (ISearchMark searchMark : updatedSearchMarks)
updateFolderAndParents(searchMark.getParent());
}
}
});
}
};
/* NewsBin Listener */
fNewsBinListener = new NewsBinListener() {
/* NewsBins got Updated */
public void entitiesUpdated(final Set<NewsBinEvent> events) {
onMarksUpdated(events);
}
/* NewsBins got Deleted */
public void entitiesDeleted(final Set<NewsBinEvent> events) {
onMarksRemoved(events);
}
/* Newsbins got Added */
public void entitiesAdded(Set<NewsBinEvent> events) {
onMarksAdded(events);
}
};
/* News Listener */
fNewsListener = new NewsAdapter() {
@Override
public void entitiesAdded(final Set<NewsEvent> events) {
JobRunner.runInUIThread(fViewer.getControl(), new Runnable() {
public void run() {
/* Return on Shutdown */
if (Controller.getDefault().isShuttingDown())
return;
/* Ask Filter */
if (fBookmarkFilter.needsRefresh(INews.class, events))
fViewer.refresh(false);
/* Ask Group */
else if (fBookmarkGrouping.needsRefresh(INews.class))
fViewer.refresh(false);
/* Updated affected Types on read-state if required */
if (requiresUpdate(events))
updateParents(events);
}
});
}
@Override
public void entitiesUpdated(final Set<NewsEvent> events) {
JobRunner.runInUIThread(fViewer.getControl(), new Runnable() {
public void run() {
/* Return on Shutdown */
if (Controller.getDefault().isShuttingDown())
return;
/* Ask Filter */
if (fBookmarkFilter.needsRefresh(INews.class, events))
fViewer.refresh(false);
/* Ask Group */
else if (fBookmarkGrouping.needsRefresh(INews.class))
fViewer.refresh(false);
/* Updated affected Types on read-state if required */
if (requiresUpdate(events))
updateParents(events);
}
});
}
};
/* Register Listeners */
DynamicDAO.addEntityListener(IFolder.class, fFolderListener);
DynamicDAO.addEntityListener(IBookMark.class, fBookMarkListener);
DynamicDAO.addEntityListener(INewsBin.class, fNewsBinListener);
DynamicDAO.addEntityListener(ISearchMark.class, fSearchMarkListener);
DynamicDAO.addEntityListener(INews.class, fNewsListener);
}
private void onMarksAdded(Set<? extends MarkEvent> events) {
/* Reveal and Select if single Entity added */
if (events.size() == 1) {
final MarkEvent event = events.iterator().next();
JobRunner.runInUIThread(fViewer.getControl(), new Runnable() {
public void run() {
expand(event.getEntity().getParent());
}
});
}
}
private void onMarksRemoved(final Set<? extends MarkEvent> events) {
if (events.isEmpty())
return;
JobRunner.runInUIThread(fViewer.getControl(), new Runnable() {
public void run() {
/* Retrieve Removed Marks */
Class<? extends IMark> clazz = null;
Set<IMark> removedMarks = null;
for (MarkEvent event : events) {
if (event.isRoot()) {
if (removedMarks == null)
removedMarks = new HashSet<IMark>();
removedMarks.add(event.getEntity());
}
if (clazz == null)
clazz = event.getEntity().getClass();
}
/* Event not interesting for us or we are disposed */
if (removedMarks == null || removedMarks.size() == 0)
return;
/* Ask Filter */
if (fBookmarkFilter.needsRefresh(clazz, events))
fViewer.refresh(false);
/* Ask Group */
else if (fBookmarkGrouping.needsRefresh(clazz))
fViewer.refresh(false);
/* React normally then */
else
fViewer.remove(removedMarks.toArray());
/* Update Read-State counters on Parents */
if (!fBookmarkGrouping.isActive()) {
for (MarkEvent event : events) {
IFolder eventParent = event.getEntity().getParent();
if (eventParent != null && eventParent.getParent() != null)
updateFolderAndParents(eventParent);
}
}
}
});
}
private void onMarksUpdated(final Set<? extends MarkEvent> events) {
if (events.isEmpty())
return;
JobRunner.runInUIThread(fViewer.getControl(), new Runnable() {
public void run() {
Class<? extends IMark> clazz = null;
Set<IMark> updatedMarks = null;
Map<IMark, IFolder> reparentedMarks = null;
/* Retrieve Updated Marks */
for (MarkEvent event : events) {
if (event.isRoot()) {
IFolder oldParent = event.getOldParent();
/* Mark got reparented */
if (oldParent != null) {
if (reparentedMarks == null)
reparentedMarks = new HashMap<IMark, IFolder>();
reparentedMarks.put(event.getEntity(), oldParent);
}
/* Normal Update */
else {
if (updatedMarks == null)
updatedMarks = new HashSet<IMark>();
updatedMarks.add(event.getEntity());
}
}
if (clazz == null)
clazz = event.getEntity().getClass();
}
/* Event not interesting for us or we are disposed */
if (updatedMarks == null && reparentedMarks == null)
return;
/* Ask Filter */
if (fBookmarkFilter.needsRefresh(clazz, events))
fViewer.refresh(false);
/* Ask Group */
else if (fBookmarkGrouping.needsRefresh(clazz))
fViewer.refresh(false);
/* Handle reparented Marks */
else if (reparentedMarks != null) {
Set<Entry<IMark, IFolder>> entries = reparentedMarks.entrySet();
Set<IFolder> parentsToUpdate = new HashSet<IFolder>();
try {
fViewer.getControl().getParent().setRedraw(false);
for (Entry<IMark, IFolder> entry : entries) {
IMark reparentedMark = entry.getKey();
IFolder oldParent = entry.getValue();
/* Reparent while keeping the Selection */
ISelection selection = fViewer.getSelection();
fViewer.remove(oldParent, new Object[] { reparentedMark });
fViewer.refresh(reparentedMark.getParent(), false);
fViewer.setSelection(selection);
/* Remember to update parents */
parentsToUpdate.add(oldParent);
parentsToUpdate.add(reparentedMark.getParent());
}
} finally {
fViewer.getControl().getParent().setRedraw(true);
}
/* Update old Parents of Reparented Marks */
for (IFolder folder : parentsToUpdate)
updateFolderAndParents(folder);
}
/* Handle Updated Marks */
if (updatedMarks != null)
fViewer.update(updatedMarks.toArray(), null);
}
});
}
private void unregisterListeners() {
DynamicDAO.removeEntityListener(IFolder.class, fFolderListener);
DynamicDAO.removeEntityListener(IBookMark.class, fBookMarkListener);
DynamicDAO.removeEntityListener(INewsBin.class, fNewsBinListener);
DynamicDAO.removeEntityListener(ISearchMark.class, fSearchMarkListener);
DynamicDAO.removeEntityListener(INews.class, fNewsListener);
}
/* Update Entities that are affected by the given NewsEvents */
private void updateParents(final Set<NewsEvent> events) {
INewsBinDAO newsBinDao = DynamicDAO.getDAO(INewsBinDAO.class);
/* Group by Feed and Bins */
Set<FeedLinkReference> affectedFeeds = new HashSet<FeedLinkReference>();
Set<IFolder> affectedBinFolders = new HashSet<IFolder>();
Set<Long> handledBins = new HashSet<Long>();
for (NewsEvent event : events) {
INews news = event.getEntity();
long parentId = news.getParentId();
if (!fBookmarkGrouping.isActive() && parentId != 0) {
if (!handledBins.contains(parentId)) {
INewsBin bin = newsBinDao.load(parentId);
if (bin != null) //Could have been deleted meanwhile
affectedBinFolders.add(bin.getParent());
handledBins.add(parentId);
}
} else
affectedFeeds.add(news.getFeedReference());
}
/* Return on Shutdown */
if (Controller.getDefault().isShuttingDown())
return;
/* Update related Entities */
for (FeedLinkReference feedRef : affectedFeeds)
updateParents(feedRef);
for (IFolder folder : affectedBinFolders)
updateFolderAndParents(folder);
}
private void updateParents(FeedLinkReference feedRef) throws PersistenceException {
/* Collect all affected BookMarks */
Collection<IBookMark> affectedBookMarks = fBookMarkDAO.loadAll(feedRef);
/* Return on Shutdown */
if (Controller.getDefault().isShuttingDown())
return;
/* Update them including Parents */
updateMarksAndParents(affectedBookMarks);
}
private void updateMarksAndParents(Collection<IBookMark> bookmarks) {
Set<IEntity> entitiesToUpdate = new HashSet<IEntity>();
entitiesToUpdate.addAll(bookmarks);
/* Collect parents */
if (!fBookmarkGrouping.isActive()) {
for (IBookMark bookmark : bookmarks) {
List<IFolder> visibleParents = new ArrayList<IFolder>();
collectParents(visibleParents, bookmark);
entitiesToUpdate.addAll(visibleParents);
/* Return on Shutdown */
if (Controller.getDefault().isShuttingDown())
return;
}
}
/* Update Entities */
fViewer.update(entitiesToUpdate.toArray(), null);
}
private void collectParents(List<IFolder> parents, IEntity entity) {
/* Determine Parent Folder */
IFolder parent = null;
if (entity instanceof IMark)
parent = ((IMark) entity).getParent();
else if (entity instanceof IFolder)
parent = ((IFolder) entity).getParent();
/* Root reached */
if (parent == null)
return;
/* Input reached */
if (fInput.equals(parent))
return;
/* Check parent visible */
parents.add(parent);
/* Recursively collect visible parents */
collectParents(parents, parent);
}
private void updateFolderAndParents(IFolder folder) {
Set<IEntity> entitiesToUpdate = new HashSet<IEntity>();
entitiesToUpdate.add(folder);
/* Collect parents */
List<IFolder> parents = new ArrayList<IFolder>();
collectParents(parents, folder);
entitiesToUpdate.addAll(parents);
/* Return on Shutdown */
if (Controller.getDefault().isShuttingDown())
return;
/* Update Entities */
fViewer.update(entitiesToUpdate.toArray(), null);
}
private void getAllMarks(IFolder folder, List<IMark> marks) {
/* Add all Marks */
marks.addAll(folder.getMarks());
/* Go through Subfolders */
List<IFolder> folders = folder.getFolders();
for (IFolder childFolder : folders)
getAllMarks(childFolder, marks);
}
private boolean requiresUpdate(Set<NewsEvent> events) {
for (NewsEvent newsEvent : events) {
INews oldNews = newsEvent.getOldNews();
INews currentNews = newsEvent.getEntity();
/* Check Change in New-State */
boolean oldStateNew = INews.State.NEW.equals(oldNews != null ? oldNews.getState() : null);
boolean currentStateNew = INews.State.NEW.equals(currentNews.getState());
if (oldStateNew != currentStateNew)
return true;
/* Check Change in Read-State */
boolean oldStateUnread = CoreUtils.isUnread(oldNews != null ? oldNews.getState() : null);
boolean newStateUnread = CoreUtils.isUnread(currentNews.getState());
if (oldStateUnread != newStateUnread)
return true;
/* Check Change in Sticky-State */
boolean oldStateSticky = oldNews != null ? oldNews.isFlagged() : false;
boolean newStateSticky = currentNews.isVisible() && currentNews.isFlagged();
if (oldStateSticky != newStateSticky)
return true;
}
return false;
}
/* Recursively expand a folder and all parents */
private void expand(IFolder folder) {
IFolder parent = folder.getParent();
if (parent != null)
expand(parent);
if (folder.getParent() != null) //Never expand Set, its visible anyways
fViewer.setExpandedState(folder, true);
}
}