com.xpn.xwiki.plugin.lucene.LucenePlugin.java Source code

Java tutorial

Introduction

Here is the source code for com.xpn.xwiki.plugin.lucene.LucenePlugin.java

Source

/*
 * See the NOTICE file distributed with this work for additional
 * information regarding copyright ownership.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package com.xpn.xwiki.plugin.lucene;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.Term;
import org.apache.lucene.queryParser.MultiFieldQueryParser;
import org.apache.lucene.queryParser.ParseException;
import org.apache.lucene.queryParser.QueryParser;
import org.apache.lucene.search.BooleanClause;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.MultiSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.Searcher;
import org.apache.lucene.search.Sort;
import org.apache.lucene.search.SortField;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.search.TopDocsCollector;
import org.apache.lucene.search.TopFieldCollector;
import org.apache.lucene.search.TopScoreDocCollector;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.apache.lucene.util.Version;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xwiki.context.Execution;
import org.xwiki.observation.ObservationManager;

import com.xpn.xwiki.XWikiContext;
import com.xpn.xwiki.api.Api;
import com.xpn.xwiki.doc.XWikiAttachment;
import com.xpn.xwiki.doc.XWikiDocument;
import com.xpn.xwiki.plugin.XWikiDefaultPlugin;
import com.xpn.xwiki.plugin.XWikiPluginInterface;
import com.xpn.xwiki.web.Utils;

/**
 * A plugin offering support for advanced searches using Lucene, a high performance, open source search engine. It uses
 * an {@link IndexUpdater} to monitor and submit wiki pages for indexing to the Lucene engine, and offers simple methods
 * for searching documents, with the possiblity to sort by one or several document fields (besides the default sort by
 * relevance), filter by one or several languages, and search in one, several or all virtual wikis.
 * 
 * @version $Id: 455e3d0619e513b72610b8105385b167b3d7528e $
 * @deprecated the plugin technology is deprecated
 */
@Deprecated
public class LucenePlugin extends XWikiDefaultPlugin {
    private static final Logger LOGGER = LoggerFactory.getLogger(LucenePlugin.class);

    public static final String DOCTYPE_WIKIPAGE = "wikipage";

    public static final String DOCTYPE_ATTACHMENT = "attachment";

    public static final String PROP_INDEX_DIR = "xwiki.plugins.lucene.indexdir";

    public static final String PROP_ANALYZER = "xwiki.plugins.lucene.analyzer";

    public static final String PROP_INDEXING_INTERVAL = "xwiki.plugins.lucene.indexinterval";

    public static final String PROP_MAX_QUEUE_SIZE = "xwiki.plugins.lucene.maxQueueSize";

    private static final String DEFAULT_ANALYZER = "org.apache.lucene.analysis.standard.StandardAnalyzer";

    /**
     * The Lucene text analyzer, can be configured in <tt>xwiki.cfg</tt> using the key {@link #PROP_ANALYZER} (
     * <tt>xwiki.plugins.lucene.analyzer</tt>).
     */
    private Analyzer analyzer;

    /**
     * Lucene index updater. Listens for changes and indexes wiki documents in a separate thread.
     */
    private IndexUpdater indexUpdater;

    /**
     * The thread running the index updater.
     */
    private Thread indexUpdaterThread;

    /**
     * List of Lucene indexes used for searching. By default there is only one such index for all the wiki. One searches
     * is created for each entry in {@link #indexDirs}.
     */
    private Searcher[] searchers;

    /**
     * Comma separated list of directories holding Lucene index data. The first such directory is used by the internal
     * indexer. Can be configured in <tt>xwiki.cfg</tt> using the key {@link #PROP_INDEX_DIR} (
     * <tt>xwiki.plugins.lucene.indexdir</tt>). If no directory is configured, then a subdirectory <tt>lucene</tt> in
     * the application's work directory is used.
     */
    private String indexDirs;

    private IndexRebuilder indexRebuilder;

    public LucenePlugin(String name, String className, XWikiContext context) {
        super(name, className, context);
    }

