com.esri.gpt.catalog.lucene.LuceneIndexAdapter.java Source code

Java tutorial

Introduction

Here is the source code for com.esri.gpt.catalog.lucene.LuceneIndexAdapter.java

Source

/* See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * Esri Inc. licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.esri.gpt.catalog.lucene;

import com.esri.gpt.catalog.context.CatalogConfiguration;
import com.esri.gpt.catalog.context.CatalogIndexAdapter;
import com.esri.gpt.catalog.context.CatalogIndexException;
import com.esri.gpt.catalog.discovery.IStoreable;
import com.esri.gpt.catalog.discovery.PropertyMeanings;
import com.esri.gpt.catalog.management.CollectionDao;
import com.esri.gpt.catalog.schema.Meaning;
import com.esri.gpt.catalog.schema.MetadataDocument;
import com.esri.gpt.catalog.schema.Schema;
import com.esri.gpt.catalog.schema.indexable.Indexables;
import com.esri.gpt.framework.collection.StringAttributeMap;
import com.esri.gpt.framework.collection.StringSet;
import com.esri.gpt.framework.context.ApplicationContext;
import com.esri.gpt.framework.context.RequestContext;
import com.esri.gpt.framework.security.metadata.MetadataAcl;
import com.esri.gpt.framework.util.Val;

import java.io.File;
import java.io.IOException;
import java.net.URLEncoder;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.MapFieldSelector;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.Term;
import org.apache.lucene.index.TermDocs;
import org.apache.lucene.index.TermEnum;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.store.AlreadyClosedException;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.apache.lucene.store.Lock;
import org.apache.lucene.store.LockObtainFailedException;
import org.apache.lucene.store.NativeFSLockFactory;
import org.apache.lucene.store.SimpleFSLockFactory;

/**
 * Maintains a Lucene index of approved metadata documents.
 */
public class LuceneIndexAdapter extends CatalogIndexAdapter {

    /** class variables ========================================================= */

    private static final String BACKGROUND_LOCKNAME = "geoportal-background.lock";

    public static final String BATCH_INDEXWRITER_KEY = "LuceneIndexAdapter.BatchIndexWriter";

    /** Logger */
    private static final Logger LOGGER = Logger.getLogger(LuceneIndexAdapter.class.getName());

    /** Hold NativeFSLockFactory objects statically within the JVM */
    private static final Map<String, NativeFSLockFactory> NATIVEFSLOCKFACTORIES = new HashMap<String, NativeFSLockFactory>();

    /** Single referenced searcher. */
    private static ReferencedSearcher REFERENCED_SEARCHER = null;

    /** Hold IndexSearcher objects statically within the JVM */
    private static final Map<String, IndexSearcher> SEARCHERS = new HashMap<String, IndexSearcher>();

    /** Single index writer. */
    private static IndexWriter SINGLE_WRITER = null;
    private static boolean SINGLE_WRITER_WASDESTROYED = false;

    /** instance variables ====================================================== */
    private boolean autoCommitSingleWriter = true;
    private LuceneConfig luceneConfig;
    private String remoteWriterUrl;
    private boolean useLocalWriter = true;
    private boolean useRemoteWriter = false;
    private boolean useSingleSearcher = false;
    private boolean useSingleWriter = false;

    /** constructors ============================================================ */

    /**
     * Constructs with an associated request context.
     * @param requestContext the request context
     */
    public LuceneIndexAdapter(RequestContext requestContext) {
        super(requestContext);
        if (requestContext == null) {
            throw new IllegalArgumentException("The requestContext can't be null.");
        }
        this.luceneConfig = requestContext.getCatalogConfiguration().getLuceneConfig();
        if (this.luceneConfig == null) {
            throw new IllegalArgumentException("The requestContext contains no LuceneConfig.");
        } else if (this.luceneConfig.getIndexLocation().length() == 0) {
            throw new IllegalArgumentException("The LuceneConfig indexLocation is empty.");
        }

        StringAttributeMap params = requestContext.getCatalogConfiguration().getParameters();
        String param = Val.chkStr(params.getValue("lucene.useSingleSearcher"));
        this.useSingleSearcher = param.equalsIgnoreCase("true");
        param = Val.chkStr(params.getValue("lucene.useSingleWriter"));
        this.useSingleWriter = param.equalsIgnoreCase("true");
        param = Val.chkStr(params.getValue("lucene.useLocalWriter"));
        this.useLocalWriter = !param.equalsIgnoreCase("false");
        param = Val.chkStr(params.getValue("lucene.useRemoteWriter"));
        this.useRemoteWriter = param.equalsIgnoreCase("true");
        this.remoteWriterUrl = Val.chkStr(params.getValue("lucene.remoteWriterUrl"));

        // check for a use remote writer override
        Boolean bUseRemoteWriter = (Boolean) requestContext.getObjectMap().get("lucene.useRemoteWriter");
        if (bUseRemoteWriter != null) {
            this.useRemoteWriter = bUseRemoteWriter.booleanValue();
        }

    }

    /** properties ============================================================== */

    /**
     * Indicates if publish or delete operations should auto-commit when a 
     * single IndexWriter approach is being used.
     * @return true if publish or delete operations should auto-commit
     */
    public boolean getAutoCommitSingleWriter() {
        return this.autoCommitSingleWriter;
    }

    /**
     * Indicates if publish or delete operations should auto-commit when a 
     * single IndexWriter approach is being used.
     * @param autoCommit true if publish or delete operations should auto-commit
     */
    public void setAutoCommitSingleWriter(boolean autoCommit) {
        this.autoCommitSingleWriter = autoCommit;
    }

    /**
     * Gets the logger.
     * @return the logger
     */
    @Override
    protected Logger getLogger() {
        return LOGGER;
    }

    /**
     * Gets the Lucene configuration.
     * @return the Lucene configuration
     */
    protected LuceneConfig getLuceneConfig() {
        return this.luceneConfig;
    }

    /**
     * Gets configured observers.
     * @return observers
     */
    protected LuceneIndexObserverArray getObservers() {
        return this.luceneConfig.getObservers();
    }

    /**
     * Indicates whether or not a single IndexWriter approach is being used.
     * @return true if a single IndexWriter approach is being used
     */
    public boolean getUsingSingleWriter() {
        return this.useSingleWriter;
    }

    /** methods ================================================================= */

    /**
     * Closes an index searcher.
     * @param searcher the searcher to close.
     */
    public void closeSearcher(IndexSearcher searcher) {
        try {
            if ((searcher != null) && !this.useSingleSearcher) {
                getLogger().finer("Closing Lucene IndexSearcher...");
                searcher.getIndexReader().close();
                searcher.close();
            } else if ((searcher != null) && this.useSingleSearcher) {
                getLogger().finer("Releasing Lucene IndexSearcher...");
                REFERENCED_SEARCHER.release(searcher);
            }
        } catch (Throwable t) {
            getLogger().log(Level.WARNING, "IndexSearcher failed to close.", t);
        }
    }

    /**
     * Closes an index writer.
     * @param writer the writer to close.
     */
    protected void closeWriter(IndexWriter writer) {

        if (this.useSingleWriter) {
            try {
                if (writer != null) {
                    getLogger().finer("Committing Lucene IndexWriter...");
                    writer.commit();
                }
            } catch (CorruptIndexException e) {
                getLogger().log(Level.SEVERE, "Error on IndexWriter.commit", e);
            } catch (IOException e) {
                getLogger().log(Level.SEVERE, "Error on IndexWriter.commit", e);
            } catch (OutOfMemoryError e) {
                getLogger().log(Level.SEVERE, "Error on IndexWriter.commit", e);
                try {
                    writer.close();
                } catch (CorruptIndexException e1) {
                    getLogger().log(Level.SEVERE, "Error on IndexWriter.commit", e);
                } catch (IOException e1) {
                    getLogger().log(Level.SEVERE, "Error on IndexWriter.commit", e);
                } finally {
                    SINGLE_WRITER = null;
                }
            }
            return;
        }

        try {
            if (writer != null) {
                getLogger().finer("Closing Lucene IndexWriter...");
                writer.close();
            }
        } catch (Throwable t) {
            getLogger().log(Level.WARNING, "IndexWriter failed to close.", t);

            // There are times when closing the IndexWriter fails (typically FileNotFound while flushing).
            // This is a bit of a disaster, if it happens we're unsure if the index is corrupted, it
            // also leaves the write lock unreleased, which basically is terminal when a 
            // NativeFSLockFactory is active (you need to stop/start the web server)
            // This is an attempt to forcibly release the lock.
            if ((writer != null) && this.luceneConfig.getUseNativeFSLockFactory()) {
                java.lang.reflect.Field wlFld = null;
                try {
                    wlFld = writer.getClass().getDeclaredField("writeLock");
                } catch (Throwable t2) {
                }
                if (wlFld != null) {
                    boolean wasReleased = false;
                    try {
                        wlFld.setAccessible(true);
                        Object wlObj = wlFld.get(writer);
                        if ((wlObj != null) && (wlObj instanceof Lock)) {
                            Lock wlLock = (Lock) wlObj;
                            wasReleased = !wlLock.isLocked();
                            if (!wasReleased) {
                                wlLock.release();
                                wasReleased = !wlLock.isLocked();
                            }
                        }
                    } catch (Throwable t2) {
                        getLogger().log(Level.WARNING, "Unable to forcibly release an abandoned write lock.", t2);
                    } finally {
                        String sMsg = "The IndexWriter failed to close, write-lock released: " + wasReleased;
                        getLogger().warning(sMsg);
                    }
                }
            }

        }
    }

    /**
     * Counts the documents within the index.
     * @throws CatalogIndexException if an exception occurs
     */
    @Override
    public int countDocuments() throws CatalogIndexException {
        IndexSearcher searcher = null;
        try {
            searcher = newSearcher();
            return searcher.maxDoc();
        } catch (Exception e) {
            String sMsg = "Error accessing index:\n " + Val.chkStr(e.getMessage());
            throw new CatalogIndexException(sMsg, e);
        } finally {
            closeSearcher(searcher);
        }
    }

