/* * Copyright (c) 2006-2011 Nuxeo SA (http://nuxeo.com/) and others. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Florent Guillaume */ package org.nuxeo.ecm.core.storage.mongodb; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.junit.Assume.assumeTrue; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.log4j.AppenderSkeleton; import org.apache.log4j.Level; import org.apache.log4j.Logger; import org.apache.log4j.spi.LoggingEvent; import org.junit.Before; import org.junit.Ignore; import org.junit.Test; import org.nuxeo.ecm.core.api.ClientException; import org.nuxeo.ecm.core.api.ClientRuntimeException; import org.nuxeo.ecm.core.api.CoreSession; import org.nuxeo.ecm.core.api.DocumentModel; import org.nuxeo.ecm.core.api.DocumentRef; import org.nuxeo.ecm.core.api.DocumentSecurityException; import org.nuxeo.ecm.core.api.PathRef; import org.nuxeo.ecm.core.api.impl.DocumentModelImpl; import org.nuxeo.ecm.core.api.security.ACE; import org.nuxeo.ecm.core.api.security.ACL; import org.nuxeo.ecm.core.api.security.ACP; import org.nuxeo.ecm.core.api.security.SecurityConstants; import org.nuxeo.ecm.core.api.security.impl.ACLImpl; import org.nuxeo.ecm.core.api.security.impl.ACPImpl; import org.nuxeo.ecm.core.model.Repository; import org.nuxeo.ecm.core.repository.RepositoryService; import org.nuxeo.runtime.api.Framework; import org.nuxeo.runtime.transaction.TransactionHelper; import org.nuxeo.runtime.transaction.TransactionRuntimeException; public class TestMongoDBRepositoryJTAJCA extends MongoDBRepositoryTXTestCase { private static final String NO_TX_CANNOT_RECONN = "No transaction, cannot reconnect"; @Before public void checkAssumptions() { assumeTrue(hasPoolingConfig()); } /** * Test that connection sharing allows use of several sessions at the same * time. */ @Test public void testSessionSharing() throws Exception { RepositoryService repositoryManager = Framework.getLocalService(RepositoryService.class); Repository repo = repositoryManager.getRepository(repositoryName); // assertEquals(1, repo.getActiveSessionsCount()); CoreSession session2 = openSessionAs(SecurityConstants.ADMINISTRATOR); // assertEquals(1, repo.getActiveSessionsCount()); try { DocumentModel doc = new DocumentModelImpl("/", "doc", "Document"); doc = session.createDocument(doc); session.save(); // check that this is immediately seen from other connection // (underlying ManagedConnection is the same) assertTrue(session2.exists(new PathRef("/doc"))); } finally { closeSession(session2); } // assertEquals(1, repo.getActiveSessionsCount()); } /** * Test that a commit implicitly does a save. */ @Test public void testSaveOnCommit() throws Exception { // first transaction DocumentModel doc = new DocumentModelImpl("/", "doc", "Document"); doc = session.createDocument(doc); // let commit do an implicit save TransactionHelper.commitOrRollbackTransaction(); // release cx TransactionHelper.startTransaction(); openSession(); // reopen cx and hold open Thread t = new Thread() { @Override public void run() { try { TransactionHelper.startTransaction(); CoreSession session2; session2 = openSessionAs(SecurityConstants.ADMINISTRATOR); try { assertTrue(session2.exists(new PathRef("/doc"))); } finally { closeSession(session2); TransactionHelper.commitOrRollbackTransaction(); } } catch (Exception e) { fail(e.toString()); } } }; t.start(); t.join(); } /** * Test that the TransactionalCoreSessionWrapper does its job. */ @Test public void testRollbackOnException() throws Exception { assertTrue(TransactionHelper.isTransactionActive()); try { session.getDocument(new PathRef("/nosuchdoc")); fail("Missing document should throw"); } catch (Exception e) { // ok } // tx still active because CoreSession.getDocument is marked // @NoRollbackOnException assertTrue(TransactionHelper.isTransactionActive()); closeSession(); TransactionHelper.commitOrRollbackTransaction(); TransactionHelper.startTransaction(); openSession(); assertTrue(TransactionHelper.isTransactionActive()); DocumentModel doc = new DocumentModelImpl("/nosuchdir", "doc", "Document"); try { session.createDocument(doc); fail("Missing parent should throw"); } catch (Exception e) { // ok } // tx not active anymore because CoreSession.createDocument is not // marked @NoRollbackOnException assertTrue(TransactionHelper.isTransactionMarkedRollback()); } /** * Test NoRollbackOnException annotation on some methods. */ @Test public void testNoRollbackOnException() throws Exception { DocumentModel folder = session.createDocumentModel("/", "folder", "Folder"); folder = session.createDocument(folder); DocumentModel doc = session.createDocumentModel("/folder", "doc", "File"); doc = session.createDocument(doc); ACP acp; ACL acl; // set ACP on folder to block bob acp = new ACPImpl(); acl = new ACLImpl(); acl.add(new ACE("Administrator", "Everything", true)); acl.add(new ACE("bob", "Everything", false)); acp.addACL(acl); folder.setACP(acp, true); // allow bob on doc acp = new ACPImpl(); acl = new ACLImpl(); acl.add(new ACE("bob", "Everything", true)); acp.addACL(acl); doc.setACP(acp, true); session.save(); closeSession(); TransactionHelper.commitOrRollbackTransaction(); TransactionHelper.startTransaction(); session = openSessionAs("bob"); try { session.getParentDocument(doc.getRef()); fail("Missing document should throw"); } catch (DocumentSecurityException e) { // ok } // tx still active because CoreSession.getParentDocument is marked // @NoRollbackOnException assertTrue(TransactionHelper.isTransactionActive()); } protected static final Log log = LogFactory.getLog(TestMongoDBRepositoryJTAJCA.class); protected static class TxWarnChecker extends AppenderSkeleton { boolean seenWarn; @Override public void close() { } @Override public boolean requiresLayout() { return false; } /* * (non-Javadoc) * @see org.apache.log4j.AppenderSkeleton#append(org.apache.log4j.spi. * LoggingEvent) */ @Override protected void append(LoggingEvent event) { if (!Level.WARN.equals(event.getLevel())) { return; } Object msg = event.getMessage(); if (msg instanceof String && (((String) msg).startsWith("Session invoked in a container without a transaction active"))) { seenWarn = true; } } } /** * Cannot use session after close if no tx. */ @Test public void testAccessWithoutTx() throws ClientException { TransactionHelper.commitOrRollbackTransaction(); TxWarnChecker checker = new TxWarnChecker(); Logger.getRootLogger().addAppender(checker); try { session.getRootDocument(); fail("should throw"); } catch (ClientRuntimeException e) { assertTrue(e.getMessage(), e.getMessage().contains(NO_TX_CANNOT_RECONN)); } } /** * Testing that if 2 modifications are done at the same time on the same * document on 2 separate transactions, one is rejected * (TransactionRuntimeException) */ // not working as is @Ignore @Test public void testConcurrentModification() throws Exception { // first transaction DocumentModel doc = session.createDocumentModel("/", "doc", "Note"); doc.getProperty("dc:title").setValue("initial"); doc = session.createDocument(doc); // let commit do an implicit save closeSession(session); TransactionHelper.commitOrRollbackTransaction(); // release cx final DocumentRef ref = new PathRef("/doc"); TransactionHelper.startTransaction(); openSession(); doc = session.getDocument(ref); doc.getProperty("dc:title").setValue("first"); session.saveDocument(doc); Thread t = new Thread() { @Override public void run() { try { TransactionHelper.startTransaction(); CoreSession session2; session2 = openSessionAs(SecurityConstants.ADMINISTRATOR); try { DocumentModel doc = session2.getDocument(ref); doc.getProperty("dc:title").setValue("second update"); session2.saveDocument(doc); } catch (Exception e) { log.error("Catched error while setting title", e); } finally { TransactionHelper.commitOrRollbackTransaction(); closeSession(session2); } } catch (Exception e) { fail(e.toString()); } } }; t.start(); t.join(); try { TransactionHelper.commitOrRollbackTransaction(); // release cx fail("expected TransactionRuntimeException"); } catch (TransactionRuntimeException e) { // expected } } @Test public void testAcquireThroughSessionId() throws Exception { DocumentModel file = session.createDocumentModel("/", "file", "File"); file = session.createDocument(file); session.save(); assertNotNull(file.getCoreSession()); } @Test public void testReconnectAfterClose() throws Exception { DocumentModel file = session.createDocumentModel("/", "file", "File"); file = session.createDocument(file); session.save(); CoreSession closedSession = session; closeSession(); TransactionHelper.commitOrRollbackTransaction(); TransactionHelper.startTransaction(); // use a closed session. because of tx, we can reconnect it assertNotNull(closedSession.getRootDocument()); // commit will close low-level session TransactionHelper.commitOrRollbackTransaction(); } @Test public void testReconnectAfterCommit() throws Exception { DocumentModel file = session.createDocumentModel("/", "file", "File"); file = session.createDocument(file); session.save(); TransactionHelper.commitOrRollbackTransaction(); TransactionHelper.startTransaction(); // keep existing CoreSession whose Session was implicitly closed // by commit // reconnect possible through tx assertNotNull(session.getRootDocument()); } @Test public void testReconnectAfterCloseNoTx() throws Exception { DocumentModel file = session.createDocumentModel("/", "file", "File"); file = session.createDocument(file); session.save(); CoreSession closedSession = session; closeSession(); TransactionHelper.commitOrRollbackTransaction(); // no startTransaction // use a closed session -> exception try { closedSession.getRootDocument(); fail("should throw"); } catch (ClientRuntimeException e) { assertTrue(e.getMessage(), e.getMessage().contains(NO_TX_CANNOT_RECONN)); } } /** * DocumentModel.getCoreSession cannot reconnect through a sid that does * not exist anymore. */ @Test public void testReconnectAfterCloseThroughSessionId() throws Exception { DocumentModel file = session.createDocumentModel("/", "file", "File"); file = session.createDocument(file); session.save(); closeSession(); TransactionHelper.commitOrRollbackTransaction(); TransactionHelper.startTransaction(); openSession(); assertNull(file.getCoreSession()); } @Test public void testMultiThreaded() throws Exception { assertNotNull(session.getRootDocument()); final CoreSession finalSession = session; Thread t = new Thread() { @Override public void run() { try { TransactionHelper.startTransaction(); try { assertNotNull(finalSession.getRootDocument()); } finally { TransactionHelper.commitOrRollbackTransaction(); } } catch (Exception e) { fail(e.toString()); } } }; t.start(); t.join(); assertNotNull(session.getRootDocument()); } @Test public void testMultiThreadedNeedsTx() throws Exception { assertNotNull(session.getRootDocument()); final CoreSession finalSession = session; final Exception[] threadException = new Exception[1]; Thread t = new Thread() { @Override public void run() { try { // no tx finalSession.getRootDocument(); } catch (Exception e) { threadException[0] = e; } } }; t.start(); t.join(); Exception e = threadException[0]; assertNotNull(e); assertTrue(e.getMessage(), e.getMessage().contains(NO_TX_CANNOT_RECONN)); } @Test public void testCloseFromOtherTx() throws Exception { assertNotNull(session.getRootDocument()); final CoreSession finalSession = session; session = null; Thread t = new Thread() { @Override public void run() { try { finalSession.close(); } catch (Exception e) { fail(e.toString()); } } }; t.start(); t.join(); } }