    @Override
    public String getName() {
        return "lucene";
    }

    @Override
    public Api getPluginApi(XWikiPluginInterface plugin, XWikiContext context) {
        return new LucenePluginApi((LucenePlugin) plugin, context);
    }

    /**
     * {@inheritDoc}
     * 
     * @see java.lang.Object#finalize()
     */
    @Override
    protected void finalize() throws Throwable {
        LOGGER.error("Lucene plugin will exit!");

        if (this.indexUpdater != null) {
            this.indexUpdater.doExit();
        }

        super.finalize();
    }

    public int rebuildIndex(XWikiContext context) {
        return this.indexRebuilder.startRebuildIndex(context);
    }

    public int startIndex(Collection<String> wikis, String hqlFilter, boolean clearIndex, boolean onlyNew,
            XWikiContext context) {
        return this.indexRebuilder.startIndex(wikis, hqlFilter, clearIndex, onlyNew, context);
    }

    /**
     * Allows to search special named lucene indexes without having to configure them in <tt>xwiki.cfg</tt>. Slower than
     * {@link #getSearchResults(String, String, String, String, XWikiContext)} since new index searcher instances are
     * created for every query.
     * 
     * @param query The base query, using the query engine supported by Lucene.
     * @param myIndexDirs Comma separated list of directories containing the lucene indexes to search.
     * @param languages Comma separated list of language codes to search in, may be <tt>null</tt> or empty to search all
     *        languages.
     * @param context The context of the request.
     * @return The list of search results.
     * @throws Exception If the index directories cannot be read, or the query is invalid.
     */
    public SearchResults getSearchResultsFromIndexes(String query, String myIndexDirs, String languages,
            XWikiContext context) throws Exception {
        Searcher[] mySearchers = createSearchers(myIndexDirs, context);
        SearchResults retval = search(query, (String) null, null, languages, mySearchers, context);
        closeSearchers(mySearchers);

        return retval;
    }

    /**
     * Allows to search special named lucene indexes without having to configure them in xwiki.cfg. Slower than
     * {@link #getSearchResults}since new index searcher instances are created for every query.
     * 
     * @param query The base query, using the query engine supported by Lucene.
     * @param sortFields A list of fields to sort results by. For each field, if the name starts with '-', then that
     *        field (excluding the -) is used for reverse sorting. If <tt>null</tt> or empty, sort by hit score.
     * @param myIndexDirs Comma separated list of directories containing the lucene indexes to search.
     * @param languages Comma separated list of language codes to search in, may be <tt>null</tt> or empty to search all
     *        languages.
     * @param context The context of the request.
     * @return The list of search results.
     * @throws Exception If the index directories cannot be read, or the query is invalid.
     */
    public SearchResults getSearchResultsFromIndexes(String query, String[] sortFields, String myIndexDirs,
            String languages, XWikiContext context) throws Exception {
        Searcher[] mySearchers = createSearchers(myIndexDirs, context);
        SearchResults retval = search(query, sortFields, null, languages, mySearchers, context);
        closeSearchers(mySearchers);

        return retval;
    }

    /**
     * Allows to search special named lucene indexes without having to configure them in <tt>xwiki.cfg</tt>. Slower than
     * {@link #getSearchResults(String, String, String, String, XWikiContext)} since new index searcher instances are
     * created for every query.
     * 
     * @param query The base query, using the query engine supported by Lucene.
     * @param sortField The name of a field to sort results by. If the name starts with '-', then the field (excluding
     *        the -) is used for reverse sorting. If <tt>null</tt> or empty, sort by hit score.
     * @param myIndexDirs Comma separated list of directories containing the lucene indexes to search.
     * @param languages Comma separated list of language codes to search in, may be <tt>null</tt> or empty to search all
     *        languages.
     * @param context The context of the request.
     * @return The list of search results.
     * @throws Exception If the index directories cannot be read, or the query is invalid.
     */
    public SearchResults getSearchResultsFromIndexes(String query, String sortField, String myIndexDirs,
            String languages, XWikiContext context) throws Exception {
        Searcher[] mySearchers = createSearchers(myIndexDirs, context);
        SearchResults retval = search(query, sortField, null, languages, mySearchers, context);
        closeSearchers(mySearchers);

        return retval;
    }