    /**
     * Deletes a collection of documents from the index.
     * @param uuids the collection of document UUIDS to delete 
     * @throws CatalogIndexException if an exception occurs  
     */
    @Override
    public void deleteDocuments(String[] uuids) throws CatalogIndexException {

        if (this.useRemoteWriter) {
            RemoteIndexer remoteIndexer = new RemoteIndexer();
            remoteIndexer.send(this.getRequestContext(), "delete", uuids);
        }
        if (!this.useLocalWriter)
            return;

        IndexWriter writer = null;
        try {
            if ((uuids != null) && (uuids.length > 0)) {
                StringBuilder sbMsg = new StringBuilder("Removing UUIDs from the catalog index:");
                ArrayList<Term> alTerms = new ArrayList<Term>();
                for (String uuid : uuids) {
                    String sUuid = Val.chkStr(uuid);
                    if (sUuid.length() > 0) {
                        alTerms.add(new Term(Storeables.FIELD_UUID, sUuid));
                        if (getLogger().isLoggable(Level.FINER)) {
                            sbMsg.append("\n ").append(sUuid);
                        }
                    }
                }
                if (alTerms.size() > 0) {
                    if (getLogger().isLoggable(Level.FINER)) {
                        getLogger().finer(sbMsg.toString());
                    }
                    Term[] aTerms = alTerms.toArray(new Term[0]);
                    writer = newWriter();
                    this.getObservers().onDocumentDelete(uuids);
                    writer.deleteDocuments(aTerms);
                    this.getObservers().onDocumentDeleted(uuids);
                }
            }
        } catch (Exception e) {
            String sMsg = "Error deleting document(s):\n " + Val.chkStr(e.getMessage());
            throw new CatalogIndexException(sMsg, e);
        } finally {
            if (this.useSingleSearcher) {
                if (this.getAutoCommitSingleWriter()) {
                    closeWriter(writer);
                }
            } else {
                closeWriter(writer);
            }
        }
    }

    /**
     * Gets the native lock factory if it has been configured for use.
     * @return the native lock factory (null if not configured for use)
     * @throws IOException if an exception occurs
     */
    private synchronized NativeFSLockFactory getNativeLockFactory() throws IOException {
        NativeFSLockFactory factory = null;
        if (this.luceneConfig.getUseNativeFSLockFactory()) {
            File dir = new File(this.luceneConfig.getIndexLocation());
            String path = dir.getCanonicalPath();
            synchronized (NATIVEFSLOCKFACTORIES) {
                factory = NATIVEFSLOCKFACTORIES.get(path);
                if (factory == null) {
                    factory = new NativeFSLockFactory(dir);
                    NATIVEFSLOCKFACTORIES.put(path, factory);
                }
            }
            //if (NATIVEFSLOCKFACTORY == null) {
            //  File lDir = new File(this.luceneConfig.getIndexLocation());
            //  NATIVEFSLOCKFACTORY = new NativeFSLockFactory(lDir);
            //}
            //return NATIVEFSLOCKFACTORY;
        }
        return factory;
    }

    /**
     * Makes an analyzer for catalog documents.
     * <br/>Currently returns a StandardAnalyzer.
     * @return the analyzer
     */
    public Analyzer newAnalyzer() {
        String className = Val.chkStr(this.luceneConfig.getAnalyzerClassName());
        try {
            if ((className.length() == 0)
                    || className.equals("org.apache.lucene.analysis.standard.StandardAnalyzer")) {
                return new StandardAnalyzer(org.apache.lucene.util.Version.LUCENE_30);
            } else {
                Class<?> cls = Class.forName(className);
                Object obj = cls.newInstance();
                if (obj instanceof Analyzer) {
                    return (Analyzer) obj;
                } else {
                    String sMsg = "The configured Lucene analyzer class name is invalid: " + className;
                    getLogger().severe(sMsg);
                }
            }
        } catch (Throwable t) {
            String sMsg = "Error instantiating Lucene analyzer: " + className;
            getLogger().log(Level.SEVERE, sMsg, t);
        }
        return new StandardAnalyzer(org.apache.lucene.util.Version.LUCENE_30);
    }

    /**
     * Gets the Lucene Directory the has been configured to store the index.
     * @return the directory
     * @throws IOException if an exception occurs
     */
    private Directory newDirectory() throws IOException {
        File fDir = new File(this.luceneConfig.getIndexLocation());
        NativeFSLockFactory nativeLockFactory = this.getNativeLockFactory();
        if (nativeLockFactory != null) {
            return FSDirectory.open(fDir, nativeLockFactory);
        } else {
            return FSDirectory.open(fDir);
        }
    }

