/* This file is part of Cyclos (www.cyclos.org). A project of the Social Trade Organisation (www.socialtrade.org). Cyclos is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. Cyclos 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 General Public License for more details. You should have received a copy of the GNU General Public License along with Cyclos; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package nl.strohalm.cyclos.utils.lucene; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.util.Calendar; import java.util.HashMap; import java.util.List; import java.util.Map; import nl.strohalm.cyclos.dao.IndexOperationDAO; import nl.strohalm.cyclos.entities.IndexOperation; import nl.strohalm.cyclos.entities.IndexOperation.EntityType; import nl.strohalm.cyclos.entities.IndexOperation.OperationType; import nl.strohalm.cyclos.entities.IndexStatus; import nl.strohalm.cyclos.entities.Indexable; import nl.strohalm.cyclos.entities.exceptions.DaoException; import nl.strohalm.cyclos.utils.ClassHelper; import org.apache.commons.io.FileUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.lucene.index.CorruptIndexException; import org.apache.lucene.index.IndexReader; import org.apache.lucene.store.Directory; import org.apache.lucene.store.FSDirectory; import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.InitializingBean; /** * Handles configuration and operation of Lucene indexes * @author luis */ public class IndexHandler implements InitializingBean, DisposableBean { private static final Log LOG = LogFactory.getLog(IndexHandler.class); /** * Returns the root directory where indexes are stored */ public static File resolveIndexRoot() { // Setup the Lucene index directory to WEB-INF/indexes directory final File bin = FileUtils.toFile(IndexHandler.class.getResource("/")); // WEB-INF/classes File root = bin.getParentFile(); // WEB-INF // When running on the standalone server, the bin is root/bin, not root/web/WEB-INF/classes if (!bin.getAbsolutePath().contains("WEB-INF") && new File(root, "web").exists()) { root = new File(root, "web/WEB-INF"); } return new File(root, "indexes"); // WEB-INF/indexes } private File indexRoot; private IndexOperationDAO indexOperationDao; private Map<Class<? extends Indexable>, DocumentMapper> documentMappers; private Map<Class<? extends Indexable>, Directory> directories; @Override public void afterPropertiesSet() throws Exception { indexRoot = resolveIndexRoot(); if (!indexRoot.exists()) { indexRoot.mkdirs(); } if (indexRoot == null) { throw new IllegalStateException("No write access to indexes directory"); } // Initialize the directories directories = new HashMap<Class<? extends Indexable>, Directory>(); for (EntityType entityType : EntityType.values()) { Class<? extends Indexable> entityClass = entityType.getEntityClass(); final File dir = getIndexDir(entityClass); FSDirectory directory = FSDirectory.open(dir); directories.put(entityClass, directory); } } @Override public void destroy() throws Exception { if (directories != null) { for (Map.Entry<Class<? extends Indexable>, Directory> entry : directories.entrySet()) { try { entry.getValue().close(); } catch (Exception e) { LOG.warn("Error closing index directory for " + entry.getKey(), e); } } directories = null; } } /** * Returns the lucene directory for the given entity type */ public Directory getDirectory(final Class<? extends Indexable> entityType) { return directories.get(entityType); } /** * Returns the {@link DocumentMapper} for the given entity type */ public DocumentMapper getDocumentMapper(final Class<? extends Indexable> entityType) { return documentMappers.get(entityType); } /** * Returns the directory where the index is stored */ public File getIndexDir(final Class<? extends Indexable> entityType) { return new File(indexRoot, ClassHelper.getClassName(entityType)); } /** * Returns the root directory for all indexes */ public File getIndexRoot() { return indexRoot; } /** * Returns the index status for the given entity type */ public IndexStatus getIndexStatus(final Class<? extends Indexable> entityType) { IndexReader reader; try { reader = doOpenReader(entityType); } catch (final FileNotFoundException e) { return IndexStatus.MISSING; } catch (final IOException e) { return IndexStatus.CORRUPT; } try { // The isCurrent call will force the check for corrupted indexes reader.isCurrent(); return IndexStatus.ACTIVE; } catch (final CorruptIndexException e) { return IndexStatus.CORRUPT; } catch (final Exception e) { LOG.warn("Error while retrieving the index status for " + entityType, e); throw new DaoException(e); } finally { try { reader.close(); } catch (final IOException e) { // Silently ignore } } } /** * Adds the given entity to index, or updates it if already on index */ public void index(final Class<? extends Indexable> entityType, final Long id) { createOperation(entityType, OperationType.ADD, id); } /** * Returns whether index dir exists */ public boolean indexesExists() { return indexRoot.exists() && indexRoot.list().length > 0; } /** * Opens a new {@link IndexReader} for the given entity type */ public IndexReader openReader(final Class<? extends Indexable> entityType) { try { return doOpenReader(entityType); } catch (final Exception e) { LOG.warn("Error while opening index for read on " + entityType, e); throw new DaoException(e); } } /** * Recreates the index for the given entity type */ public void rebuild(final Class<? extends Indexable> entityType) { createOperation(entityType, OperationType.REBUILD); } /** * Recreates the index for the given entity type if it is corrupt */ public void rebuildIfCorrupt(final Class<? extends Indexable> entityType) { createOperation(entityType, OperationType.REBUILD_IF_CORRUPT); } /** * Removes the given entities from index */ public void remove(final Class<? extends Indexable> entityType, final List<Long> ids) { for (Long id : ids) { remove(entityType, id); } } /** * Removes the given entity from index */ public void remove(final Class<? extends Indexable> entityType, final Long id) { createOperation(entityType, OperationType.REMOVE, id); } public void setDocumentMappers(final Map<Class<? extends Indexable>, DocumentMapper> documentMappers) { this.documentMappers = documentMappers; } public void setIndexOperationDao(final IndexOperationDAO indexOperationDao) { this.indexOperationDao = indexOperationDao; } private IndexOperation createOperation(final Class<? extends Indexable> entityType, final OperationType operationType) { return createOperation(entityType, operationType, null); } private IndexOperation createOperation(final Class<? extends Indexable> entityType, final OperationType operationType, final Long entityId) { IndexOperation operation = new IndexOperation(); operation.setDate(Calendar.getInstance()); operation.setEntityType(EntityType.from(entityType)); operation.setOperationType(operationType); operation.setEntityId(entityId); return indexOperationDao.insert(operation); } @SuppressWarnings("deprecation") private IndexReader doOpenReader(final Class<? extends Indexable> entityType) throws CorruptIndexException, IOException { // TODO if we ever update to Lucene 4 (alpha for now) we shoudln't pass the true parameter, as readers will be always readonly. We won't // do it now because readonly readers perform better on high concurrency return IndexReader.open(getDirectory(entityType), true); } }