    /**
     * Searches all Indexes configured in <tt>xwiki.cfg</tt> (property <code>xwiki.plugins.lucene.indexdir</code>).
     * 
     * @param query The base query, using the query engine supported by Lucene.
     * @param sortField The name of a field to sort results by. If the name starts with '-', then the field (excluding
     *        the -) is used for reverse sorting. If <tt>null</tt> or empty, sort by hit score.
     * @param virtualWikiNames Comma separated list of virtual wiki names to search in, may be <tt>null</tt> to search
     *        all virtual wikis.
     * @param languages Comma separated list of language codes to search in, may be <tt>null</tt> or empty to search all
     *        languages.
     * @return The list of search results.
     * @param context The context of the request.
     * @throws Exception If the index directories cannot be read, or the query is invalid.
     */
    public SearchResults getSearchResults(String query, String sortField, String virtualWikiNames, String languages,
            XWikiContext context) throws Exception {
        return search(query, sortField, virtualWikiNames, languages, this.searchers, context);
    }

    /**
     * Searches all Indexes configured in <tt>xwiki.cfg</tt> (property <code>xwiki.plugins.lucene.indexdir</code>).
     * 
     * @param query The base query, using the query engine supported by Lucene.
     * @param sortField The name of a field to sort results by. If the name starts with '-', then the field (excluding
     *        the -) is used for reverse sorting. If <tt>null</tt> or empty, sort by hit score.
     * @param virtualWikiNames Comma separated list of virtual wiki names to search in, may be <tt>null</tt> to search
     *        all virtual wikis.
     * @param languages Comma separated list of language codes to search in, may be <tt>null</tt> or empty to search all
     *        languages.
     * @return The list of search results.
     * @param context The context of the request.
     * @throws Exception If the index directories cannot be read, or the query is invalid.
     */
    public SearchResults getSearchResults(String query, String[] sortField, String virtualWikiNames,
            String languages, XWikiContext context) throws Exception {
        return search(query, sortField, virtualWikiNames, languages, this.searchers, context);
    }

    /**
     * Creates and submits a query to the Lucene engine.
     * 
     * @param query The base query, using the query engine supported by Lucene.
     * @param sortField The name of a field to sort results by. If the name starts with '-', then the field (excluding
     *        the -) is used for reverse sorting. If <tt>null</tt> or empty, sort by hit score.
     * @param virtualWikiNames Comma separated list of virtual wiki names to search in, may be <tt>null</tt> to search
     *        all virtual wikis.
     * @param languages Comma separated list of language codes to search in, may be <tt>null</tt> or empty to search all
     *        languages.
     * @param indexes List of Lucene indexes (searchers) to search.
     * @param context The context of the request.
     * @return The list of search results.
     * @throws IOException If the Lucene searchers encounter a problem reading the indexes.
     * @throws ParseException If the query is not valid.
     */
    private SearchResults search(String query, String sortField, String virtualWikiNames, String languages,
            Searcher[] indexes, XWikiContext context) throws IOException, ParseException {
        SortField sort = getSortField(sortField);

        // Perform the actual search
        return search(query, (sort != null) ? new Sort(sort) : null, virtualWikiNames, languages, indexes, context);
    }

