org.rssowl.core.internal.persist.search.ModelSearchImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.rssowl.core.internal.persist.search.ModelSearchImpl.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.persist.search;

import org.apache.lucene.document.Document;
import org.apache.lucene.document.NumberTools;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.index.IndexFileNameFilter;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.BooleanClause;
import org.apache.lucene.search.BooleanClause.Occur;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.BooleanQuery.TooManyClauses;
import org.apache.lucene.search.HitCollector;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.MatchAllDocsQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.apache.lucene.store.LockFactory;
import org.apache.lucene.store.LockObtainFailedException;
import org.apache.lucene.store.NativeFSLockFactory;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.IProgressMonitor;
import org.rssowl.core.internal.Activator;
import org.rssowl.core.internal.InternalOwl;
import org.rssowl.core.internal.persist.service.DBManager;
import org.rssowl.core.persist.IGuid;
import org.rssowl.core.persist.INews;
import org.rssowl.core.persist.ISearch;
import org.rssowl.core.persist.ISearchCondition;
import org.rssowl.core.persist.dao.INewsDAO;
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.persist.service.PersistenceException;
import org.rssowl.core.persist.service.ProfileLockedException;
import org.rssowl.core.util.SearchHit;

import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * The central interface for searching types from the persistence layer. The
 * implementation is contributable via extension-point mechanism.
 *
 * @author ijuma
 * @author bpasero
 */
public class ModelSearchImpl implements IModelSearch {

    /* Cached News States */
    private static final INews.State[] NEWS_STATES = INews.State.values();

    /* Number of news to resolve for indexing at once */
    private static final int INDEX_CHUNK_SIZE = 500;

    /* An increased clauses count to set in case of a MaxClouseCountException */
    static final int MAX_CLAUSE_COUNT = 65536;

    private volatile IndexSearcher fSearcher;
    private volatile Indexer fIndexer;
    private volatile Directory fDirectory;
    private final List<IndexListener> fIndexListeners = new CopyOnWriteArrayList<IndexListener>();
    private final Map<IndexSearcher, AtomicInteger> fSearchers = new ConcurrentHashMap<IndexSearcher, AtomicInteger>(
            3, 0.75f, 1);

    /*
     * @see org.rssowl.core.model.search.IModelSearch#startup()
     */
    public void startup() throws PersistenceException {
        startup(false);
    }

    private void startup(boolean clearIndex) throws PersistenceException {
        try {
            if (fDirectory == null) {
                String path = Activator.getDefault().getStateLocation().toOSString();

                /*
                 * Delete Lucene Files if clearIndex == true. While Lucene is actually
                 * capable of recreating the index without deleting files, we have seen
                 * IOExceptions while Lucene was trying to recreate the index. Making sure
                 * the index files are deleted will prevent these situations from occuring.
                 */
                if (clearIndex) {
                    File directory = new File(path);
                    File[] indexFiles = directory.listFiles(new IndexFileNameFilter());
                    try {
                        for (File file : indexFiles) {
                            file.delete();
                        }
                    } catch (Exception e) {
                        Activator.getDefault().logError(e.getMessage(), e);
                    }
                }

                /* Create Directory */
                LockFactory lockFactory = new NativeFSLockFactory(path);
                fDirectory = FSDirectory.getDirectory(path, lockFactory);
            }

            if (fIndexer == null)
                fIndexer = new Indexer(this, fDirectory);

            fIndexer.initIfNecessary(clearIndex);

            synchronized (this) {
                if (fSearcher == null)
                    fSearcher = createIndexSearcher();
            }
        } catch (LockObtainFailedException e) {
            throw new ProfileLockedException(e.getMessage(), e);
        } catch (IOException e) {
            throw new PersistenceException(e.getMessage(), e);
        }
    }

