/* ********************************************************************** ** ** 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.services; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.SafeRunner; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.jobs.Job; import org.rssowl.core.Owl; import org.rssowl.core.internal.InternalOwl; import org.rssowl.core.persist.IBookMark; import org.rssowl.core.persist.IFolder; import org.rssowl.core.persist.INews; import org.rssowl.core.persist.INews.State; 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.ISearchMarkDAO; import org.rssowl.core.persist.event.BookMarkAdapter; import org.rssowl.core.persist.event.BookMarkEvent; import org.rssowl.core.persist.event.FolderAdapter; import org.rssowl.core.persist.event.FolderEvent; import org.rssowl.core.persist.event.NewsBinAdapter; import org.rssowl.core.persist.event.NewsBinEvent; import org.rssowl.core.persist.event.SearchMarkEvent; import org.rssowl.core.persist.reference.NewsReference; import org.rssowl.core.persist.service.IModelSearch; import org.rssowl.core.persist.service.IndexListener; import org.rssowl.core.util.LoggingSafeRunnable; import org.rssowl.core.util.Pair; import org.rssowl.core.util.SearchHit; import org.rssowl.ui.internal.Controller; import java.util.ArrayList; import java.util.Collection; import java.util.EnumMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; /** * The <code>SavedSearchService</code> is responsible to listen for updates to * the search-index and updating all <code>ISearchMark</code>s as a result to * that event in order to reflect changing search results in the UI. * * @author bpasero */ public class SavedSearchService { /* Time in millies before updating the saved searches (long) */ private static final int BATCH_INTERVAL_LONG = 1000; /* Time in millies before updating the saved searches (short) */ private static final int BATCH_INTERVAL_SHORT = 100; /* Number of updated documents before using the long batch interval */ private static final int SHORT_THRESHOLD = 1; private final Job fBatchJob; private final AtomicBoolean fBatchInProcess = new AtomicBoolean(false); private final AtomicBoolean fUpdatedOnce = new AtomicBoolean(false); private final AtomicBoolean fForceQuickUpdate = new AtomicBoolean(false); private IndexListener fIndexListener; private BookMarkAdapter fBookmarkListener; private NewsBinAdapter fNewsBinListener; private FolderAdapter fFolderListener; /** Creates and Starts this Service */ public SavedSearchService() { fBatchJob = createBatchJob(); registerListeners(); } private Job createBatchJob() { Job job = new Job("") { //$NON-NLS-1$ @Override protected IStatus run(IProgressMonitor monitor) { fBatchInProcess.set(false); fForceQuickUpdate.set(false); /* Update all saved searches */ SafeRunner.run(new LoggingSafeRunnable() { public void run() throws Exception { if (!Controller.getDefault().isShuttingDown()) updateSavedSearches(true); } }); return Status.OK_STATUS; } }; job.setSystem(true); job.setUser(false); return job; } private void registerListeners() { /* Index Listener */ fIndexListener = new IndexListener() { public void indexUpdated(int entitiesCount) { updateSavedSearchesFromEvent(entitiesCount); } }; Owl.getPersistenceService().getModelSearch().addIndexListener(fIndexListener); /* Bookmark Listener: Update on Reparent */ fBookmarkListener = new BookMarkAdapter() { @Override public void entitiesUpdated(Set<BookMarkEvent> events) { for (BookMarkEvent event : events) { if (event.isRoot()) { IFolder oldParent = event.getOldParent(); IFolder parent = event.getEntity().getParent(); if (oldParent != null && !oldParent.equals(parent)) { updateSavedSearchesFromEvent(1); break; } } } } }; DynamicDAO.addEntityListener(IBookMark.class, fBookmarkListener); /* News Bin Listener: Update on Reparent */ fNewsBinListener = new NewsBinAdapter() { @Override public void entitiesUpdated(Set<NewsBinEvent> events) { for (NewsBinEvent event : events) { if (event.isRoot()) { IFolder oldParent = event.getOldParent(); IFolder parent = event.getEntity().getParent(); if (oldParent != null && !oldParent.equals(parent)) { updateSavedSearchesFromEvent(1); break; } } } } }; DynamicDAO.addEntityListener(INewsBin.class, fNewsBinListener); /* Folder Listener: Update on Reparent */ fFolderListener = new FolderAdapter() { @Override public void entitiesUpdated(Set<FolderEvent> events) { for (FolderEvent event : events) { if (event.isRoot()) { IFolder oldParent = event.getOldParent(); IFolder parent = event.getEntity().getParent(); if (oldParent != null && !oldParent.equals(parent)) { updateSavedSearchesFromEvent(1); break; } } } } }; DynamicDAO.addEntityListener(IFolder.class, fFolderListener); } private void updateSavedSearchesFromEvent(int entitiesCount) { if (!Controller.getDefault().isShuttingDown()) { if (!InternalOwl.TESTING) onIndexUpdated(entitiesCount); else updateSavedSearches(true); } } private void unregisterListeners() { Owl.getPersistenceService().getModelSearch().removeIndexListener(fIndexListener); DynamicDAO.removeEntityListener(IBookMark.class, fBookmarkListener); DynamicDAO.removeEntityListener(INewsBin.class, fNewsBinListener); DynamicDAO.removeEntityListener(IFolder.class, fFolderListener); } private void onIndexUpdated(int entitiesCount) { /* Start a new Batch if one is not in progress */ if (!fBatchInProcess.getAndSet(true)) { fBatchJob.schedule((entitiesCount <= SHORT_THRESHOLD || fForceQuickUpdate.get()) ? BATCH_INTERVAL_SHORT : BATCH_INTERVAL_LONG); return; } } /** * Tells this Service to rapidly update all saved searches when the next * indexing is done. This can be called after an atomic operation (e.g. * Marking some News as read) to force a quick update on all saved searches. */ public void forceQuickUpdate() { fForceQuickUpdate.set(true); } /** * Update the results of all <code>ISearchMark</code>s stored in RSSOwl. * * @param force If set to <code>TRUE</code>, update saved searches even if * done before. */ public void updateSavedSearches(boolean force) { if (!force && fUpdatedOnce.get()) return; Collection<ISearchMark> searchMarks = DynamicDAO.loadAll(ISearchMark.class); updateSavedSearches(searchMarks); } /** * @param searchMarks The Set of <code>ISearchMark</code> to update the * results in. */ public void updateSavedSearches(Collection<ISearchMark> searchMarks) { updateSavedSearches(searchMarks, false); } /** * @param searchMarks The Set of <code>ISearchMark</code> to update the * results in. * @param fromUserEvent Indicates whether to update the saved searches due to * a user initiated event or an automatic one. */ public void updateSavedSearches(Collection<ISearchMark> searchMarks, boolean fromUserEvent) { boolean firstUpdate = !fUpdatedOnce.get(); fUpdatedOnce.set(true); IModelSearch modelSearch = Owl.getPersistenceService().getModelSearch(); Set<SearchMarkEvent> events = new HashSet<SearchMarkEvent>(searchMarks.size()); /* For each Search Mark */ for (ISearchMark searchMark : searchMarks) { /* Return early if shutting down */ if (Controller.getDefault().isShuttingDown()) return; /* Execute the search */ List<SearchHit<NewsReference>> results = modelSearch.searchNews(searchMark.getSearchConditions(), searchMark.matchAllConditions()); /* Fill Result into Map Buckets */ Map<INews.State, List<NewsReference>> resultsMap = new EnumMap<INews.State, List<NewsReference>>(INews.State.class); Set<State> visibleStates = INews.State.getVisible(); for (SearchHit<NewsReference> searchHit : results) { /* Return early if shutting down */ if (Controller.getDefault().isShuttingDown()) return; INews.State state = (State) searchHit.getData(INews.STATE); if (visibleStates.contains(state)) { List<NewsReference> newsRefs = resultsMap.get(state); if (newsRefs == null) { newsRefs = new ArrayList<NewsReference>(results.size() / 3); resultsMap.put(state, newsRefs); } newsRefs.add(searchHit.getResult()); } } /* Set Result */ Pair<Boolean, Boolean> result = searchMark.setNewsRefs(resultsMap); boolean changed = result.getFirst(); boolean newNewsAdded = result.getSecond(); /* Create Event to indicate changed results if any */ if (changed) events.add(new SearchMarkEvent(searchMark, null, true, !firstUpdate && !fromUserEvent && newNewsAdded)); } /* Notify Listeners */ if (!events.isEmpty() && !Controller.getDefault().isShuttingDown()) DynamicDAO.getDAO(ISearchMarkDAO.class).fireNewsChanged(events); } /** Stops this service and unregisters any listeners added. */ public void stopService() { unregisterListeners(); } }