    /**
     * Creates and submits a query to the Lucene engine.
     * 
     * @param query The base query, using the query engine supported by Lucene.
     * @param sortFields A list of fields to sort results by. For each field, if the name starts with '-', then that
     *        field (excluding the -) is used for reverse sorting. If <tt>null</tt> or empty, sort by hit score.
     * @param virtualWikiNames Comma separated list of virtual wiki names to search in, may be <tt>null</tt> to search
     *        all virtual wikis.
     * @param languages Comma separated list of language codes to search in, may be <tt>null</tt> or empty to search all
     *        languages.
     * @param indexes List of Lucene indexes (searchers) to search.
     * @param context The context of the request.
     * @return The list of search results.
     * @throws IOException If the Lucene searchers encounter a problem reading the indexes.
     * @throws ParseException If the query is not valid.
     */
    private SearchResults search(String query, String[] sortFields, String virtualWikiNames, String languages,
            Searcher[] indexes, XWikiContext context) throws IOException, ParseException {
        // Turn the sorting field names into SortField objects.
        SortField[] sorts = null;
        if (sortFields != null && sortFields.length > 0) {
            sorts = new SortField[sortFields.length];
            for (int i = 0; i < sortFields.length; ++i) {
                sorts[i] = getSortField(sortFields[i]);
            }
            // Remove any null values from the list.
            int prevLength = -1;
            while (prevLength != sorts.length) {
                prevLength = sorts.length;
                sorts = ArrayUtils.removeElement(sorts, null);
            }
        }

        // Perform the actual search
        return search(query, (sorts != null) ? new Sort(sorts) : null, virtualWikiNames, languages, indexes,
                context);
    }

    /**
     * Creates and submits a query to the Lucene engine.
     * 
     * @param query The base query, using the query engine supported by Lucene.
     * @param sort A Lucene sort object, can contain one or more sort criterias. If <tt>null</tt>, sort by hit score.
     * @param virtualWikiNames Comma separated list of virtual wiki names to search in, may be <tt>null</tt> to search
     *        all virtual wikis.
     * @param languages Comma separated list of language codes to search in, may be <tt>null</tt> or empty to search all
     *        languages.
     * @param indexes List of Lucene indexes (searchers) to search.
     * @param context The context of the request.
     * @return The list of search results.
     * @throws IOException If the Lucene searchers encounter a problem reading the indexes.
     * @throws ParseException If the query is not valid.
     */
    private SearchResults search(String query, Sort sort, String virtualWikiNames, String languages,
            Searcher[] indexes, XWikiContext context) throws IOException, ParseException {
        MultiSearcher searcher = new MultiSearcher(indexes);

        // Enhance the base query with wiki names and languages.
        Query q = buildQuery(query, virtualWikiNames, languages);

        // Perform the actual search
        TopDocsCollector<? extends ScoreDoc> results;
        if (sort != null) {
            results = TopFieldCollector.create(sort, 1000, true, true, false, false);
        } else {
            results = TopScoreDocCollector.create(1000, false);
        }
        searcher.search(q, results);
        LOGGER.debug("query [{}] returned {} hits", q, results.getTotalHits());

        // Transform the raw Lucene search results into XWiki-aware results
        return new SearchResults(results, searcher, new com.xpn.xwiki.api.XWiki(context.getWiki(), context),
                context);
    }

    /**
     * Create a {@link SortField} corresponding to the field name. If the field name starts with '-', then the field
     * (excluding the leading -) will be used for reverse sorting.
     * 
     * @param sortField The name of the field to sort by. If <tt>null</tt>, return a <tt>null</tt> SortField. If starts
     *        with '-', then return a SortField that does a reverse sort on the field.
     * @return A SortFiled that sorts on the given field, or <tt>null</tt>.
     */
    private SortField getSortField(String sortField) {
        SortField sort = null;
        if (!StringUtils.isEmpty(sortField)) {
            // For the moment assuming everything is a String is enough, since we don't usually want to sort documents
            // on numerical object properties.
            sort = new SortField(StringUtils.removeStart(sortField, "-"), SortField.STRING,
                    sortField.startsWith("-"));
        }

        return sort;
    }

