From 43dad1aeffca7a9799c939b0c809089cae5a1cb4 Mon Sep 17 00:00:00 2001 From: Andrew Hind Date: Tue, 29 May 2007 10:19:57 +0000 Subject: [PATCH] Asynchronous indexing for AVM git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@5795 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261 --- config/alfresco/public-services-context.xml | 19 + .../org/alfresco/repo/avm/AVMServiceTest.java | 88 ++++- .../alfresco/repo/avm/AVMServiceTestBase.java | 4 + ...hotTriggeredIndexingMethodInterceptor.java | 237 ++++++++++- .../search/impl/lucene/AVMLuceneIndexer.java | 14 +- .../AVMLuceneIndexerAndSearcherFactory.java | 16 +- .../impl/lucene/AVMLuceneIndexerImpl.java | 371 ++++++++++++++++-- .../lucene/AbstractLuceneIndexerImpl.java | 52 ++- 8 files changed, 723 insertions(+), 78 deletions(-) diff --git a/config/alfresco/public-services-context.xml b/config/alfresco/public-services-context.xml index 3409eb45ab..82279a54ae 100644 --- a/config/alfresco/public-services-context.xml +++ b/config/alfresco/public-services-context.xml @@ -892,6 +892,23 @@ true + + SYNCHRONOUS + + + + SYNCHRONOUS:TYPE:STAGING + UNINDEXED:TYPE:STAGING_PREVIEW + UNINDEXED:TYPE:AUTHOR + UNINDEXED:TYPE:AUTHOR_PREVIEW + UNINDEXED:TYPE:WORKFLOW + UNINDEXED:TYPE:WORKFLOW_PREVIEW + UNINDEXED:TYPE:AUTHOR_WORKFLOW + UNINDEXED:TYPE:AUTHOR_WORKFLOW_PREVIEW + ASYNCHRONOUS:NAME:avmAsynchronousTest + SYNCHRONOUS:NAME:.* + + @@ -1310,6 +1327,7 @@ + diff --git a/source/java/org/alfresco/repo/avm/AVMServiceTest.java b/source/java/org/alfresco/repo/avm/AVMServiceTest.java index 08a5e1fb92..738faeebb6 100644 --- a/source/java/org/alfresco/repo/avm/AVMServiceTest.java +++ b/source/java/org/alfresco/repo/avm/AVMServiceTest.java @@ -37,6 +37,10 @@ import java.util.Map; import java.util.Set; import java.util.TreeMap; +import javax.transaction.NotSupportedException; +import javax.transaction.SystemException; +import javax.transaction.UserTransaction; + import org.alfresco.config.JNDIConstants; import org.alfresco.model.ContentModel; import org.alfresco.model.WCMModel; @@ -49,6 +53,9 @@ import org.alfresco.repo.avm.actions.SimpleAVMSubmitAction; import org.alfresco.repo.avm.util.BulkLoader; import org.alfresco.repo.content.MimetypeMap; import org.alfresco.repo.domain.PropertyValue; +import org.alfresco.repo.search.IndexMode; +import org.alfresco.repo.search.Indexer; +import org.alfresco.repo.search.impl.lucene.AVMLuceneIndexer; import org.alfresco.repo.search.impl.lucene.LuceneQueryParser; import org.alfresco.repo.search.impl.lucene.analysis.NumericEncoder; import org.alfresco.repo.transaction.RetryingTransactionHelper; @@ -94,6 +101,86 @@ import org.alfresco.util.Pair; */ public class AVMServiceTest extends AVMServiceTestBase { + /** + * Test async indexing. + * @throws Exception + */ + public void testAsyncIndex() throws Exception + { + // Make sure the slate is clean ... + UserTransaction tx = fTransactionService.getUserTransaction(); + tx.begin(); + if(fService.getStore("avmAsynchronousTest") != null) + { + fService.purgeStore("avmAsynchronousTest"); + } + StoreRef storeRef = AVMNodeConverter.ToStoreRef("avmAsynchronousTest"); + Indexer indexer = fIndexerAndSearcher.getIndexer(storeRef); + if(indexer instanceof AVMLuceneIndexer) + { + AVMLuceneIndexer avmIndexer = (AVMLuceneIndexer)indexer; + avmIndexer.deleteIndex("avmAsynchronousTest", IndexMode.SYNCHRONOUS); + } + tx.commit(); + + // TODO: Suspend and resume indexing in case we are really unlucky and hit an index before we expect it. + + SearchService searchService = fIndexerAndSearcher.getSearcher(storeRef, true); + ResultSet results; + + results = searchService.query(storeRef, "lucene", "PATH:\"//.\""); + assertEquals(0, results.length()); + results.close(); + + fService.createStore("avmAsynchronousTest"); + fService.createSnapshot("avmAsynchronousTest", null, null); + + results = searchService.query(storeRef, "lucene", "PATH:\"//.\""); + assertEquals(1, results.length()); + results.close(); + + fService.createDirectory("avmAsynchronousTest:/", "a"); + fService.createDirectory("avmAsynchronousTest:/a", "b"); + fService.createDirectory("avmAsynchronousTest:/a/b", "c"); + fService.createSnapshot("avmAsynchronousTest", null, null); + + results = searchService.query(storeRef, "lucene", "PATH:\"//.\""); + assertEquals(1, results.length()); + results.close(); + + Thread.sleep(120000); + + results = searchService.query(storeRef, "lucene", "PATH:\"//.\""); + assertEquals(4, results.length()); + results.close(); + + fService.purgeStore("avmAsynchronousTest"); + + results = searchService.query(storeRef, "lucene", "PATH:\"//.\""); + assertEquals(0, results.length()); + results.close(); + + fService.createStore("avmAsynchronousTest"); + fService.createSnapshot("avmAsynchronousTest", null, null); + fService.createDirectory("avmAsynchronousTest:/", "a"); + fService.createDirectory("avmAsynchronousTest:/a", "b"); + fService.createDirectory("avmAsynchronousTest:/a/b", "c"); + fService.createSnapshot("avmAsynchronousTest", null, null); + fService.purgeStore("avmAsynchronousTest"); + fService.createStore("avmAsynchronousTest"); + fService.createSnapshot("avmAsynchronousTest", null, null); + fService.createDirectory("avmAsynchronousTest:/", "a"); + fService.createDirectory("avmAsynchronousTest:/a", "b"); + fService.createDirectory("avmAsynchronousTest:/a/b", "c"); + fService.createSnapshot("avmAsynchronousTest", null, null); + + Thread.sleep(120000); + + results = searchService.query(storeRef, "lucene", "PATH:\"//.\""); + assertEquals(4, results.length()); + results.close(); + } + public void testForceCopyDeleted() { try @@ -125,7 +212,6 @@ public class AVMServiceTest extends AVMServiceTestBase runQueriesAgainstBasicTree("main"); fService.createFile("main:/a", "Xander"); fService.createSnapshot("layer", null, null); - runQueriesAgainstBasicTree("main"); assertEquals(2, fService.lookup(2, "layer:/a").getIndirectionVersion()); assertEquals(fService.lookup(2, "main:/a/Xander").getId(), fService.lookup(2, "layer:/a/Xander").getId()); assertNull(fService.lookup(1, "layer:/a/Xander")); diff --git a/source/java/org/alfresco/repo/avm/AVMServiceTestBase.java b/source/java/org/alfresco/repo/avm/AVMServiceTestBase.java index e49bc2a4eb..84a9e83416 100644 --- a/source/java/org/alfresco/repo/avm/AVMServiceTestBase.java +++ b/source/java/org/alfresco/repo/avm/AVMServiceTestBase.java @@ -45,6 +45,7 @@ import org.alfresco.service.cmr.search.ResultSet; import org.alfresco.service.cmr.search.ResultSetRow; import org.alfresco.service.cmr.search.SearchService; import org.alfresco.service.cmr.security.AuthenticationService; +import org.alfresco.service.transaction.TransactionService; import org.springframework.context.support.FileSystemXmlApplicationContext; import junit.framework.TestCase; @@ -79,6 +80,8 @@ public class AVMServiceTestBase extends TestCase */ private long fStartTime; + protected TransactionService fTransactionService; + protected static IndexerAndSearcher fIndexerAndSearcher; /** @@ -96,6 +99,7 @@ public class AVMServiceTestBase extends TestCase fReaper = (OrphanReaper)fContext.getBean("orphanReaper"); fSyncService = (AVMSyncService)fContext.getBean("AVMSyncService"); fIndexerAndSearcher = (IndexerAndSearcher)fContext.getBean("indexerAndSearcherFactory"); + fTransactionService = (TransactionService)fContext.getBean("transactionComponent"); AuthenticationService authService = (AuthenticationService)fContext.getBean("AuthenticationService"); authService.authenticate("admin", "admin".toCharArray()); CreateStoreTxnListener cstl = (CreateStoreTxnListener)fContext.getBean("createStoreTxnListener"); diff --git a/source/java/org/alfresco/repo/search/AVMSnapShotTriggeredIndexingMethodInterceptor.java b/source/java/org/alfresco/repo/search/AVMSnapShotTriggeredIndexingMethodInterceptor.java index f31da87319..2d7638ede8 100644 --- a/source/java/org/alfresco/repo/search/AVMSnapShotTriggeredIndexingMethodInterceptor.java +++ b/source/java/org/alfresco/repo/search/AVMSnapShotTriggeredIndexingMethodInterceptor.java @@ -24,46 +24,109 @@ */ package org.alfresco.repo.search; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +import org.alfresco.error.AlfrescoRuntimeException; import org.alfresco.repo.avm.AVMNodeConverter; +import org.alfresco.repo.domain.PropertyValue; import org.alfresco.repo.search.impl.lucene.AVMLuceneIndexer; import org.alfresco.service.cmr.avm.AVMService; +import org.alfresco.service.cmr.avm.AVMStoreDescriptor; import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.namespace.QName; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; /** * Method interceptor for atomic indexing of AVM entries * + * The proeprties can defined how stores are indexed based on type (as set by Alfresco the Web site management UI) + * or based on the name of the store. + * + * Creates and deletes are indexed synchronously. + * + * Updates may be asynchronous, synchronous or ignored by the index. + * * @author andyh */ public class AVMSnapShotTriggeredIndexingMethodInterceptor implements MethodInterceptor { + // Copy of store properties used to tag avm stores (a store propertry) + + public final static QName PROP_SANDBOX_STAGING_MAIN = QName.createQName(null, ".sandbox.staging.main"); + + public final static QName PROP_SANDBOX_STAGING_PREVIEW = QName.createQName(null, ".sandbox.staging.preview"); + + public final static QName PROP_SANDBOX_AUTHOR_MAIN = QName.createQName(null, ".sandbox.author.main"); + + public final static QName PROP_SANDBOX_AUTHOR_PREVIEW = QName.createQName(null, ".sandbox.author.preview"); + + public final static QName PROP_SANDBOX_WORKFLOW_MAIN = QName.createQName(null, ".sandbox.workflow.main"); + + public final static QName PROP_SANDBOX_WORKFLOW_PREVIEW = QName.createQName(null, ".sandbox.workflow.preview"); + + public final static QName PROP_SANDBOX_AUTHOR_WORKFLOW_MAIN = QName.createQName(null, + ".sandbox.author.workflow.main"); + + public final static QName PROP_SANDBOX_AUTHOR_WORKFLOW_PREVIEW = QName.createQName(null, + ".sandbox.author.workflow.preview"); + private AVMService avmService; private IndexerAndSearcher indexerAndSearcher; private boolean enableIndexing = true; + private IndexMode defaultMode = IndexMode.ASYNCHRONOUS; + + private Map modeCache = new HashMap(); + + private List indexingDefinitions = new ArrayList(); + public Object invoke(MethodInvocation mi) throws Throwable { if (enableIndexing) { if (mi.getMethod().getName().equals("createSnapshot")) { - String store = (String) mi.getArguments()[0]; - int before = avmService.getLatestSnapshotID(store); - Object returnValue = mi.proceed(); - int after = avmService.getLatestSnapshotID(store); - StoreRef storeRef = AVMNodeConverter.ToStoreRef(store); - Indexer indexer = indexerAndSearcher.getIndexer(storeRef); - if (indexer instanceof AVMLuceneIndexer) + // May cause any number of other stores to do snap shot under the covers via layering or do nothing + // So we have to watch what actually changes + + List stores = avmService.getStores(); + Map initialStates = new HashMap(stores.size(), 1.0f); + for (AVMStoreDescriptor desc : stores) { - AVMLuceneIndexer avmIndexer = (AVMLuceneIndexer) indexer; - avmIndexer.index(store, before, after); + String store = desc.getName(); + Integer state = Integer.valueOf(avmService.getLatestSnapshotID(store)); + initialStates.put(store, state); + } + + Object returnValue = mi.proceed(); + + // Index any stores that have moved on + for (AVMStoreDescriptor desc : stores) + { + String store = desc.getName(); + int after = avmService.getLatestSnapshotID(store); + int before = initialStates.get(store).intValue(); + if (after > before) + { + + StoreRef storeRef = AVMNodeConverter.ToStoreRef(store); + Indexer indexer = indexerAndSearcher.getIndexer(storeRef); + if (indexer instanceof AVMLuceneIndexer) + { + AVMLuceneIndexer avmIndexer = (AVMLuceneIndexer) indexer; + avmIndexer.index(store, before, after, getIndexMode(store)); + } + } } return returnValue; } - // TODO: Purge store else if (mi.getMethod().getName().equals("purgeStore")) { String store = (String) mi.getArguments()[0]; @@ -73,7 +136,7 @@ public class AVMSnapShotTriggeredIndexingMethodInterceptor implements MethodInte if (indexer instanceof AVMLuceneIndexer) { AVMLuceneIndexer avmIndexer = (AVMLuceneIndexer) indexer; - avmIndexer.deleteIndex(store); + avmIndexer.deleteIndex(store, IndexMode.SYNCHRONOUS); } return returnValue; } @@ -86,7 +149,7 @@ public class AVMSnapShotTriggeredIndexingMethodInterceptor implements MethodInte if (indexer instanceof AVMLuceneIndexer) { AVMLuceneIndexer avmIndexer = (AVMLuceneIndexer) indexer; - avmIndexer.createIndex(store); + avmIndexer.createIndex(store, IndexMode.SYNCHRONOUS); } return returnValue; } @@ -104,15 +167,15 @@ public class AVMSnapShotTriggeredIndexingMethodInterceptor implements MethodInte if (indexer instanceof AVMLuceneIndexer) { AVMLuceneIndexer avmIndexer = (AVMLuceneIndexer) indexer; - avmIndexer.deleteIndex(from); + avmIndexer.deleteIndex(from, IndexMode.SYNCHRONOUS); } indexer = indexerAndSearcher.getIndexer(toRef); if (indexer instanceof AVMLuceneIndexer) { AVMLuceneIndexer avmIndexer = (AVMLuceneIndexer) indexer; - avmIndexer.createIndex(to); - avmIndexer.index(to, 0, after); + avmIndexer.createIndex(to, IndexMode.SYNCHRONOUS); + avmIndexer.index(to, 0, after, getIndexMode(to)); } return returnValue; @@ -157,7 +220,147 @@ public class AVMSnapShotTriggeredIndexingMethodInterceptor implements MethodInte { this.enableIndexing = enableIndexing; } - - + /** + * Set the index modes.... Strings of the form ... (ASYNCHRONOUS | SYNCHRONOUS | UNINDEXED):(NAME | TYPE):regexp + * + * @param definitions + */ + public void setIndexingDefinitions(List definitions) + { + indexingDefinitions.clear(); + for (String def : definitions) + { + IndexingDefinition id = new IndexingDefinition(def); + indexingDefinitions.add(id); + } + } + + /** + * Set the default index mode = used when there are no matches + * + * @param defaultMode + */ + public void setDefaultMode(IndexMode defaultMode) + { + this.defaultMode = defaultMode; + } + + private synchronized IndexMode getIndexMode(String store) + { + IndexMode mode = modeCache.get(store); + if (mode == null) + { + for (IndexingDefinition def : indexingDefinitions) + { + if (def.definitionType == DefinitionType.NAME) + { + if (def.pattern.matcher(store).matches()) + { + mode = def.indexMode; + modeCache.put(store, mode); + break; + } + } + else + { + String storeType = getStoreType(store).toString(); + if (def.pattern.matcher(storeType).matches()) + { + mode = def.indexMode; + modeCache.put(store, mode); + break; + } + + } + } + } + // No definition + if (mode == null) + { + mode = defaultMode; + modeCache.put(store, mode); + } + return mode; + } + + private class IndexingDefinition + { + IndexMode indexMode; + + DefinitionType definitionType; + + Pattern pattern; + + IndexingDefinition(String definition) + { + String[] split = definition.split(":", 3); + if (split.length != 3) + { + throw new AlfrescoRuntimeException( + "Invalid index defintion. Must be of of the form IndexMode:DefinitionType:regular expression"); + } + indexMode = IndexMode.valueOf(split[0].toUpperCase()); + definitionType = DefinitionType.valueOf(split[1].toUpperCase()); + pattern = Pattern.compile(split[2]); + } + } + + private StoreType getStoreType(String name) + { + if (avmService.getStore(name) != null) + { + Map storeProperties = avmService.getStoreProperties(name); + if (storeProperties.containsKey(PROP_SANDBOX_STAGING_MAIN)) + { + return StoreType.STAGING; + } + else if (storeProperties.containsKey(PROP_SANDBOX_STAGING_PREVIEW)) + { + return StoreType.STAGING_PREVIEW; + } + else if (storeProperties.containsKey(PROP_SANDBOX_AUTHOR_MAIN)) + { + return StoreType.AUTHOR; + } + else if (storeProperties.containsKey(PROP_SANDBOX_AUTHOR_PREVIEW)) + { + return StoreType.AUTHOR_PREVIEW; + } + else if (storeProperties.containsKey(PROP_SANDBOX_WORKFLOW_MAIN)) + { + return StoreType.WORKFLOW; + } + else if (storeProperties.containsKey(PROP_SANDBOX_WORKFLOW_PREVIEW)) + { + return StoreType.WORKFLOW_PREVIEW; + } + else if (storeProperties.containsKey(PROP_SANDBOX_AUTHOR_WORKFLOW_MAIN)) + { + return StoreType.AUTHOR_WORKFLOW; + } + else if (storeProperties.containsKey(PROP_SANDBOX_AUTHOR_WORKFLOW_PREVIEW)) + { + return StoreType.AUTHOR_WORKFLOW_PREVIEW; + } + else + { + return StoreType.UNKNOWN; + } + } + else + { + return StoreType.UNKNOWN; + } + } + + private enum DefinitionType + { + NAME, TYPE; + } + + private enum StoreType + { + STAGING, STAGING_PREVIEW, AUTHOR, AUTHOR_PREVIEW, WORKFLOW, WORKFLOW_PREVIEW, AUTHOR_WORKFLOW, AUTHOR_WORKFLOW_PREVIEW, UNKNOWN; + } } diff --git a/source/java/org/alfresco/repo/search/impl/lucene/AVMLuceneIndexer.java b/source/java/org/alfresco/repo/search/impl/lucene/AVMLuceneIndexer.java index b123deb44e..6991c5365c 100644 --- a/source/java/org/alfresco/repo/search/impl/lucene/AVMLuceneIndexer.java +++ b/source/java/org/alfresco/repo/search/impl/lucene/AVMLuceneIndexer.java @@ -24,13 +24,16 @@ */ package org.alfresco.repo.search.impl.lucene; +import org.alfresco.repo.search.BackgroundIndexerAware; +import org.alfresco.repo.search.IndexMode; + /** * AVM specific indxer support * * @author andyh * */ -public interface AVMLuceneIndexer extends LuceneIndexer +public interface AVMLuceneIndexer extends LuceneIndexer, BackgroundIndexerAware { /** * Index a specified change to a store between two snapshots @@ -38,21 +41,24 @@ public interface AVMLuceneIndexer extends LuceneIndexer * @param store - the name of the store * @param srcVersion - the id of the snapshot before the changeset * @param dstVersion - the id of the snapshot created by the change set + * @param mode */ - public void index(String store, int srcVersion, int dstVersion); + public void index(String store, int srcVersion, int dstVersion, IndexMode mode); /** * Delete the index for the specified store. * * @param store + * @param mode */ - public void deleteIndex(String store); + public void deleteIndex(String store, IndexMode mode); /** * Create an index for the specified store. * This makes sure that the root node for the store is indexed correctly. * * @param store + * @param mode */ - public void createIndex(String store); + public void createIndex(String store, IndexMode mode); } diff --git a/source/java/org/alfresco/repo/search/impl/lucene/AVMLuceneIndexerAndSearcherFactory.java b/source/java/org/alfresco/repo/search/impl/lucene/AVMLuceneIndexerAndSearcherFactory.java index 069f528119..200cb3f9e9 100644 --- a/source/java/org/alfresco/repo/search/impl/lucene/AVMLuceneIndexerAndSearcherFactory.java +++ b/source/java/org/alfresco/repo/search/impl/lucene/AVMLuceneIndexerAndSearcherFactory.java @@ -30,6 +30,8 @@ import java.util.List; import org.alfresco.repo.avm.AVMNodeConverter; import org.alfresco.repo.content.ContentStore; import org.alfresco.repo.search.SearcherException; +import org.alfresco.repo.search.SupportsBackgroundIndexing; +import org.alfresco.repo.search.impl.lucene.fts.FullTextSearchIndexer; import org.alfresco.service.cmr.avm.AVMService; import org.alfresco.service.cmr.avm.AVMStoreDescriptor; import org.alfresco.service.cmr.avmsync.AVMSyncService; @@ -47,7 +49,7 @@ import org.apache.commons.logging.LogFactory; * @author andyh * */ -public class AVMLuceneIndexerAndSearcherFactory extends AbstractLuceneIndexerAndSearcherFactory +public class AVMLuceneIndexerAndSearcherFactory extends AbstractLuceneIndexerAndSearcherFactory implements SupportsBackgroundIndexing { private static Log s_logger = LogFactory.getLog(AVMLuceneIndexerAndSearcherFactory.class); @@ -65,6 +67,8 @@ public class AVMLuceneIndexerAndSearcherFactory extends AbstractLuceneIndexerAnd private ContentStore contentStore; + private FullTextSearchIndexer fullTextSearchIndexer; + public AVMLuceneIndexerAndSearcherFactory() { //s_logger.error("Creating AVMLuceneIndexerAndSearcherFactory"); @@ -145,6 +149,7 @@ public class AVMLuceneIndexerAndSearcherFactory extends AbstractLuceneIndexerAnd indexer.setAvmService(avmService); indexer.setAvmSyncService(avmSyncService); indexer.setContentStore(contentStore); + indexer.setFullTextSearchIndexer(fullTextSearchIndexer); return indexer; } @@ -174,4 +179,13 @@ public class AVMLuceneIndexerAndSearcherFactory extends AbstractLuceneIndexerAnd return searcher; } + /** + * Register the full text searcher (done by the seracher bean to break cyclic bean defs) + * + */ + public void setFullTextSearchIndexer(FullTextSearchIndexer fullTextSearchIndexer) + { + this.fullTextSearchIndexer = fullTextSearchIndexer; + } + } diff --git a/source/java/org/alfresco/repo/search/impl/lucene/AVMLuceneIndexerImpl.java b/source/java/org/alfresco/repo/search/impl/lucene/AVMLuceneIndexerImpl.java index 7228975e6e..b5b42d78f1 100644 --- a/source/java/org/alfresco/repo/search/impl/lucene/AVMLuceneIndexerImpl.java +++ b/source/java/org/alfresco/repo/search/impl/lucene/AVMLuceneIndexerImpl.java @@ -51,6 +51,10 @@ import org.alfresco.repo.content.ContentStore; import org.alfresco.repo.content.MimetypeMap; import org.alfresco.repo.content.transform.ContentTransformer; import org.alfresco.repo.domain.PropertyValue; +import org.alfresco.repo.search.IndexMode; +import org.alfresco.repo.search.impl.lucene.analysis.NumericEncoder; +import org.alfresco.repo.search.impl.lucene.fts.FTSIndexerAware; +import org.alfresco.repo.search.impl.lucene.fts.FullTextSearchIndexer; import org.alfresco.repo.transaction.AlfrescoTransactionSupport; import org.alfresco.service.cmr.avm.AVMException; import org.alfresco.service.cmr.avm.AVMNodeDescriptor; @@ -72,11 +76,16 @@ import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter; import org.alfresco.service.cmr.repository.datatype.TypeConversionException; import org.alfresco.service.namespace.QName; import org.alfresco.util.EqualsHelper; +import org.alfresco.util.GUID; import org.alfresco.util.ISO9075; import org.alfresco.util.Pair; import org.apache.log4j.Logger; import org.apache.lucene.document.Document; import org.apache.lucene.document.Field; +import org.apache.lucene.index.Term; +import org.apache.lucene.search.Hits; +import org.apache.lucene.search.PrefixQuery; +import org.apache.lucene.search.Searcher; /** * Update the index after a snap shot to an AVM store. (Revert is dealt with as a new snap shot is created) @@ -85,6 +94,10 @@ import org.apache.lucene.document.Field; */ public class AVMLuceneIndexerImpl extends AbstractLuceneIndexerImpl implements AVMLuceneIndexer { + private static String SNAP_SHOT_ID = "SnapShotId_"; + + private static String SNAP_SHOT_STORE = "SnapShotStore"; + static Logger s_logger = Logger.getLogger(AVMLuceneIndexerImpl.class); private AVMService avmService; @@ -98,8 +111,19 @@ public class AVMLuceneIndexerImpl extends AbstractLuceneIndexerImpl impl private Set indexedPaths = new HashSet(); + private FTSIndexerAware callBack; + + private FullTextSearchIndexer fullTextSearchIndexer; + + private int remainingCount; + + private int startVersion = -1; + + private int endVersion = -1; + /** * Set the AVM Service + * * @param avmService */ public void setAvmService(AVMService avmService) @@ -109,6 +133,7 @@ public class AVMLuceneIndexerImpl extends AbstractLuceneIndexerImpl impl /** * Set the AVM sync service + * * @param avmSyncService */ public void setAvmSyncService(AVMSyncService avmSyncService) @@ -118,6 +143,7 @@ public class AVMLuceneIndexerImpl extends AbstractLuceneIndexerImpl impl /** * Set the content service + * * @param contentStore */ public void setContentStore(ContentStore contentStore) @@ -162,10 +188,55 @@ public class AVMLuceneIndexerImpl extends AbstractLuceneIndexerImpl impl * @param srcVersion * @param dstVersion */ - public void index(String store, int srcVersion, int dstVersion) + public void index(String store, int srcVersion, int dstVersion, IndexMode mode) { checkAbleToDoWork(IndexUpdateStatus.SYNCRONOUS); + try + { + switch (mode) + { + case ASYNCHRONOUS: + asynchronousIndex(store, srcVersion, dstVersion); + break; + case SYNCHRONOUS: + synchronousIndex(store, srcVersion, dstVersion); + break; + case UNINDEXED: + // nothing to do + break; + } + } + catch (LuceneIndexException e) + { + setRollbackOnly(); + throw new LuceneIndexException("snapshot index failed", e); + } + } + public void asynchronousIndex(String store, int srcVersion, int dstVersion) + { + if(s_logger.isDebugEnabled()) + { + s_logger.debug("Async index for "+store + " from " + srcVersion + " to " + dstVersion); + } + index("\u0000BG:STORE:" + store + ":" + srcVersion + ":" + dstVersion + ":" + GUID.generate()); + fullTextSearchIndexer.requiresIndex(AVMNodeConverter.ToStoreRef(store)); + } + + public void synchronousIndex(String store, int srcVersion, int dstVersion) + { + if(startVersion == -1) + { + startVersion = srcVersion; + } + + endVersion = dstVersion; + + + if(s_logger.isDebugEnabled()) + { + s_logger.debug("Sync index for "+store + " from " + srcVersion + " to "+dstVersion); + } String path = store + ":/"; List changeList = avmSyncService.compare(srcVersion, path, dstVersion, path, null); for (AVMDifference difference : changeList) @@ -221,6 +292,8 @@ public class AVMLuceneIndexerImpl extends AbstractLuceneIndexerImpl impl } } + // record the snap shotid + reindex(SNAP_SHOT_ID + store, false); } /* @@ -232,14 +305,14 @@ public class AVMLuceneIndexerImpl extends AbstractLuceneIndexerImpl impl String store = splitPath[0]; String pathInStore = splitPath[1]; SimplePath simplePath = new SimplePath(pathInStore); - + StringBuilder pathBuilder = new StringBuilder(); pathBuilder.append(store).append(":/"); reindex(pathBuilder.toString(), false); boolean requiresSep = false; for (int i = 0; i < simplePath.size() - 1; i++) { - if(requiresSep) + if (requiresSep) { pathBuilder.append("/"); } @@ -270,25 +343,64 @@ public class AVMLuceneIndexerImpl extends AbstractLuceneIndexerImpl impl protected List createDocuments(String stringNodeRef, boolean isNew, boolean indexAllProperties, boolean includeDirectoryDocuments) { - AVMNodeDescriptor desc = avmService.lookup(-1, stringNodeRef); List docs = new ArrayList(); - if(desc == null) + if (stringNodeRef.startsWith("\u0000")) + { + Document idoc = new Document(); + idoc.add(new Field("ID", stringNodeRef, Field.Store.YES, Field.Index.UN_TOKENIZED, Field.TermVector.NO)); + docs.add(idoc); + return docs; + } + else if (stringNodeRef.startsWith(SNAP_SHOT_ID)) + { + Document sdoc = new Document(); + sdoc.add(new Field("ID", stringNodeRef, Field.Store.YES, Field.Index.UN_TOKENIZED, Field.TermVector.NO)); + String storeName = stringNodeRef.substring(SNAP_SHOT_ID.length()); + sdoc.add(new Field(SNAP_SHOT_ID, NumericEncoder.encode(avmService.getLatestSnapshotID(storeName)), + Field.Store.YES, Field.Index.UN_TOKENIZED, Field.TermVector.NO)); + sdoc.add(new Field(SNAP_SHOT_STORE, NumericEncoder.encode(avmService.getLatestSnapshotID(storeName)), + Field.Store.YES, Field.Index.UN_TOKENIZED, Field.TermVector.NO)); + docs.add(sdoc); + return docs; + } + + AVMNodeDescriptor desc = avmService.lookup(endVersion, stringNodeRef); + if (desc == null) { return docs; } if (desc.isLayeredDirectory() || desc.isLayeredFile()) { return docs; - } - - List> paths = avmService.getHeadPaths(desc); + } + + List> allPaths = avmService.getPaths(desc); + List> paths = new ArrayList> (); + for(Pair pair: allPaths) + { + if(pair.getFirst().intValue() == endVersion) + { + paths.add(pair); + } + } if(paths.size() == 0) + { + for(Pair pair: allPaths) + { + if(pair.getFirst().intValue() == -1) + { + paths.add(pair); + } + } + } + + if (paths.size() == 0) { return docs; } for (Pair path : paths) { - if(indexedPaths.contains(path.getSecond())) + if (indexedPaths.contains(path.getSecond())) { return docs; } @@ -297,37 +409,35 @@ public class AVMLuceneIndexerImpl extends AbstractLuceneIndexerImpl impl { indexedPaths.add(path.getSecond()); } - + AVMNode node = AVMDAOs.Instance().fAVMNodeDAO.getByID(desc.getId()); - if (paths.size() > 0) { if (desc != null) { - NodeRef nodeRef = AVMNodeConverter.ToNodeRef(-1, stringNodeRef); + NodeRef nodeRef = AVMNodeConverter.ToNodeRef(endVersion, stringNodeRef); Document xdoc = new Document(); - xdoc.add(new Field("ID", nodeRef.toString(), Field.Store.YES, - Field.Index.UN_TOKENIZED, Field.TermVector.NO)); + xdoc.add(new Field("ID", nodeRef.toString(), Field.Store.YES, Field.Index.UN_TOKENIZED, + Field.TermVector.NO)); for (Pair path : paths) { String[] splitPath = splitPath(path.getSecond()); @SuppressWarnings("unused") String pathInStore = splitPath[1]; - xdoc.add(new Field("ID", path.getSecond(), - Field.Store.YES, Field.Index.UN_TOKENIZED, Field.TermVector.NO)); + xdoc.add(new Field("ID", path.getSecond(), Field.Store.YES, Field.Index.UN_TOKENIZED, + Field.TermVector.NO)); } - xdoc.add(new Field("TX", AlfrescoTransactionSupport.getTransactionId(), Field.Store.YES, Field.Index.UN_TOKENIZED, Field.TermVector.NO)); boolean isAtomic = true; - Map properties = getIndexableProperties(desc, node, nodeRef, -1, stringNodeRef); + Map properties = getIndexableProperties(desc, node, nodeRef, endVersion, stringNodeRef); for (QName propertyName : properties.keySet()) { Serializable value = properties.get(propertyName); @@ -357,9 +467,9 @@ public class AVMLuceneIndexerImpl extends AbstractLuceneIndexerImpl impl String store = splitPath[0]; String pathInStore = splitPath[1]; SimplePath simplePath = new SimplePath(pathInStore); - + StringBuilder xpathBuilder = new StringBuilder(); - for(int i = 0; i < simplePath.size(); i++) + for (int i = 0; i < simplePath.size(); i++) { xpathBuilder.append("/{}").append(simplePath.get(i)); } @@ -379,7 +489,7 @@ public class AVMLuceneIndexerImpl extends AbstractLuceneIndexerImpl impl boolean requiresSep = false; for (int i = 0; i < simplePath.size() - 1; i++) { - if(requiresSep) + if (requiresSep) { pathBuilder.append("/"); } @@ -393,8 +503,7 @@ public class AVMLuceneIndexerImpl extends AbstractLuceneIndexerImpl impl qNameBuffer.append(ISO9075.getXPathName(QName.createQName("", simplePath .get(simplePath.size() - 1)))); - xdoc.add(new Field("PARENT", - ancestors.get(ancestors.size() - 1), Field.Store.YES, + xdoc.add(new Field("PARENT", ancestors.get(ancestors.size() - 1), Field.Store.YES, Field.Index.UN_TOKENIZED, Field.TermVector.NO)); // TODO: Categories and LINKASPECT @@ -407,20 +516,19 @@ public class AVMLuceneIndexerImpl extends AbstractLuceneIndexerImpl impl Document directoryEntry = new Document(); directoryEntry.add(new Field("ID", nodeRef.toString(), Field.Store.YES, Field.Index.UN_TOKENIZED, Field.TermVector.NO)); - - directoryEntry.add(new Field("ID", path.getSecond(), - Field.Store.YES, Field.Index.UN_TOKENIZED, Field.TermVector.NO)); - - - directoryEntry.add(new Field("PATH", xpath, Field.Store.YES, - Field.Index.TOKENIZED, Field.TermVector.NO)); + + directoryEntry.add(new Field("ID", path.getSecond(), Field.Store.YES, + Field.Index.UN_TOKENIZED, Field.TermVector.NO)); + + directoryEntry.add(new Field("PATH", xpath, Field.Store.YES, Field.Index.TOKENIZED, + Field.TermVector.NO)); // Find all parent nodes. for (String toAdd : ancestors) { - directoryEntry.add(new Field("ANCESTOR", toAdd, - Field.Store.NO, Field.Index.UN_TOKENIZED, Field.TermVector.NO)); + directoryEntry.add(new Field("ANCESTOR", toAdd, Field.Store.NO, + Field.Index.UN_TOKENIZED, Field.TermVector.NO)); } directoryEntry.add(new Field("ISCONTAINER", "T", Field.Store.YES, Field.Index.UN_TOKENIZED, Field.TermVector.NO)); @@ -456,7 +564,7 @@ public class AVMLuceneIndexerImpl extends AbstractLuceneIndexerImpl impl xdoc.add(new Field("TYPE", ISO9075.getXPathName(typeQName), Field.Store.YES, Field.Index.UN_TOKENIZED, Field.TermVector.NO)); - for (QName classRef : avmService.getAspects(-1, stringNodeRef)) + for (QName classRef : avmService.getAspects(endVersion, stringNodeRef)) { xdoc.add(new Field("ASPECT", ISO9075.getXPathName(classRef), Field.Store.YES, Field.Index.UN_TOKENIZED, Field.TermVector.NO)); @@ -534,11 +642,11 @@ public class AVMLuceneIndexerImpl extends AbstractLuceneIndexerImpl impl result.put(ContentModel.PROP_STORE_IDENTIFIER, nodeRef.getStoreRef().getIdentifier()); if (desc.isLayeredDirectory()) { - result.put(WCMModel.PROP_AVM_DIR_INDIRECTION, AVMNodeConverter.ToNodeRef(-1, desc.getIndirection())); + result.put(WCMModel.PROP_AVM_DIR_INDIRECTION, AVMNodeConverter.ToNodeRef(endVersion, desc.getIndirection())); } if (desc.isLayeredFile()) { - result.put(WCMModel.PROP_AVM_FILE_INDIRECTION, AVMNodeConverter.ToNodeRef(-1, desc.getIndirection())); + result.put(WCMModel.PROP_AVM_FILE_INDIRECTION, AVMNodeConverter.ToNodeRef(endVersion, desc.getIndirection())); } if (desc.isFile()) { @@ -873,13 +981,32 @@ public class AVMLuceneIndexerImpl extends AbstractLuceneIndexerImpl impl @Override protected void doCommit() throws IOException { + if (indexUpdateStatus == IndexUpdateStatus.ASYNCHRONOUS) + { + setInfo(docs, getDeletions(), false); + // FTS does not trigger indexing request + } + else + { + setInfo(docs, getDeletions(), false); + // TODO: only register if required + fullTextSearchIndexer.requiresIndex(store); + } + if (callBack != null) + { + callBack.indexCompleted(store, remainingCount, null); + } + setInfo(docs, deletions, false); } @Override protected void doRollBack() throws IOException { - + if (callBack != null) + { + callBack.indexCompleted(store, 0, null); + } } @Override @@ -1014,29 +1141,68 @@ public class AVMLuceneIndexerImpl extends AbstractLuceneIndexerImpl impl } - public void deleteIndex(String store) + public void deleteIndex(String store, IndexMode mode) { checkAbleToDoWork(IndexUpdateStatus.SYNCRONOUS); try { - deleteAll(); + switch (mode) + { + case ASYNCHRONOUS: + asyncronousDeleteIndex(store); + break; + case SYNCHRONOUS: + syncronousDeleteIndex(store); + break; + case UNINDEXED: + // nothing to do + break; + } } catch (LuceneIndexException e) { setRollbackOnly(); throw new LuceneIndexException("Delete index failed", e); } - } - public void createIndex(String store) + public void syncronousDeleteIndex(String store) + { + + if(s_logger.isDebugEnabled()) + { + s_logger.debug("Sync delete for "+store); + } + deleteAll(); + } + + public void asyncronousDeleteIndex(String store) + { + if(s_logger.isDebugEnabled()) + { + s_logger.debug("Async delete for "+store); + } + index("\u0000BG:DELETE:" + store+":"+GUID.generate()); + fullTextSearchIndexer.requiresIndex(AVMNodeConverter.ToStoreRef(store)); + } + + public void createIndex(String store, IndexMode mode) { checkAbleToDoWork(IndexUpdateStatus.SYNCRONOUS); try { - @SuppressWarnings("unused") - AVMNodeDescriptor rootDesc = avmService.getStoreRoot(-1, store); - index(store+":/"); + switch (mode) + { + case ASYNCHRONOUS: + asyncronousCreateIndex(store); + break; + case SYNCHRONOUS: + syncronousCreateIndex(store); + break; + case UNINDEXED: + // nothing to do + break; + } } catch (LuceneIndexException e) { @@ -1044,4 +1210,123 @@ public class AVMLuceneIndexerImpl extends AbstractLuceneIndexerImpl impl throw new LuceneIndexException("Create index failed", e); } } + + public void syncronousCreateIndex(String store) + { + + if(s_logger.isDebugEnabled()) + { + s_logger.debug("Sync create for "+store); + } + @SuppressWarnings("unused") + AVMNodeDescriptor rootDesc = avmService.getStoreRoot(-1, store); + index(store + ":/"); + + } + + public void asyncronousCreateIndex(String store) + { + if(s_logger.isDebugEnabled()) + { + s_logger.debug("Async create for "+store); + } + index("\u0000BG:CREATE:" + store+":"+GUID.generate()); + fullTextSearchIndexer.requiresIndex(AVMNodeConverter.ToStoreRef(store)); + } + + public void registerCallBack(FTSIndexerAware callBack) + { + this.callBack = callBack; + } + + public int updateFullTextSearch(int size) + { + checkAbleToDoWork(IndexUpdateStatus.ASYNCHRONOUS); + + try + { + PrefixQuery query = new PrefixQuery(new Term("ID", "\u0000BG:")); + + String action = null; + + Searcher searcher = null; + try + { + searcher = getSearcher(null); + // commit on another thread - appears like there is no index ...try later + if (searcher == null) + { + remainingCount = size; + return 0; + } + Hits hits; + try + { + hits = searcher.search(query); + } + catch (IOException e) + { + throw new LuceneIndexException( + "Failed to execute query to find content which needs updating in the index", e); + } + + if (hits.length() > 0) + { + Document doc = hits.doc(0); + action = doc.getField("ID").stringValue(); + String[] split = action.split(":"); + if(split[1].equals("DELETE")) + { + deleteAll("\u0000BG:"); + } + else if(split[1].equals("CREATE")) + { + syncronousCreateIndex(split[2]); + } + else if(split[1].equals("STORE")) + { + synchronousIndex(split[2], Integer.parseInt(split[3]), Integer.parseInt(split[4])); + } + deletions.add(action); + remainingCount = hits.length() - 1; + return 1; + } + else + { + remainingCount = 0; + return 0; + } + + } + finally + { + if (searcher != null) + { + try + { + searcher.close(); + } + catch (IOException e) + { + throw new LuceneIndexException("Failed to close searcher", e); + } + } + } + } + catch (IOException e) + { + setRollbackOnly(); + throw new LuceneIndexException("Failed FTS update", e); + } + catch (LuceneIndexException e) + { + setRollbackOnly(); + throw new LuceneIndexException("Failed FTS update", e); + } + } + + public void setFullTextSearchIndexer(FullTextSearchIndexer fullTextSearchIndexer) + { + this.fullTextSearchIndexer = fullTextSearchIndexer; + } } diff --git a/source/java/org/alfresco/repo/search/impl/lucene/AbstractLuceneIndexerImpl.java b/source/java/org/alfresco/repo/search/impl/lucene/AbstractLuceneIndexerImpl.java index c2257b4811..dfa1b2e3da 100644 --- a/source/java/org/alfresco/repo/search/impl/lucene/AbstractLuceneIndexerImpl.java +++ b/source/java/org/alfresco/repo/search/impl/lucene/AbstractLuceneIndexerImpl.java @@ -63,15 +63,15 @@ public abstract class AbstractLuceneIndexerImpl extends AbstractLuceneBase /** * An index */ - INDEX, + INDEX, /** * A reindex */ - REINDEX, + REINDEX, /** * A delete */ - DELETE, + DELETE, /** * A cascaded reindex (ensures directory structre is ok) */ @@ -83,13 +83,13 @@ public abstract class AbstractLuceneIndexerImpl extends AbstractLuceneBase /** * Inde is unchanged */ - UNMODIFIED, + UNMODIFIED, /** - * Index is being changein in TX + * Index is being changein in TX */ - SYNCRONOUS, + SYNCRONOUS, /** - * Index is eiong changed by a background upate + * Index is eiong changed by a background upate */ ASYNCHRONOUS; } @@ -379,7 +379,8 @@ public abstract class AbstractLuceneIndexerImpl extends AbstractLuceneBase /** * Commit this index - * @throws LuceneIndexException + * + * @throws LuceneIndexException */ public void commit() throws LuceneIndexException { @@ -441,7 +442,7 @@ public abstract class AbstractLuceneIndexerImpl extends AbstractLuceneBase * serialisation against the index as would a data base transaction. * * @return the tx state - * @throws LuceneIndexException + * @throws LuceneIndexException */ public int prepare() throws LuceneIndexException { @@ -498,7 +499,8 @@ public abstract class AbstractLuceneIndexerImpl extends AbstractLuceneBase /** * Roll back the index changes (this just means they are never added) - * @throws LuceneIndexException + * + * @throws LuceneIndexException */ public void rollback() throws LuceneIndexException { @@ -819,6 +821,7 @@ public abstract class AbstractLuceneIndexerImpl extends AbstractLuceneBase /** * Are we deleting leaves only (not meta data) + * * @return - deleting only nodes. */ public boolean getDeleteOnlyNodes() @@ -828,6 +831,7 @@ public abstract class AbstractLuceneIndexerImpl extends AbstractLuceneBase /** * Get the deletions + * * @return - the ids to delete */ public Set getDeletions() @@ -837,9 +841,18 @@ public abstract class AbstractLuceneIndexerImpl extends AbstractLuceneBase /** * Delete all entries from the index. - * */ public void deleteAll() + { + deleteAll(null); + } + + /** + * Delete all index entries which do not start with the goven prefix + * + * @param prefix + */ + public void deleteAll(String prefix) { IndexReader mainReader = null; try @@ -851,7 +864,10 @@ public abstract class AbstractLuceneIndexerImpl extends AbstractLuceneBase { Document document = mainReader.document(doc); String[] ids = document.getValues("ID"); - deletions.add(ids[ids.length - 1]); + if ((prefix == null) || nonStartwWith(ids, prefix)) + { + deletions.add(ids[ids.length - 1]); + } } } @@ -877,4 +893,16 @@ public abstract class AbstractLuceneIndexerImpl extends AbstractLuceneBase } } + private boolean nonStartwWith(String[] values, String prefix) + { + for (String value : values) + { + if (value.startsWith(prefix)) + { + return false; + } + } + return true; + } + }