Skip to content
Browse files

Standalone build.

  • Loading branch information...
1 parent d061fb0 commit 0ead3fdf5c3114a7216a4fd87df2827bec41cf81 @d6y d6y committed May 27, 2012
View
84 README.textile
@@ -0,0 +1,84 @@
+h1. Lift JTA Transaction API
+
+The semantics are the same as used in the EJB spec. E.g. Required, RequiresNew, Mandatory, Supports, Never. All these are exposed as monadic objects and high-order functions in the TransactionContext object.
+
+The implementation is hooked into the Lift's JPA implementation.
+Check the ScalaDoc (or the source) for the documentation on usage, semantics etc.
+
+There are two versions of the API, one monadic and one using high-order functions.
+
+h3. Monadic API
+
+<pre>
+ for {
+ ctx <- TransactionContext.Required
+ entity <- updatedEntities
+ if !ctx.isRollbackOnly
+ } {
+ // transactional stuff
+ ctx.getEntityManager.merge(entity)
+ }
+</pre>
+
+<pre>
+val users = for {
+ ctx <- TransactionContext.RequiresNew
+ name <- userNames
+ } yield {
+ // transactional stuff
+ val query = ctx.getEntityManager.createNamedQuery("findUserByName")
+ query.setParameter("userName", name)
+ query.getSingleResult
+ }
+</pre>
+
+h3. High-order functions API
+
+<pre>
+TransactionContext.withTxRequired {
+ ... // REQUIRED semantics
+
+ TransactionContext.withTxRequiresNew {
+ ... // REQUIRES_NEW semantics
+ }
+}
+</pre>
+
+h2. Configuration
+
+The configuration is done in the persistence.xml file + the jta.properties. Sample configuration files can be found in the src/main/resources directory.
+Here are the essential configuration options in the JPA persistence.xml file:
+
+<pre>
+<persistence xmlns="http://java.sun.com/xml/ns/persistence"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd"
+ version="1.0">
+ <persistence-unit name="LiftPersistenceUnit" transaction-type="JTA">
+ <provider>org.hibernate.ejb.HibernatePersistence</provider>
+
+ <mapping-file>...</mapping-file>
+ <class>...</class>
+
+ <properties>
+ <property name="hibernate.transaction.manager_lookup_class"
+ value="net.liftweb.transaction.hibernate.LiftTransactionManagerLookup" />
+ </properties>
+ </persistence-unit>
+</persistence>
+</pre>
+
+h2. TODOs
+
+Currently it is hard-coded to use the Atomikos Transaction library and the Hibernate JPA implementation.
+This will have to be configurable + some other options as well. See the TODOs in the code.
+
+
+
+h1. Jenkins
+
+The Jenkins build is triggered on a push to master. The server is at https://liftmodules.ci.cloudbees.com/job/jta/
+
+
+
+
View
86 build.sbt
@@ -0,0 +1,86 @@
+name := "jta"
+
+liftVersion <<= liftVersion ?? "2.4"
+
+version <<= liftVersion apply { _ + "-1.0-SNAPSHOT" }
+
+organization := "net.liftmodules"
+
+scalaVersion := "2.9.1"
+
+crossScalaVersions := Seq("2.8.1", "2.9.0-1", "2.9.1")
+
+resolvers += "Java.net Maven2 Repository" at "http://download.java.net/maven/2/"
+
+libraryDependencies <++= liftVersion { v =>
+ "net.liftweb" %% "lift-webkit" % v % "compile->default" ::
+ "net.liftweb" %% "lift-util" % v % "compile->default" ::
+ Nil
+}
+
+libraryDependencies <++= scalaVersion { sv =>
+ "com.atomikos" % "atomikos-util" % "3.2.3" % "provided" ::
+ "com.atomikos" % "transactions" % "3.2.3" % "provided" ::
+ "com.atomikos" % "transactions-api" % "3.2.3" % "provided" ::
+ "com.atomikos" % "transactions-jta" % "3.2.3" % "provided" ::
+ "javax.persistence" % "persistence-api" % "1.0" % "provided" ::
+ "javax.transaction" % "transaction-api" % "1.1" % "provided" ::
+ "org.hibernate" % "hibernate-entitymanager" % "3.4.0.GA" ::
+ "org.scala-libs" %% "scalajpa" % "1.4" ::
+ "org.scala-tools.testing" %% "specs" % (sv match {
+ case "2.8.0" => "1.6.5"
+ case "2.9.1" => "1.6.9"
+ case _ => "1.6.8"
+ }) % "test" ::
+ "org.scalacheck" %% "scalacheck" % (sv match {
+ case "2.8.0" => "1.7"
+ case "2.8.1" | "2.8.2" => "1.8"
+ case _ => "1.9"
+ }) % "test" ::
+ Nil
+}
+
+publishTo <<= version { _.endsWith("SNAPSHOT") match {
+ case true => Some("snapshots" at "https://oss.sonatype.org/content/repositories/snapshots")
+ case false => Some("releases" at "https://oss.sonatype.org/service/local/staging/deploy/maven2")
+ }
+ }
+
+
+// For local deployment:
+
+credentials += Credentials( file("sonatype.credentials") )
+
+// For the build server:
+
+credentials += Credentials( file("/private/liftmodules/sonatype.credentials") )
+
+publishMavenStyle := true
+
+publishArtifact in Test := false
+
+pomIncludeRepository := { _ => false }
+
+
+pomExtra := (
+ <url>https://github.com/liftmodules/jta</url>
+ <licenses>
+ <license>
+ <name>Apache 2.0 License</name>
+ <url>http://www.apache.org/licenses/LICENSE-2.0.html</url>
+ <distribution>repo</distribution>
+ </license>
+ </licenses>
+ <scm>
+ <url>git@github.com:liftmodules/jta.git</url>
+ <connection>scm:git:git@github.com:liftmodules/jta.git</connection>
+ </scm>
+ <developers>
+ <developer>
+ <id>liftmodules</id>
+ <name>Lift Team</name>
+ <url>http://www.liftmodules.net</url>
+ </developer>
+ </developers>
+ )
+
View
13 project/LiftModule.scala
@@ -0,0 +1,13 @@
+import sbt._
+import sbt.Keys._
+
+object LiftModuleBuild extends Build {
+
+val liftVersion = SettingKey[String]("liftVersion", "Version number of the Lift Web Framework")
+
+val project = Project("LiftModule", file("."))
+
+}
+
+
+
View
5 sonatype.credentials.template
@@ -0,0 +1,5 @@
+realm=Sonatype Nexus Repository Manager
+host=oss.sonatype.org
+user=your-jira-login
+password=you-jira-password
+
View
56 src/main/resources/jta.properties
@@ -0,0 +1,56 @@
+# ==================================================
+# === ATOMIKOS TRANSACTION MANAGER CONFIGURATION ===
+# ==================================================
+
+#Required: factory implementation class of the transaction core.
+com.atomikos.icatch.service=com.atomikos.icatch.standalone.UserTransactionServiceFactory
+
+com.atomikos.icatch.lock_logs=false
+
+com.atomikos.icatch.force_shutdown_on_vm_exit=true
+
+#Set base name of log file this name will be used as the first part of the system-generated log file name
+com.atomikos.icatch.log_base_name = tmlog
+
+#Set base name of file where messages are output (also known as the 'console file').
+com.atomikos.icatch.console_file_name = tm.out
+
+#Set output directory where console file and other files are to be put make sure this directory exists!
+com.atomikos.icatch.output_dir = tm/jta
+
+#Set directory of log files; make sure this directory exists!
+com.atomikos.icatch.log_base_dir = tm/log
+
+#Set the max timeout (in milliseconds) for local transactions
+com.atomikos.icatch.max_timeout = 30000000
+
+#The globally unique name of this transaction manager process override this value with a globally unique name
+com.atomikos.icatch.tm_unique_name = lift_tm
+
+#Set this to WARN, INFO or DEBUG to control the granularity of output to the console file.
+com.atomikos.icatch.console_log_level=DEBUG
+
+#Do you want transaction logging to be enabled or not? If set to false, then no logging overhead will be done at the risk of losing data after restart or crash.
+com.atomikos.icatch.enable_logging=false
+
+#Should two-phase commit be done in (multi-)threaded mode or not? NOTE: threads are reused on JDK 1.5 or higher. For JDK 1.4, thread reuse is enabled as soon as the concurrent backport is in the classpath.
+com.atomikos.icatch.threaded_2pc=true
+
+#Do we want to use parallel subtransactions? JTA's default is NO for J2EE compatibility
+#com.atomikos.icatch.serial_jta_transactions=true
+
+#If you want to do explicit resource registration then you need to set this value to false.
+#com.atomikos.icatch.automatic_resource_registration=true
+
+#Set the max number of active local transactions or -1 for unlimited.
+#com.atomikos.icatch.max_actives = 50
+
+#Size limit (in bytes) for the console file; negative means unlimited.
+#com.atomikos.icatch.console_file_limit=-1
+
+#For size-limited console files, this option specifies a number of rotating files to maintain.
+#com.atomikos.icatch.console_file_count=1
+
+#Set the number of log writes between checkpoints
+#com.atomikos.icatch.checkpoint_interval=500
+
View
16 src/main/resources/persistence.xml
@@ -0,0 +1,16 @@
+<persistence xmlns="http://java.sun.com/xml/ns/persistence"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd"
+ version="1.0">
+ <persistence-unit name="LiftPersistenceUnit" transaction-type="JTA">
+ <provider>org.hibernate.ejb.HibernatePersistence</provider>
+
+ <mapping-file></mapping-file>
+ <class></class>
+
+ <properties>
+ <property name="hibernate.transaction.manager_lookup_class"
+ value="net.liftweb.transaction.hibernate.LiftTransactionManagerLookup" />
+ </properties>
+ </persistence-unit>
+</persistence>
View
64 src/main/scala/net/liftweb/transaction/EntityManagerSynchronization.scala
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2009-2011 WorldWide Conferencing, LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.liftweb
+package transaction
+
+import net.liftweb.common.Loggable
+
+import javax.transaction.{ TransactionManager, Status, Synchronization, SystemException}
+import javax.persistence.EntityManager
+
+
+/**
+ * EntityManager JTA synchronization class.
+ * <p>
+ * Registered in method: [joinTransaction].
+ *
+ * @author <a href="http://jonasboner.com">Jonas Bon&#233;r</a>
+ */
+class EntityManagerSynchronization(
+ val em: EntityManager,
+ val tm: TransactionManager,
+ val closeAtTxCompletion: Boolean)
+ extends Synchronization with Loggable {
+
+ def beforeCompletion = {
+ try {
+ val status = tm.getStatus
+ if (status != Status.STATUS_ROLLEDBACK &&
+ status != Status.STATUS_ROLLING_BACK &&
+ status != Status.STATUS_MARKED_ROLLBACK) {
+ logger.debug("Flushing EntityManager...")
+ em.flush // flush EntityManager on success
+ }
+ } catch {
+ case e: SystemException => throw new RuntimeException(e)
+ }
+ }
+
+ def afterCompletion(status: Int) = {
+ val status = tm.getStatus
+ if (closeAtTxCompletion) {
+ em.close
+ }
+ if (status == Status.STATUS_ROLLEDBACK ||
+ status == Status.STATUS_ROLLING_BACK ||
+ status == Status.STATUS_MARKED_ROLLBACK) {
+ TransactionContext.closeEntityManager // destroy EntityManager on rollback
+ }
+ }
+}
View
234 src/main/scala/net/liftweb/transaction/TransactionContext.scala
@@ -0,0 +1,234 @@
+/*
+ * Copyright 2009-2011 WorldWide Conferencing, LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.liftweb
+package transaction
+
+import javax.persistence.EntityManager
+import javax.transaction.{Transaction, Status, TransactionManager}
+
+import org.scala_libs.jpa.{ScalaEntityManager, ScalaEMFactory}
+
+import common.Loggable
+
+
+/**
+ * Base monad for the transaction monad implementations.
+ *
+ * @author <a href="http://jonasboner.com">Jonas Bon&#233;r</a>
+ */
+trait TransactionMonad {
+
+ // -----------------------------
+ // Monadic definitions
+ // -----------------------------
+
+ def map[T](f: TransactionMonad => T): T
+ def flatMap[T](f: TransactionMonad => T): T
+ def foreach(f: TransactionMonad => Unit): Unit
+ def filter(f: TransactionMonad => Boolean): TransactionMonad =
+ if (f(this)) this else TransactionContext.NoOpTransactionMonad
+
+ // -----------------------------
+ // JTA Transaction definitions
+ // -----------------------------
+
+ /**
+ * Returns the current Transaction.
+ */
+ def getTransaction: Transaction = TransactionContext.getTransactionManager.getTransaction
+
+ /**
+ * Marks the current transaction as doomed.
+ */
+ def setRollbackOnly = TransactionContext.setRollbackOnly
+
+ /**
+ * Marks the current transaction as doomed.
+ */
+ def doom = TransactionContext.setRollbackOnly
+
+ /**
+ * Checks if the current transaction is doomed.
+ */
+ def isRollbackOnly = TransactionContext.isRollbackOnly
+
+ /**
+ * Checks that the current transaction is NOT doomed.
+ */
+ def isNotDoomed = !TransactionContext.isRollbackOnly
+
+ // -----------------------------
+ // JPA EntityManager definitions
+ // -----------------------------
+
+ /**
+ * Returns the current EntityManager.
+ */
+ def getEntityManager: EntityManager = TransactionContext.getEntityManager
+
+ /**
+ * Checks if an EntityManager exists in current context.
+ */
+ //def hasEntityManager: Boolean = TransactionContext.hasEntityManager
+
+ /**
+ * Closes and removes the current EntityManager.
+ * <p/>
+ * IMPORTANT: This method must always be used to close the EntityManager, never use em.close directly.
+ */
+ def closeEntityManager = TransactionContext.closeEntityManager
+}
+
+/**
+ * Manages a thread-local stack of TransactionContexts.
+ * <p/>
+ * Choose TransactionService implementation by implicit definition of the implementation of choice,
+ * e.g. <code>implicit val txService = TransactionServices.AtomikosTransactionService</code>.
+ * <p/>
+ * Example usage 1:
+ * <pre>
+ * for {
+ * ctx <- TransactionContext.Required
+ * entity <- updatedEntities
+ * if !ctx.isRollbackOnly
+ * } {
+ * // transactional stuff
+ * ctx.getEntityManager.merge(entity)
+ * }
+ * </pre>
+ * Example usage 2:
+ * <pre>
+ * val users = for {
+ * ctx <- TransactionContext.Required
+ * name <- userNames
+ * } yield {
+ * // transactional stuff
+ * val query = ctx.getEntityManager.createNamedQuery("findUserByName")
+ * query.setParameter("userName", name)
+ * query.getSingleResult
+ * }
+ * </pre>
+ *
+ * @author <a href="http://jonasboner.com">Jonas Bon&#233;r</a>
+ */
+object TransactionContext extends TransactionProtocol with Loggable {
+ // FIXME: make configurable
+ private implicit val defaultTransactionService = atomikos.AtomikosTransactionService
+
+ private[TransactionContext] val stack = new scala.util.DynamicVariable(new TransactionContext)
+
+ object Required extends TransactionMonad {
+ def map[T](f: TransactionMonad => T): T = withTxRequired { f(this) }
+ def flatMap[T](f: TransactionMonad => T): T = withTxRequired { f(this) }
+ def foreach(f: TransactionMonad => Unit): Unit = withTxRequired { f(this) }
+ }
+
+ object RequiresNew extends TransactionMonad {
+ def map[T](f: TransactionMonad => T): T = withTxRequiresNew { f(this) }
+ def flatMap[T](f: TransactionMonad => T): T = withTxRequiresNew { f(this) }
+ def foreach(f: TransactionMonad => Unit): Unit = withTxRequiresNew { f(this) }
+ }
+
+ object Supports extends TransactionMonad {
+ def map[T](f: TransactionMonad => T): T = withTxSupports { f(this) }
+ def flatMap[T](f: TransactionMonad => T): T = withTxSupports { f(this) }
+ def foreach(f: TransactionMonad => Unit): Unit = withTxSupports { f(this) }
+ }
+
+ object Mandatory extends TransactionMonad {
+ def map[T](f: TransactionMonad => T): T = withTxMandatory { f(this) }
+ def flatMap[T](f: TransactionMonad => T): T = withTxMandatory { f(this) }
+ def foreach(f: TransactionMonad => Unit): Unit = withTxMandatory { f(this) }
+ }
+
+ object Never extends TransactionMonad {
+ def map[T](f: TransactionMonad => T): T = withTxNever { f(this) }
+ def flatMap[T](f: TransactionMonad => T): T = withTxNever { f(this) }
+ def foreach(f: TransactionMonad => Unit): Unit = withTxNever { f(this) }
+ }
+
+ object NoOpTransactionMonad extends TransactionMonad {
+ def map[T](f: TransactionMonad => T): T = f(this)
+ def flatMap[T](f: TransactionMonad => T): T = f(this)
+ def foreach(f: TransactionMonad => Unit): Unit = f(this)
+ override def filter(f: TransactionMonad => Boolean): TransactionMonad = this
+ }
+
+ private[transaction] def setRollbackOnly = current.setRollbackOnly
+
+ private[transaction] def isRollbackOnly = current.isRollbackOnly
+
+ private[transaction] def getTransactionManager: TransactionManager = current.getTransactionManager
+
+ private[transaction] def getTransaction: Transaction = current.getTransactionManager.getTransaction
+
+ private[transaction] def getEntityManager: EntityManager = current.getEntityManager
+
+ private[transaction] def closeEntityManager = current.closeEntityManager
+
+ private[this] def current = stack.value
+
+ /**
+ * Continues with the invocation defined in 'body' with the brand new context define in 'newCtx', the old
+ * one is put on the stack and will automatically come back in scope when the method exits.
+ * <p/>
+ * Suspends and resumes the current JTA transaction.
+ */
+ private[transaction] def withNewContext[T](body: => T): T = {
+ val suspendedTx: Option[Transaction] =
+ if (isInExistingTransaction(getTransactionManager)) {
+ logger.debug("Suspending TX")
+ Some(getTransactionManager.suspend)
+ } else None
+ val result = stack.withValue(new TransactionContext) { body }
+ if (suspendedTx.isDefined) {
+ logger.debug("Resuming TX")
+ getTransactionManager.resume(suspendedTx.get)
+ }
+ result
+ }
+}
+
+/**
+ * Transaction context, holds the EntityManager and the TransactionManager.
+ *
+ * @author <a href="http://jonasboner.com">Jonas Bon&#233;r</a>
+ */
+class TransactionContext(private implicit val transactionService: TransactionService)
+ extends ScalaEntityManager with ScalaEMFactory {
+
+ val em: EntityManager = transactionService.entityManagerFactory.createEntityManager
+ val tm: TransactionManager = transactionService.transactionManager
+
+ private def setRollbackOnly = tm.setRollbackOnly
+
+ protected def getUnitName = "N/A"
+
+ private def isRollbackOnly: Boolean = tm.getStatus == Status.STATUS_MARKED_ROLLBACK
+
+ private def getTransactionManager: TransactionManager = tm
+
+ private def getEntityManager: EntityManager = em
+
+ private def closeEntityManager = em.close
+
+ // ---------------------------------
+ // To make ScalaEMFactory happy
+ val factory = this
+ def openEM: javax.persistence.EntityManager = em
+ def closeEM(e: javax.persistence.EntityManager) = closeEntityManager
+}
View
239 src/main/scala/net/liftweb/transaction/TransactionService.scala
@@ -0,0 +1,239 @@
+/*
+ * Copyright 2009-2011 WorldWide Conferencing, LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.liftweb
+package transaction
+
+import javax.transaction.{Transaction, TransactionManager, Status, SystemException, TransactionRequiredException}
+import javax.persistence.{Persistence, EntityManagerFactory, NonUniqueResultException, NoResultException}
+
+import common.Logger
+
+
+/**
+ * JTA Transaction service.
+ *
+ * @author <a href="http://jonasboner.com">Jonas Bon&#233;r</a>
+ */
+trait TransactionService {
+ def transactionManager: TransactionManager
+ def entityManagerFactory: EntityManagerFactory
+}
+
+/**
+ * JPA Entity Manager service.
+ *
+ * @author <a href="http://jonasboner.com">Jonas Bon&#233;r</a>
+ */
+trait EntityManagerService {
+ // TODO: make configurable?
+ lazy val JPA_PERSISTENT_UNIT = "LiftPersistenceUnit"
+ lazy val entityManagerFactory = Persistence.createEntityManagerFactory(JPA_PERSISTENT_UNIT)
+}
+
+/**
+ * <p>
+ * Trait that implements a JTA transaction service that obeys the transaction semantics defined
+ * in the transaction attribute types for the transacted methods according to the EJB 3 draft specification.
+ * The aspect handles UserTransaction, TransactionManager instance variable injection thru @javax.ejb.Inject
+ * (name subject to change as per EJB 3 spec) and method transaction levels thru @javax.ejb.TransactionAttribute.
+ * </p>
+ *
+ * <p>
+ * This trait should be inherited to implement the getTransactionManager() method that should return a concrete
+ * javax.transaction.TransactionManager implementation (from JNDI lookup etc).
+ * </p>
+ * <p>
+ * <h3>Transaction attribute semantics</h3>
+ * (From http://www.kevinboone.com/ejb-transactions.html)
+ * </p>
+ * <p>
+ * <h4>Required</h4>
+ * 'Required' is probably the best choice (at least initially) for an EJB method that will need to be transactional. In this case, if the method's caller is already part of a transaction, then the EJB method does not create a new transaction, but continues in the same transaction as its caller. If the caller is not in a transaction, then a new transaction is created for the EJB method. If something happens in the EJB that means that a rollback is required, then the extent of the rollback will include everything done in the EJB method, whatever the condition of the caller. If the caller was in a transaction, then everything done by the caller will be rolled back as well. Thus the 'required' attribute ensures that any work done by the EJB will be rolled back if necessary, and if the caller requires a rollback that too will be rolled back.
+ * </p>
+ * <p>
+ * <h4>RequiresNew</h4>
+ * 'RequiresNew' will be appropriate if you want to ensure that the EJB method is rolled back if necessary, but you don't want the rollback to propogate back to the caller. This attribute results in the creation of a new transaction for the method, regardless of the transactional state of the caller. If the caller was operating in a transaction, then its transaction is suspended until the EJB method completes. Because a new transaction is always created, there may be a slight performance penalty if this attribute is over-used.
+ * </p>
+ * <p>
+ * <h4>Mandatory</h4>
+ * With the 'mandatory' attribute, the EJB method will not even start unless its caller is in a transaction. It will throw a <code>TransactionRequiredException</code> instead. If the method does start, then it will become part of the transaction of the caller. So if the EJB method signals a failure, the caller will be rolled back as well as the EJB.
+ * </p>
+ * <p>
+ * <h4>Supports</h4>
+ * With this attribute, the EJB method does not care about the transactional context of its caller. If the caller is part of a transaction, then the EJB method will be part of the same transaction. If the EJB method fails, the transaction will roll back. If the caller is not part of a transaction, then the EJB method will still operate, but a failure will not cause anything to roll back. 'Supports' is probably the attribute that leads to the fastest method call (as there is no transactional overhead), but it can lead to unpredicatable results. If you want a method to be isolated from transactions, that is, to have no effect on the transaction of its caller, then use 'NotSupported' instead.
+ * </p>
+ * <p>
+ * <h4>NotSupported</h4>
+ * With the 'NotSupported' attribute, the EJB method will never take part in a transaction. If the caller is part of a transaction, then the caller's transaction is suspended. If the EJB method fails, there will be no effect on the caller's transaction, and no rollback will occur. Use this method if you want to ensure that the EJB method will not cause a rollback in its caller. This is appropriate if, for example, the method does something non-essential, such as logging a message. It would not be helpful if the failure of this operation caused a transaction rollback.
+ * </p>
+ * <p>
+ * <h4>Never</h4>
+ * The 'NotSupported'' attribute will ensure that the EJB method is never called by a transactional caller. Any attempt to do so will result in a <code>RemoteException</code> being thrown. This attribute is probably less useful than `NotSupported', in that NotSupported will assure that the caller's transaction is never affected by the EJB method (just as `Never' does), but will allow a call from a transactional caller if necessary.
+ * </p>
+ *
+ * @author <a href="http://jonasboner.com">Jonas Bon&#233;r</a>
+ */
+trait TransactionProtocol {
+ private val logger = Logger(classOf[TransactionProtocol])
+
+ /**
+ * Wraps body in a transaction with REQUIRED semantics.
+ * <p/>
+ * Creates a new transaction if no transaction is active in scope, else joins the outer transaction.
+ */
+ def withTxRequired[T](body: => T): T = {
+ val tm = TransactionContext.getTransactionManager
+ if (!isInExistingTransaction(tm)) {
+ tm.begin
+ try {
+ joinTransaction
+ body
+ } catch {
+ case e: Exception => handleException(tm, e)
+ } finally {
+ commitOrRollBack(tm)
+ }
+ } else body
+ }
+
+ /**
+ * Wraps body in a transaction with REQUIRES_NEW semantics.
+ * <p/>
+ * Suspends existing transaction, starts a new transaction, invokes body,
+ * commits or rollbacks new transaction, finally resumes previous transaction.
+ */
+ def withTxRequiresNew[T](body: => T): T = TransactionContext.withNewContext {
+ val tm = TransactionContext.getTransactionManager
+ tm.begin
+ try {
+ joinTransaction
+ body
+ } catch {
+ case e: Exception => handleException(tm, e)
+ } finally {
+ commitOrRollBack(tm)
+ }
+ }
+
+ /**
+ * Wraps body in a transaction with NOT_SUPPORTED semantics.
+ * <p/>
+ * Suspends existing transaction, invokes body, resumes transaction.
+ */
+ def withTxNotSupported[T](body: => T): T = TransactionContext.withNewContext {
+ body
+ }
+
+ /**
+ * Wraps body in a transaction with SUPPORTS semantics.
+ * <p/>
+ * Basicalla a No-op.
+ */
+ def withTxSupports[T](body: => T): T = {
+ // attach to current if exists else skip -> do nothing
+ body
+ }
+
+ /**
+ * Wraps body in a transaction with MANDATORY semantics.
+ * <p/>
+ * Throws a TransactionRequiredException if there is no transaction active in scope.
+ */
+ def withTxMandatory[T](body: => T): T = {
+ if (!isInExistingTransaction(TransactionContext.getTransactionManager)) throw new TransactionRequiredException("No active TX at method with TX type set to MANDATORY")
+ body
+ }
+
+ /**
+ * Wraps body in a transaction with NEVER semantics.
+ * <p/>
+ * Throws a SystemException in case of an existing transaction in scope.
+ */
+ def withTxNever[T](body: => T): T = {
+ if (isInExistingTransaction(TransactionContext.getTransactionManager)) throw new SystemException("Detected active TX at method with TX type set to NEVER")
+ body
+ }
+
+ protected def handleException(tm: TransactionManager, e: Exception) = {
+ if (isInExistingTransaction(tm)) {
+ // Do not roll back in case of NoResultException or NonUniqueResultException
+ if (!e.isInstanceOf[NoResultException] &&
+ !e.isInstanceOf[NonUniqueResultException]) {
+ logger.debug("Setting TX to ROLLBACK_ONLY, due to: " + e)
+ tm.setRollbackOnly
+ }
+ }
+ throw e
+ }
+
+ protected def commitOrRollBack(tm: TransactionManager) = {
+ if (isInExistingTransaction(tm)) {
+ if (isRollbackOnly(tm)) {
+ logger.debug("Rolling back TX marked as ROLLBACK_ONLY")
+ tm.rollback
+ } else {
+ logger.debug("Committing TX")
+ tm.commit
+ }
+ }
+ }
+
+ // ---------------------------
+ // Helper methods
+ // ---------------------------
+
+ /**
+ * Checks if a transaction is an existing transaction.
+ *
+ * @param tm the transaction manager
+ * @return boolean
+ */
+ protected def isInExistingTransaction(tm: TransactionManager): Boolean = tm.getStatus != Status.STATUS_NO_TRANSACTION
+
+ /**
+ * Checks if current transaction is set to rollback only.
+ *
+ * @param tm the transaction manager
+ * @return boolean
+ */
+ protected def isRollbackOnly(tm: TransactionManager): Boolean = tm.getStatus == Status.STATUS_MARKED_ROLLBACK
+
+ /**
+ * Join JTA transaction.
+ */
+ private def joinTransaction = {
+ val em = TransactionContext.getEntityManager
+ val tm = TransactionContext.getTransactionManager
+ tm.getTransaction.registerSynchronization(new EntityManagerSynchronization(em, tm, false))
+ em.joinTransaction // join JTA transaction
+ }
+
+ /**
+ * A ThreadLocal variable where to store suspended TX and enable pay as you go
+ * before advice - after advice data sharing in a specific case of requiresNew TX
+ */
+ private val suspendedTx = new ThreadLocal[Transaction] {
+ override def initialValue = null
+ }
+
+ private def storeInThreadLocal(tx: Transaction) = suspendedTx.set(tx)
+
+ private def fetchFromThreadLocal: Option[Transaction] = {
+ if (suspendedTx != null && suspendedTx.get() != null) Some(suspendedTx.get.asInstanceOf[Transaction])
+ else None
+ }
+}
View
57 src/main/scala/net/liftweb/transaction/atomikos/AtomikosTransactionService.scala
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2009-2010 WorldWide Conferencing, LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.liftweb {
+package transaction {
+package atomikos {
+
+import _root_.javax.transaction.{TransactionManager, SystemException}
+
+/**
+ * Atomikos implementation of the transaction service trait.
+ *
+ * @author <a href="http://jonasboner.com">Jonas Bon&#233;r</a>
+ */
+object AtomikosTransactionService extends
+ TransactionService with
+ EntityManagerService with
+ TransactionProtocol {
+
+ import com.atomikos.icatch.jta.{J2eeTransactionManager, J2eeUserTransaction}
+ import com.atomikos.icatch.config.{TSInitInfo, UserTransactionService, UserTransactionServiceImp}
+
+ // FIXME: make configurable
+ val JTA_TRANSACTION_TIMEOUT = 60
+ private val txService: UserTransactionService = new UserTransactionServiceImp
+ private val info: TSInitInfo = txService.createTSInitInfo
+
+ val transactionManager =
+ try {
+ txService.init(info)
+ val tm: TransactionManager = new J2eeTransactionManager
+ tm.setTransactionTimeout(JTA_TRANSACTION_TIMEOUT)
+ tm
+ } catch {
+ case e => throw new SystemException("Could not create a new Atomikos J2EE Transaction Manager, due to: " + e.toString)
+ }
+
+ // TODO: gracefully shutdown of the TM
+ //txService.shutdown(false)
+}
+
+}
+}
+}
View
36 src/main/scala/net/liftweb/transaction/hibernate/LiftTransactionManagerLookup.scala
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2009-2010 WorldWide Conferencing, LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.liftweb {
+package transaction {
+package hibernate {
+
+import javax.transaction.{TransactionManager, Transaction}
+
+/**
+ * Hibernate TransactionManager lookup class.
+ *
+ * @author <a href="http://jonasboner.com">Jonas Bon&#233;r</a>
+ */
+class LiftTransactionManagerLookup extends org.hibernate.transaction.TransactionManagerLookup {
+ def getTransactionManager(props: _root_.java.util.Properties): TransactionManager = TransactionContext.getTransactionManager
+ def getUserTransactionName: String = "java:comp/UserTransaction"
+ def getTransactionIdentifier(tx: Transaction) = tx
+}
+
+}
+}
+}
View
61 src/test/scala/TransactionMonadSpec.scala
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2009-2010 WorldWide Conferencing, LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.liftweb {
+package transaction {
+
+//import com.jteigen.scalatest.JUnit4Runner
+
+//import org.junit.runner.RunWith
+//import org.scalatest._
+//import org.scalatest.matchers._
+
+/**
+ * @author <a href="http://jonasboner.com">Jonas Bon&#233;r</a>
+ *
+@RunWith(classOf[JUnit4Runner])
+class TransactionMonadSpec extends Spec with ShouldMatchers {
+ describe("A TransactionMonad") {
+ it("should support foreach") {
+ for (ctx <- TransactionContext.Required) {
+ println("Context: " + ctx)
+ }
+ }
+
+ it("should support map") {
+ for (ctx <- TransactionContext.Required) {
+ println("Context: " + ctx)
+ ctx
+ }
+ }
+
+ it("should support flatMap") {
+ val userNames = "Bill" :: "Bob" :: "Alice" :: Nil
+ val users = for {
+ ctx <- TransactionContext.Required
+ name <- userNames
+ } yield {
+ val query = ctx.getEntityManager.createNamedQuery("findUserByName")
+ query.setParameter("userName", name)
+ query.getSingleResult
+ }
+ println("Users: " + users)
+ }
+ }
+}
+*/
+}
+}

0 comments on commit 0ead3fd

Please sign in to comment.
Something went wrong with that request. Please try again.