Index: src/test/java/info/magnolia/module/exchangetransactional/CopyUtilTest.java =================================================================== --- src/test/java/info/magnolia/module/exchangetransactional/CopyUtilTest.java (revision 0) +++ src/test/java/info/magnolia/module/exchangetransactional/CopyUtilTest.java (revision 0) @@ -0,0 +1,462 @@ +/** + * This file Copyright (c) 2012 Magnolia International + * Ltd. (http://www.magnolia-cms.com). All rights reserved. + * + * + * This program and the accompanying materials are made + * available under the terms of the Magnolia Network Agreement + * which accompanies this distribution, and is available at + * http://www.magnolia-cms.com/mna.html + * + * Any modifications to this file must keep this entire header + * intact. + * + */ +package info.magnolia.module.exchangetransactional; + +import static org.easymock.EasyMock.createNiceMock; +import static org.easymock.EasyMock.createStrictMock; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.verify; +import info.magnolia.cms.beans.runtime.MultipartForm; +import info.magnolia.cms.core.Content; +import info.magnolia.cms.core.Content.ContentFilter; +import info.magnolia.cms.core.HierarchyManager; +import info.magnolia.cms.core.ItemType; +import info.magnolia.cms.exchange.ExchangeException; +import info.magnolia.cms.security.ExternalUser; +import info.magnolia.cms.security.Permission; +import info.magnolia.cms.security.PermissionImpl; +import info.magnolia.cms.security.Realm; +import info.magnolia.cms.security.SecuritySupport; +import info.magnolia.cms.security.User; +import info.magnolia.cms.security.UserManager; +import info.magnolia.cms.security.auth.ACL; +import info.magnolia.cms.security.auth.Entity; +import info.magnolia.cms.security.auth.GroupList; +import info.magnolia.cms.security.auth.PrincipalCollection; +import info.magnolia.cms.security.auth.RoleList; +import info.magnolia.cms.util.SimpleUrlPattern; +import info.magnolia.context.MgnlContext; +import info.magnolia.context.UserContextImpl; +import info.magnolia.importexport.BootstrapUtil; +import info.magnolia.module.ModuleManagementException; +import info.magnolia.module.ModuleManager; +import info.magnolia.module.ModuleManagerImpl; +import info.magnolia.module.ModuleRegistry; +import info.magnolia.module.exchangesimple.BaseSyndicatorImpl; +import info.magnolia.module.model.ModuleDefinition; +import info.magnolia.module.model.reader.ModuleDefinitionReader; +import info.magnolia.test.ComponentsTestUtil; +import info.magnolia.test.RepositoryTestCase; + +import java.io.File; +import java.io.IOException; +import java.io.Reader; +import java.security.Principal; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.jcr.ImportUUIDBehavior; +import javax.jcr.lock.LockException; +import javax.security.auth.Subject; +import javax.servlet.http.HttpServletRequest; + +import com.mockrunner.mock.web.MockHttpServletRequest; +import com.mockrunner.mock.web.MockHttpServletResponse; +import com.mockrunner.mock.web.MockHttpSession; +import com.mockrunner.mock.web.MockServletContext; + +/** + * Tests related to transactional activation and ops handled by CopyUtil class. + * + * @version $Id$ + * + */ +public class CopyUtilTest extends RepositoryTestCase { + + public class TestXAReceiveFilter extends XAReceiveFilter { + private HttpServletRequest arg0; + + @Override + public void cleanUp(HttpServletRequest arg0, String status) { + // TODO Auto-generated method stub + super.cleanUp(arg0, status); + } + } + + public class TestRollbackFailingXAReceiveFilter extends TestXAReceiveFilter { + + @Override + protected synchronized String rollback(HttpServletRequest request) throws Exception { + throw new LockException(); + } + } + + private Content src; + private HierarchyManager mainContentWebHM; + private CopyUtil util; + private HierarchyManager backupHM; + private ContentFilter filter; + private Object[] mocks; + private MockHttpServletRequest request; + private HierarchyManager mainContentSystemHM; + private MockHttpServletResponse response; + ModuleRegistry registry; + + @Override + protected void setUp() throws Exception { + super.setUp(); + // prepare + SecuritySupport securitySupportMock = createNiceMock(SecuritySupport.class); + ComponentsTestUtil.setInstance(SecuritySupport.class, securitySupportMock); + UserManager userManagerMock = createNiceMock(UserManager.class); + expect(securitySupportMock.getUserManager(Realm.REALM_SYSTEM)).andReturn(userManagerMock).anyTimes(); + User anonymousUserMock = createNiceMock(User.class); + expect(userManagerMock.getAnonymousUser()).andReturn(anonymousUserMock).anyTimes(); + expect(anonymousUserMock.getName()).andReturn("anonymous").anyTimes(); + mocks = new Object[] { securitySupportMock, userManagerMock, anonymousUserMock }; + replay(mocks); + backupHM = MgnlContext.getHierarchyManager("mgnlSystem"); + mainContentSystemHM = MgnlContext.getSystemContext().getHierarchyManager("website"); + request = new MockHttpServletRequest(); + request.setSession(new MockHttpSession()); + Subject subj = new Subject(); + Entity entity = createUserEntity(subj); + request.setUserPrincipal(entity); + User user = new ExternalUser(subj) { + }; + user.setSubject(subj); + response = new MockHttpServletResponse(); + MgnlContext.initAsWebContext(request, response, new MockServletContext()); + ((UserContextImpl) MgnlContext.getWebContext()).login(user); + mainContentWebHM = MgnlContext.getHierarchyManager("website"); + assertNotSame(mainContentSystemHM, mainContentWebHM); + util = new CopyUtil(backupHM); + BootstrapUtil.bootstrap(new String[] { "/website.test.xml" }, ImportUUIDBehavior.IMPORT_UUID_COLLISION_THROW); + mainContentWebHM.save(); + src = mainContentSystemHM.getRoot().getContent("test"); + // src.createContent("subnode"); + src.createContent("para", ItemType.CONTENTNODE); + src.setNodeData("mgnl:ordering_info", "nothing"); + mainContentSystemHM.save(); + filter = new XAReceiveFilter().getRuleFilter(src); + util.copyToSystem(src, filter); + + // add some extra paragraphs (e.g. emulate activation of new paragraph + src.createContent("paraNew", ItemType.CONTENTNODE); + src.save(); + + } + + // TODO - this is an ugly hack to workaround MAGNOLIA-2593 - we should review RepositoryTestCase + @Override + protected void initDefaultImplementations() throws IOException { + // MgnlTestCase clears factory before running this method, so we have to instrument factory here rather then in setUp() before calling super.setUp() + registry = createStrictMock(ModuleRegistry.class); + ComponentsTestUtil.setInstance(ModuleRegistry.class, registry); + ComponentsTestUtil.setInstance(ModuleManager.class, new ModuleManagerImpl(null, new ModuleDefinitionReader() { + public ModuleDefinition read(Reader in) throws ModuleManagementException { + return null; + } + + public Map readAll() throws ModuleManagementException { + Map m = new HashMap(); + m.put("moduleDef", new ModuleDefinition()); + return m; + } + + public ModuleDefinition readFromResource(String resourcePath) throws ModuleManagementException { + return null; + } + }) { + @Override + public List loadDefinitions() throws ModuleManagementException { + return new ArrayList(); + } + }); + super.initDefaultImplementations(); + } + + /** + * Not a real test of the copy util functionality ... just show case of the fact that it is necessary to pass in HM that locked the content under change. + * + * @throws Exception + */ + public void testRemoveChildren() throws Exception { + + // emulate rollback + String uuid = src.getUUID(); + // lock first + Content underTheTest = mainContentWebHM.getContentByUUID(uuid); + underTheTest.lock(true, true); + + // as part of the rollback try to remove extra paragraph introduced during activation + util.removeNonExistingChildNodes(backupHM.getContentByUUID(uuid), mainContentWebHM.getContentByUUID(uuid), filter); + verify(mocks); + } + + public void testRollback() throws Exception { + TestXAReceiveFilter receiveFilter = new TestXAReceiveFilter(); + // emulate rollback + String uuid = src.getUUID(); + // lock first + Content underTheTest = mainContentWebHM.getContentByUUID(uuid); + underTheTest.lock(true, true); + + // as part of the rollback try to remove extra paragraph introduced during activation + request.setHeader(BaseSyndicatorImpl.VERSION_NAME, uuid); + request.setHeader(BaseSyndicatorImpl.REPOSITORY_NAME, "website"); + receiveFilter.rollback(request); + assertNotNull(mainContentWebHM.getContentByUUID(uuid).getContent("subnode")); + + verify(mocks); + } + + /** + * MBC test scenario + * + * @throws Exception + */ + public void testMissingBackupContent() throws Exception { + String uuid = src.getUUID(); + // while other tests need this data, this one does not + backupHM.getContentByUUID(uuid).delete(); + backupHM.save(); + TestXAReceiveFilter receiveFilter = new TestXAReceiveFilter(); + request.setHeader(BaseSyndicatorImpl.VERSION_NAME, uuid); + request.setHeader(BaseSyndicatorImpl.REPOSITORY_NAME, "website"); + request.setHeader(BaseSyndicatorImpl.WORKSPACE_NAME, "website"); + request.setHeader(BaseSyndicatorImpl.NODE_UUID, uuid); + // update of activation attempt 1 + receiveFilter.update(request); + // update of activation attempt 2 + receiveFilter.update(request); + // commit of either activation attempt 1 or 2 + receiveFilter.commit(request); + // rollback of remaining activation attempt + receiveFilter.rollback(request); + // and the node is gone completely - line below fails w/o locking + // assertNotNull(mainContentWebHM.getContentByUUID(uuid).getContent("subnode")); + + verify(mocks); + } + + /** + * MBC test scenario + * + * @throws Exception + */ + public void testFixedMissingBackupContent() throws Exception { + String uuid = src.getUUID(); + // while other tests need this data, this one does not + backupHM.getContentByUUID(uuid).delete(); + backupHM.save(); + TestXAReceiveFilter receiveFilter = new TestXAReceiveFilter(); + receiveFilter.setUnlockRetries(2); + receiveFilter.setTransactionUnlockRetries(2); + + // as part of the rollback try to remove extra paragraph introduced during activation + request.setHeader(BaseSyndicatorImpl.VERSION_NAME, uuid); + request.setHeader(BaseSyndicatorImpl.REPOSITORY_NAME, "website"); + request.setHeader(BaseSyndicatorImpl.WORKSPACE_NAME, "website"); + request.setHeader(BaseSyndicatorImpl.NODE_UUID, uuid); + request.setHeader(BaseSyndicatorImpl.ACTION, BaseSyndicatorImpl.ACTIVATE); + MultipartForm form = new MultipartForm(); + form.addDocument("resources1234.xml", "resources1234.xml", "xml", new File("target/test-classes/resources1234.xml")); + form.addDocument("test.xml.gz", "test.xml.gz", "gz", new File("target/test-classes/test.xml.gz")); + request.setAttribute(MultipartForm.REQUEST_ATTRIBUTE_NAME, form); + request.setHeader(BaseSyndicatorImpl.RESOURCE_MAPPING_FILE, "resources1234.xml"); + + request.setHeader(BaseSyndicatorImpl.CONTENT_FILTER_RULE, ItemType.CONTENT.getSystemName() + "," + ItemType.SYSTEM.getSystemName()); + // update of activation attempt 1 + receiveFilter.applyLock(request); + receiveFilter.update(request); + receiveFilter.cleanUp(request, BaseSyndicatorImpl.ACTIVATION_SUCCESSFUL); + // update of activation attempt 2 + try { + receiveFilter.applyLock(request); + fail(); + } catch (ExchangeException e) { + assertEquals("Operation not permitted, /test is locked by unfinished transaction.", e.getMessage()); + } + + verify(mocks); + } + + public void testUnlockAfterFailingRollback() throws Exception { + TestRollbackFailingXAReceiveFilter receiveFilter = new TestRollbackFailingXAReceiveFilter(); + // emulate rollback + String uuid = src.getUUID(); + + // as part of the rollback try to remove extra paragraph introduced during activation + request.setHeader(BaseSyndicatorImpl.VERSION_NAME, uuid); + request.setHeader(BaseSyndicatorImpl.REPOSITORY_NAME, "website"); + request.setHeader(BaseSyndicatorImpl.WORKSPACE_NAME, "website"); + request.setHeader(BaseSyndicatorImpl.NODE_UUID, uuid); + request.setHeader(BaseSyndicatorImpl.ACTION, BaseSyndicatorImpl.ROLLBACK); + receiveFilter.doFilter(request, response, null); + assertNotNull(mainContentWebHM.getContentByUUID(uuid).getContent("subnode")); + + // after failing rollback content is still unlocked + assertFalse(mainContentWebHM.getContentByUUID(uuid).isLocked()); + + verify(mocks); + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + MgnlContext.setInstance(null); + ComponentsTestUtil.clear(); + } + + private Entity createUserEntity(Subject subj) { + Entity entity = new Entity() { + + public String getName() { + return "superuser"; + } + + public void setName(String name) { + } + + public void addProperty(String key, Object value) { + } + + public Object getProperty(String key) { + if (key.equals(Entity.LANGUAGE)) { + return "en"; + } else if (key.equals(Entity.NAME)) { + return "superuser"; + } + return null; + } + }; + subj.getPrincipals().add(entity); + subj.getPrincipals().add(new RoleList() { + + public void setName(String name) { + } + + public boolean has(String name) { + return true; + } + + public String getName() { + return "superuser"; + } + + public Collection getList() { + return null; + } + + public void add(String name) { + } + }); + subj.getPrincipals().add(new GroupList() { + + public void setName(String name) { + } + + public boolean has(String name) { + return true; + } + + public String getName() { + return "superuser"; + } + + public Collection getList() { + return null; + } + + public void add(String name) { + } + }); + subj.getPrincipals().add(new PrincipalCollection() { + + public void setName(String name) { + // TODO Auto-generated method stub + + } + + public void set(Collection collection) { + // TODO Auto-generated method stub + + } + + public void remove(Principal principal) { + // TODO Auto-generated method stub + + } + + public String getName() { + // TODO Auto-generated method stub + return null; + } + + public Principal get(String name) { + return new ACL() { + + public void setWorkspace(String workspace) { + } + + public void setRepository(String repository) { + } + + public void setName(String name) { + } + + public void setList(List list) { + } + + public String getWorkspace() { + return "website"; + } + + public String getRepository() { + return "website"; + } + + public String getName() { + return "00"; + } + + public List getList() { + List permissions = new ArrayList(); + Permission perm = new PermissionImpl(); + perm.setPermissions(63); + perm.setPattern(new SimpleUrlPattern("*")); + permissions.add(perm); + return permissions; + } + + public void addPermission(Object permission) { + } + }; + } + + public boolean contains(String name) { + return true; + } + + public boolean contains(Principal principal) { + return true; + } + + public void clearAll() { + } + + public void add(Principal principal) { + } + } + ); + return entity; + } + +} Property changes on: src/test/java/info/magnolia/module/exchangetransactional/CopyUtilTest.java ___________________________________________________________________ Added: svn:mime-type + text/plain Index: src/test/java/info/magnolia/module/exchangetransactional/ExchangeTransactionalModule.java =================================================================== --- src/test/java/info/magnolia/module/exchangetransactional/ExchangeTransactionalModule.java (revision 0) +++ src/test/java/info/magnolia/module/exchangetransactional/ExchangeTransactionalModule.java (revision 0) @@ -0,0 +1,71 @@ +/** + * This file Copyright (c) 2012 Magnolia International + * Ltd. (http://www.magnolia-cms.com). All rights reserved. + * + * + * This program and the accompanying materials are made + * available under the terms of the Magnolia Network Agreement + * which accompanies this distribution, and is available at + * http://www.magnolia-cms.com/mna.html + * + * Any modifications to this file must keep this entire header + * intact. + * + */ +package info.magnolia.module.exchangetransactional; + +import info.magnolia.license.EnterpriseLicensedModule; +import info.magnolia.license.License; +import info.magnolia.license.LicenseConsts; +import info.magnolia.license.LicenseStatus; + +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Dummy test copy of the module class. + * + * @version $Id$ + * + */ +public class ExchangeTransactionalModule implements EnterpriseLicensedModule { + + private static Logger log = LoggerFactory.getLogger(ExchangeTransactionalModule.class); + + private static ExchangeTransactionalModule instance; + + public ExchangeTransactionalModule() { + instance = this; + } + + public static ExchangeTransactionalModule getInstance() { + return instance; + } + + // is part of the standard distribution + public String[] getSupportedEditions() { + return new String[] { LicenseConsts.EDITION_ENTERPRISE }; + } + + // module is deployable in demo mode + public boolean isDemoAllowed() { + return true; + } + + // no additional license checks, depend on main enterprise module + public LicenseStatus checkLicense(License license) { + return new LicenseStatus(LicenseStatus.STATUS_VALID, StringUtils.EMPTY, license); + } + + // no additional license checks + public boolean isForceCheck() { + return false; + } + + public static boolean isLicenseValid() { + return true; + } + +} + Property changes on: src/test/java/info/magnolia/module/exchangetransactional/ExchangeTransactionalModule.java ___________________________________________________________________ Added: svn:mime-type + text/plain Index: src/test/resources/website.test.xml =================================================================== --- src/test/resources/website.test.xml (revision 0) +++ src/test/resources/website.test.xml (revision 0) @@ -0,0 +1,57 @@ + + + + mgnl:content + + + mix:lockable + + + dd672c4f-3a99-49fc-8ad8-48c6b0d70da3 + + + + mgnl:metaData + + + superuser + + + 2012-05-10T22:23:28.408+02:00 + + + 2012-05-10T22:23:32.096+02:00 + + + stkHome + + + + + mgnl:content + + + mix:lockable + + + 0e03e21e-904a-477d-b2b7-c0f748f6234f + + + + mgnl:metaData + + + superuser + + + 2012-05-10T22:23:37.732+02:00 + + + 2012-05-10T22:23:45.091+02:00 + + + stkSection + + + + Property changes on: src/test/resources/website.test.xml ___________________________________________________________________ Added: svn:mime-type + text/plain Index: src/test/resources/resources1234.xml =================================================================== --- src/test/resources/resources1234.xml (revision 0) +++ src/test/resources/resources1234.xml (revision 0) @@ -0,0 +1,6 @@ + + + + + + Property changes on: src/test/resources/resources1234.xml ___________________________________________________________________ Added: svn:mime-type + text/plain Index: src/test/resources/test.xml.gz =================================================================== Cannot display: file marked as a binary type. svn:mime-type = application/octet-stream Property changes on: src/test/resources/test.xml.gz ___________________________________________________________________ Added: svn:mime-type + application/octet-stream Index: src/main/java/info/magnolia/module/exchangetransactional/XAReceiveFilter.java =================================================================== --- src/main/java/info/magnolia/module/exchangetransactional/XAReceiveFilter.java (revision 57261) +++ src/main/java/info/magnolia/module/exchangetransactional/XAReceiveFilter.java (working copy) @@ -34,6 +34,7 @@ import javax.jcr.NodeIterator; import javax.jcr.PathNotFoundException; import javax.jcr.RepositoryException; +import javax.jcr.lock.LockException; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; @@ -53,9 +54,13 @@ private static final Logger log = LoggerFactory.getLogger(XAReceiveFilter.class); + private int transactionUnlockRetries = 10; + + private int transactionRetryWait = 2; + @Override public void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) - throws IOException, ServletException { + throws IOException, ServletException { if (!ExchangeTransactionalModule.isLicenseValid()) { log.warn("Detected license is not valid for transactional activation. Activation disabled."); return; @@ -175,8 +180,8 @@ } private Content recreatePath(HierarchyManager thm, String currentParentPath) - throws RepositoryException, PathNotFoundException, - AccessDeniedException { + throws RepositoryException, PathNotFoundException, + AccessDeniedException { Content tParent; log.debug("Parent path {} doesn't exists. Recreating.", currentParentPath); // parent path doesn't exist in trash, create it. @@ -233,11 +238,11 @@ String uuid = request.getHeader(BaseSyndicatorImpl.VERSION_NAME); String repo = request.getHeader(BaseSyndicatorImpl.REPOSITORY_NAME); log.debug("Proceeding with rollback of {}", uuid); + Content rolledBackContent = null; + Content tContent = null; try { // rollback in system context to make sure no one playing with acls can break it. HierarchyManager thm = MgnlContext.getSystemContext().getHierarchyManager("mgnlSystem"); - HierarchyManager hm = MgnlContext.getSystemContext().getHierarchyManager(repo); - Content tContent = null; try { tContent = thm.getContentByUUID(uuid); } catch (ItemNotFoundException e) { @@ -248,15 +253,15 @@ return null; } String parentPath = tContent.getParent().getHandle(); - new CopyUtil(MgnlContext.getSystemContext().getHierarchyManager("mgnlSystem")).copyFromSystem(tContent, hm.getContentByUUID(uuid), getRuleFilter(tContent)); + HierarchyManager lockingSessionHM = MgnlContext.getHierarchyManager(repo); + new CopyUtil(MgnlContext.getSystemContext().getHierarchyManager("mgnlSystem")).copyFromSystem(tContent, lockingSessionHM.getContentByUUID(uuid), getRuleFilter(tContent)); tContent.delete(); thm.save(); // order imported node - HierarchyManager lockingSessionHM = MgnlContext.getHierarchyManager(repo); Content parent = lockingSessionHM.getContent(parentPath); - tContent = lockingSessionHM.getContentByUUID(uuid); - String name = tContent.getName(); - String siblings[] = tContent.getNodeData("mgnl:ordering_info").getString().split(";"); + rolledBackContent = lockingSessionHM.getContentByUUID(uuid); + String name = rolledBackContent.getName(); + String siblings[] = rolledBackContent.getNodeData("mgnl:ordering_info").getString().split(";"); for (int i = 0; i < siblings.length; i++) { String siblingUUID = siblings[i]; // check for existence and order @@ -271,12 +276,19 @@ log.debug("Failed to order node", re); } } - tContent.deleteNodeData("mgnl:ordering_info"); - tContent.save(); + rolledBackContent.deleteNodeData("mgnl:ordering_info"); + rolledBackContent.save(); log.debug("Rolled back {}", uuid); } catch (ItemNotFoundException e) { + String backupContentPath = StringUtils.EMPTY; + if (tContent != null && StringUtils.isNotBlank(tContent.getHandle())) { + backupContentPath = "mgnlSystem:" + tContent.getHandle(); + } + if (rolledBackContent != null && StringUtils.isNotBlank(rolledBackContent.getHandle())) { + backupContentPath += repo + ":" + rolledBackContent.getHandle(); + } // this should not happen - throw new ExchangeException(e); + throw new ExchangeException("Error during rollback. Please make sure you check state of " + uuid + "[" + backupContentPath + "] node as it might be in inconsistent state. You might try to reactivate this node in case the error leading to rollback has been fixed.", e); } return null; } @@ -365,17 +377,92 @@ */ @Override protected void applyLock(HttpServletRequest request) throws ExchangeException { - // if previous action was de-activate, node will no longer exist on commit or roll back so it can't be locked String action = request.getHeader(BaseSyndicatorImpl.ACTION); - if (action == null) { - throw new ExchangeException("Action header [" + BaseSyndicatorImpl.ACTION + "] must be set."); + int retries = getTransactionUnlockRetries(); + long retryWait = getTransactionRetryWait() * 1000; + + String uuid = request.getHeader(BaseSyndicatorImpl.NODE_UUID); + try { + // wait for ordinary lock + Content parent = waitForLock(request); + // backup content might exist for commit/rollback, but CANNOT exist for activate/deactivate + if (!BaseSyndicatorImpl.COMMIT.equals(action) && !BaseSyndicatorImpl.ROLLBACK.equals(action)) { + // end for XA also wait until backup node of given content is gone + Content backupContent = null; + try { + if (StringUtils.isNotBlank(uuid)) { + backupContent = getBackupHierarchyManager().getContentByUUID(uuid); + } + } catch (RepositoryException e) { + // ignore, it should not exist, but there's no isUUIDExists(); + } + + while (backupContent != null && retries > 0) { + log.info("Content " + backupContent.getHandle() + " is locked by transaction. Will retry " + retries + " more times."); + try { + Thread.sleep(retryWait); + } catch (InterruptedException e) { + // Restore the interrupted status + Thread.currentThread().interrupt(); + } + retries--; + // wait for ordinary lock + parent = waitForLock(request); + // end for XA also wait until backup node of given content is gone + backupContent = null; + try { + if (StringUtils.isNotBlank(uuid)) { + backupContent = getBackupHierarchyManager().getContentByUUID(uuid); + } + } catch (RepositoryException e) { + // ignore, it should not exist, but there's no isUUIDExists(); + } + } + if (parent.isLocked() || backupContent != null) { + throw new ExchangeException("Operation not permitted, " + (backupContent != null ? backupContent.getHandle() : parent.getHandle()) + " is locked" + (backupContent != null ? " by unfinished transaction." : "")); + } + } + parent.lock(true, true); + } catch (LockException le) { + // either repository does not support locking OR this node never locked + log.debug(le.getMessage()); + } catch (ItemNotFoundException e) { + if (BaseSyndicatorImpl.COMMIT.equals(action) || BaseSyndicatorImpl.ROLLBACK.equals(action)) { + log.debug("Attempt to lock non existing content {} during {}.", getUUID(request), action); + // ignore non existence of content in case of commit or rollback of deletion + return; + } + // - when deleting new piece of content on the author and mgnl tries to deactivate it on public automatically + log.warn("Attempt to lock non existing content {} during {}.", getUUID(request), action); + } catch (PathNotFoundException e) { + // - when attempting to activate the content for which parent content have not been yet activated + log.debug("Attempt to lock non existing content {}:{} during {}.", new Object[] { getHierarchyManager(request).getName(), getParentPath(request), action }); + } catch (RepositoryException re) { + // will blow fully at later stage + log.warn("Exception caught during " + action, re); } - if (action.equalsIgnoreCase(BaseSyndicatorImpl.COMMIT) || action.equalsIgnoreCase(BaseSyndicatorImpl.ROLLBACK)) { - return; - } - super.applyLock(request); } + public int getTransactionRetryWait() { + return transactionRetryWait; + } + + public int getTransactionUnlockRetries() { + return transactionUnlockRetries; + } + + public void setTransactionUnlockRetries(int transactionUnlockRetries) { + this.transactionUnlockRetries = transactionUnlockRetries; + } + + public void setTransactionRetryWait(int transactionRetryWait) { + this.transactionRetryWait = transactionRetryWait; + } + + private HierarchyManager getBackupHierarchyManager() { + return MgnlContext.getSystemContext().getHierarchyManager("mgnlSystem"); + } + /** * Creates content filtering rule for given content. */ @@ -384,4 +471,22 @@ rule.reverse(); return new RuleBasedContentFilter(rule); } + + @Override + protected void cleanUp(HttpServletRequest request, String status) { + super.cleanUp(request, status); + if (BaseSyndicatorImpl.ACTIVATION_FAILED.equals(status)) { + HierarchyManager thm = MgnlContext.getSystemContext().getHierarchyManager("mgnlSystem"); + String uuid = request.getHeader(BaseSyndicatorImpl.NODE_UUID); + if (StringUtils.isNotBlank(uuid)) { + try { + Content tContent = thm.getContentByUUID(uuid); + tContent.delete(); + thm.save(); + } catch (RepositoryException e) { + log.error("Failed to cleanup backup of {}. Activation of this content will not be possible until manual cleanup of mgnlSystem workspace is performed.", uuid, e); + } + } + } + } } Index: src/main/java/info/magnolia/module/exchangetransactional/CopyUtil.java =================================================================== --- src/main/java/info/magnolia/module/exchangetransactional/CopyUtil.java (revision 57261) +++ src/main/java/info/magnolia/module/exchangetransactional/CopyUtil.java (working copy) @@ -16,10 +16,10 @@ import info.magnolia.cms.beans.config.ContentRepository; import info.magnolia.cms.core.Content; +import info.magnolia.cms.core.HierarchyManager; import info.magnolia.cms.core.Path; +import info.magnolia.cms.core.version.BaseVersionManager; import info.magnolia.context.MgnlContext; -import info.magnolia.cms.core.HierarchyManager; -import info.magnolia.cms.core.version.BaseVersionManager; import java.io.File; import java.io.FileInputStream; @@ -53,7 +53,7 @@ */ private static Logger log = LoggerFactory.getLogger(CopyUtil.class); - private HierarchyManager targetHm; + private final HierarchyManager targetHm; /** * Exposed only for extending classes. @@ -190,7 +190,7 @@ // it seems to be a bug in jackrabbit - cloning does not work if the // node with the same uuid // exist, "removeExisting" has no effect - // if node exist with the same UUID, simply update non propected + // if node exist with the same UUID, simply update non protected // properties String workspaceName = ContentRepository.getInternalWorkspaceName(parent.getWorkspace().getName()); Content existingNode = getHierarchyManager(workspaceName).getContentByUUID(node.getUUID()); @@ -265,7 +265,7 @@ this.removeNonExistingChildNodes(source, destination, filter); } - private void removeNonExistingChildNodes(Content source, Content destination, Content.ContentFilter filter) throws RepositoryException { + protected void removeNonExistingChildNodes(Content source, Content destination, Content.ContentFilter filter) throws RepositoryException { // collect all uuids from the source node hierarchy using the given // filter Iterator children = destination.getChildren(filter).iterator(); @@ -301,7 +301,7 @@ * @param filter */ private void copyAllChildNodes(Content node1, Content node2, Content.ContentFilter filter) - throws RepositoryException { + throws RepositoryException { Iterator children = node1.getChildren(filter).iterator(); while (children.hasNext()) { Content child = (Content) children.next(); Index: src/main/resources/META-INF/magnolia/exchange-transactional.xml =================================================================== --- src/main/resources/META-INF/magnolia/exchange-transactional.xml (revision 57261) +++ src/main/resources/META-INF/magnolia/exchange-transactional.xml (working copy) @@ -26,7 +26,7 @@ exchange-simple - 4.4.6/* + 4.4.8/* Index: pom.xml =================================================================== --- pom.xml (revision 57261) +++ pom.xml (working copy) @@ -11,7 +11,7 @@ A module to ensure synchronization of content between multiple public Magnolia instances. 1.5 - 4.4.6 + 4.4.8-SNAPSHOT http://documentation.magnolia-cms.com/modules/exchange-transactional.html @@ -56,6 +56,12 @@ provided + com.mockrunner + mockrunner + test + 0.3.1 + + javax.servlet servlet-api 2.4