orderedList = reorderChildAssocs(results);
+ // Done
+ return orderedList;
+ }
+
public ChildAssociationRef getPrimaryParent(NodeRef nodeRef) throws InvalidNodeRefException
{
// Get the node
diff --git a/source/java/org/alfresco/repo/node/db/NodeDaoService.java b/source/java/org/alfresco/repo/node/db/NodeDaoService.java
index 1b445742bd..4d7af7a2d1 100644
--- a/source/java/org/alfresco/repo/node/db/NodeDaoService.java
+++ b/source/java/org/alfresco/repo/node/db/NodeDaoService.java
@@ -232,6 +232,31 @@ public interface NodeDaoService
);
}
+ /**
+ * Interface used to iterate over results from child association queries
+ * @author Derek Hulley
+ */
+ public interface ChildAssocRefQueryCallbackFilter extends ChildAssocRefQueryCallback
+ {
+ /**
+ * Method to handle raw query results and decide if the result should be filtered out or not.
+ * If true is returned, the standard {@link #handle(Pair, Pair, Pair) handler} method
+ * will be called. If false is returned, then the query result will be skipped.
+ *
+ * This provides a quick way to filter out results without having to pull in full entities.
+ *
+ * @return Return true if the standard {@link #handle(Pair, Pair, Pair) handler}
+ * method should be called, or false to filter the result out.
+ */
+ boolean isDesiredRow(
+ Pair childAssocPair,
+ Pair parentNodePair,
+ Pair childNodePair,
+ String assocChildNodeName,
+ Long assocChildNodeNameCrc
+ );
+ }
+
/**
* Get a collection of all child association references for a given parent node.
*
@@ -249,11 +274,23 @@ public interface NodeDaoService
/**
* Get a collection of all child association references for a given parent node.
*
- * @param parentNodeId the parent node
+ * @param parentNodeId the parent node
* @param resultsCallback the callback that will be called with the results
*/
@DirtySessionAnnotation(markDirty=false)
public void getChildAssocs(Long parentNodeId, QName assocQName, ChildAssocRefQueryCallback resultsCallback);
+
+ /**
+ * Get a collection of all child associations references where the child name is an exact match.
+ * This method only works if the association type fundamentally supports unique-name enforcement.
+ *
+ * @param parentNodeId the parent node
+ * @param assocTypeQName the type of the association to check.
+ * @param childNames the names of the child nodes (cm:name). These will be matched exactly.
+ * @param resultsCallback the callback that will be called with the results
+ */
+ @DirtySessionAnnotation(markDirty=false)
+ public void getChildAssocs(Long parentNodeId, QName assocTypeQName, Collection childNames, ChildAssocRefQueryCallback resultsCallback);
@DirtySessionAnnotation(markDirty=false)
public void getChildAssocsByTypeQNames(
@@ -314,8 +351,7 @@ public interface NodeDaoService
public void getNodesWithAspect(QName aspectQName, Long minNodeId, int count, NodeRefQueryCallback resultsCallback);
/**
- * @return Returns an association matching the given parent, type and child name - or null
- * if not found
+ * @return Returns an association matching the given parent, type and child name (cm:name) - or null if not found
*/
@DirtySessionAnnotation(markDirty=false)
public Pair getChildAssoc(Long parentNodeId, QName assocTypeQName, String childName);
diff --git a/source/java/org/alfresco/repo/node/db/hibernate/HibernateNodeDaoServiceImpl.java b/source/java/org/alfresco/repo/node/db/hibernate/HibernateNodeDaoServiceImpl.java
index d9b4778d38..6645160d5b 100644
--- a/source/java/org/alfresco/repo/node/db/hibernate/HibernateNodeDaoServiceImpl.java
+++ b/source/java/org/alfresco/repo/node/db/hibernate/HibernateNodeDaoServiceImpl.java
@@ -133,6 +133,7 @@ public class HibernateNodeDaoServiceImpl extends HibernateDaoSupport implements
private static final String QUERY_GET_CHILD_NODE_IDS = "node.GetChildNodeIds";
private static final String QUERY_GET_CHILD_ASSOCS_BY_ALL = "node.GetChildAssocsByAll";
private static final String QUERY_GET_CHILD_ASSOC_BY_TYPE_AND_NAME = "node.GetChildAssocByTypeAndName";
+ private static final String QUERY_GET_CHILD_ASSOC_REFS_BY_TYPE_AND_NAME_LIST = "node.GetChildAssocRefsByTypeAndNameList";
private static final String QUERY_GET_CHILD_ASSOC_REFS = "node.GetChildAssocRefs";
private static final String QUERY_GET_CHILD_ASSOC_REFS_BY_QNAME = "node.GetChildAssocRefsByQName";
private static final String QUERY_GET_CHILD_ASSOC_REFS_BY_TYPEQNAMES = "node.GetChildAssocRefsByTypeQNames";
@@ -2059,6 +2060,92 @@ public class HibernateNodeDaoServiceImpl extends HibernateDaoSupport implements
// Done
}
+ public void getChildAssocs(final Long parentNodeId, QName assocTypeQName, Collection childNames, final ChildAssocRefQueryCallback resultsCallback)
+ {
+ /*
+ * The child names are converted to the stardard form (shortened, lower-case) and used directly in an IN clause.
+ * In order to guarantee uniqueness, the CRC for each matching, stardard-form name is checked for a match before
+ * the result is passed to the client's callback handler.
+ */
+
+ // Shortcut
+ if (childNames.size() == 0)
+ {
+ return;
+ }
+ else if (childNames.size() > 1000)
+ {
+ throw new IllegalArgumentException("Unable to process more than 1000 child names in getChildAssocs");
+ }
+ final Pair assocTypeQNamePair = qnameDAO.getQName(assocTypeQName);
+ if (assocTypeQNamePair == null)
+ {
+ return;
+ }
+
+ // Convert the child names and compile the CRC values
+ final Set crcValues = new HashSet(childNames.size() * 2);
+ final Set nameValues = new HashSet(childNames.size() * 2);
+ for (String childName : childNames)
+ {
+ String childNameLower = childName.toLowerCase();
+ String childNameShort = getShortName(childNameLower);
+ Long childNameLowerCrc = new Long(getCrc(childNameLower));
+ crcValues.add(childNameLowerCrc);
+ nameValues.add(childNameShort);
+ }
+
+ Node parentNode = getNodeNotNull(parentNodeId);
+ HibernateCallback callback = new HibernateCallback()
+ {
+ public Object doInHibernate(Session session)
+ {
+ Query query = session
+ .getNamedQuery(HibernateNodeDaoServiceImpl.QUERY_GET_CHILD_ASSOC_REFS_BY_TYPE_AND_NAME_LIST)
+ .setLong("parentId", parentNodeId)
+ .setLong("typeQNameId", assocTypeQNamePair.getFirst())
+ .setParameterList("childNodeNames", nameValues);
+ DirtySessionMethodInterceptor.setQueryFlushMode(session, query);
+ return query.scroll(ScrollMode.FORWARD_ONLY);
+ }
+ };
+
+ // Create an extended callback to filter out the crc values
+ ChildAssocRefQueryCallbackFilter filterCallback = new ChildAssocRefQueryCallbackFilter()
+ {
+ public boolean isDesiredRow(
+ Pair childAssocPair,
+ Pair parentNodePair,
+ Pair childNodePair,
+ String assocChildNodeName, Long assocChildNodeNameCrc)
+ {
+ // The CRC value must be in the list
+ return crcValues.contains(assocChildNodeNameCrc);
+ }
+ /**
+ * Defers to the client's handler
+ */
+ public boolean handle(Pair childAssocPair, Pair parentNodePair, Pair childNodePair)
+ {
+ return resultsCallback.handle(childAssocPair, parentNodePair, childNodePair);
+ }
+ };
+
+ ScrollableResults queryResults = null;
+ try
+ {
+ queryResults = (ScrollableResults) getHibernateTemplate().execute(callback);
+ convertToChildAssocRefs(parentNode, queryResults, filterCallback);
+ }
+ finally
+ {
+ if (queryResults != null)
+ {
+ queryResults.close();
+ }
+ }
+ }
+
public void getChildAssocsByTypeQNames(
final Long parentNodeId,
final List assocTypeQNames,
@@ -2360,12 +2447,14 @@ public class HibernateNodeDaoServiceImpl extends HibernateDaoSupport implements
String assocQNameNamespace = qnameDAO.getNamespace((Long) row[2]).getSecond();
String assocQNameLocalName = (String) row[3];
QName assocQName = QName.createQName(assocQNameNamespace, assocQNameLocalName);
- Boolean assocIsPrimary = (Boolean) row[4];
- Integer assocIndex = (Integer) row[5];
- Long childNodeId = (Long) row[6];
- String childProtocol = (String) row[7];
- String childIdentifier = (String) row[8];
- String childUuid = (String) row[9];
+ String assocChildNodeName = (String) row[4];
+ Long assocChildNodeNameCrc = (Long) row[5];
+ Boolean assocIsPrimary = (Boolean) row[6];
+ Integer assocIndex = (Integer) row[7];
+ Long childNodeId = (Long) row[8];
+ String childProtocol = (String) row[9];
+ String childIdentifier = (String) row[10];
+ String childUuid = (String) row[11];
NodeRef childNodeRef = new NodeRef(new StoreRef(childProtocol, childIdentifier), childUuid);
ChildAssociationRef assocRef = new ChildAssociationRef(
assocTypeQName,
@@ -2376,6 +2465,22 @@ public class HibernateNodeDaoServiceImpl extends HibernateDaoSupport implements
assocIndex.intValue());
Pair assocPair = new Pair(assocId, assocRef);
Pair childNodePair = new Pair(childNodeId, childNodeRef);
+ // Check if we are doing a filtering callback
+ if (resultsCallback instanceof ChildAssocRefQueryCallbackFilter)
+ {
+ ChildAssocRefQueryCallbackFilter filterCallback = (ChildAssocRefQueryCallbackFilter) resultsCallback;
+ boolean process = filterCallback.isDesiredRow(
+ assocPair,
+ parentNodePair,
+ childNodePair,
+ assocChildNodeName,
+ assocChildNodeNameCrc);
+ if (!process)
+ {
+ // The result is filtered out
+ continue;
+ }
+ }
// Call back
resultsCallback.handle(assocPair, parentNodePair, childNodePair);
}
diff --git a/source/java/org/alfresco/repo/version/NodeServiceImpl.java b/source/java/org/alfresco/repo/version/NodeServiceImpl.java
index 7380353e7a..ad5b2b7222 100644
--- a/source/java/org/alfresco/repo/version/NodeServiceImpl.java
+++ b/source/java/org/alfresco/repo/version/NodeServiceImpl.java
@@ -326,6 +326,7 @@ public class NodeServiceImpl implements NodeService, VersionModel
/**
* Translation for version store
*/
+ @SuppressWarnings("unchecked")
public Set getAspects(NodeRef nodeRef) throws InvalidNodeRefException
{
return new HashSet(
@@ -523,6 +524,15 @@ public class NodeServiceImpl implements NodeService, VersionModel
throw new UnsupportedOperationException(MSG_UNSUPPORTED);
}
+ /**
+ * @throws UnsupportedOperationException always
+ */
+ public List getChildrenByName(NodeRef nodeRef, QName assocTypeQName, Collection childNames)
+ {
+ // This operation is not supported for a version store
+ throw new UnsupportedOperationException(MSG_UNSUPPORTED);
+ }
+
/**
* @throws UnsupportedOperationException always
*/
diff --git a/source/java/org/alfresco/service/cmr/avm/AVMService.java b/source/java/org/alfresco/service/cmr/avm/AVMService.java
index b4660c2cf9..4dbc03269c 100644
--- a/source/java/org/alfresco/service/cmr/avm/AVMService.java
+++ b/source/java/org/alfresco/service/cmr/avm/AVMService.java
@@ -170,6 +170,21 @@ public interface AVMService
* @throws AVMWrongTypeException
*/
public SortedMap getDirectoryListing(AVMNodeDescriptor dir);
+
+ /**
+ * Get a non-recursive directory listing of a directory node
+ * identified by a node descriptor. Only return children matching the given pattern.
+ * '*' match any number of characters - equivalent to SQL '%'
+ * '?' match a single character - equivalent to SQL '_'
+ * '\' escape - valid sequences "\\", "\*" and "\?"
+ *
+ * @param dir The directory node descriptor.
+ * @param childPattern
+ * @return A sorted Map of names to node descriptors.
+ * @throws AVMNotFoundException
+ * @throws AVMWrongTypeException
+ */
+ public SortedMap getDirectoryListing(AVMNodeDescriptor dir, String childNamePattern);
/**
* Non-recursively get the names of nodes that have been deleted in
diff --git a/source/java/org/alfresco/service/cmr/repository/NodeService.java b/source/java/org/alfresco/service/cmr/repository/NodeService.java
index 211391fb08..a97bcece32 100644
--- a/source/java/org/alfresco/service/cmr/repository/NodeService.java
+++ b/source/java/org/alfresco/service/cmr/repository/NodeService.java
@@ -25,6 +25,7 @@
package org.alfresco.service.cmr.repository;
import java.io.Serializable;
+import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -531,6 +532,21 @@ public interface NodeService
QName assocTypeQName,
String childName);
+ /**
+ * Get the nodes with the given names within the context of the parent node.
+ *
+ * {@inheritDoc #getChildByName(NodeRef, QName, String)}
+ *
+ * @param childNames a collection of up to 1000 child names to match on
+ *
+ * @see {@link #getChildByName(NodeRef, QName, String)}
+ */
+ @Auditable(key = Auditable.Key.ARG_0 ,parameters = {"nodeRef", "assocTypeQName", "childName"})
+ public List getChildrenByName(
+ NodeRef nodeRef,
+ QName assocTypeQName,
+ Collection childNames);
+
/**
* Fetches the primary parent-child relationship.
*
diff --git a/source/java/org/alfresco/wcm/AbstractWCMServiceImplTest.java b/source/java/org/alfresco/wcm/AbstractWCMServiceImplTest.java
new file mode 100644
index 0000000000..f0e9f11099
--- /dev/null
+++ b/source/java/org/alfresco/wcm/AbstractWCMServiceImplTest.java
@@ -0,0 +1,104 @@
+/*
+ * 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.wcm;
+
+import junit.framework.TestCase;
+
+import org.alfresco.model.ContentModel;
+import org.alfresco.service.cmr.security.AuthenticationService;
+import org.alfresco.service.cmr.security.PersonService;
+import org.alfresco.util.ApplicationContextHelper;
+import org.alfresco.util.PropertyMap;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.support.ClassPathXmlApplicationContext;
+
+/**
+ * Abstract WCM Service implementation unit test
+ *
+ * @author janv
+ */
+public class AbstractWCMServiceImplTest extends TestCase
+{
+ // override jbpm.job.executor idleInterval to 10s (was 1.5m) for WCM unit tests
+ protected static ApplicationContext ctx =new ClassPathXmlApplicationContext(
+ new String[] {ApplicationContextHelper.CONFIG_LOCATIONS[0], "classpath:wcm/wcm-jbpm-context.xml"}
+ );
+
+ protected static final long SUBMIT_DELAY = 20000L; // 20s - to allow async submit direct workflow to complete (as per 10s idleInterval above)
+
+ //
+ // test data
+ //
+
+ protected static final String TEST_RUN = ""+System.currentTimeMillis();
+ protected static final boolean CLEAN = true; // cleanup during teardown
+
+ //
+ // services
+ //
+
+ protected AuthenticationService authenticationService;
+ protected PersonService personService;
+
+
+ @Override
+ protected void setUp() throws Exception
+ {
+ // Get the required services
+ authenticationService = (AuthenticationService)ctx.getBean("AuthenticationService");
+ personService = (PersonService)ctx.getBean("PersonService");
+ }
+
+ @Override
+ protected void tearDown() throws Exception
+ {
+ }
+
+ protected void createUser(String userName)
+ {
+ if (authenticationService.authenticationExists(userName) == false)
+ {
+ authenticationService.createAuthentication(userName, "PWD".toCharArray());
+
+ PropertyMap ppOne = new PropertyMap(4);
+ ppOne.put(ContentModel.PROP_USERNAME, userName);
+ ppOne.put(ContentModel.PROP_FIRSTNAME, "firstName");
+ ppOne.put(ContentModel.PROP_LASTNAME, "lastName");
+ ppOne.put(ContentModel.PROP_EMAIL, "email@email.com");
+ ppOne.put(ContentModel.PROP_JOBTITLE, "jobTitle");
+
+ personService.createPerson(ppOne);
+ }
+ }
+
+ protected void deleteUser(String userName)
+ {
+ if (authenticationService.authenticationExists(userName) == true)
+ {
+ personService.deletePerson(userName);
+ authenticationService.deleteAuthentication(userName);
+ }
+ }
+}
diff --git a/source/java/org/alfresco/wcm/WCMTestSuite.java b/source/java/org/alfresco/wcm/WCMTestSuite.java
index d4d881a510..cf9235914f 100644
--- a/source/java/org/alfresco/wcm/WCMTestSuite.java
+++ b/source/java/org/alfresco/wcm/WCMTestSuite.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2005-2008 Alfresco Software Limited.
+ * 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
diff --git a/source/java/org/alfresco/wcm/asset/AssetServiceImplTest.java b/source/java/org/alfresco/wcm/asset/AssetServiceImplTest.java
index b090662c24..8cf1e82307 100644
--- a/source/java/org/alfresco/wcm/asset/AssetServiceImplTest.java
+++ b/source/java/org/alfresco/wcm/asset/AssetServiceImplTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2005-2008 Alfresco Software Limited.
+ * 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
@@ -32,42 +32,27 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import junit.framework.TestCase;
-
import org.alfresco.model.ContentModel;
import org.alfresco.repo.content.MimetypeMap;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.service.cmr.avm.AVMNotFoundException;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentWriter;
-import org.alfresco.service.cmr.security.AuthenticationService;
-import org.alfresco.service.cmr.security.PersonService;
import org.alfresco.service.namespace.QName;
-import org.alfresco.util.ApplicationContextHelper;
-import org.alfresco.util.PropertyMap;
+import org.alfresco.wcm.AbstractWCMServiceImplTest;
import org.alfresco.wcm.sandbox.SandboxInfo;
import org.alfresco.wcm.sandbox.SandboxService;
import org.alfresco.wcm.util.WCMUtil;
import org.alfresco.wcm.webproject.WebProjectInfo;
import org.alfresco.wcm.webproject.WebProjectService;
-import org.springframework.context.ApplicationContext;
/**
* Asset Service implementation unit test
*
* @author janv
*/
-public class AssetServiceImplTest extends TestCase
+public class AssetServiceImplTest extends AbstractWCMServiceImplTest
{
- private static final ApplicationContext ctx = ApplicationContextHelper.getApplicationContext();
-
- //
- // test data
- //
-
- private static final String TEST_RUN = ""+System.currentTimeMillis();
- private static final boolean CLEAN = true; // cleanup during teardown
-
// base web project
private static final String TEST_WEBPROJ_DNS = "testAsset-"+TEST_RUN;
private static final String TEST_WEBPROJ_NAME = "testAsset Web Project Display Name - "+TEST_RUN;
@@ -83,7 +68,7 @@ public class AssetServiceImplTest extends TestCase
private static final String USER_ONE = TEST_USER+"-One";
private static final String USER_TWO = TEST_USER+"-Two";
private static final String USER_THREE = TEST_USER+"-Three";
-
+
//
// services
//
@@ -92,20 +77,16 @@ public class AssetServiceImplTest extends TestCase
private SandboxService sbService;
private AssetService assetService;
- private AuthenticationService authenticationService;
- private PersonService personService;
-
@Override
protected void setUp() throws Exception
{
+ super.setUp();
+
// Get the required services
wpService = (WebProjectService)ctx.getBean("WebProjectService");
sbService = (SandboxService)ctx.getBean("SandboxService");
assetService = (AssetService)ctx.getBean("AssetService");
-
- authenticationService = (AuthenticationService)ctx.getBean("AuthenticationService");
- personService = (PersonService)ctx.getBean("PersonService");
-
+
// By default run as Admin
AuthenticationUtil.setFullyAuthenticatedUser(USER_ADMIN);
@@ -140,32 +121,6 @@ public class AssetServiceImplTest extends TestCase
super.tearDown();
}
- private void createUser(String userName)
- {
- if (authenticationService.authenticationExists(userName) == false)
- {
- authenticationService.createAuthentication(userName, "PWD".toCharArray());
-
- PropertyMap ppOne = new PropertyMap(4);
- ppOne.put(ContentModel.PROP_USERNAME, userName);
- ppOne.put(ContentModel.PROP_FIRSTNAME, "firstName");
- ppOne.put(ContentModel.PROP_LASTNAME, "lastName");
- ppOne.put(ContentModel.PROP_EMAIL, "email@email.com");
- ppOne.put(ContentModel.PROP_JOBTITLE, "jobTitle");
-
- personService.createPerson(ppOne);
- }
- }
-
- private void deleteUser(String userName)
- {
- if (authenticationService.authenticationExists(userName) == true)
- {
- personService.deletePerson(userName);
- authenticationService.deleteAuthentication(userName);
- }
- }
-
private void checkAssetInfo(AssetInfo assetInfo, String expectedName, String expectedPath, String expectedCreator, boolean expectedIsFile, boolean expectedIsFolder, boolean expectedIsDeleted, boolean expectedIsLocked, String expectedLockOwner)
{
assertNotNull(assetInfo);
@@ -720,7 +675,7 @@ public class AssetServiceImplTest extends TestCase
checkAssetInfo(myMovedFile1Asset, "myFile1", path+"/myFolder2/myFile1", USER_ONE, true, false, false, false, null);
}
- public void testProperties()
+ public void testProperties() throws InterruptedException
{
// create web project (also creates staging sandbox and admin's author sandbox)
WebProjectInfo wpInfo = wpService.createWebProject(TEST_WEBPROJ_DNS+"-properties", TEST_WEBPROJ_NAME+"-properties", TEST_WEBPROJ_TITLE, TEST_WEBPROJ_DESCRIPTION, TEST_WEBPROJ_DEFAULT_WEBAPP, TEST_WEBPROJ_DONT_USE_AS_TEMPLATE, null);
@@ -760,6 +715,8 @@ public class AssetServiceImplTest extends TestCase
sbService.submitWebApp(sbStoreId, defaultWebApp, "submit1 label", "submit1 comment");
+ Thread.sleep(SUBMIT_DELAY);
+
assertNull(assetService.getLockOwner(myFile1Asset));
// update (or set, if not already set) specific properties - eg. title and description
@@ -873,7 +830,7 @@ public class AssetServiceImplTest extends TestCase
}
- public void testSimpleLockFile()
+ public void testSimpleLockFile() throws InterruptedException
{
// create web project (also creates staging sandbox and admin's author sandbox)
WebProjectInfo wpInfo = wpService.createWebProject(TEST_WEBPROJ_DNS+"-simpleLock", TEST_WEBPROJ_NAME+"-simpleLock", TEST_WEBPROJ_TITLE, TEST_WEBPROJ_DESCRIPTION, TEST_WEBPROJ_DEFAULT_WEBAPP, TEST_WEBPROJ_DONT_USE_AS_TEMPLATE, null);
@@ -918,6 +875,8 @@ public class AssetServiceImplTest extends TestCase
sbService.submitWebApp(sbStoreId, defaultWebApp, "submit1 label", "submit1 comment");
+ Thread.sleep(SUBMIT_DELAY);
+
assertNull(assetService.getLockOwner(myFile1Asset));
assertTrue(assetService.hasLockAccess(myFile1Asset));
diff --git a/source/java/org/alfresco/wcm/sandbox/SandboxService.java b/source/java/org/alfresco/wcm/sandbox/SandboxService.java
index be6910ac67..4a99a796c1 100644
--- a/source/java/org/alfresco/wcm/sandbox/SandboxService.java
+++ b/source/java/org/alfresco/wcm/sandbox/SandboxService.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2005-2008 Alfresco Software Limited.
+ * 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
@@ -24,6 +24,7 @@
*/
package org.alfresco.wcm.sandbox;
+import java.io.Serializable;
import java.util.Date;
import java.util.List;
import java.util.Map;
@@ -238,21 +239,6 @@ public interface SandboxService
*/
public void submitList(String sbStoreId, List relativePaths, String submitLabel, String submitDescription);
- /**
- * Submit list of changed assets for given sandbox (eg. from user sandbox to staging sandbox)
- *
- * NOTE: for backwards compatibility - subject to change - hence deprecated for now
- *
- * @param sbStoreId sandbox store id
- * @param assetPaths list of assets, as relative paths (eg. /www/avm_webapps/ROOT/MyFolderToSubmit)
- * @param expirationDates map of AVM src paths to expiration dates
- * @param submitLabel label for submitted snapshot
- * @param submitDescription description for submitted snapshot
- *
- * @deprecated
- */
- public void submitList(String sbStoreId, List relativePaths, Map expirationDates, String submitLabel, String submitDescription);
-
/**
* Submit list of changed assets for given sandbox (eg. from user sandbox to staging sandbox)
*
@@ -266,13 +252,25 @@ public interface SandboxService
/**
* Submit list of changed assets for given sandbox (eg. from user sandbox to staging sandbox)
*
+ * NOTE: for backwards compatibility - subject to change - hence deprecated for now
+ *
* @param sbStoreId sandbox store id
- * @param assets list of assets
- * @param expirationDates map of for those assets set with an expiration date, or can be null (if no expiration dates)
+ * @param assetPaths list of assets, as relative paths (eg. /www/avm_webapps/ROOT/MyFolderToSubmit)
+ * @param workflowName selected workflow name - if null, will use default submit direct workflow
+ * @param workflowParams configured workflow params
* @param submitLabel label for submitted snapshot
* @param submitDescription description for submitted snapshot
+ * @param expirationDates optional map of for those assets set with an expiration date, or can be null (if no expiration dates)
+ * @param launchDate optional launch date
+ * @param validateLinks if true then will validate links (if link validation is enabled)
+ * @param autoDeploy if true then will auto-deploy on workflow approval
+ *
+ * @deprecated subject to change
*/
- public void submitListAssets(String sbStoreId, List assets, Map expirationDates, String submitLabel, String submitDescription);
+ public void submitListAssets(String sbStoreId, List relativePaths,
+ String workflowName, Map workflowParams,
+ String submitLabel, String submitDescription,
+ Map expirationDates, Date launchDate, boolean validateLinks, boolean autoDeploy);
/**
* Revert all changed assets for given sandbox (eg. in user sandbox)
diff --git a/source/java/org/alfresco/wcm/sandbox/SandboxServiceImpl.java b/source/java/org/alfresco/wcm/sandbox/SandboxServiceImpl.java
index 856619500b..7ad436a1bc 100644
--- a/source/java/org/alfresco/wcm/sandbox/SandboxServiceImpl.java
+++ b/source/java/org/alfresco/wcm/sandbox/SandboxServiceImpl.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2005-2008 Alfresco Software Limited.
+ * 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
@@ -35,7 +35,7 @@ import java.util.Map;
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.mbeans.VirtServerRegistry;
import org.alfresco.model.WCMAppModel;
-import org.alfresco.repo.avm.AVMDAOs;
+import org.alfresco.model.WCMWorkflowModel;
import org.alfresco.repo.avm.AVMNodeConverter;
import org.alfresco.repo.avm.actions.AVMRevertStoreAction;
import org.alfresco.repo.avm.actions.AVMUndoSandboxListAction;
@@ -43,20 +43,26 @@ import org.alfresco.repo.domain.PropertyValue;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.security.permissions.AccessDeniedException;
import org.alfresco.repo.transaction.AlfrescoTransactionSupport;
+import org.alfresco.repo.transaction.RetryingTransactionHelper;
import org.alfresco.repo.transaction.TransactionListenerAdapter;
+import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
+import org.alfresco.repo.workflow.WorkflowModel;
import org.alfresco.service.cmr.action.Action;
import org.alfresco.service.cmr.action.ActionService;
import org.alfresco.service.cmr.avm.AVMNodeDescriptor;
import org.alfresco.service.cmr.avm.AVMService;
import org.alfresco.service.cmr.avm.VersionDescriptor;
-import org.alfresco.service.cmr.avm.locking.AVMLockingService;
import org.alfresco.service.cmr.avmsync.AVMDifference;
import org.alfresco.service.cmr.avmsync.AVMSyncService;
import org.alfresco.service.cmr.dictionary.DataTypeDefinition;
import org.alfresco.service.cmr.repository.NodeRef;
+import org.alfresco.service.cmr.workflow.WorkflowDefinition;
+import org.alfresco.service.cmr.workflow.WorkflowPath;
import org.alfresco.service.cmr.workflow.WorkflowService;
import org.alfresco.service.cmr.workflow.WorkflowTask;
+import org.alfresco.service.cmr.workflow.WorkflowTaskState;
import org.alfresco.service.namespace.QName;
+import org.alfresco.service.transaction.TransactionService;
import org.alfresco.util.NameMatcher;
import org.alfresco.util.Pair;
import org.alfresco.util.ParameterCheck;
@@ -84,16 +90,19 @@ public class SandboxServiceImpl implements SandboxService
/** Logger */
private static Log logger = LogFactory.getLog(SandboxServiceImpl.class);
+ private static final String WORKFLOW_SUBMITDIRECT = "jbpm$wcmwf:submitdirect";
+
private WebProjectService wpService;
private SandboxFactory sandboxFactory;
private AVMService avmService;
- private AVMLockingService avmLockingService;
private AVMSyncService avmSyncService;
private NameMatcher nameMatcher;
private VirtServerRegistry virtServerRegistry;
private ActionService actionService;
private WorkflowService workflowService;
private AssetService assetService;
+ private TransactionService transactionService;
+
public void setWebProjectService(WebProjectService wpService)
{
@@ -110,11 +119,6 @@ public class SandboxServiceImpl implements SandboxService
this.avmService = avmService;
}
- public void setAvmLockingService(AVMLockingService avmLockingService)
- {
- this.avmLockingService = avmLockingService;
- }
-
public void setAvmSyncService(AVMSyncService avmSyncService)
{
this.avmSyncService = avmSyncService;
@@ -144,6 +148,12 @@ public class SandboxServiceImpl implements SandboxService
{
this.assetService = assetService;
}
+
+ public void setTransactionService(TransactionService transactionService)
+ {
+ this.transactionService = transactionService;
+ }
+
/* (non-Javadoc)
* @see org.alfresco.wcm.sandbox.SandboxService#createAuthorSandbox(java.lang.String)
@@ -491,16 +501,6 @@ public class SandboxServiceImpl implements SandboxService
{
ParameterCheck.mandatoryString("sbStoreId", sbStoreId);
- submitList(sbStoreId, relativePaths, null, submitLabel, submitComment);
- }
-
- /* (non-Javadoc)
- * @see org.alfresco.wcm.sandbox.SandboxService#submitList(java.lang.String, java.util.List, java.util.Map, java.lang.String, java.lang.String)
- */
- public void submitList(String sbStoreId, List relativePaths, Map expirationDates, String submitLabel, String submitComment)
- {
- ParameterCheck.mandatoryString("sbStoreId", sbStoreId);
-
List assets = new ArrayList(relativePaths.size());
for (String relativePath : relativePaths)
@@ -513,7 +513,7 @@ public class SandboxServiceImpl implements SandboxService
}
}
- submitListAssets(sbStoreId, assets, expirationDates, submitLabel, submitComment);
+ submitListAssets(sbStoreId, assets, submitLabel, submitComment);
}
/* (non-Javadoc)
@@ -522,18 +522,7 @@ public class SandboxServiceImpl implements SandboxService
public void submitListAssets(String sbStoreId, List assets, String submitLabel, String submitComment)
{
ParameterCheck.mandatoryString("sbStoreId", sbStoreId);
-
- submitListAssets(sbStoreId, assets, null, submitLabel, submitComment);
- }
-
- /* (non-Javadoc)
- * @see org.alfresco.wcm.sandbox.SandboxService#submitListAssets(java.lang.String, java.util.List, java.util.Map, java.lang.String, java.lang.String)
- */
- public void submitListAssets(String sbStoreId, List assets, Map expirationDates, final String submitLabel, final String submitComment)
- {
- ParameterCheck.mandatoryString("sbStoreId", sbStoreId);
-
- // direct submit to the staging area (without workflow)
+ ParameterCheck.mandatoryString("submitLabel", submitLabel);
// TODO - consider submit to higher-level sandbox, not just to staging
if (! WCMUtil.isUserStore(sbStoreId))
@@ -541,61 +530,322 @@ public class SandboxServiceImpl implements SandboxService
throw new AlfrescoRuntimeException("Not an author sandbox: "+sbStoreId);
}
- // construct diffs for selected assets for submission
- String wpStoreId = WCMUtil.getWebProjectStoreId(sbStoreId);
- String stagingSandboxId = WCMUtil.buildStagingStoreName(wpStoreId);
-
- final List diffs = new ArrayList(assets.size());
-
+ List relativePaths = new ArrayList(assets.size());
for (AssetInfo asset : assets)
{
- String relativePath = WCMUtil.getStoreRelativePath(asset.getAvmPath());
-
- String srcPath = sbStoreId + WCMUtil.AVM_STORE_SEPARATOR + relativePath;
- String dstPath = stagingSandboxId + WCMUtil.AVM_STORE_SEPARATOR + relativePath;
-
- AVMDifference diff = new AVMDifference(-1, srcPath, -1, dstPath, AVMDifference.NEWER);
- diffs.add(diff);
+ relativePaths.add(asset.getPath());
+ }
+
+ // via submit direct workflow
+ submitViaWorkflow(sbStoreId, relativePaths, null, null, submitLabel, submitComment, null, null, false, false);
+ }
+
+ /* (non-Javadoc)
+ * @see org.alfresco.wcm.sandbox.SandboxService#submitListAssets(java.lang.String, java.util.List, java.lang.String, java.util.Map, java.lang.String, java.lang.String, java.util.Map, java.util.Date, boolean, boolean)
+ */
+ public void submitListAssets(String sbStoreId, List relativePaths,
+ String workflowName, Map workflowParams,
+ String submitLabel, String submitComment,
+ Map expirationDates, Date launchDate, boolean validateLinks, boolean autoDeploy)
+ {
+ // via selected workflow
+ submitViaWorkflow(sbStoreId, relativePaths, workflowName, workflowParams, submitLabel, submitComment,
+ expirationDates, launchDate, validateLinks, autoDeploy);
+ }
+
+ /**
+ * Submits the selected items via the configured workflow.
+ *
+ * This method uses 2 separate transactions to perform the submit.
+ * The first one creates the workflow sandbox. The virtualisation
+ * server is then informed of the new stores. The second
+ * transaction then starts the appropriate workflow. This approach
+ * is needed to allow link validation to be performed on the
+ * workflow sandbox.
+ */
+ private void submitViaWorkflow(final String sbStoreId, final List relativePaths, String workflowName, Map workflowParams,
+ final String submitLabel, final String submitComment,
+ final Map expirationDates, final Date launchDate, final boolean validateLinks, final boolean autoDeploy)
+ {
+ final String wpStoreId = WCMUtil.getWebProjectStoreId(sbStoreId);
+ final String stagingSandboxId = WCMUtil.buildStagingStoreName(wpStoreId);
+
+ final String finalWorkflowName;
+ final Map finalWorkflowParams;
+
+ if ((workflowName == null) || (workflowName.equals("")))
+ {
+ finalWorkflowName = WORKFLOW_SUBMITDIRECT;
+ finalWorkflowParams = new HashMap();
+ }
+ else
+ {
+ finalWorkflowName = workflowName;
+ finalWorkflowParams = workflowParams;
+ }
+
+ RetryingTransactionHelper txnHelper = transactionService.getRetryingTransactionHelper();
+
+ final List srcPaths = new ArrayList(relativePaths.size());
- if (expirationDates != null)
+ String derivedWebApp = null;
+ boolean multiWebAppsFound = false;
+
+ for (String relativePath : relativePaths)
+ {
+ // Example srcPath:
+ // mysite--alice:/www/avm_webapps/ROOT/foo.txt
+ String srcPath = sbStoreId + WCMUtil.AVM_STORE_SEPARATOR + relativePath;
+ srcPaths.add(srcPath);
+
+ // derive webapp for now (TODO check usage)
+ String srcWebApp = WCMUtil.getWebapp(srcPath);
+ if (srcWebApp != null)
+ {
+ if (derivedWebApp == null)
+ {
+ derivedWebApp = srcWebApp;
+ }
+ else if (! derivedWebApp.equals(srcWebApp))
+ {
+ multiWebAppsFound = true;
+ }
+ }
+ }
+
+ final String webApp = (multiWebAppsFound == false ? derivedWebApp : null);
+
+ RetryingTransactionCallback> sandboxCallback = new RetryingTransactionCallback>()
+ {
+ public Pair execute() throws Throwable
+ {
+ // call the actual implementation
+ return createWorkflowSandbox(finalWorkflowName, finalWorkflowParams, stagingSandboxId, srcPaths, expirationDates);
+ }
+ };
+
+ // create the workflow sandbox firstly
+ final Pair workflowInfo = txnHelper.doInTransaction(sandboxCallback, false, true);
+
+ if (workflowInfo != null)
+ {
+ final SandboxInfo wfSandboxInfo = workflowInfo.getFirst();
+ String virtUpdatePath = workflowInfo.getSecond();
+
+ // inform the virtualisation server if the workflow sandbox was created
+ if (virtUpdatePath != null)
+ {
+ WCMUtil.updateVServerWebapp(virtServerRegistry, virtUpdatePath, true);
+ }
+
+ try
+ {
+ RetryingTransactionCallback workflowCallback = new RetryingTransactionCallback()
+ {
+ public String execute() throws Throwable
+ {
+ // call the actual implementation
+ startWorkflow(wpStoreId, sbStoreId, wfSandboxInfo, webApp, finalWorkflowName, finalWorkflowParams, submitLabel, submitComment, launchDate, validateLinks, autoDeploy);
+ return null;
+ }
+ };
+
+ // start the workflow
+ txnHelper.doInTransaction(workflowCallback, false, true);
+ }
+ catch (Throwable err)
+ {
+ cleanupWorkflowSandbox(wfSandboxInfo);
+ throw new AlfrescoRuntimeException("Failed to submit to workflow", err);
+ }
+ }
+ }
+
+ /**
+ * Creates a workflow sandbox for all the submitted items
+ *
+ * @param context Faces context
+ */
+ protected Pair createWorkflowSandbox(String workflowName, Map workflowParams, String stagingSandboxId, final List srcPaths, Map expirationDates)
+ {
+ // The virtualization server might need to be notified
+ // because one or more of the files submitted could alter
+ // the behavior the virtual webapp in the target of the submit.
+ // For example, the user might be submitting a new jar or web.xml file.
+ //
+ // This must take place after the transaction has been completed;
+ // therefore, a variable is needed to store the path to the
+ // updated webapp so it can happen in doPostCommitProcessing.
+ String virtUpdatePath = null;
+ SandboxInfo sandboxInfo = null;
+
+ // create container for our avm workflow package
+
+ if (! workflowName.equals(WORKFLOW_SUBMITDIRECT))
+ {
+ // Create workflow sandbox for workflow package
+ sandboxInfo = sandboxFactory.createWorkflowSandbox(stagingSandboxId);
+ }
+ else
+ {
+ // default to direct submit workflow
+
+ // NOTE: read only workflow sandbox is lighter to construct than full workflow sandbox
+ sandboxInfo = sandboxFactory.createReadOnlyWorkflowSandbox(stagingSandboxId);
+ }
+
+ // Example workflow main store name:
+ // mysite--workflow-9161f640-b020-11db-8015-130bf9b5b652
+ String workflowMainStoreName = sandboxInfo.getMainStoreName();
+
+ List diffs = new ArrayList(srcPaths.size());
+
+ // get diff list - also process expiration dates, if any, and set virt svr update path
+
+ for (String srcPath : srcPaths)
+ {
+ // We *always* want to update virtualization server
+ // when a workflow sandbox is given data in the
+ // context of a submit workflow. Without this,
+ // it would be impossible to see workflow data
+ // in context. The raw operation to create a
+ // workflow sandbox does not notify the virtualization
+ // server that it exists because it's useful to
+ // defer this operation until everything is already
+ // in place; this allows pointlessly fine-grained
+ // notifications to be suppressed (they're expensive).
+ //
+ // Therefore, just derive the name of the webapp
+ // in the workflow sandbox from the 1st item in
+ // the submit list (even if it's not in WEB-INF),
+ // and force the virt server notification after the
+ // transaction has completed via doPostCommitProcessing.
+ if (virtUpdatePath == null)
+ {
+ // The virtUpdatePath looks just like the srcPath
+ // except that it belongs to a the main store of
+ // the workflow sandbox instead of the sandbox
+ // that originated the submit.
+ virtUpdatePath = workflowMainStoreName + srcPath.substring(srcPath.indexOf(':'),srcPath.length());
+ }
+
+ if ((expirationDates != null) && (! expirationDates.isEmpty()))
{
// process the expiration date (if any)
processExpirationDate(srcPath, expirationDates);
}
+
+ diffs.add(new AVMDifference(-1, srcPath, -1, WCMUtil.getCorrespondingPath(srcPath, workflowMainStoreName), AVMDifference.NEWER));
+ }
- // recursively remove locks from this item
- recursivelyRemoveLocks(wpStoreId, -1, avmService.lookup(-1, srcPath, true), srcPath);
+ // write changes to layer so files are marked as modified
+ avmSyncService.update(diffs, null, false, false, false, false, null, null);
+
+ return new Pair(sandboxInfo, virtUpdatePath);
+ }
- // check to see if destPath forces a notification of the virtualization server
- // (e.g.: it might be a path to a jar file within WEB-INF/lib).
- if (VirtServerUtils.requiresUpdateNotification(dstPath))
+ /**
+ * Starts the configured workflow to allow the submitted items to be link
+ * checked and reviewed.
+ */
+ protected void startWorkflow(String wpStoreId, String sbStoreId, SandboxInfo wfSandboxInfo, String webApp, String workflowName, Map workflowParams,
+ String submitLabel, String submitComment, Date launchDate, boolean validateLinks, boolean autoDeploy)
+ {
+ ParameterCheck.mandatoryString("workflowName", workflowName);
+ ParameterCheck.mandatory("workflowParams", workflowParams);
+
+ // start the workflow to get access to the start task
+ WorkflowDefinition wfDef = workflowService.getDefinitionByName(workflowName);
+ WorkflowPath path = workflowService.startWorkflow(wfDef.id, null);
+
+ if (path != null)
+ {
+ // extract the start task
+ List tasks = workflowService.getTasksForWorkflowPath(path.id);
+ if (tasks.size() == 1)
{
- // Bind the post-commit transaction listener with data required for virtualization server notification
- UpdateSandboxTransactionListener tl = new UpdateSandboxTransactionListener(dstPath);
- AlfrescoTransactionSupport.bindListener(tl);
+ WorkflowTask startTask = tasks.get(0);
+
+ if (startTask.state == WorkflowTaskState.IN_PROGRESS)
+ {
+ final NodeRef workflowPackage = WCMWorkflowUtil.createWorkflowPackage(workflowService, avmService, wfSandboxInfo);
+
+ workflowParams.put(WorkflowModel.ASSOC_PACKAGE, workflowPackage);
+
+ // add submission parameters
+ workflowParams.put(WorkflowModel.PROP_WORKFLOW_DESCRIPTION, submitComment);
+ workflowParams.put(WCMWorkflowModel.PROP_LABEL, submitLabel);
+ workflowParams.put(WCMWorkflowModel.PROP_FROM_PATH,
+ WCMUtil.buildStoreRootPath(sbStoreId));
+ workflowParams.put(WCMWorkflowModel.PROP_LAUNCH_DATE, launchDate);
+ workflowParams.put(WCMWorkflowModel.PROP_VALIDATE_LINKS,
+ new Boolean(validateLinks));
+ workflowParams.put(WCMWorkflowModel.PROP_AUTO_DEPLOY,
+ new Boolean(autoDeploy));
+ workflowParams.put(WCMWorkflowModel.PROP_WEBAPP,
+ webApp);
+ workflowParams.put(WCMWorkflowModel.ASSOC_WEBPROJECT,
+ wpService.getWebProjectNodeFromStore(wpStoreId));
+
+ // update start task with submit parameters
+ workflowService.updateTask(startTask.id, workflowParams, null, null);
+
+ // end the start task to trigger the first 'proper' task in the workflow
+ workflowService.endTask(startTask.id, null);
+ }
}
}
-
- // write changes to layer so files are marked as modified
-
- // Submit is done as system as the staging store is read only
- // We could add support to runIgnoringStoreACls
-
- // TODO review flatten - assumes webapps, hence currently flattens at /www/avm_webapps level
- // also review flatten for SimpleAVMSubmitAction and AVMSubmitAction
- final String sandboxPath = WCMUtil.buildSandboxRootPath(sbStoreId);
- final String stagingPath = WCMUtil.buildSandboxRootPath(stagingSandboxId);
+ }
- AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork