Fixed ALFCOM-2888: Policy based storage - Content move does not rollback after integrity event on cm:storeName

- Content move is now done in a post-commit phase because rollback is not possible on content stores
 - Added unit tests for case
 - Added onAddAspect handling to detect auto-addition of aspect


git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@14380 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261
This commit is contained in:
Derek Hulley
2009-05-20 15:40:22 +00:00
parent 24b8ae3b16
commit 039cabc726
2 changed files with 112 additions and 3 deletions

View File

@@ -27,6 +27,7 @@ package org.alfresco.repo.content.routing;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@@ -40,6 +41,9 @@ import org.alfresco.repo.dictionary.constraint.ListOfValuesConstraint;
import org.alfresco.repo.node.NodeServicePolicies;
import org.alfresco.repo.policy.JavaBehaviour;
import org.alfresco.repo.policy.PolicyComponent;
import org.alfresco.repo.transaction.AlfrescoTransactionSupport;
import org.alfresco.repo.transaction.TransactionListenerAdapter;
import org.alfresco.repo.transaction.TransactionalResourceHelper;
import org.alfresco.service.cmr.dictionary.DataTypeDefinition;
import org.alfresco.service.cmr.dictionary.DictionaryService;
import org.alfresco.service.cmr.dictionary.PropertyDefinition;
@@ -66,12 +70,16 @@ import org.springframework.beans.factory.InitializingBean;
*/
public class StoreSelectorAspectContentStore
extends AbstractRoutingContentStore
implements InitializingBean, NodeServicePolicies.OnUpdatePropertiesPolicy
implements InitializingBean,
NodeServicePolicies.OnUpdatePropertiesPolicy,
NodeServicePolicies.OnAddAspectPolicy
{
private static final String ERR_INVALID_DEFAULT_STORE = "content.routing.err.invalid_default_store";
private static final String KEY_CONTENT_MOVE_DETAILS = "StoreSelectorAspectContentStore.ContentMoveDetails";
private static Log logger = LogFactory.getLog(StoreSelectorAspectContentStore.class);
private ContentMoveTransactionListener transactionListener;
private NodeService nodeService;
private PolicyComponent policyComponent;
private DictionaryService dictionaryService;
@@ -151,11 +159,19 @@ public class StoreSelectorAspectContentStore
{
AlfrescoRuntimeException.create(ERR_INVALID_DEFAULT_STORE, defaultStoreName, storesByName.keySet());
}
// Register to receive change updates relevant to the aspect
// Register to receive property change updates
policyComponent.bindClassBehaviour(
QName.createQName(NamespaceService.ALFRESCO_URI, "onAddAspect"),
ContentModel.ASPECT_STORE_SELECTOR,
new JavaBehaviour(this, "onAddAspect"));
policyComponent.bindClassBehaviour(
QName.createQName(NamespaceService.ALFRESCO_URI, "onUpdateProperties"),
ContentModel.ASPECT_STORE_SELECTOR,
new JavaBehaviour(this, "onUpdateProperties"));
new JavaBehaviour(this, "onUpdateProperties"));
// Construct the transaction listener that will be bound in
transactionListener = new ContentMoveTransactionListener();
}
@Override
@@ -218,9 +234,53 @@ public class StoreSelectorAspectContentStore
return storesByName.get(storeName);
}
/**
* Class to carry info into the post-transaction phase
*/
private static class ContentMoveDetail
{
private final ContentStore oldStore;
private final ContentStore newStore;
private final String contentUrl;
private ContentMoveDetail(ContentStore oldStore, ContentStore newStore, String contentUrl)
{
this.oldStore = oldStore;
this.newStore = newStore;
this.contentUrl = contentUrl;
}
}
/**
* Ensures that the content is copied between stores only if the transaction is successful.
*
* @author Derek Hulley
* @since 3.2
*/
private class ContentMoveTransactionListener extends TransactionListenerAdapter
{
@Override
public void afterCommit()
{
List<ContentMoveDetail> contentMoveDetails = TransactionalResourceHelper.getList(KEY_CONTENT_MOVE_DETAILS);
for (ContentMoveDetail contentMoveDetail : contentMoveDetails)
{
moveContent(contentMoveDetail.oldStore, contentMoveDetail.newStore, contentMoveDetail.contentUrl);
}
}
}
/**
* Move content from the old store to the new store
*/
private void scheduleContentMove(ContentStore oldStore, ContentStore newStore, String contentUrl)
{
// Add the details of the copy to the transaction
List<ContentMoveDetail> contentMoveDetails = TransactionalResourceHelper.getList(KEY_CONTENT_MOVE_DETAILS);
ContentMoveDetail detail = new ContentMoveDetail(oldStore, newStore, contentUrl);
contentMoveDetails.add(detail);
// Bind the listener to the transaction
AlfrescoTransactionSupport.bindListener(transactionListener);
}
private void moveContent(ContentStore oldStore, ContentStore newStore, String contentUrl)
{
ContentReader reader = oldStore.getReader(contentUrl);
@@ -246,6 +306,21 @@ public class StoreSelectorAspectContentStore
}
}
/**
* Ensures that all content is moved to the correct store.
* <p>
* Spoofs a call to {@link #onUpdateProperties(NodeRef, Map, Map)}.
*/
public void onAddAspect(NodeRef nodeRef, QName aspectTypeQName)
{
Map<QName, Serializable> after = nodeService.getProperties(nodeRef);
// Pass the call through. It is only interested in a single property.
onUpdateProperties(
nodeRef,
Collections.<QName, Serializable>emptyMap(),
after);
}
/**
* Keeps the content in the correct store based on changes to the <b>cm:storeName</b> property
*/
@@ -322,7 +397,7 @@ public class StoreSelectorAspectContentStore
// Move content from the old store to the new store
for (String contentUrl : contentUrls)
{
moveContent(oldStore, newStore, contentUrl);
scheduleContentMove(oldStore, newStore, contentUrl);
}
}

View File

@@ -250,4 +250,38 @@ public class StoreSelectorAspectContentStoreTest extends TestCase
assertFalse("Store1 should NOT have content", storesByName.get(STORE_ONE).exists(contentUrl));
assertTrue("Store2 should have content", storesByName.get(STORE_TWO).exists(contentUrl));
}
/**
* Ensure that the store move does not occur if an invalid <b>cm:storeName</b> property us used.
*/
public void testPropertyChangeWithIntegrityError() throws Exception
{
// setStoreNameProperty(STORE_ONE);
String contentUrl = writeToFile();
assertTrue("Store1 should have content", storesByName.get(STORE_ONE).exists(contentUrl));
assertFalse("Store2 should NOT have content", storesByName.get(STORE_TWO).exists(contentUrl));
// Change the property
setStoreNameProperty(STORE_TWO);
// It should have moved
assertFalse("Store1 should NOT have content", storesByName.get(STORE_ONE).exists(contentUrl));
assertTrue("Store2 should have content", storesByName.get(STORE_TWO).exists(contentUrl));
RetryingTransactionCallback<Object> setInvalidStoreNameCallback = new RetryingTransactionCallback<Object>()
{
public Object execute() throws Throwable
{
setStoreNameProperty("bogus");
return null;
}
};
try
{
transactionService.getRetryingTransactionHelper().doInTransaction(setInvalidStoreNameCallback, false, true);
fail("Expected integrity error for bogus store name");
}
catch (IntegrityException e)
{
// Expected
}
assertTrue("Store2 should have content", storesByName.get(STORE_TWO).exists(contentUrl));
}
}