/* ********************************************************************** ** ** 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; import org.apache.lucene.document.Document; import org.apache.lucene.index.CorruptIndexException; import org.apache.lucene.index.IndexWriter; import org.apache.lucene.search.HitCollector; import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.Query; import org.apache.lucene.store.NoLockFactory; import org.apache.lucene.store.RAMDirectory; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IConfigurationElement; import org.eclipse.core.runtime.IExtensionRegistry; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.InvalidRegistryObjectException; import org.eclipse.core.runtime.Platform; import org.eclipse.core.runtime.SafeRunner; import org.eclipse.osgi.util.NLS; import org.rssowl.core.IApplicationService; import org.rssowl.core.INewsAction; import org.rssowl.core.Owl; import org.rssowl.core.internal.persist.Description; import org.rssowl.core.internal.persist.MergeResult; import org.rssowl.core.internal.persist.News; import org.rssowl.core.internal.persist.SortedLongArrayList; import org.rssowl.core.internal.persist.pref.DefaultPreferences; import org.rssowl.core.internal.persist.search.Indexer; import org.rssowl.core.internal.persist.search.ModelSearchImpl; import org.rssowl.core.internal.persist.search.ModelSearchQueries; import org.rssowl.core.internal.persist.search.NewsDocument; import org.rssowl.core.internal.persist.search.SearchDocument; import org.rssowl.core.internal.persist.service.DB4OIDGenerator; import org.rssowl.core.internal.persist.service.DBHelper; import org.rssowl.core.internal.persist.service.DBManager; import org.rssowl.core.internal.persist.service.DatabaseEvent; import org.rssowl.core.internal.persist.service.DatabaseListener; import org.rssowl.core.internal.persist.service.EventManager; import org.rssowl.core.internal.persist.service.EventsMap; import org.rssowl.core.persist.IAttachment; import org.rssowl.core.persist.IBookMark; import org.rssowl.core.persist.IConditionalGet; import org.rssowl.core.persist.IEntity; import org.rssowl.core.persist.IFeed; import org.rssowl.core.persist.IFilterAction; import org.rssowl.core.persist.IGuid; import org.rssowl.core.persist.ILabel; import org.rssowl.core.persist.INews; import org.rssowl.core.persist.ISearch; import org.rssowl.core.persist.ISearchCondition; import org.rssowl.core.persist.ISearchFilter; import org.rssowl.core.persist.dao.DynamicDAO; import org.rssowl.core.persist.dao.INewsDAO; import org.rssowl.core.persist.dao.ISearchFilterDAO; import org.rssowl.core.persist.event.NewsEvent; import org.rssowl.core.persist.event.runnable.NewsEventRunnable; import org.rssowl.core.persist.reference.NewsReference; import org.rssowl.core.persist.service.IDGenerator; import org.rssowl.core.util.CoreUtils; import org.rssowl.core.util.DateUtils; import org.rssowl.core.util.LoggingSafeRunnable; import org.rssowl.core.util.RetentionStrategy; import org.rssowl.core.util.SyncUtils; import com.db4o.ObjectContainer; import com.db4o.ext.Db4oException; import java.io.IOException; import java.io.Serializable; import java.net.URI; import java.util.ArrayList; 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.List; import java.util.ListIterator; import java.util.Map; import java.util.Set; import java.util.TreeSet; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; /** * db4o and Lucene implementation of IApplicationService. */ public class ApplicationServiceImpl implements IApplicationService { /* ID of the contributed News Actions */ private static final String NEWS_ACTION_EXTENSION_POINT = "org.rssowl.core.NewsAction"; //$NON-NLS-1$ private final Map<String, INewsAction> fNewsActions; private volatile ObjectContainer fDb; private volatile ReadWriteLock fLock; private volatile Lock fWriteLock; /** * Creates an instance of this class. */ public ApplicationServiceImpl() { fNewsActions = new HashMap<String, INewsAction>(); loadNewsActions(); DBManager.getDefault().addEntityStoreListener(new DatabaseListener() { public void databaseOpened(DatabaseEvent event) { fDb = event.getObjectContainer(); fLock = event.getLock(); fWriteLock = fLock.writeLock(); } public void databaseClosed(DatabaseEvent event) { fDb = null; } }); } private void loadNewsActions() { IExtensionRegistry reg = Platform.getExtensionRegistry(); IConfigurationElement elements[] = reg.getConfigurationElementsFor(NEWS_ACTION_EXTENSION_POINT); for (IConfigurationElement element : elements) { try { String id = element.getAttribute("id"); //$NON-NLS-1$ fNewsActions.put(id, (INewsAction) element.createExecutableExtension("class"));//$NON-NLS-1$ } catch (InvalidRegistryObjectException e) { Activator.getDefault().logError(e.getMessage(), e); } catch (CoreException e) { Activator.getDefault().getLog().log(e.getStatus()); } } } /* * @see * org.rssowl.core.IApplicationService#handleFeedReload(org.rssowl.core.persist * .IBookMark, org.rssowl.core.persist.IFeed, * org.rssowl.core.persist.IConditionalGet, boolean, boolean, * org.eclipse.core.runtime.IProgressMonitor) */ public final void handleFeedReload(final IBookMark bookMark, IFeed interpretedFeed, IConditionalGet conditionalGet, boolean deleteConditionalGet, boolean runRetention, final IProgressMonitor monitor) { fWriteLock.lock(); MergeResult mergeResult = null; try { /* Resolve reloaded Feed */ IFeed feed = bookMark.getFeedLinkReference().resolve(); /* Feed could have been deleted meanwhile! */ if (feed == null) return; /* Return early on cancellation */ if (monitor.isCanceled() || Owl.isShuttingDown()) return; /* Copy over Properties to reloaded Feed to keep them */ Map<String, Serializable> feedProperties = feed.getProperties(); if (feedProperties != null) { feedProperties.entrySet(); for (Map.Entry<String, Serializable> entry : feedProperties.entrySet()) interpretedFeed.setProperty(entry.getKey(), entry.getValue()); } /* Return early on cancellation */ if (monitor.isCanceled() || Owl.isShuttingDown()) return; /* Create labels as necessary from Sync and assign to news */ boolean isSynced = SyncUtils.isSynchronized(bookMark); if (isSynced) { /* Determine those Labels the user has explicitly deleted and ignore */ String[] labelsToIgnore = Owl.getPreferenceService().getGlobalScope().getStrings(DefaultPreferences.DELETED_LABELS); List<String> labelsToIgnoreList = (labelsToIgnore != null) ? new ArrayList<String>(labelsToIgnore.length) : Collections.<String> emptyList(); if (labelsToIgnore != null) { for (String label : labelsToIgnore) { labelsToIgnoreList.add(label); } } /* Collect All Incoming Labels */ boolean hasLabels = false; Set<String> incomingLabels = new HashSet<String>(); for (INews item : interpretedFeed.getNews()) { Object labelsObj = item.getProperty(SyncUtils.GOOGLE_LABELS); if (labelsObj != null && labelsObj instanceof String[]) { String[] labels = (String[]) labelsObj; for (String label : labels) { if (!labelsToIgnoreList.contains(label)) incomingLabels.add(label); } hasLabels = true; } } /* Determine the New Labels to Create */ if (!incomingLabels.isEmpty()) { /* Existing Labels */ Collection<ILabel> existingLabels = DynamicDAO.loadAll(ILabel.class); Map<String, ILabel> mapNameToLabel = new HashMap<String, ILabel>(); for (ILabel label : existingLabels) { mapNameToLabel.put(label.getName(), label); } /* New Labels to Create */ Set<ILabel> labelsToCreate = new HashSet<ILabel>(); for (String incomingLabel : incomingLabels) { if (!mapNameToLabel.containsKey(incomingLabel)) { ILabel newLabel = Owl.getModelFactory().createLabel(null, incomingLabel); newLabel.setColor("0,0,0"); //$NON-NLS-1$ newLabel.setOrder(mapNameToLabel.size()); mapNameToLabel.put(incomingLabel, newLabel); labelsToCreate.add(newLabel); } } /* Save new Labels */ if (!labelsToCreate.isEmpty()) DynamicDAO.saveAll(labelsToCreate); /* Assign Labels to News */ for (INews item : interpretedFeed.getNews()) { Object labelsObj = item.getProperty(SyncUtils.GOOGLE_LABELS); if (labelsObj != null && labelsObj instanceof String[]) { String[] labels = (String[]) labelsObj; for (String labelName : labels) { ILabel label = mapNameToLabel.get(labelName); if (label != null) item.addLabel(label); } } item.removeProperty(SyncUtils.GOOGLE_LABELS); } } /* Otherwise make sure to clean up properties for Labels */ else if (hasLabels) { for (INews item : interpretedFeed.getNews()) { item.removeProperty(SyncUtils.GOOGLE_LABELS); } } /* Return early on cancellation */ if (monitor.isCanceled() || Owl.isShuttingDown()) return; } /* Merge with existing */ mergeResult = feed.mergeAndCleanUp(interpretedFeed); final List<INews> newNewsAdded = getNewNewsAdded(feed); /* Now adjust News State based on Sync */ if (isSynced) { for (INews item : newNewsAdded) { /* News Marked Read */ if (item.getProperty(SyncUtils.GOOGLE_MARKED_READ) != null) { item.setState(INews.State.READ); item.removeProperty(SyncUtils.GOOGLE_MARKED_READ); } /* News Marked Unread */ else if (item.getProperty(SyncUtils.GOOGLE_MARKED_UNREAD) != null) { item.setState(INews.State.UNREAD); item.removeProperty(SyncUtils.GOOGLE_MARKED_UNREAD); } } } /* Return early on cancellation */ if (monitor.isCanceled() || Owl.isShuttingDown()) return; /* Update Date of last added news in Bookmark */ if (!newNewsAdded.isEmpty()) { Date mostRecentDate = DateUtils.getRecentDate(newNewsAdded); Date previousMostRecentDate = bookMark.getMostRecentNewsDate(); if (previousMostRecentDate == null || mostRecentDate.after(previousMostRecentDate)) { bookMark.setMostRecentNewsDate(mostRecentDate); fDb.set(bookMark); } } /* Return early on cancellation */ if (monitor.isCanceled() || Owl.isShuttingDown()) return; /* Update state of added news if equivalent news already exists */ SafeRunner.run(new LoggingSafeRunnable() { public void run() throws Exception { //See Bug 1216 (NPE in ModelSearchImpl.getCurrentSearcher) if (Owl.getPreferenceService().getGlobalScope().getBoolean(DefaultPreferences.MARK_READ_DUPLICATES)) updateStateOfUnsavedNewNews(newNewsAdded, monitor); } }); /* Return early on cancellation */ if (monitor.isCanceled() || Owl.isShuttingDown()) return; /* Retention Policy */ final List<INews> deletedNews = runRetention ? RetentionStrategy.process(bookMark, feed) : Collections.<INews>emptyList(); for (INews news : deletedNews) mergeResult.addUpdatedObject(news); /* Return early on cancellation */ if (monitor.isCanceled() || Owl.isShuttingDown()) return; /* Set ID to News and handle Description entity */ IDGenerator generator = Owl.getPersistenceService().getIDGenerator(); for (INews news : newNewsAdded) { /* Return early on cancellation */ if (monitor.isCanceled() || Owl.isShuttingDown()) return; long id; if (generator instanceof DB4OIDGenerator) id = ((DB4OIDGenerator) generator).getNext(false); else id = generator.getNext(); news.setId(id); String description = ((News) news).getTransientDescription(); if (description != null) { mergeResult.addUpdatedObject(new Description(news, description)); } } /* Return early on cancellation */ if (monitor.isCanceled() || Owl.isShuttingDown()) return; /* Run News Filters */ final AtomicBoolean someNewsFiltered = new AtomicBoolean(false); SafeRunner.run(new LoggingSafeRunnable() { public void run() throws Exception { newNewsAdded.removeAll(deletedNews); if (!newNewsAdded.isEmpty()) { boolean result = runNewsFilters(newNewsAdded, bookMark.getFeedLinkReference().getLinkAsText(), monitor); someNewsFiltered.set(result); } } }); /* Return early on cancellation and if no filter was running */ if ((monitor.isCanceled() || Owl.isShuttingDown()) && !someNewsFiltered.get()) return; try { lockNewsObjects(mergeResult); saveFeed(mergeResult); /* Update Conditional GET */ if (conditionalGet != null) { if (deleteConditionalGet) fDb.delete(conditionalGet); else fDb.ext().set(conditionalGet, 1); } DBHelper.preCommit(fDb); fDb.commit(); } finally { unlockNewsObjects(mergeResult); } } catch (Db4oException e) { DBHelper.rollbackAndPE(fDb, e); } finally { fWriteLock.unlock(); } DBHelper.cleanUpAndFireEvents(); } private Set<ISearchFilter> loadEnabledFilters(String feedLink) { /* Load Filters */ Collection<ISearchFilter> filters = DynamicDAO.getDAO(ISearchFilterDAO.class).loadAll(); if (filters.isEmpty()) return Collections.emptySet(); /* Sort filters by ID */ Set<ISearchFilter> enabledFilters = new TreeSet<ISearchFilter>(new Comparator<ISearchFilter>() { public int compare(ISearchFilter f1, ISearchFilter f2) { if (f1.equals(f2)) return 0; return f1.getOrder() < f2.getOrder() ? -1 : 1; } }); /* Only consider enabled filters */ for (ISearchFilter filter : filters) { if (filter.isEnabled()) enabledFilters.add(filter); } /* Return early if only disabled filters found */ if (enabledFilters.isEmpty()) return Collections.emptySet(); /* Return early if the first filter "Matches All" news */ if (!needToIndex(enabledFilters)) return enabledFilters; /* Finally remove those filters that are scoped to different feeds */ CoreUtils.removeFiltersByScope(enabledFilters, feedLink); return enabledFilters; } private boolean needToIndex(Set<ISearchFilter> filters) { ISearchFilter firstFilter = filters.iterator().next(); return firstFilter.getSearch() != null; } private boolean runNewsFilters(final List<INews> news, String feedLink, final IProgressMonitor monitor) throws Exception { /* Load Enabled Filters that are scoped to given Feed */ Set<ISearchFilter> enabledFilters = loadEnabledFilters(feedLink); /* Nothing to do */ if (enabledFilters.isEmpty()) return false; /* Return early on cancellation */ if (monitor.isCanceled() || Owl.isShuttingDown()) return false; /* Need to index News and perform Searches */ RAMDirectory directory = null; final IndexSearcher[] searcher = new IndexSearcher[1]; if (needToIndex(enabledFilters)) { boolean indexDescription = needToIndexDescription(enabledFilters); directory = new RAMDirectory(); directory.setLockFactory(NoLockFactory.getNoLockFactory()); /* Index News */ try { IndexWriter indexWriter = new IndexWriter(directory, Indexer.createAnalyzer()); for (int i = 0; i < news.size(); i++) { /* Return early on cancellation */ if (monitor.isCanceled() || Owl.isShuttingDown()) return false; NewsDocument document = new NewsDocument(news.get(i)); document.addFields(indexDescription); document.getDocument().getField(SearchDocument.ENTITY_ID_TEXT).setValue(String.valueOf(i)); indexWriter.addDocument(document.getDocument()); } indexWriter.close(); searcher[0] = new IndexSearcher(directory); } catch (Exception e) { directory.close(); throw e; } } /* Remember the news already filtered */ List<INews> filteredNews = new ArrayList<INews>(news.size()); boolean filterMatchedAll = false; /* Iterate over Filters */ for (ISearchFilter filter : enabledFilters) { /* No Search Required */ if (filter.getSearch() == null) { filterMatchedAll = true; List<INews> remainingNews = new ArrayList<INews>(news); remainingNews.removeAll(filteredNews); if (!remainingNews.isEmpty()) applyFilter(filter, remainingNews); /* Done - we only support 1 filter per News */ break; } /* Search Required */ else if (directory != null && searcher[0] != null) { /* Return early if cancelled and nothing filtered yet */ if ((monitor.isCanceled() || Owl.isShuttingDown()) && filteredNews.isEmpty()) return false; try { final List<INews> matchingNews = new ArrayList<INews>(); /* Perform Query */ Query query = ModelSearchQueries.createQuery(filter.getSearch()); searcher[0].search(query, new HitCollector() { @Override public void collect(int doc, float score) { try { Document document = searcher[0].doc(doc); int index = Integer.valueOf(document.get(SearchDocument.ENTITY_ID_TEXT)); matchingNews.add(news.get(index)); } catch (CorruptIndexException e) { Activator.getDefault().logError(e.getMessage(), e); } catch (IOException e) { Activator.getDefault().logError(e.getMessage(), e); } } }); /* Apply Filter */ matchingNews.removeAll(filteredNews); if (!matchingNews.isEmpty()) { applyFilter(filter, matchingNews); filteredNews.addAll(matchingNews); } } catch (IOException e) { directory.close(); throw e; } } } /* Free RAMDirectory if it was built */ if (directory != null) directory.close(); return filterMatchedAll || !filteredNews.isEmpty(); } private boolean needToIndexDescription(Set<ISearchFilter> filters) { for (ISearchFilter filter : filters) { ISearch search = filter.getSearch(); if (search != null) { List<ISearchCondition> conditions = search.getSearchConditions(); for (ISearchCondition condition : conditions) { int fieldId = condition.getField().getId(); if (fieldId == IEntity.ALL_FIELDS || fieldId == INews.DESCRIPTION) return true; } } } return false; } private void applyFilter(final ISearchFilter filter, final List<INews> news) { final Map<INews, INews> replacements = new HashMap<INews, INews>(); Collection<IFilterAction> actions = CoreUtils.getActions(filter); //Need to sort structural actions to end for (final IFilterAction action : actions) { final INewsAction newsAction = fNewsActions.get(action.getActionId()); if (newsAction != null) { SafeRunner.run(new LoggingSafeRunnable() { public void run() throws Exception { newsAction.run(news, replacements, action.getData()); } }); } } /* Notify listeners */ SafeRunner.run(new LoggingSafeRunnable() { public void run() throws Exception { DynamicDAO.getDAO(ISearchFilterDAO.class).fireFilterApplied(filter, news); } }); } private void lockNewsObjects(MergeResult mergeResult) { for (Object object : mergeResult.getUpdatedObjects()) { if (object instanceof News) { ((News) object).acquireReadLockSpecial(); } } } private void unlockNewsObjects(MergeResult mergeResult) { if (mergeResult != null) { for (Object object : mergeResult.getUpdatedObjects()) { if (object instanceof News) { News news = (News) object; news.releaseReadLockSpecial(); news.clearTransientDescription(); } } } } private List<INews> getNewNewsAdded(IFeed feed) { List<INews> newsList = feed.getNewsByStates(EnumSet.of(INews.State.NEW)); for (ListIterator<INews> it = newsList.listIterator(newsList.size()); it.hasPrevious();) { INews news = it.previous(); if (news.getId() != null) //News added during merge have no id assigned yet it.remove(); } return newsList; } private void updateStateOfUnsavedNewNews(List<INews> news, IProgressMonitor monitor) { if (news.isEmpty()) return; /* Find Links and GUIDs */ List<URI> links = new ArrayList<URI>(); List<IGuid> guids = new ArrayList<IGuid>(); for (INews item : news) { if (SyncUtils.isSynchronized(item)) continue; //Not offering state sync from duplicates for synced news items if (item.getGuid() != null) guids.add(item.getGuid()); else if (item.getLink() != null) links.add(item.getLink()); } if (links.isEmpty() && guids.isEmpty()) return; /* Search existing News by Links and GUIDs */ ModelSearchImpl modelSearch = (ModelSearchImpl) Owl.getPersistenceService().getModelSearch(); Map<URI, List<NewsReference>> linkToNewsRefs = modelSearch.searchNewsByLinks(links, false, monitor); Map<IGuid, List<NewsReference>> guidToNewsRefs = modelSearch.searchNewsByGuids(guids, false, monitor); for (INews item : news) { /* Return early on cancellation */ if (monitor.isCanceled() || Owl.isShuttingDown()) return; /* Lookup equivalent news via GUID */ List<NewsReference> equivalentNewsRefs = guidToNewsRefs.get(item.getGuid()); if (equivalentNewsRefs != null && !equivalentNewsRefs.isEmpty()) { NewsReference newsRef = equivalentNewsRefs.get(0); INews resolvedNews = newsRef.resolve(); if (resolvedNews != null && resolvedNews.isVisible()) item.setState(resolvedNews.getState()); else { logWarning(NLS.bind(Messages.ApplicationServiceImpl_ERROR_STALE_LUCENE_INDEX, newsRef.getId())); CoreUtils.reportIndexIssue(); } } /* Lookup equivalent news via Link */ else { equivalentNewsRefs = linkToNewsRefs.get(item.getLink()); if (equivalentNewsRefs != null && !equivalentNewsRefs.isEmpty()) { NewsReference newsRef = equivalentNewsRefs.get(0); INews resolvedNews = newsRef.resolve(); if (resolvedNews != null && resolvedNews.isVisible()) item.setState(resolvedNews.getState()); else { logWarning(NLS.bind(Messages.ApplicationServiceImpl_ERROR_STALE_LUCENE_INDEX, newsRef.getId())); CoreUtils.reportIndexIssue(); } } } } } private void logWarning(String message) { Activator activator = Activator.getDefault(); activator.getLog().log(activator.createWarningStatus(message, null)); } private void saveFeed(MergeResult mergeResult) { SortedLongArrayList descriptionUpdatedIds = new SortedLongArrayList(10); /* Removed Objects */ for (Object o : mergeResult.getRemovedObjects()) { if (o instanceof INews) EventManager.getInstance().addItemBeingDeleted(((INews) o).getFeedReference()); else if (o instanceof IAttachment) EventManager.getInstance().addItemBeingDeleted(((IAttachment) o).getNews()); else if (o instanceof Description) descriptionUpdatedIds.add(((Description) o).getNews().getId()); fDb.delete(o); } /* Updated Objects */ List<Object> otherObjects = new ArrayList<Object>(); for (Object o : mergeResult.getUpdatedObjects()) { if (o instanceof INews) DBHelper.saveUpdatedNews(fDb, (INews) o); else { if (o instanceof Description) descriptionUpdatedIds.add(((Description) o).getNews().getId()); otherObjects.add(o); } } for (Object o : otherObjects) { if (o instanceof IFeed) fDb.ext().set(o, 2); else fDb.ext().set(o, 1); } NewsEventRunnable eventRunnables = DBHelper.getNewsEventRunnables(EventsMap.getInstance().getEventRunnables()); if (eventRunnables != null) { for (NewsEvent event : eventRunnables.getAllEvents()) descriptionUpdatedIds.removeByElement(event.getEntity().getId().longValue()); } INewsDAO newsDao = DynamicDAO.getDAO(INewsDAO.class); for (int i = 0, c = descriptionUpdatedIds.size(); i < c; ++i) { long newsId = descriptionUpdatedIds.get(i); INews news = newsDao.load(newsId); INews oldNews = DBHelper.peekPersistedNews(fDb, news); EventsMap.getInstance().putUpdateEvent(new NewsEvent(oldNews, news, false)); } } }