    /**
     * Makes a searcher for catalog documents.
     * <p/>The searcher is created from the value returned by 
     * getCatalogIndexPath().
     * @return the writer
     * @throws CorruptIndexException if the index is corrupt 
     * @throws IOException if the directory cannot be read/written to, 
     *         or if it does not exist and create is false or if there is any 
     *         other low-level IO error
     */
    public synchronized IndexSearcher newSearcher() throws CorruptIndexException, IOException {

        IndexSearcher searcher = null;
        if (!this.useSingleSearcher) {
            getLogger().finer("Opening Lucene IndexSearcher...");
            IndexReader reader = IndexReader.open(this.newDirectory(), true);
            searcher = new IndexSearcher(reader);
        } else {

            if (REFERENCED_SEARCHER == null) {
                REFERENCED_SEARCHER = new ReferencedSearcher(this.newDirectory());
                return REFERENCED_SEARCHER.get();
            } else {
                try {
                    REFERENCED_SEARCHER.checkForReopen();
                } catch (InterruptedException e) {
                    throw new IOException("Interrupted while opening single searcher.", e);
                }
                return REFERENCED_SEARCHER.get();
            }

            /*
            File fDir = new File(this.luceneConfig.getIndexLocation());
            String path = fDir.getCanonicalPath();
            synchronized (SEARCHERS) {
              searcher = SEARCHERS.get(path);
              if (searcher != null) {
                try {
                  if (!searcher.getIndexReader().isCurrent()) {
                    SEARCHERS.remove(path);
                    searcher.getIndexReader().close();
                    searcher.close();
                    searcher = null;
                  }
                } catch (AlreadyClosedException e) {
                  SEARCHERS.remove(path);
                  searcher = null;
                }
              }
              if (searcher == null) {
                IndexReader reader = IndexReader.open(this.newDirectory(),true);
                searcher = new IndexSearcher(reader);
                SEARCHERS.put(path,searcher);
              } 
            }
            */

        }
        return searcher;
    }

    /**
     * Makes a writer for catalog documents.
     * <p/>The writer is created from the values returned by 
     * getCatalogIndexPath() and newCatalogAnalyzer(). 
     * <br/>The index will be creating it if it does not already exist.
     * <br/>
     * The writer must be closed after use.
     * @return the writer
     * @throws CorruptIndexException if the index is corrupt 
     * @throws LockObtainFailedException if another writer has this index 
     *         open (write.lock could not be obtained) 
     * @throws IOException if the directory cannot be read/written to, 
     *         or if it does not exist and create is false or if there is any 
     *         other low-level IO error
     */
    protected IndexWriter newWriter() throws CorruptIndexException, LockObtainFailedException, IOException {
        return newWriter(false);
    }

    /**
     * Makes a writer for catalog documents.
     * <p/>The writer is created from the values returned by 
     * getCatalogIndexPath() and newCatalogAnalyzer(). 
     * <br/>The index will be creating it if it does not already exist.
     * <br/>
     * The writer must be closed after use.
     * @param forCompleteRebuild true if index will be completely rebuilt
     * @return the writer
     * @throws CorruptIndexException if the index is corrupt 
     * @throws LockObtainFailedException if another writer has this index 
     *         open (write.lock could not be obtained) 
     * @throws IOException if the directory cannot be read/written to, 
     *         or if it does not exist and create is false or if there is any 
     *         other low-level IO error
     */
    private IndexWriter newWriter(boolean forCompleteRebuild)
            throws CorruptIndexException, LockObtainFailedException, IOException {

        if (!this.useLocalWriter) {
            throw new IOException("This instance is not using a local writer.");
        }
        if (this.useSingleWriter)
            return this.getSingleWriter(forCompleteRebuild);

        File f = new File(this.luceneConfig.getIndexLocation());
        getLogger().log(Level.FINER, "Creating Lucene IndexWriter for: {0}", f.getAbsolutePath());
        IndexWriter.MaxFieldLength mfl = IndexWriter.MaxFieldLength.UNLIMITED;
        if (!forCompleteRebuild) {
            return new IndexWriter(this.newDirectory(), this.newAnalyzer(), mfl);
        } else {
            return new IndexWriter(this.newDirectory(), this.newAnalyzer(), true, mfl);
        }
    }

    /**
     * Gets the single writer instance.
     * @param forCompleteRebuild true if index will be completely rebuilt
     * @return the writer
     * @throws CorruptIndexException if the index is corrupt 
     * @throws LockObtainFailedException if another writer has this index 
     *         open (write.lock could not be obtained) 
     * @throws IOException if the directory cannot be read/written to, 
     *         or if it does not exist and create is false or if there is any 
     *         other low-level IO error
     */
    private synchronized IndexWriter getSingleWriter(boolean forCompleteRebuild)
            throws CorruptIndexException, LockObtainFailedException, IOException {
        if (SINGLE_WRITER_WASDESTROYED) {
            throw new IOException("The single IndexWriter instance was destroyed.");
        }
        if (SINGLE_WRITER != null)
            return SINGLE_WRITER;

        File f = new File(this.luceneConfig.getIndexLocation());
        getLogger().log(Level.FINER, "Creating Lucene IndexWriter for: {0}", f.getAbsolutePath());
        IndexWriter.MaxFieldLength mfl = IndexWriter.MaxFieldLength.UNLIMITED;
        Directory directory = this.newDirectory();

        if (IndexWriter.isLocked(directory)) {
            IndexWriter.unlock(directory);
        }

        if (!forCompleteRebuild) {
            IndexWriter tmp = new IndexWriter(directory, this.newAnalyzer(), mfl);
            SINGLE_WRITER = tmp;
        } else {
            IndexWriter tmp = new IndexWriter(directory, this.newAnalyzer(), true, mfl);
            SINGLE_WRITER = tmp;
        }
        return SINGLE_WRITER;
    }

