/* ********************************************************************** ** ** 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.core.internal.persist.service; import org.rssowl.core.Owl; import org.rssowl.core.internal.InternalOwl; import org.rssowl.core.internal.persist.BookMark; import org.rssowl.core.internal.persist.Description; import org.rssowl.core.internal.persist.Feed; import org.rssowl.core.internal.persist.News; import org.rssowl.core.internal.persist.NewsBin; import org.rssowl.core.persist.IAttachment; import org.rssowl.core.persist.IBookMark; import org.rssowl.core.persist.ICategory; import org.rssowl.core.persist.IConditionalGet; import org.rssowl.core.persist.IEntity; import org.rssowl.core.persist.IFeed; import org.rssowl.core.persist.IFolder; import org.rssowl.core.persist.ILabel; import org.rssowl.core.persist.IMark; import org.rssowl.core.persist.INews; import org.rssowl.core.persist.INewsBin; import org.rssowl.core.persist.IPerson; import org.rssowl.core.persist.IPreference; import org.rssowl.core.persist.ISearch; import org.rssowl.core.persist.ISearchCondition; import org.rssowl.core.persist.ISearchFilter; import org.rssowl.core.persist.ISearchMark; import org.rssowl.core.persist.dao.IConditionalGetDAO; import org.rssowl.core.persist.dao.INewsCounterDAO; import org.rssowl.core.persist.event.AttachmentEvent; import org.rssowl.core.persist.event.BookMarkEvent; import org.rssowl.core.persist.event.CategoryEvent; import org.rssowl.core.persist.event.FeedEvent; import org.rssowl.core.persist.event.FolderEvent; import org.rssowl.core.persist.event.LabelEvent; import org.rssowl.core.persist.event.ModelEvent; import org.rssowl.core.persist.event.NewsBinEvent; import org.rssowl.core.persist.event.NewsEvent; import org.rssowl.core.persist.event.PersonEvent; import org.rssowl.core.persist.event.PreferenceEvent; import org.rssowl.core.persist.event.SearchConditionEvent; import org.rssowl.core.persist.event.SearchEvent; import org.rssowl.core.persist.event.SearchFilterEvent; import org.rssowl.core.persist.event.SearchMarkEvent; import org.rssowl.core.persist.reference.FeedLinkReference; import org.rssowl.core.persist.service.IDGenerator; import com.db4o.ObjectContainer; import com.db4o.events.Event4; import com.db4o.events.EventArgs; import com.db4o.events.EventListener4; import com.db4o.events.EventRegistry; import com.db4o.events.EventRegistryFactory; import com.db4o.events.ObjectEventArgs; import com.db4o.query.Query; import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; /** * Central manager for events and related actions on the database. */ public class EventManager implements DatabaseListener { /** * Iterator implementation that iterates from the end of the list. Useful if * items need to be removed from the list during iteration and specific method * needs to be called for removal. */ private final static class ReverseIterator<T> implements Iterable<T>, Iterator<T> { private final List<T> fList; private int index; static <T> ReverseIterator<T> createInstance(List<T> list) { return new ReverseIterator<T>(list); } private ReverseIterator(List<T> list) { fList = list; index = list.size() - 1; } public final Iterator<T> iterator() { return this; } public final boolean hasNext() { return index > -1; } public final T next() { return fList.get(index--); } public final void remove() { throw new UnsupportedOperationException(); } } private final ThreadLocal<Set<Object>> fItemsBeingDeleted = new ThreadLocal<Set<Object>>(); private static final String PARENT_DELETED_KEY = "rssowl.db4o.EventManager.parentDeleted"; //$NON-NLS-1$ private final static EventManager INSTANCE = new EventManager(); private ObjectContainer fDb; private IConditionalGetDAO fConditionalGetDAO; private IDGenerator fIDGenerator; private INewsCounterDAO fNewsCounterDAO; private EventManager() { initEntityStoreListener(); } private IDGenerator getIDGenerator() { if (fIDGenerator == null) fIDGenerator = Owl.getPersistenceService().getIDGenerator(); return fIDGenerator; } private IConditionalGetDAO getConditionalGetDAO() { if (fConditionalGetDAO == null) fConditionalGetDAO = Owl.getPersistenceService().getDAOService().getConditionalGetDAO(); return fConditionalGetDAO; } private INewsCounterDAO getNewsCounterDAO() { if (fNewsCounterDAO == null) fNewsCounterDAO = InternalOwl.getDefault().getPersistenceService().getDAOService().getNewsCounterDAO(); return fNewsCounterDAO; } private void initEventRegistry() { EventRegistry eventRegistry = EventRegistryFactory.forObjectContainer(fDb); EventListener4 updatedListener = new EventListener4() { public void onEvent(Event4 e, EventArgs args) { processUpdatedEvent(args); } }; EventListener4 creatingListener = new EventListener4() { public void onEvent(Event4 e, EventArgs args) { processCreatingEvent(args); } }; EventListener4 createdListener = new EventListener4() { public void onEvent(Event4 e, EventArgs args) { processCreatedEvent(args); } }; EventListener4 deletingListener = new EventListener4() { public void onEvent(Event4 e, EventArgs args) { processDeletingEvent(args); } }; EventListener4 deletedListener = new EventListener4() { public void onEvent(Event4 e, EventArgs args) { processDeletedEvent(args); } }; EventListener4 activatedListener = new EventListener4() { public void onEvent(Event4 e, EventArgs args) { processActivated(args); } }; eventRegistry.activated().addListener(activatedListener); eventRegistry.created().addListener(createdListener); eventRegistry.creating().addListener(creatingListener); eventRegistry.updated().addListener(updatedListener); eventRegistry.deleting().addListener(deletingListener); eventRegistry.deleted().addListener(deletedListener); } private void processActivated(EventArgs args) { IEntity entity = getEntity(args); if (entity == null) return; if (entity instanceof News) ((News) entity).init(); else if (entity instanceof BookMark) initBookMark((BookMark) entity); } private void initBookMark(BookMark entity) { entity.setNewsCounter(getNewsCounterDAO().load()); } private void processUpdatedEvent(EventArgs args) { IEntity entity = getEntity(args); if (entity == null) return; ModelEvent event = createModelEvent(entity); if (event != null) EventsMap.getInstance().putUpdateEvent(event); } /* * Test items: News created, needs to save a description, but before assign * news id to description News updated from NewsDAO or recursively from Feed * dao (handleFeedReloaded does it on its own), must check if description * changed. If it did, then update description too and make sure that news * event is fired (e.g. nothing else changed) News deleted, needs to delete * description on handle feed reloaded, if description.getValue is null, * delete */ private void processCreatingEvent(EventArgs args) { IEntity entity = getEntity(args); if (entity != null) { setId(entity); if (entity instanceof BookMark) initBookMark((BookMark) entity); } } private void processCreatedEvent(EventArgs args) { IEntity entity = getEntity(args); if (entity == null) return; ModelEvent event = createModelEvent(entity); if (event != null) EventsMap.getInstance().putPersistEvent(event); } private void processDeletingEvent(EventArgs args) { IEntity entity = getEntity(args); if (entity == null) return; if (entity instanceof INews) cascadeNewsDeletion((INews) entity); else if (entity instanceof IFeed) cascadeFeedDeletion((IFeed) entity); else if (entity instanceof IMark) cascadeMarkDeletion((IMark) entity); else if (entity instanceof IFolder) removeFromParentFolderAndCascade((IFolder) entity); else if (entity instanceof IAttachment) removeFromParentNews((IAttachment) entity); else if (entity instanceof ISearchCondition) cascadeSearchConditionDeletion((ISearchCondition) entity); else if (entity instanceof ISearchFilter) cascadeSearchFilterDeletion((ISearchFilter) entity); else if (entity instanceof ISearch) cascadeSearchDeletion((ISearch) entity); } private void cascadeSearchFilterDeletion(ISearchFilter entity) { fDb.delete(entity.getSearch()); } private void cascadeNewsBinDeletion(INewsBin entity) { Set<FeedLinkReference> removedFeedRefs = new HashSet<FeedLinkReference>(); DBHelper.removeNews(fDb, removedFeedRefs, entity.getNewsRefs()); DBHelper.removeFeedsAfterNewsBinUpdate(fDb, removedFeedRefs); if (entity instanceof NewsBin) fDb.delete(((NewsBin) entity).internalGetNewsContainer()); } private void cascadeSearchConditionDeletion(ISearchCondition searchCondition) { ISearchMark searchMark = loadSearchMark(searchCondition); if (searchMark != null) { if (!itemsBeingDeletedContains(searchMark)) { if (searchMark.removeSearchCondition(searchCondition)) fDb.ext().set(searchMark, 2); } } fDb.delete(searchCondition.getField()); } private ISearchMark loadSearchMark(ISearchCondition searchCondition) { ISearchMark mark = Owl.getPersistenceService().getDAOService().getSearchMarkDAO().load(searchCondition); return mark; } private void cascadeNewsDeletion(INews news) { addItemBeingDeleted(news); if (news.getParentId() == 0) removeFromParentFeed(news); /* * It seems like the categories are not activated at this stage at times. * This seems like a db4o bug, but not totally sure. In any case, we play * safe and activate the news fully in that case. */ if (news.getCategories().isEmpty()) fDb.activate(news, Integer.MAX_VALUE); fDb.delete(news.getGuid()); fDb.delete(news.getSource()); fDb.delete(news.getAuthor()); for (ICategory category : ReverseIterator.createInstance(news.getCategories())) fDb.delete(category); for (IAttachment attachment : ReverseIterator.createInstance(news.getAttachments())) fDb.delete(attachment); Description description = DBHelper.getDescriptionDAO().load(news.getId()); if (description != null) fDb.delete(description); } private void cascadeMarkDeletion(IMark mark) { removeFromParentFolder(mark); if (mark instanceof IBookMark) deleteFeedIfNecessary((IBookMark) mark); else if (mark instanceof ISearchMark) cascadeSearchMarkDeletion((ISearchMark) mark); else if (mark instanceof INewsBin) cascadeNewsBinDeletion((INewsBin) mark); } private void cascadeSearchMarkDeletion(ISearchMark mark) { addItemBeingDeleted(mark); for (ISearchCondition condition : mark.getSearchConditions()) fDb.delete(condition); } private void cascadeSearchDeletion(ISearch search) { for (ISearchCondition condition : search.getSearchConditions()) fDb.delete(condition); } private void cascadeFeedDeletion(IFeed feed) { addItemBeingDeleted(new FeedLinkReference(feed.getLink())); fDb.delete(feed.getImage()); fDb.delete(feed.getAuthor()); for (ICategory category : ReverseIterator.createInstance(feed.getCategories())) { fDb.delete(category); } for (INews news : ReverseIterator.createInstance(feed.getNews())) { fDb.delete(news); } IConditionalGet conditionalGet = getConditionalGetDAO().load(feed.getLink()); if (conditionalGet != null) fDb.delete(conditionalGet); removeFromItemsBeingDeleted(feed); } private void removeFromParentNews(IAttachment attachment) { INews news = attachment.getNews(); if (itemsBeingDeletedContains(news)) return; news.removeAttachment(attachment); fDb.set(news); } private void removeFromParentFolderAndCascade(IFolder folder) { IFolder parentFolder = folder.getParent(); if (parentFolder != null) { parentFolder.removeChild(folder); fDb.set(parentFolder); } for (IFolder child : ReverseIterator.createInstance(folder.getFolders())) { cascadeFolderDeletion(child); } for (IMark mark : ReverseIterator.createInstance(folder.getMarks())) { mark.setProperty(PARENT_DELETED_KEY, true); fDb.delete(mark); } } private void cascadeFolderDeletion(IFolder folder) { for (IFolder child : ReverseIterator.createInstance(folder.getFolders())) { cascadeFolderDeletion(child); } for (IMark mark : ReverseIterator.createInstance(folder.getMarks())) { mark.setProperty(PARENT_DELETED_KEY, true); fDb.delete(mark); } folder.setParent(null); fDb.delete(folder); } private void removeFromParentFolder(IMark mark) { IFolder parentFolder = mark.getParent(); parentFolder.removeChild(mark); if (mark.getProperty(PARENT_DELETED_KEY) == null) fDb.set(parentFolder); else mark.removeProperty(PARENT_DELETED_KEY); } private void removeFromParentFeed(INews news) { FeedLinkReference feedRef = news.getFeedReference(); if (itemsBeingDeletedContains(feedRef)) return; IFeed feed = feedRef.resolve(); /* If the news was still within parent, update parent */ if (feed.removeNews(news)) fDb.ext().set(feed, 2); } private boolean removeFromItemsBeingDeleted(Object entity) { Set<Object> entities = fItemsBeingDeleted.get(); if (entities == null) return false; return entities.remove(entity); } private boolean itemsBeingDeletedContains(Object entity) { Set<Object> entities = fItemsBeingDeleted.get(); if (entities == null) return false; return entities.contains(entity); } private void deleteFeedIfNecessary(IBookMark mark) { Query query = fDb.query(); query.constrain(Feed.class); query.descend("fLinkText").constrain(mark.getFeedLinkReference().getLink().toString()); //$NON-NLS-1$ @SuppressWarnings("unchecked") List<IFeed> feeds = query.execute(); for (IFeed feed : feeds) { FeedLinkReference feedRef = new FeedLinkReference(feed.getLink()); if (DBHelper.countBookMarkReference(fDb, feedRef) == 1) { if (DBHelper.feedHasNewsWithCopies(fDb, feedRef)) { List<INews> newsList = new ArrayList<INews>(feed.getNews()); for (INews news : newsList) { feed.removeNews(news); addItemBeingDeleted(feed); fDb.delete(news); } fDb.ext().set(feed, 2); } else fDb.delete(feed); } } } private void processDeletedEvent(EventArgs args) { IEntity entity = getEntity(args); if (entity == null) return; ModelEvent event = createModelEvent(entity); if (event != null) EventsMap.getInstance().putRemoveEvent(event); } private IEntity getEntity(EventArgs args) { ObjectEventArgs queryArgs = ((ObjectEventArgs) args); Object o = queryArgs.object(); if (o instanceof IEntity) { IEntity entity = (IEntity) o; return entity; } return null; } private ModelEvent createModelEvent(IEntity entity) { ModelEvent modelEvent = null; Map<IEntity, ModelEvent> templatesMap = EventsMap.getInstance().getEventTemplatesMap(); ModelEvent template = templatesMap.get(entity); //TODO In some cases, the template is complete. We can save some object allocation by reusing it. boolean root = isRoot(template); boolean merged = isMerged(template); if (entity instanceof INews) { modelEvent = createNewsEvent((INews) entity, template, root, merged); } else if (entity instanceof IAttachment) { IAttachment attachment = (IAttachment) entity; modelEvent = new AttachmentEvent(attachment, root); } else if (entity instanceof ICategory) { ICategory category = (ICategory) entity; modelEvent = new CategoryEvent(category, root); } else if (entity instanceof IFeed) { IFeed feed = (IFeed) entity; modelEvent = new FeedEvent(feed, root); } else if (entity instanceof IPerson) { IPerson person = (IPerson) entity; modelEvent = new PersonEvent(person, root); } else if (entity instanceof IBookMark) { IBookMark mark = (IBookMark) entity; BookMarkEvent eventTemplate = (BookMarkEvent) template; IFolder oldParent = eventTemplate == null ? null : eventTemplate.getOldParent(); modelEvent = new BookMarkEvent(mark, oldParent, root); } else if (entity instanceof ISearchMark) { ISearchMark mark = (ISearchMark) entity; SearchMarkEvent eventTemplate = (SearchMarkEvent) template; IFolder oldParent = eventTemplate == null ? null : eventTemplate.getOldParent(); modelEvent = new SearchMarkEvent(mark, oldParent, root); } else if (entity instanceof INewsBin) { INewsBin newsBin = (INewsBin) entity; NewsBinEvent eventTemplate = (NewsBinEvent) template; IFolder oldParent = eventTemplate == null ? null : eventTemplate.getOldParent(); modelEvent = new NewsBinEvent(newsBin, oldParent, root); } else if (entity instanceof IFolder) { IFolder folder = (IFolder) entity; FolderEvent eventTemplate = (FolderEvent) template; IFolder oldParent = eventTemplate == null ? null : eventTemplate.getOldParent(); modelEvent = new FolderEvent(folder, oldParent, root); } else if (entity instanceof ILabel) { ILabel label = (ILabel) entity; LabelEvent eventTemplate = (LabelEvent) template; ILabel oldLabel = eventTemplate == null ? null : eventTemplate.getOldLabel(); modelEvent = new LabelEvent(oldLabel, label, root); } else if (entity instanceof ISearchCondition) { ISearchCondition searchCond = (ISearchCondition) entity; modelEvent = new SearchConditionEvent(searchCond, root); } else if (entity instanceof IPreference) { IPreference pref = (IPreference) entity; modelEvent = new PreferenceEvent(pref); } else if (entity instanceof ISearch) { ISearch search = (ISearch) entity; modelEvent = new SearchEvent(search, root); } else if (entity instanceof ISearchFilter) { ISearchFilter filter = (ISearchFilter) entity; modelEvent = new SearchFilterEvent(filter, root); } return modelEvent; } private ModelEvent createNewsEvent(INews news, ModelEvent template, boolean root, boolean merged) { ModelEvent modelEvent; NewsEvent newsTemplate = (NewsEvent) template; INews oldNews = newsTemplate == null ? null : newsTemplate.getOldNews(); modelEvent = new NewsEvent(oldNews, news, root, merged); return modelEvent; } private boolean isRoot(ModelEvent template) { if (template == null) return false; return template.isRoot(); } private boolean isMerged(ModelEvent template) { if (template == null) return false; return template instanceof NewsEvent && ((NewsEvent)template).isMerged(); } private void setId(IEntity entity) { if (entity.getId() == null) { long id; IDGenerator idGenerator = getIDGenerator(); if (idGenerator instanceof DB4OIDGenerator) id = ((DB4OIDGenerator) idGenerator).getNext(false); else id = idGenerator.getNext(); /* * We must release the read lock before we can change the id of the news. * This should be fine because if the News has no id, it means that it's * not known to anyone but the caller and we will acquire the read lock * again before issuing any event. */ if (entity instanceof News) { News n = (News) entity; n.releaseReadLockSpecial(); try { entity.setId(id); } finally { n.acquireReadLockSpecial(); } } else { entity.setId(id); } } } void initEntityStoreListener() { DBManager.getDefault().addEntityStoreListener(this); } /* * @see * org.rssowl.core.internal.persist.service.DatabaseListener#databaseOpened * (org.rssowl.core.internal.persist.service.DatabaseEvent) */ public void databaseOpened(DatabaseEvent event) { fDb = event.getObjectContainer(); initEventRegistry(); } /* * @see * org.rssowl.core.internal.persist.service.DatabaseListener#databaseClosed * (org.rssowl.core.internal.persist.service.DatabaseEvent) */ public void databaseClosed(DatabaseEvent event) { fDb = null; } public final void addItemBeingDeleted(Object entity) { Set<Object> entities = fItemsBeingDeleted.get(); if (entities == null) { entities = new HashSet<Object>(3); fItemsBeingDeleted.set(entities); } entities.add(entity); } /** * Clears any temporary storage used by the EventManager for the thread-bound * transaction. */ public void clear() { fItemsBeingDeleted.set(null); } /** * @return singleton instance */ public final static EventManager getInstance() { return INSTANCE; } }