diff --git a/config/alfresco/content-services-context.xml b/config/alfresco/content-services-context.xml
index 9da0c7486d..82670e0520 100644
--- a/config/alfresco/content-services-context.xml
+++ b/config/alfresco/content-services-context.xml
@@ -113,6 +113,12 @@
+
+
+
+
+
+
diff --git a/source/java/org/alfresco/repo/content/routing/StoreSelectorAspectContentStore.java b/source/java/org/alfresco/repo/content/routing/StoreSelectorAspectContentStore.java
index fb8647513b..94b9cdcae3 100644
--- a/source/java/org/alfresco/repo/content/routing/StoreSelectorAspectContentStore.java
+++ b/source/java/org/alfresco/repo/content/routing/StoreSelectorAspectContentStore.java
@@ -24,7 +24,9 @@
*/
package org.alfresco.repo.content.routing;
+import java.io.Serializable;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.List;
import java.util.Map;
@@ -35,8 +37,21 @@ import org.alfresco.repo.content.ContentContext;
import org.alfresco.repo.content.ContentStore;
import org.alfresco.repo.content.NodeContentContext;
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.service.cmr.dictionary.DataTypeDefinition;
+import org.alfresco.service.cmr.dictionary.DictionaryService;
+import org.alfresco.service.cmr.dictionary.PropertyDefinition;
+import org.alfresco.service.cmr.repository.ContentData;
+import org.alfresco.service.cmr.repository.ContentReader;
+import org.alfresco.service.cmr.repository.ContentWriter;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
+import org.alfresco.service.cmr.repository.datatype.DefaultTypeConverter;
+import org.alfresco.service.namespace.NamespaceService;
+import org.alfresco.service.namespace.QName;
+import org.alfresco.util.EqualsHelper;
import org.alfresco.util.PropertyCheck;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@@ -49,13 +64,17 @@ import org.springframework.beans.factory.InitializingBean;
* @author Derek Hulley
* @since 3.2
*/
-public class StoreSelectorAspectContentStore extends AbstractRoutingContentStore implements InitializingBean
+public class StoreSelectorAspectContentStore
+ extends AbstractRoutingContentStore
+ implements InitializingBean, NodeServicePolicies.OnUpdatePropertiesPolicy
{
private static final String ERR_INVALID_DEFAULT_STORE = "content.routing.err.invalid_default_store";
private static Log logger = LogFactory.getLog(StoreSelectorAspectContentStore.class);
private NodeService nodeService;
+ private PolicyComponent policyComponent;
+ private DictionaryService dictionaryService;
private Map storesByName;
private List stores;
private String defaultStoreName;
@@ -72,6 +91,22 @@ public class StoreSelectorAspectContentStore extends AbstractRoutingContentStore
this.nodeService = nodeService;
}
+ /**
+ * @param policyComponent register to receive updates to the cm:storeSelector aspect
+ */
+ public void setPolicyComponent(PolicyComponent policyComponent)
+ {
+ this.policyComponent = policyComponent;
+ }
+
+ /**
+ * @param dictionaryService used to check for content property types
+ */
+ public void setDictionaryService(DictionaryService dictionaryService)
+ {
+ this.dictionaryService = dictionaryService;
+ }
+
/**
* @param storesByName a map of content stores keyed by a common name
*/
@@ -116,6 +151,11 @@ public class StoreSelectorAspectContentStore extends AbstractRoutingContentStore
{
AlfrescoRuntimeException.create(ERR_INVALID_DEFAULT_STORE, defaultStoreName, storesByName.keySet());
}
+ // Register to receive property change updates
+ policyComponent.bindClassBehaviour(
+ QName.createQName(NamespaceService.ALFRESCO_URI, "onUpdateProperties"),
+ ContentModel.ASPECT_STORE_SELECTOR,
+ new JavaBehaviour(this, "onUpdateProperties"));
}
@Override
@@ -166,6 +206,126 @@ public class StoreSelectorAspectContentStore extends AbstractRoutingContentStore
return store;
}
+ /**
+ * Helper method to select a store, taking into account null and invalid values.
+ */
+ private ContentStore selectStore(String storeName)
+ {
+ if (storeName == null || !storesByName.containsKey(storeName))
+ {
+ storeName = defaultStoreName;
+ }
+ return storesByName.get(storeName);
+ }
+
+ /**
+ * Move content from the old store to the new store
+ */
+ private void moveContent(ContentStore oldStore, ContentStore newStore, String contentUrl)
+ {
+ ContentReader reader = oldStore.getReader(contentUrl);
+ if (!reader.exists())
+ {
+ // Nothing to copy
+ return;
+ }
+ ContentContext ctx = new ContentContext(null, contentUrl);
+ ContentWriter writer = newStore.getWriter(ctx);
+ // Copy it
+ writer.putContent(reader);
+ // Remove the old content
+ oldStore.delete(contentUrl);
+ // Done
+ if (logger.isDebugEnabled())
+ {
+ logger.debug(
+ "Store selector moved content: \n" +
+ " Old store: " + oldStore + "\n" +
+ " New Store: " + newStore + "\n" +
+ " Content: " + contentUrl);
+ }
+ }
+
+ /**
+ * Keeps the content in the correct store based on changes to the cm:storeName property
+ */
+ public void onUpdateProperties(
+ NodeRef nodeRef,
+ Map before,
+ Map after)
+ {
+ String storeNameBefore = (String) before.get(ContentModel.PROP_STORE_NAME);
+ String storeNameAfter = (String) after.get(ContentModel.PROP_STORE_NAME);
+ if (EqualsHelper.nullSafeEquals(storeNameBefore, storeNameAfter))
+ {
+ // We're not interested in the change
+ return;
+ }
+ // Find out which store to move the content to
+ ContentStore oldStore = selectStore(storeNameBefore);
+ ContentStore newStore = selectStore(storeNameAfter);
+ // Don't do anything if the store did not change
+ if (oldStore == newStore)
+ {
+ return;
+ }
+ // Find all content properties and move the content
+ List contentUrls = new ArrayList(1);
+ for (QName propertyQName : after.keySet())
+ {
+ PropertyDefinition propDef = dictionaryService.getProperty(propertyQName);
+ if (propDef == null)
+ {
+ // Ignore
+ continue;
+ }
+ if (!propDef.getDataType().getName().equals(DataTypeDefinition.CONTENT))
+ {
+ // It is not content
+ continue;
+ }
+ // The property value
+ Serializable propertyValue = after.get(propertyQName);
+ if (propertyValue == null)
+ {
+ // Ignore missing values
+ }
+ // Get the content URLs, being sensitive to collections
+ if (propDef.isMultiValued())
+ {
+ Collection contentValues =
+ DefaultTypeConverter.INSTANCE.getCollection(ContentData.class, propertyValue);
+ if (contentValues.size() == 0)
+ {
+ // No content
+ continue;
+ }
+ for (ContentData contentValue : contentValues)
+ {
+ String contentUrl = contentValue.getContentUrl();
+ if (contentUrl != null)
+ {
+ contentUrls.add(contentUrl);
+ }
+ }
+ }
+ else
+ {
+ ContentData contentValue = DefaultTypeConverter.INSTANCE.convert(ContentData.class, propertyValue);
+ String contentUrl = contentValue.getContentUrl();
+ if (contentUrl != null)
+ {
+ contentUrls.add(contentUrl);
+ }
+ }
+ }
+ // Move content from the old store to the new store
+ for (String contentUrl : contentUrls)
+ {
+ moveContent(oldStore, newStore, contentUrl);
+ }
+ }
+
/**
* A constraint that acts as a list of values, where the values are the store names
* injected into the {@link StoreSelectorAspectContentStore}.
diff --git a/source/java/org/alfresco/repo/content/routing/StoreSelectorAspectContentStoreTest.java b/source/java/org/alfresco/repo/content/routing/StoreSelectorAspectContentStoreTest.java
index d6ab836277..e565af236a 100644
--- a/source/java/org/alfresco/repo/content/routing/StoreSelectorAspectContentStoreTest.java
+++ b/source/java/org/alfresco/repo/content/routing/StoreSelectorAspectContentStoreTest.java
@@ -237,4 +237,17 @@ public class StoreSelectorAspectContentStoreTest extends TestCase
assertTrue("Content not in store " + storeName, store.exists(contentUrl));
}
}
+
+ public void testPropertyChange() 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));
+ }
}