    /**
     * Obtains a write lock intended for background synchronization and 
     * optimization processes.
     * <br/>There will be no wait time for this type of lock, if not obtained 
     * immediately an exception will be thrown.
     * <br/>It is recommended to use LuceneIndexAdapter.touck() prior to
     * obtaining the background lock to ensure a proper directory structure.
     * <br/>The lock must be closed when the background process is complete.
     * @return the obtained lock
     * @throws LockObtainFailedException if the lock wwas not obtained
     */
    public synchronized Lock obtainBackgroundLock() throws LockObtainFailedException {
        Lock lock = null;
        String pfx = "Unable to obtain background lock.";
        if (this.luceneConfig.getUseNativeFSLockFactory()) {
            try {
                NativeFSLockFactory nativeLockFactory = this.getNativeLockFactory();
                lock = nativeLockFactory.makeLock(BACKGROUND_LOCKNAME);
            } catch (IOException e) {
                String msg = pfx + " " + e.getMessage();
                getLogger().log(Level.WARNING, pfx, e);
                throw new LockObtainFailedException(msg);
            }
        } else {
            try {
                File fDir = new File(this.luceneConfig.getIndexLocation());
                SimpleFSLockFactory factory = new SimpleFSLockFactory(fDir);
                factory.setLockPrefix("lucene-simple");
                lock = factory.makeLock(BACKGROUND_LOCKNAME);
            } catch (IOException e) {
                String msg = pfx + " " + e.getMessage();
                getLogger().log(Level.WARNING, pfx, e);
                throw new LockObtainFailedException(msg);
            }
        }
        try {
            boolean wasObtained = lock.obtain();
            if (!wasObtained) {
                String msg = "Unable to obtain background lock for: " + lock.toString();
                throw new LockObtainFailedException(msg);
            }
            return lock;
        } catch (LockObtainFailedException e) {
            throw e;
        } catch (IOException e) {
            String msg = pfx + " " + e.getMessage();
            getLogger().log(Level.WARNING, pfx, e);
            throw new LockObtainFailedException(msg);
        }
    }

    /**
     * Fired when the servlet context is shutting down.
     * @param context the application context
     */
    public static synchronized void onContextDestroy(ApplicationContext context) {
        SINGLE_WRITER_WASDESTROYED = true;

        if (SINGLE_WRITER != null) {
            try {
                IndexWriter tmp = SINGLE_WRITER;
                SINGLE_WRITER = null;
                if (tmp != null) {
                    tmp.close();
                }
            } catch (Exception e) {
                LOGGER.log(Level.SEVERE, "Error while closing single IndexWriter on destroy.", e);
            }
        }

        if (REFERENCED_SEARCHER != null) {
            try {
                REFERENCED_SEARCHER.close();
            } catch (Exception e) {
                LOGGER.log(Level.SEVERE, "Error while closing single IndexReader on destroy.", e);
            }
        }

    }

    /**
     * Fired when the servlet context is starting up.
     * @param context the application context
     */
    public static synchronized void onContextInit(ApplicationContext context) {
        StringAttributeMap params = context.getConfiguration().getCatalogConfiguration().getParameters();
        String param = Val.chkStr(params.getValue("lucene.useLocalWriter"));
        boolean bUseLocalWriter = !param.equalsIgnoreCase("false");

        if (bUseLocalWriter) {
            RequestContext reqCtx = null;
            try {
                reqCtx = RequestContext.extract(null);
                LuceneIndexAdapter adapter = new LuceneIndexAdapter(reqCtx);
                adapter.touch();
            } catch (Exception e) {
                LOGGER.log(Level.SEVERE, "Error while touching IndexWriter on init.", e);
            } finally {
                if (reqCtx != null)
                    reqCtx.onExecutionPhaseCompleted();
            }
        }
    }