    /**
     * @param query
     * @param virtualWikiNames comma separated list of virtual wiki names
     * @param languages comma separated list of language codes to search in, may be null to search all languages
     */
    private Query buildQuery(String query, String virtualWikiNames, String languages) throws ParseException {
        // build a query like this: <user query string> AND <wikiNamesQuery> AND
        // <languageQuery>
        BooleanQuery bQuery = new BooleanQuery();
        Query parsedQuery = null;

        // for object search
        if (query.startsWith("PROP ")) {
            String property = query.substring(0, query.indexOf(":"));
            query = query.substring(query.indexOf(":") + 1, query.length());
            QueryParser qp = new QueryParser(Version.LUCENE_34, property, this.analyzer);
            parsedQuery = qp.parse(query);
            bQuery.add(parsedQuery, BooleanClause.Occur.MUST);
        } else if (query.startsWith("MULTI ")) {
            // for fulltext search
            List<String> fieldList = IndexUpdater.fields;
            String[] fields = fieldList.toArray(new String[fieldList.size()]);
            BooleanClause.Occur[] flags = new BooleanClause.Occur[fields.length];
            for (int i = 0; i < flags.length; i++) {
                flags[i] = BooleanClause.Occur.SHOULD;
            }
            parsedQuery = MultiFieldQueryParser.parse(Version.LUCENE_34, query, fields, flags, this.analyzer);
            bQuery.add(parsedQuery, BooleanClause.Occur.MUST);
        } else {
            String[] fields = new String[] { IndexFields.FULLTEXT, IndexFields.DOCUMENT_TITLE,
                    IndexFields.DOCUMENT_NAME, IndexFields.FILENAME };
            BooleanClause.Occur[] flags = new BooleanClause.Occur[fields.length];
            for (int i = 0; i < flags.length; i++) {
                flags[i] = BooleanClause.Occur.SHOULD;
            }
            QueryParser parser = new MultiFieldQueryParser(Version.LUCENE_34, fields, this.analyzer);
            parsedQuery = parser.parse(query);
            // Since the sub-queries are OR-ed, each sub-query score is normally divided by the number of sub-queries,
            // which would cause extra-small scores whenever there's a hit on only one sub-query;
            // compensate this by boosting the whole outer query
            parsedQuery.setBoost(fields.length);
            bQuery.add(parsedQuery, BooleanClause.Occur.MUST);
        }

        if (virtualWikiNames != null && virtualWikiNames.length() > 0) {
            bQuery.add(buildOredTermQuery(virtualWikiNames, IndexFields.DOCUMENT_WIKI), BooleanClause.Occur.MUST);
        }
        if (languages != null && languages.length() > 0) {
            bQuery.add(buildOredTermQuery(languages, IndexFields.DOCUMENT_LANGUAGE), BooleanClause.Occur.MUST);
        }

        return bQuery;
    }

    /**
     * @param values comma separated list of values to look for
     * @return A query returning documents matching one of the given values in the given field
     */
    private Query buildOredTermQuery(final String values, final String fieldname) {
        String[] valueArray = values.split("\\,");
        if (valueArray.length > 1) {
            // build a query like this: <valueArray[0]> OR <valueArray[1]> OR ...
            BooleanQuery orQuery = new BooleanQuery();
            for (int i = 0; i < valueArray.length; i++) {
                orQuery.add(new TermQuery(new Term(fieldname, valueArray[i].trim())), BooleanClause.Occur.SHOULD);
            }

            return orQuery;
        }

        // exactly one value, no OR'ed Terms necessary
        return new TermQuery(new Term(fieldname, valueArray[0]));
    }

    @Override
    public synchronized void init(XWikiContext context) {
        LOGGER.debug("Lucene plugin: in init");

        this.indexDirs = context.getWiki().Param(PROP_INDEX_DIR);
        if (StringUtils.isEmpty(this.indexDirs)) {
            File workDir = context.getWiki().getWorkSubdirectory("lucene", context);
            this.indexDirs = workDir.getAbsolutePath();
        }
        String indexDir = StringUtils.split(this.indexDirs, ",")[0];

        File f = new File(indexDir);
        Directory directory;
        try {
            if (!f.exists()) {
                f.mkdirs();
            }
            directory = FSDirectory.open(f);
        } catch (IOException e) {
            LOGGER.error("Failed to open the index directory: ", e);
            throw new RuntimeException(e);
        }

        init(directory, context);
    }

