diff --git a/spring-boot-autoconfigure/pom.xml b/spring-boot-autoconfigure/pom.xml
index b6de2d4b7b..cb05053708 100755
--- a/spring-boot-autoconfigure/pom.xml
+++ b/spring-boot-autoconfigure/pom.xml
@@ -577,6 +577,16 @@
jooq
true
+
+ org.jboss.narayana.jta
+ jta
+ true
+
+
+ org.jboss.narayana.jts
+ narayana-jts-integration
+ true
+
org.springframework.boot
diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/jta/JtaAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/jta/JtaAutoConfiguration.java
index c52f82a6d8..5a75fb70c6 100644
--- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/jta/JtaAutoConfiguration.java
+++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/jta/JtaAutoConfiguration.java
@@ -40,7 +40,7 @@ import org.springframework.context.annotation.Import;
ActiveMQAutoConfiguration.class, HornetQAutoConfiguration.class,
HibernateJpaAutoConfiguration.class })
@Import({ JndiJtaConfiguration.class, BitronixJtaConfiguration.class,
- AtomikosJtaConfiguration.class })
+ AtomikosJtaConfiguration.class, NarayanaJtaConfiguration.class })
@EnableConfigurationProperties(JtaProperties.class)
public class JtaAutoConfiguration {
diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/jta/NarayanaJtaConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/jta/NarayanaJtaConfiguration.java
new file mode 100644
index 0000000000..0fa78ffac4
--- /dev/null
+++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/jta/NarayanaJtaConfiguration.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2012-2015 the original author or authors.
+ *
+ * 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 org.springframework.boot.autoconfigure.transaction.jta;
+
+import javax.jms.Message;
+import javax.transaction.TransactionManager;
+import javax.transaction.UserTransaction;
+
+import com.arjuna.ats.jbossatx.jta.RecoveryManagerService;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.jta.XAConnectionFactoryWrapper;
+import org.springframework.boot.jta.XADataSourceWrapper;
+import org.springframework.boot.jta.narayana.NarayanaBeanFactoryPostProcessor;
+import org.springframework.boot.jta.narayana.NarayanaConfigurationBean;
+import org.springframework.boot.jta.narayana.NarayanaProperties;
+import org.springframework.boot.jta.narayana.NarayanaRecoveryManagerBean;
+import org.springframework.boot.jta.narayana.NarayanaXAConnectionFactoryWrapper;
+import org.springframework.boot.jta.narayana.NarayanaXADataSourceWrapper;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.DependsOn;
+import org.springframework.transaction.PlatformTransactionManager;
+import org.springframework.transaction.jta.JtaTransactionManager;
+
+/**
+ * JTA Configuration for Narayana.
+ *
+ * @author Gytis Trikleris
+ */
+@Configuration
+@ConditionalOnClass({ JtaTransactionManager.class, com.arjuna.ats.jta.UserTransaction.class })
+@ConditionalOnMissingBean(PlatformTransactionManager.class)
+public class NarayanaJtaConfiguration {
+
+ @Autowired
+ private JtaProperties jtaProperties;
+
+ @Bean
+ @ConditionalOnMissingBean
+ public NarayanaProperties narayanaProperties() {
+ return new NarayanaProperties();
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ public NarayanaConfigurationBean narayanaConfigurationBean(NarayanaProperties narayanaProperties) {
+ if (this.jtaProperties.getLogDir() != null) {
+ narayanaProperties.setLogDir(this.jtaProperties.getLogDir());
+ }
+
+ if (this.jtaProperties.getTransactionManagerId() != null) {
+ narayanaProperties.setTransactionManagerId(this.jtaProperties.getTransactionManagerId());
+ }
+
+ return new NarayanaConfigurationBean(narayanaProperties);
+ }
+
+ @Bean
+ @DependsOn("narayanaConfigurationBean")
+ @ConditionalOnMissingBean
+ public UserTransaction narayanaUserTransaction() {
+ return com.arjuna.ats.jta.UserTransaction.userTransaction();
+ }
+
+ @Bean
+ @DependsOn("narayanaConfigurationBean")
+ @ConditionalOnMissingBean
+ public TransactionManager narayanaTransactionManager() {
+ return com.arjuna.ats.jta.TransactionManager.transactionManager();
+ }
+
+ @Bean
+ @DependsOn("narayanaConfigurationBean")
+ public RecoveryManagerService narayanaRecoveryManagerService() {
+ return new RecoveryManagerService();
+ }
+
+ @Bean
+ public NarayanaRecoveryManagerBean narayanaRecoveryManagerBean(RecoveryManagerService recoveryManagerService) {
+ return new NarayanaRecoveryManagerBean(recoveryManagerService);
+ }
+
+ @Bean
+ public JtaTransactionManager transactionManager(UserTransaction userTransaction, TransactionManager transactionManager) {
+ return new JtaTransactionManager(userTransaction, transactionManager);
+ }
+
+ @Bean
+ @ConditionalOnMissingBean(XADataSourceWrapper.class)
+ public XADataSourceWrapper xaDataSourceWrapper(NarayanaRecoveryManagerBean narayanaRecoveryManagerBean,
+ NarayanaProperties narayanaProperties) {
+ return new NarayanaXADataSourceWrapper(narayanaRecoveryManagerBean, narayanaProperties);
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ public static NarayanaBeanFactoryPostProcessor narayanaBeanFactoryPostProcessor() {
+ return new NarayanaBeanFactoryPostProcessor();
+ }
+
+ @Configuration
+ @ConditionalOnClass(Message.class)
+ static class NarayanaJtaJmsConfiguration {
+
+ @Bean
+ @ConditionalOnMissingBean(XAConnectionFactoryWrapper.class)
+ public NarayanaXAConnectionFactoryWrapper xaConnectionFactoryWrapper(TransactionManager transactionManager,
+ NarayanaRecoveryManagerBean narayanaRecoveryManagerBean, NarayanaProperties narayanaProperties) {
+ return new NarayanaXAConnectionFactoryWrapper(transactionManager, narayanaRecoveryManagerBean, narayanaProperties);
+ }
+
+ }
+
+}
diff --git a/spring-boot-dependencies/pom.xml b/spring-boot-dependencies/pom.xml
index 361d2a730e..597ac18d13 100644
--- a/spring-boot-dependencies/pom.xml
+++ b/spring-boot-dependencies/pom.xml
@@ -174,6 +174,8 @@
9f96c74
0.30
1.6.3
+ 5.3.2.Final
+ 7.3.0.Final
3.2.1
@@ -399,6 +401,11 @@
spring-boot-starter-jta-bitronix
1.4.0.BUILD-SNAPSHOT
+
+ org.springframework.boot
+ spring-boot-starter-jta-narayana
+ 1.4.0.BUILD-SNAPSHOT
+
org.springframework.boot
spring-boot-starter-undertow
@@ -2212,6 +2219,31 @@
wsdl4j
${wsdl4j.version}
+
+ org.jboss.narayana.jta
+ jta
+ ${narayana.version}
+
+
+ org.jboss.narayana.jta
+ jdbc
+ ${narayana.version}
+
+
+ org.jboss.narayana.jta
+ jms
+ ${narayana.version}
+
+
+ org.jboss.narayana.jts
+ narayana-jts-integration
+ ${narayana.version}
+
+
+ org.jboss
+ jboss-transaction-spi
+ ${jboss-transaction-spi.version}
+
diff --git a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc
index 91564175fa..e8e42ffd58 100644
--- a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc
+++ b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc
@@ -715,6 +715,19 @@ content into your application; rather pick only the properties that you need.
spring.jta.bitronix.properties.skip-corrupted-logs=false # Skip corrupted transactions log entries.
spring.jta.bitronix.properties.warn-about-zero-resource-transaction=true # Log a warning for transactions executed without a single enlisted resource.
+ # NARAYANA
+ spring.jta.narayana.one-phase-commit=true # Enable or disable one phase commit optimisation
+ spring.jta.narayana.default-timeout=60 # Set default transaction timeout in seconds
+ spring.jta.narayana.periodic-recovery-period=120 # Set interval in which periodic recovery scans are performed in seconds
+ spring.jta.narayana.recovery-backoff-period=10 # Set back off period between first and second phases of the recovery scan in seconds
+ spring.jta.narayana.xa-resource-orphan-filters=com.arjuna.ats.internal.jta.recovery.arjunacore.JTATransactionLogXAResourceOrphanFilter,com.arjuna.ats.internal.jta.recovery.arjunacore.JTANodeNameXAResourceOrphanFilter # List of XAResourceOrphanFilter implementations
+ spring.jta.narayana.recovery-modules=com.arjuna.ats.internal.arjuna.recovery.AtomicActionRecoveryModule,com.arjuna.ats.internal.jta.recovery.arjunacore.XARecoveryModule # List of RecoveryModule implementations
+ spring.jta.narayana.expiry-scanners=com.arjuna.ats.internal.arjuna.recovery.ExpiredTransactionStatusManagerScanner # List of ExpiryScanner implementations
+ spring.jta.narayana.recovery-db-user= # Database username to be used by recovery manager
+ spring.jta.narayana.recovery-db-pass= # Database password to be used by recovery manager
+ spring.jta.narayana.recovery-jms-user= # JMS username to be used by recovery manager
+ spring.jta.narayana.recovery-jms-pass= # JMS password to be used by recovery manager
+
# EMBEDDED MONGODB ({sc-spring-boot-autoconfigure}/mongo/embedded/EmbeddedMongoProperties.{sc-ext}[EmbeddedMongoProperties])
spring.mongodb.embedded.features=SYNC_DELAY # Comma-separated list of features to enable.
spring.mongodb.embedded.version=2.6.10 # Version of Mongo to use.
diff --git a/spring-boot-samples/pom.xml b/spring-boot-samples/pom.xml
index 427eabc9d9..6681adbeef 100644
--- a/spring-boot-samples/pom.xml
+++ b/spring-boot-samples/pom.xml
@@ -63,6 +63,7 @@
spring-boot-sample-jpa
spring-boot-sample-jta-atomikos
spring-boot-sample-jta-bitronix
+ spring-boot-sample-jta-narayana
spring-boot-sample-jta-jndi
spring-boot-sample-liquibase
spring-boot-sample-logback
diff --git a/spring-boot-samples/spring-boot-sample-jta-narayana/pom.xml b/spring-boot-samples/spring-boot-sample-jta-narayana/pom.xml
new file mode 100644
index 0000000000..4d7d9c32c6
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-jta-narayana/pom.xml
@@ -0,0 +1,57 @@
+
+
+ 4.0.0
+
+ spring-boot-samples
+ org.springframework.boot
+ 1.4.0.BUILD-SNAPSHOT
+
+ spring-boot-sample-jta-narayana
+ Spring Boot Narayana JTA Sample
+ Spring Boot Narayana JTA Sample
+ http://projects.spring.io/spring-boot/
+
+ ${basedir}/../..
+
+
+
+ org.springframework
+ spring-jms
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+ org.springframework.boot
+ spring-boot-starter-jta-narayana
+
+
+ org.springframework.boot
+ spring-boot-starter-hornetq
+
+
+ org.hornetq
+ hornetq-jms-server
+
+
+ com.h2database
+ h2
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
\ No newline at end of file
diff --git a/spring-boot-samples/spring-boot-sample-jta-narayana/src/main/java/sample/narayana/Account.java b/spring-boot-samples/spring-boot-sample-jta-narayana/src/main/java/sample/narayana/Account.java
new file mode 100644
index 0000000000..f588e09f72
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-jta-narayana/src/main/java/sample/narayana/Account.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2012-2014 the original author or authors.
+ *
+ * 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 sample.narayana;
+
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.Id;
+
+@Entity
+public class Account {
+
+ @Id
+ @GeneratedValue
+ private Long id;
+
+ private String username;
+
+ Account() {
+ }
+
+ public Account(String username) {
+ this.username = username;
+ }
+
+ public String getUsername() {
+ return this.username;
+ }
+
+}
diff --git a/spring-boot-samples/spring-boot-sample-jta-narayana/src/main/java/sample/narayana/AccountRepository.java b/spring-boot-samples/spring-boot-sample-jta-narayana/src/main/java/sample/narayana/AccountRepository.java
new file mode 100644
index 0000000000..eb8ec8e980
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-jta-narayana/src/main/java/sample/narayana/AccountRepository.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2012-2014 the original author or authors.
+ *
+ * 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 sample.narayana;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface AccountRepository extends JpaRepository {
+
+}
diff --git a/spring-boot-samples/spring-boot-sample-jta-narayana/src/main/java/sample/narayana/AccountService.java b/spring-boot-samples/spring-boot-sample-jta-narayana/src/main/java/sample/narayana/AccountService.java
new file mode 100644
index 0000000000..72f0d756d4
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-jta-narayana/src/main/java/sample/narayana/AccountService.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2012-2014 the original author or authors.
+ *
+ * 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 sample.narayana;
+
+import javax.transaction.Transactional;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.jms.core.JmsTemplate;
+import org.springframework.stereotype.Service;
+
+@Service
+@Transactional
+public class AccountService {
+
+ private final JmsTemplate jmsTemplate;
+
+ private final AccountRepository accountRepository;
+
+ @Autowired
+ public AccountService(JmsTemplate jmsTemplate, AccountRepository accountRepository) {
+ this.jmsTemplate = jmsTemplate;
+ this.accountRepository = accountRepository;
+ }
+
+ public void createAccountAndNotify(String username) {
+ this.jmsTemplate.convertAndSend("accounts", username);
+ this.accountRepository.save(new Account(username));
+ if ("error".equals(username)) {
+ throw new SampleRuntimeException("Simulated error");
+ }
+ }
+
+}
diff --git a/spring-boot-samples/spring-boot-sample-jta-narayana/src/main/java/sample/narayana/Messages.java b/spring-boot-samples/spring-boot-sample-jta-narayana/src/main/java/sample/narayana/Messages.java
new file mode 100644
index 0000000000..90ff54fb1b
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-jta-narayana/src/main/java/sample/narayana/Messages.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2012-2014 the original author or authors.
+ *
+ * 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 sample.narayana;
+
+import org.springframework.jms.annotation.JmsListener;
+import org.springframework.stereotype.Component;
+
+@Component
+public class Messages {
+
+ @JmsListener(destination = "accounts")
+ public void onMessage(String content) {
+ System.out.println("----> " + content);
+ }
+
+}
diff --git a/spring-boot-samples/spring-boot-sample-jta-narayana/src/main/java/sample/narayana/SampleNarayanaApplication.java b/spring-boot-samples/spring-boot-sample-jta-narayana/src/main/java/sample/narayana/SampleNarayanaApplication.java
new file mode 100644
index 0000000000..9585d14489
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-jta-narayana/src/main/java/sample/narayana/SampleNarayanaApplication.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2012-2014 the original author or authors.
+ *
+ * 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 sample.narayana;
+
+import java.io.Closeable;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.ApplicationContext;
+
+@SpringBootApplication
+public class SampleNarayanaApplication {
+
+ public static void main(String[] args) throws Exception {
+ ApplicationContext context = SpringApplication.run(SampleNarayanaApplication.class, args);
+ AccountService service = context.getBean(AccountService.class);
+ AccountRepository repository = context.getBean(AccountRepository.class);
+ service.createAccountAndNotify("josh");
+ System.out.println("Count is " + repository.count());
+ try {
+ // Using username "error" will cause service to throw SampleRuntimeException
+ service.createAccountAndNotify("error");
+ } catch (SampleRuntimeException ex) {
+ // Log message to let test case know that exception was thrown
+ System.out.println(ex.getMessage());
+ }
+ System.out.println("Count is " + repository.count());
+ ((Closeable) context).close();
+ }
+
+}
diff --git a/spring-boot-samples/spring-boot-sample-jta-narayana/src/main/java/sample/narayana/SampleRuntimeException.java b/spring-boot-samples/spring-boot-sample-jta-narayana/src/main/java/sample/narayana/SampleRuntimeException.java
new file mode 100644
index 0000000000..f65d484897
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-jta-narayana/src/main/java/sample/narayana/SampleRuntimeException.java
@@ -0,0 +1,12 @@
+package sample.narayana;
+
+/**
+ * @author Gytis Trikleris
+ */
+public class SampleRuntimeException extends RuntimeException {
+
+ public SampleRuntimeException(String message) {
+ super(message);
+ }
+
+}
diff --git a/spring-boot-samples/spring-boot-sample-jta-narayana/src/main/resources/application.properties b/spring-boot-samples/spring-boot-sample-jta-narayana/src/main/resources/application.properties
new file mode 100644
index 0000000000..ffbc046eb3
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-jta-narayana/src/main/resources/application.properties
@@ -0,0 +1,5 @@
+spring.hornetq.mode=embedded
+spring.hornetq.embedded.enabled=true
+spring.hornetq.embedded.queues=accounts
+
+logging.level.com.arjuna=INFO
\ No newline at end of file
diff --git a/spring-boot-samples/spring-boot-sample-jta-narayana/src/test/java/sample/narayana/SampleNarayanaApplicationTests.java b/spring-boot-samples/spring-boot-sample-jta-narayana/src/test/java/sample/narayana/SampleNarayanaApplicationTests.java
new file mode 100644
index 0000000000..4ec0ab2b85
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-jta-narayana/src/test/java/sample/narayana/SampleNarayanaApplicationTests.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2012-2014 the original author or authors.
+ *
+ * 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 sample.narayana;
+
+import org.hamcrest.Matcher;
+import org.hamcrest.core.SubstringMatcher;
+import org.junit.Rule;
+import org.junit.Test;
+import org.springframework.boot.test.OutputCapture;
+
+import static org.junit.Assert.assertThat;
+
+/**
+ * @author Gytis Trikleris
+ */
+public class SampleNarayanaApplicationTests {
+
+ @Rule
+ public OutputCapture outputCapture = new OutputCapture();
+
+ @Test
+ public void testTransactionRollback() throws Exception {
+ SampleNarayanaApplication.main(new String[] {});
+ String output = this.outputCapture.toString();
+ assertThat(output, containsString(1, "---->"));
+ assertThat(output, containsString(1, "----> josh"));
+ assertThat(output, containsString(2, "Count is 1"));
+ assertThat(output, containsString(1, "Simulated error"));
+ }
+
+ private Matcher super String> containsString(final int times, String s) {
+ return new SubstringMatcher(s) {
+
+ @Override
+ protected String relationship() {
+ return "containing " + times + " times";
+ }
+
+ @Override
+ protected boolean evalSubstringOf(String s) {
+ int i = 0;
+ while (s.contains(this.substring)) {
+ s = s.substring(s.indexOf(this.substring) + this.substring.length());
+ i++;
+ }
+ return i == times;
+ }
+
+ };
+ }
+
+}
diff --git a/spring-boot-starters/pom.xml b/spring-boot-starters/pom.xml
index 4b8579cb5c..40aa71d0b6 100644
--- a/spring-boot-starters/pom.xml
+++ b/spring-boot-starters/pom.xml
@@ -48,6 +48,7 @@
spring-boot-starter-jooq
spring-boot-starter-jta-atomikos
spring-boot-starter-jta-bitronix
+ spring-boot-starter-jta-narayana
spring-boot-starter-logging
spring-boot-starter-log4j2
spring-boot-starter-mail
diff --git a/spring-boot-starters/spring-boot-starter-jta-narayana/pom.xml b/spring-boot-starters/spring-boot-starter-jta-narayana/pom.xml
new file mode 100644
index 0000000000..9b689932e0
--- /dev/null
+++ b/spring-boot-starters/spring-boot-starter-jta-narayana/pom.xml
@@ -0,0 +1,48 @@
+
+
+ 4.0.0
+
+ spring-boot-starters
+ org.springframework.boot
+ 1.4.0.BUILD-SNAPSHOT
+
+ spring-boot-starter-jta-narayana
+ Spring Boot Narayana JTA Starter
+ Spring Boot Narayana JTA Starter
+ http://projects.spring.io/spring-boot/
+
+ ${basedir}/../..
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+ org.jboss.narayana.jta
+ jta
+
+
+ org.jboss.narayana.jta
+ jdbc
+
+
+ org.jboss.narayana.jta
+ jms
+
+
+ org.jboss.narayana.jts
+ narayana-jts-integration
+
+
+ org.jboss
+ jboss-transaction-spi
+
+
+ javax.transaction
+ javax.transaction-api
+
+
+
\ No newline at end of file
diff --git a/spring-boot-starters/spring-boot-starter-jta-narayana/src/main/resources/META-INF/spring.provides b/spring-boot-starters/spring-boot-starter-jta-narayana/src/main/resources/META-INF/spring.provides
new file mode 100644
index 0000000000..ff483355d0
--- /dev/null
+++ b/spring-boot-starters/spring-boot-starter-jta-narayana/src/main/resources/META-INF/spring.provides
@@ -0,0 +1 @@
+provides: jta, jdbc, jms, jboss-transaction-spi
\ No newline at end of file
diff --git a/spring-boot/pom.xml b/spring-boot/pom.xml
index c853b19759..6dbef42a7c 100644
--- a/spring-boot/pom.xml
+++ b/spring-boot/pom.xml
@@ -229,6 +229,31 @@
snakeyaml
true
+
+ org.jboss.narayana.jta
+ jta
+ true
+
+
+ org.jboss.narayana.jta
+ jdbc
+ true
+
+
+ org.jboss.narayana.jta
+ jms
+ true
+
+
+ org.jboss.narayana.jts
+ narayana-jts-integration
+ true
+
+
+ org.jboss
+ jboss-transaction-spi
+ true
+
org.springframework.boot
diff --git a/spring-boot/src/main/java/org/springframework/boot/jta/narayana/DataSourceXAResourceRecoveryHelper.java b/spring-boot/src/main/java/org/springframework/boot/jta/narayana/DataSourceXAResourceRecoveryHelper.java
new file mode 100644
index 0000000000..6ff48c18f9
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/jta/narayana/DataSourceXAResourceRecoveryHelper.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright 2012-2015 the original author or authors.
+ *
+ * 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 org.springframework.boot.jta.narayana;
+
+import java.sql.SQLException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.sql.XAConnection;
+import javax.sql.XADataSource;
+import javax.transaction.xa.XAException;
+import javax.transaction.xa.XAResource;
+import javax.transaction.xa.Xid;
+
+import com.arjuna.ats.jta.recovery.XAResourceRecoveryHelper;
+
+/**
+ * XAResourceRecoveryHelper implementation which gets Xids, which needs to be recovered, from the database.
+ *
+ * @author Gytis Trikleris
+ */
+public class DataSourceXAResourceRecoveryHelper implements XAResourceRecoveryHelper, XAResource {
+
+ private static final Logger LOGGER = Logger.getLogger(DataSourceXAResourceRecoveryHelper.class.getName());
+
+ private final XADataSource xaDataSource;
+
+ private final String user;
+
+ private final String pass;
+
+ private XAConnection xaConnection;
+
+ private XAResource delegate;
+
+ public DataSourceXAResourceRecoveryHelper(XADataSource xaDataSource) {
+ this(xaDataSource, null, null);
+ }
+
+ public DataSourceXAResourceRecoveryHelper(XADataSource xaDataSource, String user, String pass) {
+ this.xaDataSource = xaDataSource;
+ this.user = user;
+ this.pass = pass;
+ }
+
+ @Override
+ public boolean initialise(String properties) {
+ return true;
+ }
+
+ @Override
+ public XAResource[] getXAResources() {
+ if (connect()) {
+ return new XAResource[] { this };
+ }
+
+ return new XAResource[0];
+ }
+
+ @Override
+ public Xid[] recover(int i) throws XAException {
+ try {
+ return this.delegate.recover(i);
+ }
+ finally {
+ if (i == XAResource.TMENDRSCAN) {
+ disconnect();
+ }
+ }
+ }
+
+ @Override
+ public void start(Xid xid, int i) throws XAException {
+ this.delegate.start(xid, i);
+ }
+
+ @Override
+ public void end(Xid xid, int i) throws XAException {
+ this.delegate.end(xid, i);
+ }
+
+ @Override
+ public int prepare(Xid xid) throws XAException {
+ return this.delegate.prepare(xid);
+ }
+
+ @Override
+ public void commit(Xid xid, boolean b) throws XAException {
+ this.delegate.commit(xid, b);
+ }
+
+ @Override
+ public void rollback(Xid xid) throws XAException {
+ this.delegate.rollback(xid);
+ }
+
+ @Override
+ public boolean isSameRM(XAResource xaResource) throws XAException {
+ return this.delegate.isSameRM(xaResource);
+ }
+
+ @Override
+ public void forget(Xid xid) throws XAException {
+ this.delegate.forget(xid);
+ }
+
+ @Override
+ public int getTransactionTimeout() throws XAException {
+ return this.delegate.getTransactionTimeout();
+ }
+
+ @Override
+ public boolean setTransactionTimeout(int i) throws XAException {
+ return this.delegate.setTransactionTimeout(i);
+ }
+
+ private boolean connect() {
+ if (this.delegate != null) {
+ return true;
+ }
+
+ try {
+ this.xaConnection = getXaConnection();
+ this.delegate = this.xaConnection.getXAResource();
+ }
+ catch (SQLException e) {
+ LOGGER.log(Level.WARNING, "Failed to create connection", e);
+ return false;
+ }
+
+ return true;
+ }
+
+ private void disconnect() throws XAException {
+ try {
+ this.xaConnection.close();
+ }
+ catch (SQLException e) {
+ LOGGER.log(Level.WARNING, "Failed to close connection", e);
+ }
+ finally {
+ this.xaConnection = null;
+ this.delegate = null;
+ }
+ }
+
+ private XAConnection getXaConnection() throws SQLException {
+ if (this.user == null && this.pass == null) {
+ return this.xaDataSource.getXAConnection();
+ }
+
+ return this.xaDataSource.getXAConnection(this.user, this.pass);
+ }
+
+}
diff --git a/spring-boot/src/main/java/org/springframework/boot/jta/narayana/NarayanaBeanFactoryPostProcessor.java b/spring-boot/src/main/java/org/springframework/boot/jta/narayana/NarayanaBeanFactoryPostProcessor.java
new file mode 100644
index 0000000000..8b7a4513d9
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/jta/narayana/NarayanaBeanFactoryPostProcessor.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2012-2015 the original author or authors.
+ *
+ * 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 org.springframework.boot.jta.narayana;
+
+import javax.transaction.TransactionManager;
+
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
+import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
+import org.springframework.core.Ordered;
+
+/**
+ * {@link BeanFactoryPostProcessor} to automatically setup correct beans ordering.
+ *
+ * @author Gytis Trikleris
+ */
+public class NarayanaBeanFactoryPostProcessor implements BeanFactoryPostProcessor, Ordered {
+
+ private static final String[] NO_BEANS = {};
+
+ private static final int ORDER = Ordered.LOWEST_PRECEDENCE;
+
+ @Override
+ public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
+ String[] transactionManagers = beanFactory.getBeanNamesForType(TransactionManager.class, true, false);
+ String[] recoveryManagers = beanFactory.getBeanNamesForType(NarayanaRecoveryManagerBean.class, true, false);
+
+ addBeanDependencies(beanFactory, transactionManagers, "javax.sql.DataSource");
+ addBeanDependencies(beanFactory, recoveryManagers, "javax.sql.DataSource");
+ addBeanDependencies(beanFactory, transactionManagers, "javax.jms.ConnectionFactory");
+ addBeanDependencies(beanFactory, recoveryManagers, "javax.jms.ConnectionFactory");
+ }
+
+ private void addBeanDependencies(ConfigurableListableBeanFactory beanFactory, String[] beanNames, String dependencyType) {
+ for (String beanName : beanNames) {
+ addBeanDependencies(beanFactory, beanName, dependencyType);
+ }
+ }
+
+ private void addBeanDependencies(ConfigurableListableBeanFactory beanFactory, String beanName, String dependencyType) {
+ for (String dependentBeanName : getBeanNamesForType(beanFactory, dependencyType)) {
+ beanFactory.registerDependentBean(beanName, dependentBeanName);
+ }
+ }
+
+ private String[] getBeanNamesForType(ConfigurableListableBeanFactory beanFactory, String type) {
+ try {
+ return beanFactory.getBeanNamesForType(Class.forName(type), true, false);
+ }
+ catch (ClassNotFoundException ex) {
+ // Ignore
+ } catch (NoClassDefFoundError ex) {
+ // Ignore
+ }
+ return NO_BEANS;
+ }
+
+ @Override
+ public int getOrder() {
+ return ORDER;
+ }
+
+}
diff --git a/spring-boot/src/main/java/org/springframework/boot/jta/narayana/NarayanaConfigurationBean.java b/spring-boot/src/main/java/org/springframework/boot/jta/narayana/NarayanaConfigurationBean.java
new file mode 100644
index 0000000000..311a2c503f
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/jta/narayana/NarayanaConfigurationBean.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2012-2015 the original author or authors.
+ *
+ * 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 org.springframework.boot.jta.narayana;
+
+import java.util.List;
+
+import com.arjuna.ats.arjuna.common.CoordinatorEnvironmentBean;
+import com.arjuna.ats.arjuna.common.CoreEnvironmentBean;
+import com.arjuna.ats.arjuna.common.CoreEnvironmentBeanException;
+import com.arjuna.ats.arjuna.common.ObjectStoreEnvironmentBean;
+import com.arjuna.ats.arjuna.common.RecoveryEnvironmentBean;
+import com.arjuna.ats.jta.common.JTAEnvironmentBean;
+import com.arjuna.common.internal.util.propertyservice.BeanPopulator;
+
+import org.springframework.beans.factory.InitializingBean;
+
+/**
+ * Bean that configures Narayana transaction manager.
+ *
+ * @author Gytis Trikleris
+ */
+public class NarayanaConfigurationBean implements InitializingBean {
+
+ private static final String JBOSSTS_PROPERTIES_FILE_NAME = "jbossts-properties.xml";
+
+ private final NarayanaProperties narayanaProperties;
+
+ public NarayanaConfigurationBean(NarayanaProperties narayanaProperties) {
+ this.narayanaProperties = narayanaProperties;
+ }
+
+ @Override
+ public void afterPropertiesSet() throws Exception {
+ if (isPropertiesFileAvailable()) {
+ return;
+ }
+
+ setNodeIdentifier(this.narayanaProperties.getTransactionManagerId());
+ setObjectStoreDir(this.narayanaProperties.getLogDir());
+ setCommitOnePhase(this.narayanaProperties.isOnePhaseCommit());
+ setDefaultTimeout(this.narayanaProperties.getDefaultTimeout());
+ setPeriodicRecoveryPeriod(this.narayanaProperties.getPeriodicRecoveryPeriod());
+ setRecoveryBackoffPeriod(this.narayanaProperties.getRecoveryBackoffPeriod());
+ setXaResourceOrphanFilters(this.narayanaProperties.getXaResourceOrphanFilters());
+ setRecoveryModules(this.narayanaProperties.getRecoveryModules());
+ setExpiryScanners(this.narayanaProperties.getExpiryScanners());
+ }
+
+ private boolean isPropertiesFileAvailable() {
+ return Thread.currentThread().getContextClassLoader().getResource(JBOSSTS_PROPERTIES_FILE_NAME) != null;
+ }
+
+ private void setNodeIdentifier(String nodeIdentifier) throws CoreEnvironmentBeanException {
+ BeanPopulator.getDefaultInstance(CoreEnvironmentBean.class).setNodeIdentifier(nodeIdentifier);
+ }
+
+ private void setObjectStoreDir(String objectStoreDir) {
+ BeanPopulator.getDefaultInstance(ObjectStoreEnvironmentBean.class).setObjectStoreDir(objectStoreDir);
+ BeanPopulator.getNamedInstance(ObjectStoreEnvironmentBean.class, "communicationStore")
+ .setObjectStoreDir(objectStoreDir);
+ BeanPopulator.getNamedInstance(ObjectStoreEnvironmentBean.class, "stateStore").setObjectStoreDir(objectStoreDir);
+ }
+
+ private void setCommitOnePhase(boolean isCommitOnePhase) {
+ BeanPopulator.getDefaultInstance(CoordinatorEnvironmentBean.class).setCommitOnePhase(isCommitOnePhase);
+ }
+
+ private void setDefaultTimeout(int defaultTimeout) {
+ BeanPopulator.getDefaultInstance(CoordinatorEnvironmentBean.class).setDefaultTimeout(defaultTimeout);
+ }
+
+ private void setPeriodicRecoveryPeriod(int periodicRecoveryPeriod) {
+ BeanPopulator.getDefaultInstance(RecoveryEnvironmentBean.class).setPeriodicRecoveryPeriod(periodicRecoveryPeriod);
+ }
+
+ private void setRecoveryBackoffPeriod(int recoveryBackoffPeriod) {
+ BeanPopulator.getDefaultInstance(RecoveryEnvironmentBean.class).setRecoveryBackoffPeriod(recoveryBackoffPeriod);
+ }
+
+ private void setXaResourceOrphanFilters(List xaResourceOrphanFilters) {
+ BeanPopulator.getDefaultInstance(JTAEnvironmentBean.class).setXaResourceOrphanFilterClassNames(xaResourceOrphanFilters);
+ }
+
+ private void setRecoveryModules(List recoveryModules) {
+ BeanPopulator.getDefaultInstance(RecoveryEnvironmentBean.class).setRecoveryModuleClassNames(recoveryModules);
+ }
+
+ private void setExpiryScanners(List expiryScanners) {
+ BeanPopulator.getDefaultInstance(RecoveryEnvironmentBean.class).setExpiryScannerClassNames(expiryScanners);
+ }
+
+}
diff --git a/spring-boot/src/main/java/org/springframework/boot/jta/narayana/NarayanaDataSourceBean.java b/spring-boot/src/main/java/org/springframework/boot/jta/narayana/NarayanaDataSourceBean.java
new file mode 100644
index 0000000000..1421ec499f
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/jta/narayana/NarayanaDataSourceBean.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2012-2015 the original author or authors.
+ *
+ * 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 org.springframework.boot.jta.narayana;
+
+import java.io.PrintWriter;
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.sql.SQLFeatureNotSupportedException;
+import java.util.Properties;
+import java.util.logging.Logger;
+
+import javax.sql.DataSource;
+import javax.sql.XADataSource;
+
+import com.arjuna.ats.internal.jdbc.ConnectionManager;
+import com.arjuna.ats.jdbc.TransactionalDriver;
+
+/**
+ * {@link DataSource} implementation wrapping {@link XADataSource} and using {@link ConnectionManager} to acquire connections.
+ *
+ * @author Gytis Trikleris
+ */
+public class NarayanaDataSourceBean implements DataSource {
+
+ private final XADataSource xaDataSource;
+
+ public NarayanaDataSourceBean(XADataSource xaDataSource) {
+ this.xaDataSource = xaDataSource;
+ }
+
+ @Override
+ public Connection getConnection() throws SQLException {
+ Properties properties = new Properties();
+ properties.put(TransactionalDriver.XADataSource, this.xaDataSource);
+
+ return ConnectionManager.create(null, properties);
+ }
+
+ @Override
+ public Connection getConnection(String username, String password) throws SQLException {
+ Properties properties = new Properties();
+ properties.put(TransactionalDriver.XADataSource, this.xaDataSource);
+ properties.put(TransactionalDriver.userName, username);
+ properties.put(TransactionalDriver.password, password);
+
+ return ConnectionManager.create(null, properties);
+ }
+
+ @Override
+ public PrintWriter getLogWriter() throws SQLException {
+ return this.xaDataSource.getLogWriter();
+ }
+
+ @Override
+ public void setLogWriter(PrintWriter out) throws SQLException {
+ this.xaDataSource.setLogWriter(out);
+ }
+
+ @Override
+ public void setLoginTimeout(int seconds) throws SQLException {
+ this.xaDataSource.setLoginTimeout(seconds);
+ }
+
+ @Override
+ public int getLoginTimeout() throws SQLException {
+ return this.xaDataSource.getLoginTimeout();
+ }
+
+ @Override
+ public Logger getParentLogger() throws SQLFeatureNotSupportedException {
+ throw new SQLFeatureNotSupportedException();
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public T unwrap(Class iface) throws SQLException {
+ if (isWrapperFor(iface)) {
+ return (T) this;
+ }
+ else if (isWrapperFor(iface, this.xaDataSource.getClass())) {
+ return (T) this.xaDataSource;
+ }
+
+ throw new SQLException(getClass() + " is not a wrapper for " + iface);
+ }
+
+ @Override
+ public boolean isWrapperFor(Class> iface) throws SQLException {
+ return isWrapperFor(iface, getClass());
+ }
+
+ private boolean isWrapperFor(Class> iface, Class> wrapperIface) {
+ return iface.isAssignableFrom(wrapperIface);
+ }
+
+}
diff --git a/spring-boot/src/main/java/org/springframework/boot/jta/narayana/NarayanaProperties.java b/spring-boot/src/main/java/org/springframework/boot/jta/narayana/NarayanaProperties.java
new file mode 100644
index 0000000000..1962fe43fb
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/jta/narayana/NarayanaProperties.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright 2012-2015 the original author or authors.
+ *
+ * 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 org.springframework.boot.jta.narayana;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * Subset of Narayana properties which can be configured via Spring configuration. Use jbossts-properties.xml for complete
+ * configuration.
+ *
+ * @author Gytis Trikleris
+ */
+@ConfigurationProperties(prefix = NarayanaProperties.PROPERTIES_PREFIX)
+public class NarayanaProperties {
+
+ /**
+ * Prefix for Narayana specific properties.
+ */
+ public static final String PROPERTIES_PREFIX = "spring.jta.narayana";
+
+ /**
+ * Transaction object store directory. Default: target/tx-object-store.
+ */
+ private String logDir = "target/tx-object-store";
+
+ /**
+ * Unique transaction manager id. Default: 1.
+ */
+ private String transactionManagerId = "1";
+
+ /**
+ * Enable one phase commit optimisation. Default: true.
+ */
+ private boolean onePhaseCommit = true;
+
+ /**
+ * Transaction timeout in seconds. Default: 60.
+ */
+ private int defaultTimeout = 60;
+
+ /**
+ * Interval in which periodic recovery scans are performed in seconds. Default: 120
+ */
+ private int periodicRecoveryPeriod = 120;
+
+ /**
+ * Back off period between first and second phases of the recovery scan in seconds. Default: 10
+ */
+ private int recoveryBackoffPeriod = 10;
+
+ /**
+ * Database username to be used by recovery manager. Default: null
+ */
+ private String recoveryDbUser = null;
+
+ /**
+ * Database password to be used by recovery manager. Default: null
+ */
+ private String recoveryDbPass = null;
+
+ /**
+ * JMS username to be used by recovery manager. Default: null
+ */
+ private String recoveryJmsUser = null;
+
+ /**
+ * JMS password to be used by recovery manager. Default: null
+ */
+ private String recoveryJmsPass = null;
+
+ /**
+ * List of orphan filters. Default:
+ *
+ * - com.arjuna.ats.internal.jta.recovery.arjunacore.JTATransactionLogXAResourceOrphanFilter
+ * - com.arjuna.ats.internal.jta.recovery.arjunacore.JTANodeNameXAResourceOrphanFilter
+ *
+ */
+ private List xaResourceOrphanFilters = Arrays.asList(
+ "com.arjuna.ats.internal.jta.recovery.arjunacore.JTATransactionLogXAResourceOrphanFilter",
+ "com.arjuna.ats.internal.jta.recovery.arjunacore.JTANodeNameXAResourceOrphanFilter");
+
+ /**
+ * List of recovery modules. Default:
+ *
+ * - com.arjuna.ats.internal.arjuna.recovery.AtomicActionRecoveryModule
+ * - com.arjuna.ats.internal.jta.recovery.arjunacore.XARecoveryModule
+ *
+ */
+ private List recoveryModules = Arrays.asList("com.arjuna.ats.internal.arjuna.recovery.AtomicActionRecoveryModule",
+ "com.arjuna.ats.internal.jta.recovery.arjunacore.XARecoveryModule");
+
+ /**
+ * List of expiry scanners. Default:
+ *
+ * - com.arjuna.ats.internal.arjuna.recovery.ExpiredTransactionStatusManagerScanner
+ *
+ */
+ private List expiryScanners = Arrays
+ .asList("com.arjuna.ats.internal.arjuna.recovery.ExpiredTransactionStatusManagerScanner");
+
+ public String getLogDir() {
+ return this.logDir;
+ }
+
+ public void setLogDir(String logDir) {
+ this.logDir = logDir;
+ }
+
+ public String getTransactionManagerId() {
+ return this.transactionManagerId;
+ }
+
+ public void setTransactionManagerId(String transactionManagerId) {
+ this.transactionManagerId = transactionManagerId;
+ }
+
+ public boolean isOnePhaseCommit() {
+ return this.onePhaseCommit;
+ }
+
+ public void setOnePhaseCommit(boolean onePhaseCommit) {
+ this.onePhaseCommit = onePhaseCommit;
+ }
+
+ public int getDefaultTimeout() {
+ return this.defaultTimeout;
+ }
+
+ public int getPeriodicRecoveryPeriod() {
+ return this.periodicRecoveryPeriod;
+ }
+
+ public void setPeriodicRecoveryPeriod(int periodicRecoveryPeriod) {
+ this.periodicRecoveryPeriod = periodicRecoveryPeriod;
+ }
+
+ public int getRecoveryBackoffPeriod() {
+ return this.recoveryBackoffPeriod;
+ }
+
+ public void setRecoveryBackoffPeriod(int recoveryBackoffPeriod) {
+ this.recoveryBackoffPeriod = recoveryBackoffPeriod;
+ }
+
+ public void setDefaultTimeout(int defaultTimeout) {
+ this.defaultTimeout = defaultTimeout;
+ }
+
+ public List getXaResourceOrphanFilters() {
+ return this.xaResourceOrphanFilters;
+ }
+
+ public void setXaResourceOrphanFilters(List xaResourceOrphanFilters) {
+ this.xaResourceOrphanFilters = xaResourceOrphanFilters;
+ }
+
+ public List getRecoveryModules() {
+ return this.recoveryModules;
+ }
+
+ public void setRecoveryModules(List recoveryModules) {
+ this.recoveryModules = recoveryModules;
+ }
+
+ public List getExpiryScanners() {
+ return this.expiryScanners;
+ }
+
+ public void setExpiryScanners(List expiryScanners) {
+ this.expiryScanners = expiryScanners;
+ }
+
+ public String getRecoveryDbUser() {
+ return this.recoveryDbUser;
+ }
+
+ public void setRecoveryDbUser(String recoveryDbUser) {
+ this.recoveryDbUser = recoveryDbUser;
+ }
+
+ public String getRecoveryDbPass() {
+ return this.recoveryDbPass;
+ }
+
+ public void setRecoveryDbPass(String recoveryDbPass) {
+ this.recoveryDbPass = recoveryDbPass;
+ }
+
+ public String getRecoveryJmsUser() {
+ return this.recoveryJmsUser;
+ }
+
+ public void setRecoveryJmsUser(String recoveryJmsUser) {
+ this.recoveryJmsUser = recoveryJmsUser;
+ }
+
+ public String getRecoveryJmsPass() {
+ return this.recoveryJmsPass;
+ }
+
+ public void setRecoveryJmsPass(String recoveryJmsPass) {
+ this.recoveryJmsPass = recoveryJmsPass;
+ }
+}
diff --git a/spring-boot/src/main/java/org/springframework/boot/jta/narayana/NarayanaRecoveryManagerBean.java b/spring-boot/src/main/java/org/springframework/boot/jta/narayana/NarayanaRecoveryManagerBean.java
new file mode 100644
index 0000000000..62c2f976bd
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/jta/narayana/NarayanaRecoveryManagerBean.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2012-2015 the original author or authors.
+ *
+ * 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 org.springframework.boot.jta.narayana;
+
+import com.arjuna.ats.arjuna.recovery.RecoveryManager;
+import com.arjuna.ats.arjuna.recovery.RecoveryModule;
+import com.arjuna.ats.internal.jta.recovery.arjunacore.XARecoveryModule;
+import com.arjuna.ats.jbossatx.jta.RecoveryManagerService;
+import com.arjuna.ats.jta.recovery.XAResourceRecoveryHelper;
+
+import org.springframework.beans.factory.DisposableBean;
+import org.springframework.beans.factory.InitializingBean;
+
+/**
+ * Bean to set up Narayana recovery manager.
+ *
+ * @author Gytis Trikleris
+ */
+public class NarayanaRecoveryManagerBean implements InitializingBean, DisposableBean {
+
+ private final RecoveryManagerService recoveryManagerService;
+
+ public NarayanaRecoveryManagerBean(RecoveryManagerService recoveryManagerService) {
+ this.recoveryManagerService = recoveryManagerService;
+ }
+
+ @Override
+ public void afterPropertiesSet() throws Exception {
+ this.recoveryManagerService.create();
+ this.recoveryManagerService.start();
+ }
+
+ @Override
+ public void destroy() throws Exception {
+ this.recoveryManagerService.stop();
+ this.recoveryManagerService.destroy();
+ }
+
+ void registerXAResourceRecoveryHelper(XAResourceRecoveryHelper xaResourceRecoveryHelper) {
+ getXARecoveryModule(RecoveryManager.manager()).addXAResourceRecoveryHelper(xaResourceRecoveryHelper);
+ }
+
+ private XARecoveryModule getXARecoveryModule(RecoveryManager recoveryManager) {
+ for (RecoveryModule recoveryModule : recoveryManager.getModules()) {
+ if (recoveryModule instanceof XARecoveryModule) {
+ return (XARecoveryModule) recoveryModule;
+ }
+ }
+
+ throw new IllegalStateException("XARecoveryModule is not registered with recovery manager");
+ }
+
+}
diff --git a/spring-boot/src/main/java/org/springframework/boot/jta/narayana/NarayanaXAConnectionFactoryWrapper.java b/spring-boot/src/main/java/org/springframework/boot/jta/narayana/NarayanaXAConnectionFactoryWrapper.java
new file mode 100644
index 0000000000..06c4f67265
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/jta/narayana/NarayanaXAConnectionFactoryWrapper.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2012-2014 the original author or authors.
+ *
+ * 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 org.springframework.boot.jta.narayana;
+
+import javax.jms.ConnectionFactory;
+import javax.jms.XAConnectionFactory;
+import javax.transaction.TransactionManager;
+
+import com.arjuna.ats.jta.recovery.XAResourceRecoveryHelper;
+import org.jboss.narayana.jta.jms.ConnectionFactoryProxy;
+import org.jboss.narayana.jta.jms.JmsXAResourceRecoveryHelper;
+import org.jboss.narayana.jta.jms.TransactionHelperImpl;
+
+import org.springframework.boot.jta.XAConnectionFactoryWrapper;
+
+/**
+ * {@link XAConnectionFactoryWrapper} that uses {@link ConnectionFactoryProxy} to wrap an {@link XAConnectionFactory}.
+ *
+ * @author Gytis Trikleris
+ */
+public class NarayanaXAConnectionFactoryWrapper implements XAConnectionFactoryWrapper {
+
+ private final TransactionManager transactionManager;
+
+ private final NarayanaRecoveryManagerBean narayanaRecoveryManagerBean;
+
+ private final NarayanaProperties narayanaProperties;
+
+ public NarayanaXAConnectionFactoryWrapper(TransactionManager transactionManager,
+ NarayanaRecoveryManagerBean narayanaRecoveryManagerBean, NarayanaProperties narayanaProperties) {
+ this.transactionManager = transactionManager;
+ this.narayanaRecoveryManagerBean = narayanaRecoveryManagerBean;
+ this.narayanaProperties = narayanaProperties;
+ }
+
+ @Override
+ public ConnectionFactory wrapConnectionFactory(XAConnectionFactory xaConnectionFactory) {
+ this.narayanaRecoveryManagerBean.registerXAResourceRecoveryHelper(getRecoveryHelper(xaConnectionFactory));
+
+ return new ConnectionFactoryProxy(xaConnectionFactory, new TransactionHelperImpl(this.transactionManager));
+ }
+
+ private XAResourceRecoveryHelper getRecoveryHelper(XAConnectionFactory xaConnectionFactory) {
+ if (this.narayanaProperties.getRecoveryJmsUser() == null && this.narayanaProperties.getRecoveryJmsPass() == null) {
+ return new JmsXAResourceRecoveryHelper(xaConnectionFactory);
+ }
+
+ return new JmsXAResourceRecoveryHelper(xaConnectionFactory, this.narayanaProperties.getRecoveryJmsUser(),
+ this.narayanaProperties.getRecoveryJmsPass());
+ }
+
+}
diff --git a/spring-boot/src/main/java/org/springframework/boot/jta/narayana/NarayanaXADataSourceWrapper.java b/spring-boot/src/main/java/org/springframework/boot/jta/narayana/NarayanaXADataSourceWrapper.java
new file mode 100644
index 0000000000..7bb844164f
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/jta/narayana/NarayanaXADataSourceWrapper.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2012-2015 the original author or authors.
+ *
+ * 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 org.springframework.boot.jta.narayana;
+
+import javax.sql.DataSource;
+import javax.sql.XADataSource;
+
+import com.arjuna.ats.jta.recovery.XAResourceRecoveryHelper;
+
+import org.springframework.boot.jta.XADataSourceWrapper;
+
+/**
+ * {@link XADataSourceWrapper} that uses {@link NarayanaDataSourceBean} to wrap an {@link XADataSource}.
+ *
+ * @author Gytis Trikleris
+ */
+public class NarayanaXADataSourceWrapper implements XADataSourceWrapper {
+
+ private final NarayanaRecoveryManagerBean narayanaRecoveryManagerBean;
+
+ private final NarayanaProperties narayanaProperties;
+
+ public NarayanaXADataSourceWrapper(NarayanaRecoveryManagerBean narayanaRecoveryManagerBean,
+ NarayanaProperties narayanaProperties) {
+ this.narayanaRecoveryManagerBean = narayanaRecoveryManagerBean;
+ this.narayanaProperties = narayanaProperties;
+ }
+
+ @Override
+ public DataSource wrapDataSource(XADataSource dataSource) {
+ this.narayanaRecoveryManagerBean.registerXAResourceRecoveryHelper(getRecoveryHelper(dataSource));
+
+ return new NarayanaDataSourceBean(dataSource);
+ }
+
+ private XAResourceRecoveryHelper getRecoveryHelper(XADataSource dataSource) {
+ if (this.narayanaProperties.getRecoveryDbUser() == null && this.narayanaProperties.getRecoveryDbPass() == null) {
+ return new DataSourceXAResourceRecoveryHelper(dataSource);
+ }
+
+ return new DataSourceXAResourceRecoveryHelper(dataSource, this.narayanaProperties.getRecoveryDbUser(),
+ this.narayanaProperties.getRecoveryDbPass());
+ }
+
+}
diff --git a/spring-boot/src/main/java/org/springframework/boot/jta/narayana/package-info.java b/spring-boot/src/main/java/org/springframework/boot/jta/narayana/package-info.java
new file mode 100644
index 0000000000..70a99f09ae
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/jta/narayana/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2012-2015 the original author or authors.
+ *
+ * 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.
+ */
+
+/**
+ * Support classes for Narayana JTA.
+ */
+package org.springframework.boot.jta.narayana;
diff --git a/spring-boot/src/test/java/org/springframework/boot/jta/narayana/DataSourceXAResourceRecoveryHelperTests.java b/spring-boot/src/test/java/org/springframework/boot/jta/narayana/DataSourceXAResourceRecoveryHelperTests.java
new file mode 100644
index 0000000000..ab9b74d23c
--- /dev/null
+++ b/spring-boot/src/test/java/org/springframework/boot/jta/narayana/DataSourceXAResourceRecoveryHelperTests.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright 2012-2014 the original author or authors.
+ *
+ * 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 org.springframework.boot.jta.narayana;
+
+import java.sql.SQLException;
+
+import javax.sql.XAConnection;
+import javax.sql.XADataSource;
+import javax.transaction.xa.XAException;
+import javax.transaction.xa.XAResource;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.BDDMockito.mock;
+import static org.mockito.BDDMockito.times;
+import static org.mockito.BDDMockito.verify;
+import static org.mockito.BDDMockito.when;
+import static org.mockito.Matchers.anyString;
+
+/**
+ * @author Gytis Trikleris
+ */
+public class DataSourceXAResourceRecoveryHelperTests {
+
+ private XADataSource xaDataSource;
+
+ private XAConnection xaConnection;
+
+ private XAResource xaResource;
+
+ private DataSourceXAResourceRecoveryHelper recoveryHelper;
+
+ @Before
+ public void before() throws SQLException {
+ this.xaDataSource = mock(XADataSource.class);
+ this.xaConnection = mock(XAConnection.class);
+ this.xaResource = mock(XAResource.class);
+ this.recoveryHelper = new DataSourceXAResourceRecoveryHelper(this.xaDataSource);
+
+ when(this.xaDataSource.getXAConnection()).thenReturn(this.xaConnection);
+ when(this.xaConnection.getXAResource()).thenReturn(this.xaResource);
+ }
+
+ @Test
+ public void shouldCreateConnectionAndGetXAResource() throws SQLException {
+ XAResource[] xaResources = this.recoveryHelper.getXAResources();
+ assertThat(xaResources.length).isEqualTo(1);
+ assertThat(xaResources[0]).isSameAs(this.recoveryHelper);
+ verify(this.xaDataSource, times(1)).getXAConnection();
+ verify(this.xaConnection, times(1)).getXAResource();
+ }
+
+ @Test
+ public void shouldCreateConnectionWithCredentialsAndGetXAResource() throws SQLException {
+ when(this.xaDataSource.getXAConnection(anyString(), anyString())).thenReturn(this.xaConnection);
+ this.recoveryHelper = new DataSourceXAResourceRecoveryHelper(this.xaDataSource, "username", "password");
+
+ XAResource[] xaResources = this.recoveryHelper.getXAResources();
+ assertThat(xaResources.length).isEqualTo(1);
+ assertThat(xaResources[0]).isSameAs(this.recoveryHelper);
+ verify(this.xaDataSource, times(1)).getXAConnection("username", "password");
+ verify(this.xaConnection, times(1)).getXAResource();
+ }
+
+ @Test
+ public void shouldFailToCreateConnectionAndNotGetXAResource() throws SQLException {
+ when(this.xaDataSource.getXAConnection()).thenThrow(new SQLException("Test exception"));
+
+ XAResource[] xaResources = this.recoveryHelper.getXAResources();
+
+ assertThat(xaResources.length).isEqualTo(0);
+ verify(this.xaDataSource, times(1)).getXAConnection();
+ verify(this.xaConnection, times(0)).getXAResource();
+ }
+
+ @Test
+ public void shouldDelegateRecoverCall() throws XAException {
+ this.recoveryHelper.getXAResources();
+ this.recoveryHelper.recover(XAResource.TMSTARTRSCAN);
+
+ verify(this.xaResource, times(1)).recover(XAResource.TMSTARTRSCAN);
+ }
+
+ @Test
+ public void shouldDelegateRecoverCallAndCloseConnection() throws XAException, SQLException {
+ this.recoveryHelper.getXAResources();
+ this.recoveryHelper.recover(XAResource.TMENDRSCAN);
+
+ verify(this.xaResource, times(1)).recover(XAResource.TMENDRSCAN);
+ verify(this.xaConnection, times(1)).close();
+ }
+
+ @Test
+ public void shouldDelegateStartCall() throws XAException {
+ this.recoveryHelper.getXAResources();
+ this.recoveryHelper.start(null, 0);
+
+ verify(this.xaResource, times(1)).start(null, 0);
+ }
+
+ @Test
+ public void shouldDelegateEndCall() throws XAException {
+ this.recoveryHelper.getXAResources();
+ this.recoveryHelper.end(null, 0);
+
+ verify(this.xaResource, times(1)).end(null, 0);
+ }
+
+ @Test
+ public void shouldDelegatePrepareCall() throws XAException {
+ this.recoveryHelper.getXAResources();
+ this.recoveryHelper.prepare(null);
+
+ verify(this.xaResource, times(1)).prepare(null);
+ }
+
+ @Test
+ public void shouldDelegateCommitCall() throws XAException {
+ this.recoveryHelper.getXAResources();
+ this.recoveryHelper.commit(null, true);
+
+ verify(this.xaResource, times(1)).commit(null, true);
+ }
+
+ @Test
+ public void shouldDelegateRollbackCall() throws XAException {
+ this.recoveryHelper.getXAResources();
+ this.recoveryHelper.rollback(null);
+
+ verify(this.xaResource, times(1)).rollback(null);
+ }
+
+ @Test
+ public void shouldDelegateIsSameRMCall() throws XAException {
+ this.recoveryHelper.getXAResources();
+ this.recoveryHelper.isSameRM(null);
+
+ verify(this.xaResource, times(1)).isSameRM(null);
+ }
+
+ @Test
+ public void shouldDelegateForgetCall() throws XAException {
+ this.recoveryHelper.getXAResources();
+ this.recoveryHelper.forget(null);
+
+ verify(this.xaResource, times(1)).forget(null);
+ }
+
+ @Test
+ public void shouldDelegateGetTransactionTimeoutCall() throws XAException {
+ this.recoveryHelper.getXAResources();
+ this.recoveryHelper.getTransactionTimeout();
+
+ verify(this.xaResource, times(1)).getTransactionTimeout();
+ }
+
+ @Test
+ public void shouldDelegateSetTransactionTimeoutCall() throws XAException {
+ this.recoveryHelper.getXAResources();
+ this.recoveryHelper.setTransactionTimeout(0);
+
+ verify(this.xaResource, times(1)).setTransactionTimeout(0);
+ }
+
+}
+
diff --git a/spring-boot/src/test/java/org/springframework/boot/jta/narayana/NarayanaBeanFactoryPostProcessorTests.java b/spring-boot/src/test/java/org/springframework/boot/jta/narayana/NarayanaBeanFactoryPostProcessorTests.java
new file mode 100644
index 0000000000..c0e25db46c
--- /dev/null
+++ b/spring-boot/src/test/java/org/springframework/boot/jta/narayana/NarayanaBeanFactoryPostProcessorTests.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2012-2015 the original author or authors.
+ *
+ * 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 org.springframework.boot.jta.narayana;
+
+import javax.jms.ConnectionFactory;
+import javax.sql.DataSource;
+import javax.transaction.TransactionManager;
+
+import org.junit.Test;
+
+import org.springframework.beans.factory.support.DefaultListableBeanFactory;
+import org.springframework.context.annotation.AnnotationConfigApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import static org.mockito.BDDMockito.mock;
+import static org.mockito.BDDMockito.spy;
+import static org.mockito.BDDMockito.verify;
+
+/**
+ * @author Gytis Trikleris
+ */
+public class NarayanaBeanFactoryPostProcessorTests {
+
+ private AnnotationConfigApplicationContext context;
+
+ @Test
+ public void setsDependsOn() {
+ DefaultListableBeanFactory beanFactory = spy(new DefaultListableBeanFactory());
+
+ this.context = new AnnotationConfigApplicationContext(beanFactory);
+ this.context.register(Config.class);
+ this.context.refresh();
+
+ verify(beanFactory).registerDependentBean("narayanaTransactionManager", "dataSource");
+ verify(beanFactory).registerDependentBean("narayanaTransactionManager", "connectionFactory");
+ verify(beanFactory).registerDependentBean("narayanaRecoveryManagerBean", "dataSource");
+ verify(beanFactory).registerDependentBean("narayanaRecoveryManagerBean", "connectionFactory");
+
+ this.context.close();
+ }
+
+ @Configuration
+ static class Config {
+
+ @Bean
+ public DataSource dataSource() {
+ return mock(DataSource.class);
+ }
+
+ @Bean
+ public ConnectionFactory connectionFactory() {
+ return mock(ConnectionFactory.class);
+ }
+
+ @Bean
+ public TransactionManager narayanaTransactionManager() {
+ return mock(TransactionManager.class);
+ }
+
+ @Bean
+ public NarayanaRecoveryManagerBean narayanaRecoveryManagerBean() {
+ return mock(NarayanaRecoveryManagerBean.class);
+ }
+
+ @Bean
+ public static NarayanaBeanFactoryPostProcessor narayanaPostProcessor() {
+ return new NarayanaBeanFactoryPostProcessor();
+ }
+
+ }
+
+}
diff --git a/spring-boot/src/test/java/org/springframework/boot/jta/narayana/NarayanaConfigurationBeanTests.java b/spring-boot/src/test/java/org/springframework/boot/jta/narayana/NarayanaConfigurationBeanTests.java
new file mode 100644
index 0000000000..e60e82781d
--- /dev/null
+++ b/spring-boot/src/test/java/org/springframework/boot/jta/narayana/NarayanaConfigurationBeanTests.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2012-2014 the original author or authors.
+ *
+ * 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 org.springframework.boot.jta.narayana;
+
+import java.util.Arrays;
+import java.util.List;
+
+import com.arjuna.ats.arjuna.common.CoordinatorEnvironmentBean;
+import com.arjuna.ats.arjuna.common.CoreEnvironmentBean;
+import com.arjuna.ats.arjuna.common.ObjectStoreEnvironmentBean;
+import com.arjuna.ats.arjuna.common.RecoveryEnvironmentBean;
+import com.arjuna.ats.jta.common.JTAEnvironmentBean;
+import com.arjuna.common.internal.util.propertyservice.BeanPopulator;
+
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Gytis Trikleris
+ */
+public class NarayanaConfigurationBeanTests {
+
+ @Test
+ public void shouldSetDefaultProperties() throws Exception {
+ NarayanaProperties narayanaProperties = new NarayanaProperties();
+ NarayanaConfigurationBean narayanaConfigurationBean = new NarayanaConfigurationBean(narayanaProperties);
+ narayanaConfigurationBean.afterPropertiesSet();
+
+ assertThat(BeanPopulator.getDefaultInstance(CoreEnvironmentBean.class).getNodeIdentifier()).isEqualTo("1");
+ assertThat(BeanPopulator.getDefaultInstance(ObjectStoreEnvironmentBean.class).getObjectStoreDir())
+ .isEqualTo("target/tx-object-store");
+ assertThat(BeanPopulator.getNamedInstance(ObjectStoreEnvironmentBean.class, "communicationStore").getObjectStoreDir())
+ .isEqualTo("target/tx-object-store");
+ assertThat(BeanPopulator.getNamedInstance(ObjectStoreEnvironmentBean.class, "stateStore").getObjectStoreDir())
+ .isEqualTo("target/tx-object-store");
+ assertThat(BeanPopulator.getDefaultInstance(CoordinatorEnvironmentBean.class).isCommitOnePhase()).isTrue();
+ assertThat(BeanPopulator.getDefaultInstance(CoordinatorEnvironmentBean.class).getDefaultTimeout()).isEqualTo(60);
+ assertThat(BeanPopulator.getDefaultInstance(RecoveryEnvironmentBean.class).getPeriodicRecoveryPeriod()).isEqualTo(120);
+ assertThat(BeanPopulator.getDefaultInstance(RecoveryEnvironmentBean.class).getRecoveryBackoffPeriod()).isEqualTo(10);
+
+
+ List xaResourceOrphanFilters = Arrays.asList(
+ "com.arjuna.ats.internal.jta.recovery.arjunacore.JTATransactionLogXAResourceOrphanFilter",
+ "com.arjuna.ats.internal.jta.recovery.arjunacore.JTANodeNameXAResourceOrphanFilter");
+ assertThat(BeanPopulator.getDefaultInstance(JTAEnvironmentBean.class).getXaResourceOrphanFilterClassNames())
+ .isEqualTo(xaResourceOrphanFilters);
+
+ List recoveryModules = Arrays.asList("com.arjuna.ats.internal.arjuna.recovery.AtomicActionRecoveryModule",
+ "com.arjuna.ats.internal.jta.recovery.arjunacore.XARecoveryModule");
+ assertThat(BeanPopulator.getDefaultInstance(RecoveryEnvironmentBean.class).getRecoveryModuleClassNames())
+ .isEqualTo(recoveryModules);
+
+ List expiryScanners = Arrays
+ .asList("com.arjuna.ats.internal.arjuna.recovery.ExpiredTransactionStatusManagerScanner");
+ assertThat(BeanPopulator.getDefaultInstance(RecoveryEnvironmentBean.class).getExpiryScannerClassNames())
+ .isEqualTo(expiryScanners);
+
+ assertThat(BeanPopulator.getDefaultInstance(JTAEnvironmentBean.class).getXaResourceRecoveryClassNames()).isEmpty();
+ }
+
+ @Test
+ public void shouldSetModifiedProperties() throws Exception {
+ NarayanaProperties narayanaProperties = new NarayanaProperties();
+ narayanaProperties.setTransactionManagerId("test-id");
+ narayanaProperties.setLogDir("test-dir");
+ narayanaProperties.setDefaultTimeout(1);
+ narayanaProperties.setPeriodicRecoveryPeriod(2);
+ narayanaProperties.setRecoveryBackoffPeriod(3);
+ narayanaProperties.setOnePhaseCommit(false);
+ narayanaProperties.setXaResourceOrphanFilters(Arrays.asList("test-filter-1", "test-filter-2"));
+ narayanaProperties.setRecoveryModules(Arrays.asList("test-module-1", "test-module-2"));
+ narayanaProperties.setExpiryScanners(Arrays.asList("test-scanner-1", "test-scanner-2"));
+
+ NarayanaConfigurationBean narayanaConfigurationBean = new NarayanaConfigurationBean(narayanaProperties);
+ narayanaConfigurationBean.afterPropertiesSet();
+
+ assertThat(BeanPopulator.getDefaultInstance(CoreEnvironmentBean.class).getNodeIdentifier()).isEqualTo("test-id");
+ assertThat(BeanPopulator.getDefaultInstance(ObjectStoreEnvironmentBean.class).getObjectStoreDir())
+ .isEqualTo("test-dir");
+ assertThat(BeanPopulator.getNamedInstance(ObjectStoreEnvironmentBean.class, "communicationStore").getObjectStoreDir())
+ .isEqualTo("test-dir");
+ assertThat(BeanPopulator.getNamedInstance(ObjectStoreEnvironmentBean.class, "stateStore").getObjectStoreDir())
+ .isEqualTo("test-dir");
+ assertThat(BeanPopulator.getDefaultInstance(CoordinatorEnvironmentBean.class).isCommitOnePhase()).isFalse();
+ assertThat(BeanPopulator.getDefaultInstance(CoordinatorEnvironmentBean.class).getDefaultTimeout()).isEqualTo(1);
+ assertThat(BeanPopulator.getDefaultInstance(RecoveryEnvironmentBean.class).getPeriodicRecoveryPeriod()).isEqualTo(2);
+ assertThat(BeanPopulator.getDefaultInstance(RecoveryEnvironmentBean.class).getRecoveryBackoffPeriod()).isEqualTo(3);
+ assertThat(BeanPopulator.getDefaultInstance(JTAEnvironmentBean.class).getXaResourceOrphanFilterClassNames())
+ .isEqualTo(Arrays.asList("test-filter-1", "test-filter-2"));
+ assertThat(BeanPopulator.getDefaultInstance(RecoveryEnvironmentBean.class).getRecoveryModuleClassNames())
+ .isEqualTo(Arrays.asList("test-module-1", "test-module-2"));
+ assertThat(BeanPopulator.getDefaultInstance(RecoveryEnvironmentBean.class).getExpiryScannerClassNames())
+ .isEqualTo(Arrays.asList("test-scanner-1", "test-scanner-2"));
+ }
+
+}
diff --git a/spring-boot/src/test/java/org/springframework/boot/jta/narayana/NarayanaDataSourceBeanTests.java b/spring-boot/src/test/java/org/springframework/boot/jta/narayana/NarayanaDataSourceBeanTests.java
new file mode 100644
index 0000000000..3521a8f1d7
--- /dev/null
+++ b/spring-boot/src/test/java/org/springframework/boot/jta/narayana/NarayanaDataSourceBeanTests.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2012-2014 the original author or authors.
+ *
+ * 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 org.springframework.boot.jta.narayana;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.util.Properties;
+
+import javax.sql.DataSource;
+import javax.sql.XAConnection;
+import javax.sql.XADataSource;
+
+import com.arjuna.ats.internal.jdbc.ConnectionImple;
+import com.arjuna.ats.jdbc.TransactionalDriver;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.BDDMockito.mock;
+import static org.mockito.BDDMockito.times;
+import static org.mockito.BDDMockito.verify;
+import static org.mockito.BDDMockito.when;
+
+/**
+ * @author Gytis Trikleris
+ */
+public class NarayanaDataSourceBeanTests {
+
+ private XADataSource mockDataSource;
+
+ private NarayanaDataSourceBean dataSourceBean;
+
+ @Before
+ public void before() {
+ this.mockDataSource = mock(XADataSource.class);
+ this.dataSourceBean = new NarayanaDataSourceBean(this.mockDataSource);
+ }
+
+ @Test
+ public void shouldBeAWrapper() throws SQLException {
+ assertThat(this.dataSourceBean.isWrapperFor(DataSource.class)).isTrue();
+ }
+
+ @Test
+ public void shouldNotBeAWrapper() throws SQLException {
+ assertThat(this.dataSourceBean.isWrapperFor(XADataSource.class)).isFalse();
+ }
+
+ @Test
+ public void shouldUnwrapDataSource() throws SQLException {
+ assertThat(this.dataSourceBean.unwrap(DataSource.class)).isInstanceOf(DataSource.class);
+ assertThat(this.dataSourceBean.unwrap(DataSource.class)).isSameAs(this.dataSourceBean);
+ }
+
+ @Test
+ public void shouldUnwrapXaDataSource() throws SQLException {
+ assertThat(this.dataSourceBean.unwrap(XADataSource.class)).isInstanceOf(XADataSource.class);
+ assertThat(this.dataSourceBean.unwrap(XADataSource.class)).isSameAs(this.mockDataSource);
+ }
+
+ @Test
+ public void shouldGetConnectionAndCommit() throws SQLException {
+ Connection mockConnection = mock(Connection.class);
+ XAConnection mockXaConnection = mock(XAConnection.class);
+ when(mockXaConnection.getConnection()).thenReturn(mockConnection);
+ when(this.mockDataSource.getXAConnection()).thenReturn(mockXaConnection);
+
+ Properties properties = new Properties();
+ properties.put(TransactionalDriver.XADataSource, this.mockDataSource);
+
+ Connection connection = this.dataSourceBean.getConnection();
+ assertThat(connection).isInstanceOf(ConnectionImple.class);
+
+ connection.commit();
+
+ verify(this.mockDataSource, times(1)).getXAConnection();
+ verify(mockXaConnection, times(1)).getConnection();
+ verify(mockConnection, times(1)).commit();
+ }
+
+ @Test
+ public void shouldGetConnectionAndCommitWithCredentials() throws SQLException {
+ String username = "testUsername";
+ String password = "testPassword";
+ Connection mockConnection = mock(Connection.class);
+ XAConnection mockXaConnection = mock(XAConnection.class);
+ when(mockXaConnection.getConnection()).thenReturn(mockConnection);
+ when(this.mockDataSource.getXAConnection(username, password)).thenReturn(mockXaConnection);
+
+ Properties properties = new Properties();
+ properties.put(TransactionalDriver.XADataSource, this.mockDataSource);
+ properties.put(TransactionalDriver.userName, username);
+ properties.put(TransactionalDriver.password, password);
+
+ Connection connection = this.dataSourceBean.getConnection(username, password);
+ assertThat(connection).isInstanceOf(ConnectionImple.class);
+
+ connection.commit();
+
+ verify(this.mockDataSource, times(1)).getXAConnection(username, password);
+ verify(mockXaConnection, times(1)).getConnection();
+ verify(mockConnection, times(1)).commit();
+ }
+
+}
diff --git a/spring-boot/src/test/java/org/springframework/boot/jta/narayana/NarayanaRecoveryManagerBeanTests.java b/spring-boot/src/test/java/org/springframework/boot/jta/narayana/NarayanaRecoveryManagerBeanTests.java
new file mode 100644
index 0000000000..5e66bba76d
--- /dev/null
+++ b/spring-boot/src/test/java/org/springframework/boot/jta/narayana/NarayanaRecoveryManagerBeanTests.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2012-2014 the original author or authors.
+ *
+ * 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 org.springframework.boot.jta.narayana;
+
+import com.arjuna.ats.jbossatx.jta.RecoveryManagerService;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+/**
+ * @author Gytis Trikleris
+ */
+public class NarayanaRecoveryManagerBeanTests {
+
+ private RecoveryManagerService recoveryManagerService;
+
+ private NarayanaRecoveryManagerBean narayanaRecoveryManagerBean;
+
+ @Before
+ public void before() {
+ this.recoveryManagerService = mock(RecoveryManagerService.class);
+ this.narayanaRecoveryManagerBean = new NarayanaRecoveryManagerBean(this.recoveryManagerService);
+ }
+
+ @Test
+ public void shouldCreateAndStartRecoveryManagerService() throws Exception {
+ this.narayanaRecoveryManagerBean.afterPropertiesSet();
+
+ verify(this.recoveryManagerService, times(1)).create();
+ verify(this.recoveryManagerService, times(1)).start();
+ }
+
+ @Test
+ public void shouldStopAndDestroyRecoveryManagerService() throws Exception {
+ this.narayanaRecoveryManagerBean.destroy();
+
+ verify(this.recoveryManagerService, times(1)).stop();
+ verify(this.recoveryManagerService, times(1)).destroy();
+ }
+
+}
diff --git a/spring-boot/src/test/java/org/springframework/boot/jta/narayana/NarayanaXAConnectionFactoryWrapperTests.java b/spring-boot/src/test/java/org/springframework/boot/jta/narayana/NarayanaXAConnectionFactoryWrapperTests.java
new file mode 100644
index 0000000000..6105d5be2a
--- /dev/null
+++ b/spring-boot/src/test/java/org/springframework/boot/jta/narayana/NarayanaXAConnectionFactoryWrapperTests.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2012-2014 the original author or authors.
+ *
+ * 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 org.springframework.boot.jta.narayana;
+
+import javax.jms.ConnectionFactory;
+import javax.jms.XAConnectionFactory;
+import javax.transaction.TransactionManager;
+
+import org.jboss.narayana.jta.jms.ConnectionFactoryProxy;
+import org.jboss.narayana.jta.jms.JmsXAResourceRecoveryHelper;
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.BDDMockito.mock;
+import static org.mockito.BDDMockito.times;
+import static org.mockito.BDDMockito.verify;
+import static org.mockito.BDDMockito.when;
+import static org.mockito.Matchers.any;
+
+/**
+ * @author Gytis Trikleris
+ */
+public class NarayanaXAConnectionFactoryWrapperTests {
+
+ private XAConnectionFactory connectionFactory = mock(XAConnectionFactory.class);
+
+ private TransactionManager transactionManager = mock(TransactionManager.class);
+
+ private NarayanaRecoveryManagerBean narayanaRecoveryManagerBean = mock(NarayanaRecoveryManagerBean.class);
+
+ private NarayanaProperties narayanaProperties = mock(NarayanaProperties.class);
+
+ private NarayanaXAConnectionFactoryWrapper wrapper = new NarayanaXAConnectionFactoryWrapper(this.transactionManager,
+ this.narayanaRecoveryManagerBean, this.narayanaProperties);
+
+ @Test
+ public void wrap() {
+ ConnectionFactory wrapped = this.wrapper.wrapConnectionFactory(this.connectionFactory);
+ assertThat(wrapped).isInstanceOf(ConnectionFactoryProxy.class);
+ verify(this.narayanaRecoveryManagerBean, times(1))
+ .registerXAResourceRecoveryHelper(any(JmsXAResourceRecoveryHelper.class));
+ verify(this.narayanaProperties, times(1)).getRecoveryJmsUser();
+ verify(this.narayanaProperties, times(1)).getRecoveryJmsPass();
+ }
+
+ @Test
+ public void wrapWithCredentials() {
+ when(this.narayanaProperties.getRecoveryJmsUser()).thenReturn("userName");
+ when(this.narayanaProperties.getRecoveryJmsPass()).thenReturn("password");
+ ConnectionFactory wrapped = this.wrapper.wrapConnectionFactory(this.connectionFactory);
+
+ assertThat(wrapped).isInstanceOf(ConnectionFactoryProxy.class);
+ verify(this.narayanaRecoveryManagerBean, times(1))
+ .registerXAResourceRecoveryHelper(any(JmsXAResourceRecoveryHelper.class));
+ verify(this.narayanaProperties, times(2)).getRecoveryJmsUser();
+ verify(this.narayanaProperties, times(1)).getRecoveryJmsPass();
+ }
+
+}
diff --git a/spring-boot/src/test/java/org/springframework/boot/jta/narayana/NarayanaXADataSourceWrapperTests.java b/spring-boot/src/test/java/org/springframework/boot/jta/narayana/NarayanaXADataSourceWrapperTests.java
new file mode 100644
index 0000000000..2b031898fd
--- /dev/null
+++ b/spring-boot/src/test/java/org/springframework/boot/jta/narayana/NarayanaXADataSourceWrapperTests.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2012-2014 the original author or authors.
+ *
+ * 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 org.springframework.boot.jta.narayana;
+
+import javax.sql.DataSource;
+import javax.sql.XADataSource;
+
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.BDDMockito.mock;
+import static org.mockito.BDDMockito.times;
+import static org.mockito.BDDMockito.verify;
+import static org.mockito.BDDMockito.when;
+import static org.mockito.Matchers.any;
+
+/**
+ * @author Gytis Trikleris
+ */
+public class NarayanaXADataSourceWrapperTests {
+
+ private XADataSource dataSource = mock(XADataSource.class);
+
+ private NarayanaRecoveryManagerBean narayanaRecoveryManagerBean = mock(NarayanaRecoveryManagerBean.class);
+
+ private NarayanaProperties narayanaProperties = mock(NarayanaProperties.class);
+
+ private NarayanaXADataSourceWrapper wrapper = new NarayanaXADataSourceWrapper(this.narayanaRecoveryManagerBean,
+ this.narayanaProperties);
+
+ @Test
+ public void wrap() {
+ DataSource wrapped = this.wrapper.wrapDataSource(this.dataSource);
+
+ assertThat(wrapped).isInstanceOf(NarayanaDataSourceBean.class);
+ verify(this.narayanaRecoveryManagerBean, times(1))
+ .registerXAResourceRecoveryHelper(any(DataSourceXAResourceRecoveryHelper.class));
+ verify(this.narayanaProperties, times(1)).getRecoveryDbUser();
+ verify(this.narayanaProperties, times(1)).getRecoveryDbPass();
+ }
+
+ @Test
+ public void wrapWithCredentials() {
+ when(this.narayanaProperties.getRecoveryDbUser()).thenReturn("userName");
+ when(this.narayanaProperties.getRecoveryDbPass()).thenReturn("password");
+ DataSource wrapped = this.wrapper.wrapDataSource(this.dataSource);
+
+ assertThat(wrapped).isInstanceOf(NarayanaDataSourceBean.class);
+ verify(this.narayanaRecoveryManagerBean, times(1))
+ .registerXAResourceRecoveryHelper(any(DataSourceXAResourceRecoveryHelper.class));
+ verify(this.narayanaProperties, times(2)).getRecoveryDbUser();
+ verify(this.narayanaProperties, times(1)).getRecoveryDbPass();
+ }
+
+}