    @Override
    public void publishDocument(String uuid, Timestamp updateDate, Schema schema, String acl)
            throws CatalogIndexException {
        uuid = Val.chkStr(uuid);
        if (uuid.length() == 0) {
            throw new IllegalArgumentException("The supplied document UUID was empty.");
        }

        if (this.useRemoteWriter) {
            RemoteIndexer remoteIndexer = new RemoteIndexer();
            remoteIndexer.send(this.getRequestContext(), "publish", uuid);
        }
        if (!this.useLocalWriter)
            return;

        IndexWriter writer = null;
        PreparedStatement st = null;
        PreparedStatement stCol = null;
        try {

            // determine if the XML should always be stored within the index
            StringAttributeMap params = this.getRequestContext().getCatalogConfiguration().getParameters();
            String s = Val.chkStr(params.getValue("lucene.alwaysStoreXmlInIndex"));
            boolean alwaysStoreXmlInIndex = !s.equalsIgnoreCase("false");

            // determine if collections are being used
            List<String[]> collections = null;
            CollectionDao colDao = new CollectionDao(this.getRequestContext());
            boolean hasCollections = false;
            boolean useCollections = colDao.getUseCollections();
            String sColMemberTable = colDao.getCollectionMemberTableName();
            String sqlCol = "SELECT COLUUID FROM " + sColMemberTable + " WHERE DOCUUID=?";
            if (useCollections) {
                collections = colDao.queryCollections();
                hasCollections = (collections.size() > 0);
            }

            // determine the storeables
            Document document = new Document();
            Storeables storeables = null;
            PropertyMeanings meanings = null;
            Indexables indexables = schema.getIndexables();
            if ((indexables != null) && (indexables.getIndexableContext() != null)) {
                meanings = indexables.getIndexableContext().getPropertyMeanings();
                storeables = (Storeables) indexables.getIndexableContext().getStoreables();
            }
            if (storeables == null) {
                useCollections = false;
                meanings = schema.getMeaning().getPropertyMeanings();
                storeables = (Storeables) schema.getMeaning().getStoreables();
            }

            // resolve the thumbnail URL          
            if (Val.chkStr(schema.getMeaning().getThumbnailUrl()).length() == 0) {
                String thumbBinary = Val.chkStr(schema.getMeaning().getThumbnailBinary());
                if ((thumbBinary != null) && (thumbBinary.length() > 0)) {
                    String thumbUrl = "/thumbnail?uuid=" + URLEncoder.encode(uuid, "UTF-8");
                    //IStoreable storeable = schema.getMeaning().getStoreables().get(Meaning.MEANINGTYPE_THUMBNAIL_URL);
                    IStoreable storeable = storeables.get(Meaning.MEANINGTYPE_THUMBNAIL_URL);
                    if (storeable != null) {
                        storeable.setValue(thumbUrl);
                    } else {
                        storeables.ensure(meanings, Meaning.MEANINGTYPE_THUMBNAIL_URL).setValue(thumbUrl);
                    }
                }
            }

            // build the ACL property for the document
            acl = Val.chkStr(acl);
            MetadataAcl oAcl = new MetadataAcl(this.getRequestContext());
            String[] aclValues = oAcl.makeDocumentAcl(acl);
            AclProperty aclProp = new AclProperty(Storeables.FIELD_ACL);
            aclProp.setValues(aclValues);

            // build the document to store 
            storeables.ensure(meanings, Storeables.FIELD_UUID).setValue(uuid);
            storeables.ensure(meanings, Storeables.FIELD_DATEMODIFIED).setValue(updateDate);
            storeables.add(aclProp);

            String fldName = null;
            Field fld = null;

            // document XML
            String xml = Val.chkStr(schema.getActiveDocumentXml());
            String testBrief = Val.chkStr(schema.getCswBriefXslt());
            if (alwaysStoreXmlInIndex || (testBrief.length() > 0)) {
                fldName = Storeables.FIELD_XML;
                LOGGER.log(Level.FINER, "Appending field: {0}", fldName);
                fld = new Field(fldName, xml, Field.Store.YES, Field.Index.NO, Field.TermVector.NO);
                document.add(fld);
            }

            // add additional indexable fields based upon the SQL database record
            boolean bReadDB = true;
            if (bReadDB) {
                CatalogConfiguration cfg = this.getRequestContext().getCatalogConfiguration();
                this.getRequestContext().getCatalogConfiguration().getResourceTableName();
                String sql = "SELECT SITEUUID, TITLE FROM " + cfg.getResourceTableName() + " WHERE DOCUUID=?";
                Connection con = this.returnConnection().getJdbcConnection();
                this.logExpression(sql);
                st = con.prepareStatement(sql);
                st.setString(1, uuid);
                ResultSet rs = st.executeQuery();
                if (rs.next()) {

                    String dbVal = Val.chkStr(rs.getString("SITEUUID"));
                    if (dbVal.length() > 0) {
                        //storeables.ensure(meanings,Storeables.FIELD_SITEUUID).setValue(dbVal);
                        fldName = Storeables.FIELD_SITEUUID;
                        LOGGER.log(Level.FINER, "Appending field: {0} ={1}", new Object[] { fldName, dbVal });
                        fld = new Field(fldName, dbVal, Field.Store.YES, Field.Index.NOT_ANALYZED,
                                Field.TermVector.NO);
                        document.add(fld);
                    }

                    dbVal = Val.chkStr(rs.getString("TITLE"));
                    if (dbVal.length() > 0) {
                        // if the title is found and is different than that in the database
                        // it means that title from the database is typed by the user. In
                        // that case make 'title.org' element based on the current title.
                        IStoreable iTitle = storeables.get(Meaning.MEANINGTYPE_TITLE);
                        if (iTitle != null) {
                            Object[] values = iTitle.getValues();
                            if (values.length > 0 && values[0] instanceof String) {
                                String val = (String) values[0];
                                if (!val.equals(dbVal)) {
                                    storeables.ensure(meanings, Meaning.MEANINGTYPE_TITLE_ORG).setValue(val);
                                }
                            }
                        }
                        // ensure the title from the database
                        storeables.ensure(meanings, Meaning.MEANINGTYPE_TITLE).setValue(dbVal);
                    }

                }
                st.close();
                st = null;

                // determine collection membership
                if (useCollections && hasCollections) {
                    ArrayList<String> alCol = new ArrayList<String>();
                    stCol = con.prepareStatement(sqlCol);
                    stCol.setString(1, uuid);
                    ResultSet rsCol = stCol.executeQuery();
                    while (rsCol.next()) {
                        String sCUuid = rsCol.getString(1);
                        for (String[] col : collections) {
                            if (sCUuid.equals(col[0])) {
                                alCol.add(col[1]);
                                break;
                            }
                        }
                    }
                    stCol.close();
                    stCol = null;
                    if (alCol.size() > 0) {
                        fldName = "isPartOf";
                        Storeable storeable = (Storeable) storeables.ensure(meanings, fldName);
                        if (storeable == null) {
                            // TODO: add a warning message to the log
                        } else {
                            indexables.getIndexableContext().addStorableValues(meanings.get(fldName),
                                    alCol.toArray(new String[0]));
                        }
                    }
                }
            }

            for (IStoreable ist : storeables.collection()) {
                Storeable storeable = (Storeable) ist;
                storeable.appendForWrite(document);
            }

            // schema key
            String schemaKey = Val.chkStr(schema.getKey());
            if (schemaKey.length() > 0) {
                fldName = Storeables.FIELD_SCHEMA_KEY;
                LOGGER.log(Level.FINER, "Appending field: {0} ={1}", new Object[] { fldName, schemaKey });
                fld = new Field(fldName, schemaKey, Field.Store.YES, Field.Index.NOT_ANALYZED, Field.TermVector.NO);
                document.add(fld);
            }

            // cswOutputSchema, cswBriefXml, cswSummaryXml
            String cswOutputSchema = Val.chkStr(schema.getCswOutputSchema());
            if (cswOutputSchema.length() > 0) {
                fldName = Storeables.FIELD_SCHEMA;
                LOGGER.log(Level.FINER, "Appending field: {0} ={1}", new Object[] { fldName, cswOutputSchema });
                fld = new Field(fldName, cswOutputSchema, Field.Store.YES, Field.Index.NOT_ANALYZED,
                        Field.TermVector.NO);
                document.add(fld);
            }
            String briefXslt = Val.chkStr(schema.getCswBriefXslt());
            if (briefXslt.length() > 0) {
                MetadataDocument mdDoc = new MetadataDocument();
                String briefXml = mdDoc.transform(xml, briefXslt);
                fldName = Storeables.FIELD_XML_BRIEF;
                LOGGER.log(Level.FINER, "Appending field: {0}", fldName);
                fld = new Field(fldName, briefXml, Field.Store.YES, Field.Index.NO, Field.TermVector.NO);
                document.add(fld);
            }
            String summaryXslt = Val.chkStr(schema.getCswSummaryXslt());
            if (summaryXslt.length() > 0) {
                MetadataDocument mdDoc = new MetadataDocument();
                String summaryXml = mdDoc.transform(xml, summaryXslt);
                fldName = Storeables.FIELD_XML_SUMMARY;
                LOGGER.log(Level.FINER, "Appending field: {0}", fldName);
                fld = new Field(fldName, summaryXml, Field.Store.YES, Field.Index.NO, Field.TermVector.NO);
                document.add(fld);
            }

            // check for to see if a batch IndexWriter has been placed within the RequestContext objectMap,
            // this is useful for batch processes where open/close/optimize on the index is costly
            IndexWriter batchWriter = null;
            Object o = this.getRequestContext().getObjectMap().get(LuceneIndexAdapter.BATCH_INDEXWRITER_KEY);
            if (o != null && (o instanceof IndexWriter)) {
                batchWriter = (IndexWriter) o;
            }

            // write the document (use update to replace an existing document),
            Term term = new Term(Storeables.FIELD_UUID, uuid);
            this.getObservers().onDocumentUpdate(document, uuid);
            if (batchWriter != null) {
                batchWriter.updateDocument(term, document);
            } else {
                writer = newWriter();
                writer.updateDocument(term, document);
            }
            this.getObservers().onDocumentUpdated(document, uuid);

        } catch (Exception e) {
            String sMsg = "Error indexing document:\n " + Val.chkStr(e.getMessage());
            throw new CatalogIndexException(sMsg, e);
        } finally {
            try {
                if (st != null)
                    st.close();
            } catch (Exception ef) {
            }
            try {
                if (stCol != null)
                    stCol.close();
            } catch (Exception ef) {
            }
            if (this.useSingleSearcher) {
                if (this.getAutoCommitSingleWriter()) {
                    closeWriter(writer);
                }
            } else {
                closeWriter(writer);
            }
        }
    }

