MOB-29: Content Storage Lifecycle Management

- Some collapsing of common code in Constraint hierarchy.
 - Added aspect 'cm:storeSelector' with property 'cm:storeName'.
 - Defined a new, unused content store 'storeSelectorContentStore' (see content-services-context.xml).
   If this store is used, then setting the 'cm:storeName' property will force content to be stored in one
   of the named stores.  A LIST constraint is indirectly enforced for the 'cm:storeName' property.

git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@14329 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261
This commit is contained in:
Derek Hulley
2009-05-15 04:36:20 +00:00
parent d1d35999de
commit e1ada58fef
27 changed files with 982 additions and 166 deletions

View File

@@ -37,6 +37,7 @@ import org.alfresco.repo.content.metadata.OpenOfficeMetadataExtracterTest;
import org.alfresco.repo.content.metadata.PdfBoxMetadataExtracterTest;
import org.alfresco.repo.content.replication.ContentStoreReplicatorTest;
import org.alfresco.repo.content.replication.ReplicatingContentStoreTest;
import org.alfresco.repo.content.routing.StoreSelectorAspectContentStoreTest;
import org.alfresco.repo.content.transform.BinaryPassThroughContentTransformerTest;
import org.alfresco.repo.content.transform.ComplexContentTransformerTest;
import org.alfresco.repo.content.transform.ContentTransformerRegistryTest;
@@ -96,6 +97,7 @@ public class ContentTestSuite extends TestSuite
suite.addTestSuite(MimetypeMapTest.class);
suite.addTestSuite(RoutingContentServiceTest.class);
suite.addTestSuite(RoutingContentStoreTest.class);
suite.addTestSuite(StoreSelectorAspectContentStoreTest.class);
return suite;
}

View File

@@ -62,6 +62,19 @@ public class NodeContentContext extends ContentContext
this.propertyQName = propertyQName;
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder(128);
sb.append("NodeContentContext")
.append("[ contentUrl=").append(getContentUrl())
.append(", existing=").append((getExistingContentReader() == null ? false : true))
.append(", nodeRef=").append(nodeRef)
.append(", propertyQName=").append(propertyQName)
.append("]");
return sb.toString();
}
/**
* @return Returns the node holding the content metadata
*/

View File

@@ -0,0 +1,205 @@
/*
* Copyright (C) 2005-2009 Alfresco Software Limited.
*
* This program 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.
* This program 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 this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* As a special exception to the terms and conditions of version 2.0 of
* the GPL, you may redistribute this Program in connection with Free/Libre
* and Open Source Software ("FLOSS") applications as described in Alfresco's
* FLOSS exception. You should have recieved a copy of the text describing
* the FLOSS exception, and it is also available here:
* http://www.alfresco.com/legal/licensing"
*/
package org.alfresco.repo.content.routing;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.model.ContentModel;
import org.alfresco.repo.content.AbstractRoutingContentStore;
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.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.util.PropertyCheck;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;
/**
* Implementation of a {@link AbstractRoutingContentStore routing content store} that diverts
* and moves content based on the <b>cm:storeSelector</b> aspect.
*
* @author Derek Hulley
* @since 3.2
*/
public class StoreSelectorAspectContentStore extends AbstractRoutingContentStore implements InitializingBean
{
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 Map<String, ContentStore> storesByName;
private List<ContentStore> stores;
private String defaultStoreName;
public StoreSelectorAspectContentStore()
{
}
/**
* @param nodeService the service to access the properties
*/
public void setNodeService(NodeService nodeService)
{
this.nodeService = nodeService;
}
/**
* @param storesByName a map of content stores keyed by a common name
*/
public void setStoresByName(Map<String, ContentStore> storesByName)
{
this.storesByName = storesByName;
this.stores = new ArrayList<ContentStore>(storesByName.values());
}
/**
* @return Returns the stores keyed by store name
*/
public Map<String, ContentStore> getStoresByName()
{
return storesByName;
}
/**
* Set the name of the store to select if the content being created is not associated
* with any specific value in the <b>cm:storeSelector</b> or if the aspect is not
* present.
*
* @param defaultStoreName the name of one of the stores
*
* @see #setStoresByName(Map)
*/
public void setDefaultStoreName(String defaultStoreName)
{
this.defaultStoreName = defaultStoreName;
}
/**
* Checks that the required properties are present
*/
public void afterPropertiesSet() throws Exception
{
PropertyCheck.mandatory(this, "nodeService", nodeService);
PropertyCheck.mandatory(this, "storesByName", storesByName);
PropertyCheck.mandatory(this, "defaultStoreName", defaultStoreName);
// Check that the default store name is valid
if (storesByName.get(defaultStoreName) == null)
{
AlfrescoRuntimeException.create(ERR_INVALID_DEFAULT_STORE, defaultStoreName, storesByName.keySet());
}
}
@Override
protected List<ContentStore> getAllStores()
{
return stores;
}
@Override
protected ContentStore selectWriteStore(ContentContext ctx)
{
ContentStore store;
String storeNameProp;
if (!(ctx instanceof NodeContentContext))
{
storeNameProp = "<NodeRef not available>";
store = storesByName.get(defaultStoreName);
}
else
{
NodeRef nodeRef = ((NodeContentContext) ctx).getNodeRef(); // Never null
storeNameProp = (String) nodeService.getProperty(nodeRef, ContentModel.PROP_STORE_NAME);
if (storeNameProp == null)
{
storeNameProp = "<null>";
store = storesByName.get(defaultStoreName);
}
else
{
store = storesByName.get(storeNameProp);
if (store == null)
{
// There was no store with that name
storeNameProp = "<unmapped store: " + storeNameProp + ">";
store = storesByName.get(defaultStoreName);
}
}
}
// Done
if (logger.isDebugEnabled())
{
logger.debug(
"ContentStore selected: \n" +
" Node context: " + ctx + "\n" +
" Store name: " + storeNameProp + "\n" +
" Store Selected: " + store);
}
return store;
}
/**
* A constraint that acts as a list of values, where the values are the store names
* injected into the {@link StoreSelectorAspectContentStore}.
* <p>
* If the store is not active or is incorrectly configured, then this constraint
* will contain a single value of 'Default'. Any attempt to set another value will
* lead to constraint failures.
*
* @author Derek Hulley
* @since 3.2
*/
public static class StoreSelectorConstraint extends ListOfValuesConstraint
{
private StoreSelectorAspectContentStore store;
/**
* Required default constructor
*/
public StoreSelectorConstraint()
{
}
public void setStore(StoreSelectorAspectContentStore store)
{
this.store = store;
}
@Override
public void initialize()
{
checkPropertyNotNull("store", store);
List<String> allowedValues = new ArrayList<String>(store.getStoresByName().keySet());
super.setAllowedValues(allowedValues);
// Now initialize as we have set the LOV
super.initialize();
}
}
}