    /*
     * @see org.rssowl.core.model.search.IModelSearch#shutdown()
     */
    public void shutdown(boolean emergency) throws PersistenceException {
        try {

            /*
             * Close fIndexer first because it's more important (reduces the chance of
             * a corrupt index). Can be null if exception thrown during start-up
             */
            if (fIndexer != null)
                fIndexer.shutdown(emergency);

            /*
             * We don't bother to close searchers if it's an emergency. They will be
             * released when the process exits.
             */
            if (emergency)
                return;

            synchronized (this) {

                /* We first close all the searchers whose refCount is 0 */
                for (Map.Entry<IndexSearcher, AtomicInteger> mapEntry : fSearchers.entrySet()) {
                    if (mapEntry.getValue().get() == 0)
                        dispose(mapEntry.getKey());
                }

                while (!fSearchers.isEmpty()) {
                    try {

                        /*
                         * We sleep with a lock held because the Threads that we're waiting
                         * to make progress don't acquire a lock
                         */
                        Thread.sleep(50);
                    }

                    /* If interrupted, we just leave the rest of the searchers open */
                    catch (InterruptedException e) {
                        return;
                    }

                    /* Try again for the ones that are left */
                    for (Map.Entry<IndexSearcher, AtomicInteger> mapEntry : fSearchers.entrySet()) {
                        if (mapEntry.getValue().get() == 0)
                            dispose(mapEntry.getKey());
                    }
                }

                fSearcher = null;
            }
        } catch (IOException e) {
            throw new PersistenceException(e.getMessage(), e);
        }
    }

    private BooleanClause createIsCopyTermQuery(boolean copy) {
        String field = String.valueOf(INews.PARENT_ID);
        TermQuery termQuery = new TermQuery(new Term(field, NumberTools.longToString(0)));
        Occur occur = copy ? Occur.MUST_NOT : Occur.MUST;
        return new BooleanClause(termQuery, occur);
    }

    private static final class SimpleHitCollector extends HitCollector {

        private final IndexSearcher fSearcher;
        private final List<NewsReference> fResultList;
        private final Map<Long, Long> fSearchResultNewsIds = new HashMap<Long, Long>();

        SimpleHitCollector(IndexSearcher searcher, List<NewsReference> resultList) {
            fSearcher = searcher;
            fResultList = resultList;
        }

        @Override
        public void collect(int doc, float score) {
            try {
                Document document = fSearcher.doc(doc);

                /* Receive Stored Fields */
                long newsId = Long.parseLong(document.get(SearchDocument.ENTITY_ID_TEXT));

                /*
                 * Under some circumstances the index might contain the same news twice.
                 * This can happen in situations where RSSOwl is quitting in an emergent
                 * way (e.g. the OS shutting down while RSSOwl is running). To avoid
                 * issues, we filter out duplicate results from the search. See
                 * http://dev.rssowl.org/show_bug.cgi?id=1264
                 */
                if (!fSearchResultNewsIds.containsKey(newsId)) {
                    fResultList.add(new NewsReference(newsId));
                    fSearchResultNewsIds.put(newsId, newsId);
                }
            } catch (IOException e) {
                Activator.safeLogError(e.getMessage(), e);
            }
        }
    }

    /**
     * @param guids the List of {@link IGuid} to search news for.
     * @param copy If <code>true</code>, only consider copied News.
     * @param monitor to react on cancellation
     * @return a List of {@link NewsReference} matching the given search and
     * grouped by {@link IGuid}.
     */
    public Map<IGuid, List<NewsReference>> searchNewsByGuids(List<IGuid> guids, boolean copy,
            IProgressMonitor monitor) {
        Map<IGuid, List<NewsReference>> linkToRefs = new HashMap<IGuid, List<NewsReference>>(guids.size());
        IndexSearcher currentSearcher = getCurrentSearcher();
        try {
            for (IGuid guid : guids) {

                /* Return early on cancellation */
                if (monitor.isCanceled())
                    return linkToRefs;

                BooleanQuery query = createGuidQuery(guid, copy);
                List<NewsReference> newsRefs = simpleSearch(currentSearcher, query);
                if (!newsRefs.isEmpty())
                    linkToRefs.put(guid, newsRefs);
            }
            return linkToRefs;
        } finally {
            disposeIfNecessary(currentSearcher);
        }
    }

    /**
     * @param links The Links to search news for.
     * @param copy If <code>true</code>, only consider copied News.
     * @param monitor to react on cancellation
     * @return a List of {@link NewsReference} matching the given search and
     * grouped by the {@link URI}.
     */
    public Map<URI, List<NewsReference>> searchNewsByLinks(List<URI> links, boolean copy,
            IProgressMonitor monitor) {
        Map<URI, List<NewsReference>> linkToRefs = new HashMap<URI, List<NewsReference>>(links.size());
        IndexSearcher currentSearcher = getCurrentSearcher();
        try {
            for (URI link : links) {

                /* Return early on cancellation */
                if (monitor.isCanceled())
                    return linkToRefs;

                BooleanQuery query = createNewsByLinkBooleanQuery(link, copy);
                List<NewsReference> newsRefs = simpleSearch(currentSearcher, query);
                if (!newsRefs.isEmpty())
                    linkToRefs.put(link, newsRefs);
            }
            return linkToRefs;
        } finally {
            disposeIfNecessary(currentSearcher);
        }
    }

    /**
     * @param link The Link to search news for.
     * @param copy If <code>true</code>, only consider copied News.
     * @return a List of {@link NewsReference} matching the given search.
     */
    public List<NewsReference> searchNewsByLink(URI link, boolean copy) {
        Assert.isNotNull(link, "link"); //$NON-NLS-1$
        BooleanQuery query = createNewsByLinkBooleanQuery(link, copy);
        return simpleSearch(query);
    }

    private BooleanQuery createNewsByLinkBooleanQuery(URI link, boolean copy) {
        BooleanQuery query = new BooleanQuery(true);
        query.add(new TermQuery(new Term(String.valueOf(INews.LINK), link.toString().toLowerCase())), Occur.MUST);
        query.add(createIsCopyTermQuery(copy));
        return query;
    }

    /**
     * @param guid the {@link IGuid} to search news for.
     * @param copy If <code>true</code>, only consider copied News.
     * @return a List of {@link NewsReference} matching the given search.
     */
    public List<NewsReference> searchNewsByGuid(IGuid guid, boolean copy) {
        Assert.isNotNull(guid, "guid"); //$NON-NLS-1$
        BooleanQuery query = createGuidQuery(guid, copy);
        return simpleSearch(query);
    }

    private BooleanQuery createGuidQuery(IGuid guid, boolean copy) {
        BooleanQuery query = new BooleanQuery(true);
        query.add(new TermQuery(new Term(String.valueOf(INews.GUID), guid.getValue().toLowerCase())), Occur.MUST);
        query.add(createIsCopyTermQuery(copy));
        return query;
    }

    private List<NewsReference> simpleSearch(Query query) {
        /* Make sure the searcher is in sync */
        IndexSearcher currentSearcher = getCurrentSearcher();
        try {
            List<NewsReference> newsRefs = simpleSearch(currentSearcher, query);
            return newsRefs;
        } finally {
            disposeIfNecessary(currentSearcher);
        }
    }

    private List<NewsReference> simpleSearch(IndexSearcher currentSearcher, Query query) {
        List<NewsReference> resultList = new ArrayList<NewsReference>(2);

        /* Use custom hit collector for performance reasons */
        try {
            currentSearcher.search(query, new SimpleHitCollector(currentSearcher, resultList));
            return resultList;
        } catch (IOException e) {
            throw new PersistenceException(e);
        }
    }

    private void disposeIfNecessary(IndexSearcher currentSearcher) {
        AtomicInteger referenceCount = fSearchers.get(currentSearcher);
        if (referenceCount.decrementAndGet() == 0 && fSearcher != currentSearcher) {
            try {

                /*
                 * May be called by getCurrentSearcher at the same time, but safe
                 * because dispose is safe to be called many times for the same
                 * searcher.
                 */
                dispose(currentSearcher);
            } catch (IOException e) {
                throw new PersistenceException(e);
            }
        }
    }

    /*
     * @see
     * org.rssowl.core.persist.service.IModelSearch#searchNews(org.rssowl.core
     * .persist.ISearch)
     */
    public List<SearchHit<NewsReference>> searchNews(ISearch search) throws PersistenceException {
        return searchNews(search.getSearchConditions(), search.matchAllConditions());
    }

    /*
     * @see org.rssowl.core.model.search.IModelSearch#searchNews(java.util.List,
     * boolean)
     */
    public List<SearchHit<NewsReference>> searchNews(Collection<ISearchCondition> conditions,
            boolean matchAllConditions) throws PersistenceException {
        return searchNews(conditions, null, matchAllConditions);
    }

    /*
     * @see
     * org.rssowl.core.persist.service.IModelSearch#searchNews(java.util.Collection
     * , org.rssowl.core.persist.ISearchCondition, boolean)
     */
    public List<SearchHit<NewsReference>> searchNews(Collection<ISearchCondition> conditions,
            ISearchCondition scope, boolean matchAllConditions) throws PersistenceException {
        try {
            return doSearchNews(conditions, scope, matchAllConditions);
        }

        /* Too Many Clauses - Increase Clauses Limit */
        catch (TooManyClauses e) {

            /* Disable Clauses Limit */
            if (BooleanQuery.getMaxClauseCount() != ModelSearchImpl.MAX_CLAUSE_COUNT) {
                BooleanQuery.setMaxClauseCount(MAX_CLAUSE_COUNT);
                return doSearchNews(conditions, scope, matchAllConditions);
            }

            /* Maximum reached */
            throw new PersistenceException(Messages.ModelSearchImpl_ERROR_WILDCARDS, e);
        }
    }

    private List<SearchHit<NewsReference>> doSearchNews(Collection<ISearchCondition> conditions,
            ISearchCondition scope, boolean matchAllConditions) throws PersistenceException {

        /* Perform the search */
        try {
            Query bQuery = ModelSearchQueries.createQuery(conditions, scope, matchAllConditions);

            /* Make sure the searcher is in sync */
            final IndexSearcher currentSearcher = getCurrentSearcher();
            final List<SearchHit<NewsReference>> resultList = new ArrayList<SearchHit<NewsReference>>();
            final Map<Long, Long> searchResultNewsIds = new HashMap<Long, Long>();

            /* Use custom hit collector for performance reasons */
            HitCollector collector = new HitCollector() {
                @Override
                public void collect(int doc, float score) {
                    try {
                        Document document = currentSearcher.doc(doc);

                        /* Receive Stored Fields */
                        long newsId = Long.parseLong(document.get(SearchDocument.ENTITY_ID_TEXT));
                        INews.State newsState = NEWS_STATES[Integer
                                .parseInt(document.get(NewsDocument.STATE_ID_TEXT))];

                        Map<Integer, INews.State> data = new HashMap<Integer, INews.State>(1);
                        data.put(INews.STATE, newsState);

                        /*
                         * Under some circumstances the index might contain the same news
                         * twice. This can happen in situations where RSSOwl is quitting in
                         * an emergent way (e.g. the OS shutting down while RSSOwl is
                         * running). To avoid issues, we filter out duplicate results from
                         * the search. See http://dev.rssowl.org/show_bug.cgi?id=1264
                         */
                        if (!searchResultNewsIds.containsKey(newsId)) {
                            resultList.add(new SearchHit<NewsReference>(new NewsReference(newsId), score, data));
                            searchResultNewsIds.put(newsId, newsId);
                        }
                    } catch (IOException e) {
                        Activator.safeLogError(e.getMessage(), e);
                    }
                }
            };

            /* Perform the Search */
            try {
                currentSearcher.search(bQuery, collector);
                return resultList;
            } finally {
                disposeIfNecessary(currentSearcher);
            }
        } catch (IOException e) {
            throw new PersistenceException(Messages.ModelSearchImpl_ERROR_SEARCH, e);
        }
    }

    private IndexSearcher createIndexSearcher() throws CorruptIndexException, IOException {
        IndexSearcher searcher = new IndexSearcher(IndexReader.open(fDirectory));
        fSearchers.put(searcher, new AtomicInteger(0));
        return searcher;
    }

    private IndexSearcher getCurrentSearcher() throws PersistenceException {
        try {
            boolean flushed = fIndexer.flushIfNecessary();

            /* Get the current searcher before acquiring lock in case we block */
            IndexSearcher currentSearcher = fSearcher;

            synchronized (this) {

                /*
                 * If there are changes and currentSearcher == fSearcher, it means we
                 * won the race for the lock, so we reopen the searcher. If flushed is
                 * true, but currentSearcher != fSearcher it means that another thread
                 * has reopened the reader while we were blocked waiting for the lock.
                 */
                if (flushed && currentSearcher == fSearcher) {
                    IndexReader currentReader = fSearcher.getIndexReader();
                    IndexReader newReader = currentReader.reopen();
                    if (newReader != currentReader) {

                        IndexSearcher newSearcher = new IndexSearcher(newReader);
                        fSearchers.put(newSearcher, new AtomicInteger(1));

                        /*
                         * Assign to field before we check the referenceCount to ensure that
                         * disposeIfNecessary will dispose the searcher if it has the last
                         * reference, is yet to check if fSearcher has been changed (if this
                         * was done after referenceCount.get() == 0, we could leak a
                         * searcher).
                         */
                        fSearcher = newSearcher;

                        AtomicInteger referenceCount = fSearchers.get(currentSearcher);
                        if (referenceCount != null && referenceCount.get() == 0) {

                            /*
                             * May be called by disposeIfNecessary at the same time, but safe
                             * because dispose is safe to be called many times for the same
                             * searcher.
                             */
                            dispose(currentSearcher);
                        }

                        return fSearcher;
                    }
                }
                fSearchers.get(fSearcher).incrementAndGet();
                return fSearcher;
            }
        } catch (IOException e) {
            throw new PersistenceException(e.getMessage(), e);
        }
    }

    /**
     * Can be called multiple times safely because: - close is safe to be called
     * many times in IndexReader and IndexSearcher - No IndexSearcher is ever
     * added again into the fSearchers map so calling remove two or more times is
     * harmless.
     */
    private void dispose(IndexSearcher searcher) throws IOException {
        fSearchers.remove(searcher);
        searcher.close();
        searcher.getIndexReader().close();
    }

    /*
     * @see org.rssowl.core.model.search.IModelSearch#clearIndex()
     */
    public void clearIndex() throws PersistenceException {
        try {
            synchronized (this) {
                IndexSearcher currentSearcher = fSearcher;
                fIndexer.clearIndex();
                fSearcher = createIndexSearcher();

                /*
                 * We block until the current reader has been closed or can be closed.
                 * Most times we should be able to succeed without having to sleep.
                 */
                while (true) {
                    AtomicInteger refCount = fSearchers.get(currentSearcher);
                    if (refCount == null)
                        break;
                    else if (refCount.get() == 0) {

                        /*
                         * This may be called at the same time from disposeIfNecessary, but
                         * that's fine.
                         */
                        dispose(currentSearcher);
                        break;
                    } else {
                        try {

                            /*
                             * We sleep with a lock held because the Threads that we're
                             * waiting to make progress don't acquire a lock
                             */
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            throw new PersistenceException("Failed to close IndexSearcher: " + fSearcher); //$NON-NLS-1$
                        }
                    }
                }
            }
        } catch (IOException e) {
            throw new PersistenceException(e.getMessage(), e);
        }
    }

    /*
     * @see
     * org.rssowl.core.persist.service.IModelSearch#addIndexListener(org.rssowl
     * .core.persist.service.IndexListener)
     */
    public void addIndexListener(IndexListener listener) {
        fIndexListeners.add(listener);
    }

    /*
     * @see
     * org.rssowl.core.persist.service.IModelSearch#removeIndexListener(org.rssowl
     * .core.persist.service.IndexListener)
     */
    public void removeIndexListener(IndexListener listener) {
        fIndexListeners.remove(listener);
    }

    /*
     * @see org.rssowl.core.persist.service.IModelSearch#optimize()
     */
    public void optimize() {
        try {
            fIndexer.optimize();
        } catch (IOException e) {
            throw new PersistenceException(e.getMessage(), e);
        }
    }

    void notifyIndexUpdated(int docCount) {
        for (IndexListener listener : fIndexListeners) {
            listener.indexUpdated(docCount);
        }
    }

    /*
     * @see org.rssowl.core.persist.service.IModelSearch#reIndexOnNextStartup()
     */
    public void reIndexOnNextStartup() throws PersistenceException {
        try {
            DBManager.getDefault().getReIndexFile().createNewFile();
        } catch (IOException e) {
            throw new PersistenceException(e);
        }
    }

    /*
     * @see
     * org.rssowl.core.persist.service.IModelSearch#reindexAll(org.eclipse.core
     * .runtime.IProgressMonitor)
     */
    public void reindexAll(IProgressMonitor monitor) throws PersistenceException {

        /* May be used before Owl is completely set-up */
        Collection<INews> newsList = InternalOwl.getDefault().getPersistenceService().getDAOService().getNewsDAO()
                .loadAll();

        /* User might have cancelled the operation */
        if (monitor.isCanceled())
            return;

        /* Begin Task */
        monitor.beginTask(Messages.ModelSearchImpl_PROGRESS_WAIT, newsList.size());
        monitor.subTask(Messages.ModelSearchImpl_REINDEX_SEARCH_INDEX);

        /* Reindex in Chunks to reduce memory consumption */
        reindexInChunks(newsList.iterator(), monitor);

        /* Finished */
        monitor.done();
    }

    private void reindexInChunks(Iterator<INews> iterator, IProgressMonitor monitor) {
        boolean isFirstRun = true;

        /* User might have cancelled the operation */
        if (monitor.isCanceled())
            return;

        /* Startup Modelsearch and clean the index directory */
        startup(true);

        /* Lock the indexer for the duration of the reindexing */
        synchronized (fIndexer) {
            boolean userCanceled = false;

            /* Delete the Index first */
            clearIndex();

            /* Proceed until finished indexing all News Items */
            while (iterator.hasNext()) {

                /* Obtain the next chunk of news from the List */
                List<INews> newsChunkToBeIndexed = new ArrayList<INews>(INDEX_CHUNK_SIZE);
                for (int i = 0; i < INDEX_CHUNK_SIZE && iterator.hasNext(); i++)
                    newsChunkToBeIndexed.add(iterator.next());

                /* Return if nothing to do */
                if (newsChunkToBeIndexed.isEmpty())
                    break;

                /* Flush frequently to optimize memory usage during reindexing */
                if (!isFirstRun)
                    fIndexer.flushIfNecessary();

                /* Index News Items */
                for (INews newsitem : newsChunkToBeIndexed) {

                    /* User might have canceled, so give feedback that work needs to complete */
                    if (!userCanceled && monitor.isCanceled()) {
                        monitor.setTaskName(Messages.ModelSearchImpl_WAIT_TASK_COMPLETION);
                        userCanceled = true;
                    }

                    /* We don't pass the whole list at once to be able to report progress. */
                    List<INews> indexList = new ArrayList<INews>(1);
                    indexList.add(newsitem);
                    fIndexer.index(indexList, false, false); //Disable ACID Support
                    monitor.worked(1);
                }

                isFirstRun = false;
            }
        }

        /* Finally we refresh the searchers (this will trigger flushIfNecessary()) */
        IndexSearcher currentSearcher = null;
        try {
            currentSearcher = getCurrentSearcher();
        } finally {
            if (currentSearcher != null)
                disposeIfNecessary(currentSearcher);
        }
    }

    /*
     * @see
     * org.rssowl.core.persist.service.IModelSearch#cleanUp(org.eclipse.core.runtime
     * .IProgressMonitor)
     */
    public void cleanUp(IProgressMonitor monitor) throws PersistenceException {

        /* Retrieve all News from the Index */
        List<NewsReference> results = simpleSearch(new MatchAllDocsQuery());

        /* User might have cancelled the operation */
        if (monitor.isCanceled())
            return;

        /* Begin Task */
        monitor.beginTask(Messages.ModelSearchImpl_PROGRESS_WAIT, results.size());
        monitor.subTask(Messages.ModelSearchImpl_CLEANUP_SEARCH_INDEX);

        /* Find News to delete */
        Set<NewsReference> newsToDelete = new HashSet<NewsReference>();
        INewsDAO newsDao = InternalOwl.getDefault().getPersistenceService().getDAOService().getNewsDAO();
        for (NewsReference newsRef : results) {

            /* User might have cancelled the operation */
            if (monitor.isCanceled())
                return;

            /* Delete if news no longer exists in DB */
            if (!newsDao.exists(newsRef.getId()))
                newsToDelete.add(newsRef);

            /* Delete if news either NULL or not visible */
            else {
                INews resolvedNews = newsDao.load(newsRef.getId());
                if (resolvedNews == null || !resolvedNews.isVisible())
                    newsToDelete.add(newsRef);
            }

            /* Report Progress */
            monitor.worked(1);
        }

        /* Remove News to Delete from Index */
        synchronized (fIndexer) {
            try {
                fIndexer.removeFromIndex(newsToDelete);
            } catch (IOException e) {
                throw new PersistenceException(e.getMessage(), e);
            }
        }

        /* Finished */
        monitor.done();
    }

    /*
     * @see org.rssowl.core.persist.service.IModelSearch#cleanUpOnNextStartup()
     */
    public void cleanUpOnNextStartup() throws PersistenceException {
        try {
            DBManager.getDefault().getCleanUpIndexFile().createNewFile();
        } catch (IOException e) {
            throw new PersistenceException(e);
        }
    }
}