    /**
     * Purges the entire catalog index.
     * @throws CatalogIndexException if an exception occurs
     */
    @Override
    public void purgeIndex() throws CatalogIndexException {
        if (!this.useLocalWriter)
            return;
        IndexWriter writer = null;
        try {
            getLogger().info("Emptying Lucene index...");
            writer = newWriter(true);
        } catch (Exception e) {
            String sMsg = "Error purging index:\n " + Val.chkStr(e.getMessage());
            throw new CatalogIndexException(sMsg, e);
        } finally {
            closeWriter(writer);
        }
    }

    /**
     * Queries the ACL values indexed for a document.
     * @param uuid the document UUID
     * @return the ACL values (can be null)
     * @throws CatalogIndexException if an exception occurs
     */
    @Override
    public String[] queryAcls(String uuid) throws CatalogIndexException {
        ArrayList<String> values = new ArrayList<String>();
        IndexSearcher searcher = null;
        TermDocs termDocs = null;
        try {
            uuid = Val.chkStr(uuid);
            if (uuid.length() > 0) {
                searcher = newSearcher();
                String[] aFields = new String[] { Storeables.FIELD_ACL };
                MapFieldSelector selector = new MapFieldSelector(aFields);
                searcher = newSearcher();
                IndexReader reader = searcher.getIndexReader();
                termDocs = reader.termDocs();
                termDocs.seek(new Term(Storeables.FIELD_UUID, uuid));
                if (termDocs.next()) {
                    Document document = reader.document(termDocs.doc(), selector);
                    Field[] fields = document.getFields(Storeables.FIELD_ACL);
                    if ((fields != null) && (fields.length > 0)) {
                        for (Field field : fields) {
                            values.add(field.stringValue());
                        }
                    }
                }
            }
        } catch (IOException e) {
            String sMsg = "Error accessing index:\n " + Val.chkStr(e.getMessage());
            throw new CatalogIndexException(sMsg, e);
        } finally {
            try {
                if (termDocs != null)
                    termDocs.close();
            } catch (Exception ef) {
            }
            closeSearcher(searcher);
        }
        return values.toArray(new String[0]);
    }

