org.rssowl.core.internal.ApplicationServiceImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.rssowl.core.internal.ApplicationServiceImpl.java

Source

/*   **********************************************************************  **
 **   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));
        }
    }
}