    public void init(Directory directory, XWikiContext context) {
        int indexingInterval;
        try {
            indexingInterval = 1000 * (int) context.getWiki().ParamAsLong(PROP_INDEXING_INTERVAL, 30);
        } catch (NumberFormatException e) {
            LOGGER.warn("Invalid indexing interval in configuration.");
            indexingInterval = 30000;
        }

        int maxQueueSize;
        try {
            maxQueueSize = (int) context.getWiki().ParamAsLong(LucenePlugin.PROP_MAX_QUEUE_SIZE, 1000);
        } catch (NumberFormatException e) {
            LOGGER.warn("Invalid max queue size in configuration.");
            maxQueueSize = 1000;
        }

        IndexUpdater indexUpdater = new IndexUpdater(directory, indexingInterval, maxQueueSize, this, context);

        init(indexUpdater, context);
    }

    public void init(IndexUpdater indexUpdater, XWikiContext context) {
        Directory directory = indexUpdater.getDirectory();

        boolean needInitialRebuild = true;
        try {
            needInitialRebuild = !IndexReader.indexExists(directory);
        } catch (IOException e) {
            LOGGER.warn("Failed to check if index exists: {}", e.getMessage());
        }

        IndexRebuilder indexRebuilder = new IndexRebuilder(indexUpdater, context);
        if (needInitialRebuild) {
            indexRebuilder.startRebuildIndex(context);
            LOGGER.info("Launched initial lucene indexing");
        }

        init(indexUpdater, indexRebuilder, context);
    }

    public void init(IndexUpdater indexUpdater, IndexRebuilder indexRebuilder, XWikiContext context) {
        super.init(context);

        try {
            @SuppressWarnings("unchecked")
            Class<? extends Analyzer> clazz = (Class<? extends Analyzer>) Class
                    .forName(context.getWiki().Param(PROP_ANALYZER, DEFAULT_ANALYZER));
            this.analyzer = clazz.getConstructor(Version.class).newInstance(Version.LUCENE_34);
        } catch (Exception e) {
            LOGGER.error("Error instantiating analyzer: {}", e.getMessage());
            LOGGER.warn("Using default analyzer class: " + DEFAULT_ANALYZER);
            try {
                @SuppressWarnings("unchecked")
                Class<? extends Analyzer> clazz = (Class<? extends Analyzer>) Class.forName(DEFAULT_ANALYZER);
                this.analyzer = clazz.getConstructor(Version.class).newInstance(Version.LUCENE_34);
            } catch (Exception e1) {
                throw new RuntimeException("Instantiation of default analyzer " + DEFAULT_ANALYZER + " failed", e1);
            }
        }

        LOGGER.debug("Assigning index updater: {}", indexUpdater);

        if (this.indexDirs == null) {
            this.indexDirs = context.getWiki().Param(PROP_INDEX_DIR);
            if (StringUtils.isEmpty(this.indexDirs)) {
                File workDir = context.getWiki().getWorkSubdirectory("lucene", context);
                this.indexDirs = workDir.getAbsolutePath();
            }
        }

        this.indexUpdater = indexUpdater;
        this.indexUpdater.setAnalyzer(this.analyzer);
        this.indexUpdaterThread = new Thread(indexUpdater, "Lucene Index Updater");
        this.indexUpdaterThread.start();
        this.indexRebuilder = indexRebuilder;

        openSearchers(context);

        // Register the Index Updater as an Event Listener so that modified documents/attachments are added to the
        // Lucene indexing queue.
        // If the Index Updater is already registered don't do anything.
        ObservationManager observationManager = Utils.getComponent(ObservationManager.class);
        if (observationManager.getListener(indexUpdater.getName()) == null) {
            observationManager.addListener(indexUpdater);
        }

        LOGGER.debug("Lucene plugin initialized.");
    }

    /**
     * {@inheritDoc}
     * 
     * @see com.xpn.xwiki.plugin.XWikiDefaultPlugin#flushCache(com.xpn.xwiki.XWikiContext)
     */
    @Override
    public void flushCache(XWikiContext context) {
        // take care of crappy code calling #flushCache with no context...
        if (context == null) {
            context = (XWikiContext) Utils.getComponent(Execution.class).getContext()
                    .getProperty(XWikiContext.EXECUTIONCONTEXT_KEY);
        }

        if (this.indexUpdater != null) {
            Utils.getComponent(ObservationManager.class).removeListener(this.indexUpdater.getName());

            // set the thread to exit
            this.indexUpdater.doExit();

            try {
                // wait for the thread to finish
                this.indexUpdaterThread.join();
            } catch (InterruptedException ex) {
                LOGGER.warn("Error while waiting for indexUpdaterThread to die.", ex);
            }

            this.indexUpdater = null;
            this.indexUpdaterThread = null;
        }

        this.indexRebuilder = null;

        try {
            closeSearchers(this.searchers);
        } catch (IOException e) {
            LOGGER.warn("Cannot close searchers: {}", e.getMessage());
        }

        this.analyzer = null;

        init(context);
    }

    /**
     * Creates an array of Searchers for a number of lucene indexes.
     * 
     * @param indexDirs Comma separated list of Lucene index directories to create searchers for.
     * @return Array of searchers
     */
    public Searcher[] createSearchers(String indexDirs, XWikiContext context) throws Exception {
        String[] dirs = StringUtils.split(indexDirs, ",");
        List<IndexSearcher> searchersList = new ArrayList<IndexSearcher>();
        IndexWriterConfig cfg = new IndexWriterConfig(Version.LUCENE_34, this.analyzer);
        for (String dir : dirs) {
            Directory d = FSDirectory.open(new File(dir));
            while (true) {
                try {
                    if (!IndexReader.indexExists(d)) {
                        // If there's no index there, create an empty one; otherwise the reader
                        // constructor will throw an exception and fail to initialize
                        new IndexWriter(d, cfg).close();
                    }

                    searchersList.add(new IndexSearcher(d, true));
                    break;
                } catch (CorruptIndexException e) {
                    handleCorruptIndex(context);
                }
            }
        }

        return searchersList.toArray(new Searcher[searchersList.size()]);
    }

    /**
     * Opens the searchers for the configured index Dirs after closing any already existing ones.
     */
    protected synchronized void openSearchers(XWikiContext context) {
        try {
            closeSearchers(this.searchers);
            this.searchers = createSearchers(this.indexDirs, context);
        } catch (Exception e) {
            LOGGER.error("Error opening searchers for index dirs [{}]", context.getWiki().Param(PROP_INDEX_DIR), e);
            throw new RuntimeException(
                    "Error opening searchers for index dirs " + context.getWiki().Param(PROP_INDEX_DIR), e);
        }
    }

    /**
     * @throws IOException
     */
    protected static void closeSearchers(Searcher[] searchers) throws IOException {
        if (searchers != null) {
            for (int i = 0; i < searchers.length; i++) {
                if (searchers[i] != null) {
                    searchers[i].close();
                }
            }
        }
    }

    public String getIndexDirs() {
        return this.indexDirs;
    }

    public long getQueueSize() {
        return this.indexUpdater.getQueueSize();
    }

    public void queueDocument(XWikiDocument doc, XWikiContext context) {
        this.indexUpdater.queueDocument(doc, context, false);
    }

    public void queueAttachment(XWikiDocument doc, XWikiAttachment attach, XWikiContext context) {
        this.indexUpdater.queueAttachment(attach, context, false);
    }

    public void queueAttachment(XWikiDocument doc, XWikiContext context) {
        this.indexUpdater.queueAttachments(doc, context);
    }

    /**
     * @return the number of documents Lucene index writer.
     */
    public long getLuceneDocCount() {
        return this.indexUpdater.getLuceneDocCount();
    }

    /**
     * Handle a corrupt index by clearing it and rebuilding from scratch.
     */
    void handleCorruptIndex(XWikiContext context) throws IOException {
        rebuildIndex(context);
    }
}