    /**
     * Queries the system modified date associated with an indexed document.
     * @param uuid the document UUID
     * @return the update date (null if none was found)
     * @throws CatalogIndexException if an exception occurs
     */
    @Override
    public Timestamp queryModifiedDate(String uuid) throws CatalogIndexException {
        Timestamp tsUpdate = null;
        IndexSearcher searcher = null;
        TermDocs termDocs = null;
        try {
            uuid = Val.chkStr(uuid);
            if (uuid.length() > 0) {
                String[] aFields = new String[] { Storeables.FIELD_DATEMODIFIED };
                MapFieldSelector selector = new MapFieldSelector(aFields);
                searcher = newSearcher();
                IndexReader reader = searcher.getIndexReader();
                termDocs = reader.termDocs();
                termDocs.seek(new Term(Storeables.FIELD_UUID, uuid));
                if (termDocs.next()) {
                    Document document = reader.document(termDocs.doc(), selector);
                    String sUpdate = document.get(Storeables.FIELD_DATEMODIFIED);
                    tsUpdate = new Timestamp(Long.valueOf(sUpdate));
                }
            }
        } catch (IOException e) {
            String sMsg = "Error accessing index:\n " + Val.chkStr(e.getMessage());
            throw new CatalogIndexException(sMsg, e);
        } finally {
            try {
                if (termDocs != null)
                    termDocs.close();
            } catch (Exception ef) {
            }
            closeSearcher(searcher);
        }
        return tsUpdate;
    }

    /**
     * Reads the document UUIDs within the index.
     * @param maxUuids the maximum number to read
     * @param startIndex the index to begin reading
     * @return the set of UUIDs
     * @throws CatalogIndexException if an exception occurs
     */
    private StringSet readUuids(int startIndex, int maxUuids) throws CatalogIndexException {
        StringSet ssUuids = new StringSet();
        IndexSearcher searcher = null;
        TermEnum terms = null;
        try {
            String sField = Storeables.FIELD_UUID;
            searcher = newSearcher();
            terms = searcher.getIndexReader().terms(new Term(sField, ""));
            int nCount = 0;
            while (sField.equals(terms.term().field())) {
                if (nCount >= startIndex) {
                    ssUuids.add(terms.term().text());
                }
                nCount++;
                if (nCount >= (startIndex + maxUuids))
                    break;
                if (!terms.next())
                    break;
            }

        } catch (Exception e) {
            String sMsg = "Error accessing index:\n " + Val.chkStr(e.getMessage());
            throw new CatalogIndexException(sMsg, e);
        } finally {
            try {
                if (terms != null)
                    terms.close();
            } catch (Exception ef) {
            }
            closeSearcher(searcher);
        }
        return ssUuids;
    }

    /**
     * Opens and closes a writer.
     * <br/>This ensures that a directory folder structure exists.
     * @throws CatalogIndexException if an exception occurs
     */
    public void touch() throws CatalogIndexException {
        if (!this.useLocalWriter)
            return;
        IndexWriter writer = null;
        try {
            writer = newWriter();
        } catch (Exception e) {
            String sMsg = "Error accessing index:\n " + Val.chkStr(e.getMessage());
            throw new CatalogIndexException(sMsg, e);
        } finally {
            closeWriter(writer);
        }
    }

}