diff --git a/source/java/org/alfresco/repo/tagging/UpdateTagScopesActionExecuter.java b/source/java/org/alfresco/repo/tagging/UpdateTagScopesActionExecuter.java
index 6a3e816472..206f6db6cf 100644
--- a/source/java/org/alfresco/repo/tagging/UpdateTagScopesActionExecuter.java
+++ b/source/java/org/alfresco/repo/tagging/UpdateTagScopesActionExecuter.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2005-2010 Alfresco Software Limited.
+ * Copyright (C) 2005-2014 Alfresco Software Limited.
*
* This file is part of Alfresco
*
@@ -439,30 +439,44 @@ public class UpdateTagScopesActionExecuter extends ActionExecuterAbstractBase
}
}
}
-
- // Order the list
- Collections.sort(tags);
-
- // Write new content back to tag scope
- String tagContent = TaggingServiceImpl.tagDetailsToString(tags);
- ContentWriter contentWriter = contentService.getWriter(tagScopeNode, ContentModel.PROP_TAGSCOPE_CACHE, true);
- contentWriter.setEncoding("UTF-8");
- contentWriter.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN);
- contentWriter.putContent(tagContent);
-
+
+ // ACE-1979: emptying tag scope cache by setting content property for the cache to null to avoid zero-size writes. Orphaned content will be deleted with content store
+ // cleaner job
+ if (tags.isEmpty())
+ {
+ nodeService.setProperty(tagScopeNode, ContentModel.PROP_TAGSCOPE_CACHE, null);
+
+ if (logger.isDebugEnabled())
+ {
+ logger.debug("Updated tag scope: '" + tagScopeNode + "'. No tags were found. Emptying tags cache by setting content property to null...");
+ }
+ }
+ else
+ {
+ // Order the list
+ Collections.sort(tags);
+
+ // Write new content back to tag scope
+ String tagContent = TaggingServiceImpl.tagDetailsToString(tags);
+ ContentWriter contentWriter = contentService.getWriter(tagScopeNode, ContentModel.PROP_TAGSCOPE_CACHE, true);
+ contentWriter.setEncoding("UTF-8");
+ contentWriter.setMimetype(MimetypeMap.MIMETYPE_TEXT_PLAIN);
+ contentWriter.putContent(tagContent);
+
+ // Log this if required
+ if(logger.isDebugEnabled())
+ {
+ logger.debug(
+ "Updated tag scope " + tagScopeNode + " with " + updates + ", " +
+ "new contents are { " + tagContent.replace("\n", " : ") + " } " +
+ "from old contents of " + previousTagState
+ );
+ }
+ }
+
// We're done making our changes
// Allow behaviours to fire again if they want to
behaviourFilter.enableBehaviour();
-
- // Log this if required
- if(logger.isDebugEnabled())
- {
- logger.debug(
- "Updated tag scope " + tagScopeNode + " with " + updates + ", " +
- "new contents are { " + tagContent.replace("\n", " : ") + " } " +
- "from old contents of " + previousTagState
- );
- }
}
}
diff --git a/source/test-java/org/alfresco/Repository01TestSuite.java b/source/test-java/org/alfresco/Repository01TestSuite.java
index 3b469ef3b4..9fed80ebb7 100644
--- a/source/test-java/org/alfresco/Repository01TestSuite.java
+++ b/source/test-java/org/alfresco/Repository01TestSuite.java
@@ -354,6 +354,7 @@ public class Repository01TestSuite extends TestSuite
static void tests53(TestSuite suite)
{
suite.addTestSuite(org.alfresco.repo.tagging.TaggingServiceImplTest.class);
+ suite.addTestSuite(org.alfresco.repo.tagging.UpdateTagScopesActionExecuterTest.class);
}
static void tests55(TestSuite suite)
diff --git a/source/test-java/org/alfresco/repo/tagging/UpdateTagScopesActionExecuterTest.java b/source/test-java/org/alfresco/repo/tagging/UpdateTagScopesActionExecuterTest.java
new file mode 100644
index 0000000000..7a7e795669
--- /dev/null
+++ b/source/test-java/org/alfresco/repo/tagging/UpdateTagScopesActionExecuterTest.java
@@ -0,0 +1,333 @@
+/*
+ * Copyright (C) 2005-2014 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Alfresco is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Alfresco. If not, see .
+ */
+package org.alfresco.repo.tagging;
+
+import java.io.Serializable;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+import javax.transaction.Status;
+import javax.transaction.UserTransaction;
+
+import junit.framework.TestCase;
+
+import org.alfresco.model.ContentModel;
+import org.alfresco.repo.nodelocator.CompanyHomeNodeLocator;
+import org.alfresco.repo.security.authentication.AuthenticationUtil;
+import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
+import org.alfresco.service.ServiceRegistry;
+import org.alfresco.service.cmr.action.Action;
+import org.alfresco.service.cmr.action.ActionService;
+import org.alfresco.service.cmr.action.ActionTrackingService;
+import org.alfresco.service.cmr.action.ExecutionSummary;
+import org.alfresco.service.cmr.model.FileFolderService;
+import org.alfresco.service.cmr.model.FileInfo;
+import org.alfresco.service.cmr.repository.ChildAssociationRef;
+import org.alfresco.service.cmr.repository.ContentData;
+import org.alfresco.service.cmr.repository.NodeRef;
+import org.alfresco.service.cmr.repository.NodeService;
+import org.alfresco.service.cmr.tagging.TaggingService;
+import org.alfresco.service.transaction.TransactionService;
+import org.alfresco.util.ApplicationContextHelper;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.context.ApplicationContext;
+
+/**
+ * Test for {@link UpdateTagScopesActionExecuter}
+ *
+ * @author Dmitry Velichkevich
+ */
+public class UpdateTagScopesActionExecuterTest extends TestCase
+{
+ private static final int TAGSCOPE_LAYERS = 3;
+
+ private static final int TEST_TAGS_AMOUNT = 3;
+
+ private static final int TEST_DOCUMENTS_AMOUNT = 3;
+
+
+ private static final String ACTION_TRACKING_SERVICE_BEAN_NAME = "actionTrackingService";
+
+ private static final String UPDATE_TAGSCOPE_ACTION_EXECUTER_BEAN_NAME = "update-tagscope";
+
+ private static final String TEST_TAG_NAME_PATTERN = "testTag%d-%d-%d";
+
+ private static final String TEST_FOLDER_NAME_PATTERN = "TestFolder-%d";
+
+ private static final String TEST_DOCUMENT_NAME_PATTERN = "InFolder-%d-TestDocument-%d.txt";
+
+
+ private ApplicationContext applicationContext = ApplicationContextHelper.getApplicationContext();
+
+ private NodeService nodeService;
+
+ private ActionService actionService;
+
+ private TaggingService taggingService;
+
+ private FileFolderService fileFolderService;
+
+ private TransactionService transactionService;
+
+ private UpdateTagScopesActionExecuter actionExecuter;
+
+ private ActionTrackingService actionTrackingService;
+
+ private UserTransaction transaction;
+
+ private List expectedTagScopes;
+
+ private List testTags;
+
+ @Before
+ @Override
+ public void setUp() throws Exception
+ {
+ final ServiceRegistry registry = (ServiceRegistry) applicationContext.getBean(ServiceRegistry.SERVICE_REGISTRY);
+
+ nodeService = registry.getNodeService();
+ actionService = registry.getActionService();
+ actionExecuter = (UpdateTagScopesActionExecuter) applicationContext.getBean(UPDATE_TAGSCOPE_ACTION_EXECUTER_BEAN_NAME);
+ taggingService = registry.getTaggingService();
+ fileFolderService = registry.getFileFolderService();
+ transactionService = registry.getTransactionService();
+ actionTrackingService = (ActionTrackingService) applicationContext.getBean(ACTION_TRACKING_SERVICE_BEAN_NAME);
+
+ AuthenticationUtil.setAdminUserAsFullyAuthenticatedUser();
+
+ expectedTagScopes = new LinkedList();
+ testTags = new LinkedList();
+
+ transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback()
+ {
+ @Override
+ public Void execute() throws Throwable
+ {
+ createTestContent(registry, expectedTagScopes);
+ return null;
+ }
+ }, false, true);
+
+ waitForTagScopeUpdate();
+
+ transaction = transactionService.getUserTransaction();
+ transaction.begin();
+ }
+
+ /**
+ * Creates simple hierarchy with documents tagged on the first layer only
+ *
+ * @param registry - {@link ServiceRegistry} instance
+ * @param createdTagScopes - {@link List}<{@link NodeRef}> instance which contains all tag scope folders
+ */
+ private void createTestContent(ServiceRegistry registry, List createdTagScopes)
+ {
+ NodeRef rootNode = registry.getNodeLocatorService().getNode(CompanyHomeNodeLocator.NAME, null, null);
+
+ NodeRef currentParent = rootNode;
+ for (int i = 0; i < TAGSCOPE_LAYERS; i++)
+ {
+ FileInfo newFolder = fileFolderService.create(currentParent, String.format(TEST_FOLDER_NAME_PATTERN, i), ContentModel.TYPE_FOLDER);
+ currentParent = newFolder.getNodeRef();
+
+ if (null != createdTagScopes)
+ {
+ createdTagScopes.add(currentParent);
+ }
+
+ nodeService.addAspect(currentParent, ContentModel.ASPECT_TAGSCOPE, null);
+
+ for (int j = 0; j < TEST_DOCUMENTS_AMOUNT; j++)
+ {
+ FileInfo newDocument = fileFolderService.create(currentParent, String.format(TEST_DOCUMENT_NAME_PATTERN, i, j), ContentModel.TYPE_CONTENT);
+ nodeService.addAspect(newDocument.getNodeRef(), ContentModel.ASPECT_TAGGABLE, null);
+
+ if (0 == i)
+ {
+ for (int k = 0; k < TEST_TAGS_AMOUNT; k++)
+ {
+ String tagName = String.format(TEST_TAG_NAME_PATTERN, k, j, i);
+ testTags.add(tagName);
+ taggingService.addTag(newDocument.getNodeRef(), tagName);
+ }
+ }
+ }
+ }
+ }
+
+ private void waitForTagScopeUpdate() throws Exception
+ {
+ List executingActions = null;
+
+ do
+ {
+ synchronized (this)
+ {
+ wait(1000);
+ }
+
+ executingActions = actionTrackingService.getExecutingActions(UpdateTagScopesActionExecuter.NAME);
+ } while (!executingActions.isEmpty());
+ }
+
+ @After
+ @Override
+ public void tearDown() throws Exception
+ {
+ final NodeRef rootTestFolder = expectedTagScopes.iterator().next();
+
+ for (String tagName : testTags)
+ {
+ taggingService.deleteTag(rootTestFolder.getStoreRef(), tagName);
+ }
+
+ testTags.clear();
+ testTags = null;
+
+ if (Status.STATUS_ROLLEDBACK != transaction.getStatus())
+ {
+ transaction.rollback();
+ }
+
+ transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback()
+ {
+ @Override
+ public Void execute() throws Throwable
+ {
+ nodeService.deleteNode(rootTestFolder);
+ return null;
+ }
+ }, false, true);
+
+ AuthenticationUtil.clearCurrentSecurityContext();
+
+ expectedTagScopes.clear();
+ expectedTagScopes = null;
+ }
+
+ /**
+ * Tests that tag scopes are properly updated. Cache on the first layer MUST NOT be empty. All other tag scopes MUST BE null
+ *
+ * @throws Exception
+ */
+ @Test
+ public void testSimpleTagScopesUpsdate() throws Exception
+ {
+ Action tagScopeUpdateAction = actionService.createAction(UpdateTagScopesActionExecuter.NAME);
+ tagScopeUpdateAction.setParameterValue(UpdateTagScopesActionExecuter.PARAM_TAG_SCOPES, (Serializable) expectedTagScopes);
+ actionExecuter.execute(tagScopeUpdateAction, null);
+
+ Iterator iterator = expectedTagScopes.iterator();
+ assertTrue(iterator.hasNext());
+
+ NodeRef taggedTagScope = iterator.next();
+ assertNotNull(taggedTagScope);
+
+ ContentData contentData = getTagScopeCacheContentDataProperty(taggedTagScope);
+ assertNotNull(contentData);
+ assertTrue(contentData.getSize() > 0L);
+
+ assertTrue(iterator.hasNext());
+
+ for (NodeRef tagScopeFolder = iterator.next(); iterator.hasNext(); tagScopeFolder = iterator.next())
+ {
+ assertNotNull(tagScopeFolder);
+ contentData = getTagScopeCacheContentDataProperty(tagScopeFolder);
+ assertNull(contentData);
+ }
+ }
+
+ /**
+ * ACE-1979: tag scope cache must be emptied when tag scope doesn't contain tags anymore. The fix nullifies
+ * content data property for the tag scope cache. This approach allows avoiding immediate update in content store and postponing it untill content store cleaner job is executed
+ *
+ * @throws Exception
+ */
+ @Test
+ public void testTagScopesUpdateWhenTagsAreRemoved() throws Exception
+ {
+ Action tagScopeUpdateAction = actionService.createAction(UpdateTagScopesActionExecuter.NAME);
+ tagScopeUpdateAction.setParameterValue(UpdateTagScopesActionExecuter.PARAM_TAG_SCOPES, (Serializable) expectedTagScopes);
+ actionExecuter.execute(tagScopeUpdateAction, null);
+
+ waitForTagScopeUpdate();
+
+ final NodeRef taggedTagScope = expectedTagScopes.iterator().next();
+ assertNotNull(taggedTagScope);
+
+ ContentData contentData = getTagScopeCacheContentDataProperty(taggedTagScope);
+ assertNotNull(contentData);
+ actionTrackingService.getExecutingActions(UpdateTagScopesActionExecuter.NAME);
+ assertTrue(contentData.getSize() > 0L);
+
+ transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback()
+ {
+ @Override
+ public Void execute() throws Throwable
+ {
+ for (ChildAssociationRef child : nodeService.getChildAssocs(taggedTagScope, Collections.singleton(ContentModel.TYPE_CONTENT)))
+ {
+ taggingService.removeTags(child.getChildRef(), testTags);
+ }
+
+ return null;
+ }
+ }, false, true);
+
+ waitForTagScopeUpdate();
+
+ actionExecuter.execute(tagScopeUpdateAction, null);
+
+ for (NodeRef tagScopeFolder : expectedTagScopes)
+ {
+ assertNotNull(tagScopeFolder);
+ contentData = getTagScopeCacheContentDataProperty(tagScopeFolder);
+ assertNull(contentData);
+ }
+ }
+
+ /**
+ * @param nodeRef - {@link NodeRef} instance which represents tag scope folder
+ * @return {@link ContentModel#PROP_TAGSCOPE_CACHE} {@link ContentData} property instance for the given nodeRef
or
+ */
+ private ContentData getTagScopeCacheContentDataProperty(final NodeRef nodeRef)
+ {
+ ContentData result = null;
+
+ Serializable contentProperty = transactionService.getRetryingTransactionHelper().doInTransaction(new RetryingTransactionCallback()
+ {
+ @Override
+ public Serializable execute() throws Throwable
+ {
+ return nodeService.getProperty(nodeRef, ContentModel.PROP_TAGSCOPE_CACHE);
+ }
+ }, false, true);
+
+ if (contentProperty instanceof ContentData)
+ {
+ result = (ContentData) contentProperty;
+ }
+
+ return result;
+ }
+}