View File

@@ -0,0 +1,240 @@
/*
* Copyright (C) 2005-2009 Alfresco Software Limited.
*
* This program 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.
* This program 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 this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* As a special exception to the terms and conditions of version 2.0 of
* the GPL, you may redistribute this Program in connection with Free/Libre
* and Open Source Software ("FLOSS") applications as described in Alfresco's
* FLOSS exception. You should have recieved a copy of the text describing
* the FLOSS exception, and it is also available here:
* http://www.alfresco.com/legal/licensing"
*/
package org.alfresco.repo.content.routing;
import java.util.HashMap;
import java.util.Map;
import junit.framework.TestCase;
import org.alfresco.model.ContentModel;
import org.alfresco.repo.content.ContentServiceImpl;
import org.alfresco.repo.content.ContentStore;
import org.alfresco.repo.content.filestore.FileContentStore;
import org.alfresco.repo.content.routing.StoreSelectorAspectContentStore.StoreSelectorConstraint;
import org.alfresco.repo.node.integrity.IntegrityException;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
import org.alfresco.service.ServiceRegistry;
import org.alfresco.service.cmr.model.FileFolderService;
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.StoreRef;
import org.alfresco.service.transaction.TransactionService;
import org.alfresco.util.ApplicationContextHelper;
import org.alfresco.util.TempFileProvider;
import org.springframework.context.ConfigurableApplicationContext;
/**
* Tests {@link StoreSelectorAspectContentStore}
*
* @author Derek Hulley
* @since 3.2
*/
public class StoreSelectorAspectContentStoreTest extends TestCase
{
private static final String STORE_ONE = "Store1";
private static final String STORE_TWO = "Store2";
private static final String STORE_THREE = "Store3";
private static ConfigurableApplicationContext ctx =
(ConfigurableApplicationContext) ApplicationContextHelper.getApplicationContext();
private TransactionService transactionService;
private NodeService nodeService;
private FileFolderService fileFolderService;
private Map<String, ContentStore> storesByName;
private FileContentStore fileStore1;
private FileContentStore fileStore2;
private FileContentStore fileStore3;
private StoreSelectorAspectContentStore store;
private NodeRef contentNodeRef;
@Override
public void setUp() throws Exception
{
super.setUp();
ServiceRegistry serviceRegistry = (ServiceRegistry) ctx.getBean(ServiceRegistry.SERVICE_REGISTRY);
transactionService = serviceRegistry.getTransactionService();
nodeService = serviceRegistry.getNodeService();
fileFolderService = serviceRegistry.getFileFolderService();
AuthenticationUtil.pushAuthentication();
AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName());
fileStore1 = new FileContentStore(
ctx,
TempFileProvider.getSystemTempDir() + "/fileStore1");
fileStore2 = new FileContentStore(
ctx,
TempFileProvider.getSystemTempDir() + "/fileStore2");
fileStore3 = new FileContentStore(
ctx,
TempFileProvider.getSystemTempDir() + "/fileStore3");
storesByName = new HashMap<String, ContentStore>(7);
storesByName.put(STORE_ONE, fileStore1);
storesByName.put(STORE_TWO, fileStore2);
storesByName.put(STORE_THREE, fileStore3);
store = (StoreSelectorAspectContentStore) ctx.getBean("storeSelectorContentStore");
store.setStoresByName(storesByName);
store.setDefaultStoreName(STORE_ONE);
store.afterPropertiesSet();
// Force the constraint to re-initialize
StoreSelectorConstraint storeConstraint = (StoreSelectorConstraint) ctx.getBean("storeSelectorContentStore.constraint");
storeConstraint.initialize();
// Change the content service's default store
ContentServiceImpl contentService = (ContentServiceImpl) ctx.getBean("contentService");
contentService.setStore(store);
// Create a content node
RetryingTransactionCallback<NodeRef> makeNodeCallback = new RetryingTransactionCallback<NodeRef>()
{
public NodeRef execute() throws Throwable
{
StoreRef storeRef = nodeService.createStore(
StoreRef.PROTOCOL_TEST,
getName() + "_" + System.currentTimeMillis());
NodeRef rootNodeRef = nodeService.getRootNode(storeRef);
// Create a folder
NodeRef folderNodeRef = nodeService.createNode(
rootNodeRef,
ContentModel.ASSOC_CHILDREN,
ContentModel.ASSOC_CHILDREN,
ContentModel.TYPE_FOLDER).getChildRef();
// Add some content
return fileFolderService.create(
folderNodeRef,
getName() + ".txt",
ContentModel.TYPE_CONTENT).getNodeRef();
}
};
contentNodeRef = transactionService.getRetryingTransactionHelper().doInTransaction(makeNodeCallback);
}
@Override
public void tearDown() throws Exception
{
AuthenticationUtil.popAuthentication();
}
/**
* Writes to the file
* @return Returns the new content URL
*/
private String writeToFile()
{
RetryingTransactionCallback<String> writeContentCallback = new RetryingTransactionCallback<String>()
{
public String execute() throws Throwable
{
ContentWriter writer = fileFolderService.getWriter(contentNodeRef);
writer.putContent("Some test content");
return writer.getContentUrl();
}
};
return transactionService.getRetryingTransactionHelper().doInTransaction(writeContentCallback);
}
/**
* Set the name of the store that must hold the content
* @param storeName the name of the store
*/
private void setStoreNameProperty(String storeName)
{
// The nodeService is transactional
nodeService.setProperty(contentNodeRef, ContentModel.PROP_STORE_NAME, storeName);
}
/**
* Ensure that a <tt>null</tt> <b>cm:storeName</b> property is acceptable.
*/
public void testNullStoreNameProperty() throws Exception
{
try
{
setStoreNameProperty(null);
}
catch (Throwable e)
{
throw new Exception("Failed to set store name property to null", e);
}
}
/**
* Ensure that an invalid <b>cm:storeName</b> property is kicked out.
*/
public void testInvalidStoreNameProperty() throws Exception
{
RetryingTransactionCallback<Object> setInvalidStoreNameCallback = new RetryingTransactionCallback<Object>()
{
public Object execute() throws Throwable
{
setStoreNameProperty("bogus");
return null;
}
};
try
{
transactionService.getRetryingTransactionHelper().doInTransaction(setInvalidStoreNameCallback, false, true);
setStoreNameProperty("bogus");
fail("Expected integrity error for bogus store name");
}
catch (IntegrityException e)
{
// Expected
}
}
/**
* Check that the default store is used if the property is not set
*/
public void testWriteWithoutAspect() throws Exception
{
String contentUrl = writeToFile();
// The content should be in the default store
assertTrue("Default store does not have content", fileStore1.exists(contentUrl));
assertFalse("Mapped store should not have content", fileStore2.exists(contentUrl));
assertFalse("Mapped store should not have content", fileStore3.exists(contentUrl));
}
public void testSimpleWritesWithAspect() throws Exception
{
for (Map.Entry<String, ContentStore> entry : storesByName.entrySet())
{
String storeName = entry.getKey();
ContentStore store = entry.getValue();
setStoreNameProperty(storeName);
String contentUrl = writeToFile();
assertTrue("Content not in store " + storeName, store.exists(contentUrl));
}
}
}