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..ac243d57f1
--- /dev/null
+++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/jta/NarayanaJtaConfiguration.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2012-2016 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.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
+ * @since 1.4.0
+ */
+@Configuration
+@ConditionalOnClass({ JtaTransactionManager.class,
+ com.arjuna.ats.jta.UserTransaction.class })
+@ConditionalOnMissingBean(PlatformTransactionManager.class)
+public class NarayanaJtaConfiguration {
+
+ private final JtaProperties jtaProperties;
+
+ public NarayanaJtaConfiguration(JtaProperties jtaProperties) {
+ this.jtaProperties = jtaProperties;
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ public NarayanaProperties narayanaProperties() {
+ return new NarayanaProperties();
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ public NarayanaConfigurationBean narayanaConfiguration(
+ NarayanaProperties properties) {
+ if (this.jtaProperties.getLogDir() != null) {
+ properties.setLogDir(this.jtaProperties.getLogDir());
+ }
+ if (this.jtaProperties.getTransactionManagerId() != null) {
+ properties.setTransactionManagerId(
+ this.jtaProperties.getTransactionManagerId());
+ }
+ return new NarayanaConfigurationBean(properties);
+ }
+
+ @Bean
+ @DependsOn("narayanaConfiguration")
+ @ConditionalOnMissingBean
+ public UserTransaction narayanaUserTransaction() {
+ return com.arjuna.ats.jta.UserTransaction.userTransaction();
+ }
+
+ @Bean
+ @DependsOn("narayanaConfiguration")
+ @ConditionalOnMissingBean
+ public TransactionManager narayanaTransactionManager() {
+ return com.arjuna.ats.jta.TransactionManager.transactionManager();
+ }
+
+ @Bean
+ @DependsOn("narayanaConfiguration")
+ public RecoveryManagerService narayanaRecoveryManagerService() {
+ return new RecoveryManagerService();
+ }
+
+ @Bean
+ public NarayanaRecoveryManagerBean narayanaRecoveryManager(
+ 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..3de1f716d7 100644
--- a/spring-boot-dependencies/pom.xml
+++ b/spring-boot-dependencies/pom.xml
@@ -100,6 +100,7 @@
1.1.6
2.2.10
3.3.0.Final
+ 7.3.0.Final
2.0.6
2.8.1
2.22.2
@@ -124,6 +125,7 @@
1.10.19
2.14.2
5.1.38
+ 5.3.2.Final
1.9.22
2.0.0
9.4.1208.jre7
@@ -429,6 +431,11 @@
spring-boot-starter-mustache
1.4.0.BUILD-SNAPSHOT
+
+ org.springframework.boot
+ spring-boot-starter-jta-narayana
+ 1.4.0.BUILD-SNAPSHOT
+
org.springframework.boot
spring-boot-starter-remote-shell
@@ -1733,11 +1740,36 @@
javassist
${javassist.version}
+
+ org.jboss
+ jboss-transaction-spi
+ ${jboss-transaction-spi.version}
+
org.jboss.logging
jboss-logging
${jboss-logging.version}
+
+ org.jboss.narayana.jta
+ jdbc
+ ${narayana.version}
+
+
+ org.jboss.narayana.jta
+ jms
+ ${narayana.version}
+
+
+ org.jboss.narayana.jta
+ jta
+ ${narayana.version}
+
+
+ org.jboss.narayana.jts
+ narayana-jts-integration
+ ${narayana.version}
+
org.jdom
jdom2
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..20f9b7d7a1 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.default-timeout=60 # Set default transaction timeout in seconds
+ spring.jta.narayana.expiry-scanners=com.arjuna.ats.internal.arjuna.recovery.ExpiredTransactionStatusManagerScanner # List of ExpiryScanner implementations
+ spring.jta.narayana.one-phase-commit=true # Enable or disable one phase commit optimisation
+ 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.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
+ 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.xa-resource-orphan-filters=com.arjuna.ats.internal.jta.recovery.arjunacore.JTATransactionLogXAResourceOrphanFilter,com.arjuna.ats.internal.jta.recovery.arjunacore.JTANodeNameXAResourceOrphanFilter # List of XAResourceOrphanFilter implementations
+
# 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..eac68e382d
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-jta-narayana/pom.xml
@@ -0,0 +1,56 @@
+
+
+ 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
+
+
+
+
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..ddada8120c
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-jta-narayana/src/main/java/sample/narayana/Account.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2012-2016 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..ebf3ef4deb
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-jta-narayana/src/main/java/sample/narayana/AccountRepository.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2012-2016 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..4333fdae4a
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-jta-narayana/src/main/java/sample/narayana/AccountService.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2012-2016 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..7ccf4218b5
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-jta-narayana/src/main/java/sample/narayana/Messages.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2012-2016 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..29054ef91c
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-jta-narayana/src/main/java/sample/narayana/SampleNarayanaApplication.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2012-2016 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..1e6a697e0b
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-jta-narayana/src/main/java/sample/narayana/SampleRuntimeException.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2012-2016 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;
+
+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..df9c59fb3b
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-jta-narayana/src/test/java/sample/narayana/SampleNarayanaApplicationTests.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2012-2016 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.assertj.core.api.Condition;
+import org.junit.Rule;
+import org.junit.Test;
+
+import org.springframework.boot.test.rule.OutputCapture;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Basic integration tests for demo application.
+ *
+ * @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).has(substring(1, "---->"));
+ assertThat(output).has(substring(1, "----> josh"));
+ assertThat(output).has(substring(2, "Count is 1"));
+ assertThat(output).has(substring(1, "Simulated error"));
+ }
+
+ private Condition substring(final int times, final String substring) {
+ return new Condition(
+ "containing '" + substring + "' " + times + " times") {
+
+ @Override
+ public boolean matches(String value) {
+ int i = 0;
+ while (value.contains(substring)) {
+ int beginIndex = value.indexOf(substring) + substring.length();
+ value = value.substring(beginIndex);
+ 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..c39459c499
--- /dev/null
+++ b/spring-boot-starters/spring-boot-starter-jta-narayana/pom.xml
@@ -0,0 +1,47 @@
+
+
+ 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
+ jboss-transaction-spi
+
+
+ org.jboss.narayana.jta
+ jdbc
+
+
+ org.jboss.narayana.jta
+ jms
+
+
+ org.jboss.narayana.jta
+ jta
+
+
+ org.jboss.narayana.jts
+ narayana-jts-integration
+
+
+ javax.transaction
+ javax.transaction-api
+
+
+
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..5f33ee2980
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/jta/narayana/DataSourceXAResourceRecoveryHelper.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright 2012-2016 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 javax.transaction.xa.Xid;
+
+import com.arjuna.ats.jta.recovery.XAResourceRecoveryHelper;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.util.Assert;
+
+/**
+ * XAResourceRecoveryHelper implementation which gets XIDs, which needs to be recovered,
+ * from the database.
+ *
+ * @author Gytis Trikleris
+ * @since 1.4.0
+ */
+public class DataSourceXAResourceRecoveryHelper
+ implements XAResourceRecoveryHelper, XAResource {
+
+ private static final XAResource[] NO_XA_RESOURCES = {};
+
+ private static final Log logger = LogFactory
+ .getLog(DataSourceXAResourceRecoveryHelper.class);
+
+ private final XADataSource xaDataSource;
+
+ private final String user;
+
+ private final String password;
+
+ private XAConnection xaConnection;
+
+ private XAResource delegate;
+
+ /**
+ * Create a new {@link DataSourceXAResourceRecoveryHelper} instance.
+ * @param xaDataSource the XA data source
+ */
+ public DataSourceXAResourceRecoveryHelper(XADataSource xaDataSource) {
+ this(xaDataSource, null, null);
+ }
+
+ /**
+ * Create a new {@link DataSourceXAResourceRecoveryHelper} instance.
+ * @param xaDataSource the XA data source
+ * @param user the database user or {@code null}
+ * @param password the database password or {@code null}
+ */
+ public DataSourceXAResourceRecoveryHelper(XADataSource xaDataSource, String user,
+ String password) {
+ Assert.notNull(xaDataSource, "XADataSource must not be null");
+ this.xaDataSource = xaDataSource;
+ this.user = user;
+ this.password = password;
+ }
+
+ @Override
+ public boolean initialise(String properties) {
+ return true;
+ }
+
+ @Override
+ public XAResource[] getXAResources() {
+ if (connect()) {
+ return new XAResource[] { this };
+ }
+ return NO_XA_RESOURCES;
+ }
+
+ private boolean connect() {
+ if (this.delegate == null) {
+ try {
+ this.xaConnection = getXaConnection();
+ this.delegate = this.xaConnection.getXAResource();
+ }
+ catch (SQLException ex) {
+ logger.warn("Failed to create connection", ex);
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private XAConnection getXaConnection() throws SQLException {
+ if (this.user == null && this.password == null) {
+ return this.xaDataSource.getXAConnection();
+ }
+ return this.xaDataSource.getXAConnection(this.user, this.password);
+ }
+
+ @Override
+ public Xid[] recover(int flag) throws XAException {
+ try {
+ return getDelegate(true).recover(flag);
+ }
+ finally {
+ if (flag == XAResource.TMENDRSCAN) {
+ disconnect();
+ }
+ }
+ }
+
+ private void disconnect() throws XAException {
+ try {
+ this.xaConnection.close();
+ }
+ catch (SQLException e) {
+ logger.warn("Failed to close connection", e);
+ }
+ finally {
+ this.xaConnection = null;
+ this.delegate = null;
+ }
+ }
+
+ @Override
+ public void start(Xid xid, int flags) throws XAException {
+ getDelegate(true).start(xid, flags);
+ }
+
+ @Override
+ public void end(Xid xid, int flags) throws XAException {
+ getDelegate(true).end(xid, flags);
+ }
+
+ @Override
+ public int prepare(Xid xid) throws XAException {
+ return getDelegate(true).prepare(xid);
+ }
+
+ @Override
+ public void commit(Xid xid, boolean onePhase) throws XAException {
+ getDelegate(true).commit(xid, onePhase);
+ }
+
+ @Override
+ public void rollback(Xid xid) throws XAException {
+ getDelegate(true).rollback(xid);
+ }
+
+ @Override
+ public boolean isSameRM(XAResource xaResource) throws XAException {
+ return getDelegate(true).isSameRM(xaResource);
+ }
+
+ @Override
+ public void forget(Xid xid) throws XAException {
+ getDelegate(true).forget(xid);
+ }
+
+ @Override
+ public int getTransactionTimeout() throws XAException {
+ return getDelegate(true).getTransactionTimeout();
+ }
+
+ @Override
+ public boolean setTransactionTimeout(int seconds) throws XAException {
+ return getDelegate(true).setTransactionTimeout(seconds);
+ }
+
+ private XAResource getDelegate(boolean required) {
+ Assert.state(this.delegate != null || !required,
+ "Connection has not been opened");
+ return this.delegate;
+ }
+
+}
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..7ca2ca5b53
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/jta/narayana/NarayanaBeanFactoryPostProcessor.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2012-2016 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
+ * @since 1.4.0
+ */
+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..08094c375c
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/jta/narayana/NarayanaConfigurationBean.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2012-2016 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
+ * @since 1.4.0
+ */
+public class NarayanaConfigurationBean implements InitializingBean {
+
+ private static final String JBOSSTS_PROPERTIES_FILE_NAME = "jbossts-properties.xml";
+
+ private final NarayanaProperties properties;
+
+ public NarayanaConfigurationBean(NarayanaProperties narayanaProperties) {
+ this.properties = narayanaProperties;
+ }
+
+ @Override
+ public void afterPropertiesSet() throws Exception {
+ if (isPropertiesFileAvailable()) {
+ return;
+ }
+ setNodeIdentifier(this.properties.getTransactionManagerId());
+ setObjectStoreDir(this.properties.getLogDir());
+ setCommitOnePhase(this.properties.isOnePhaseCommit());
+ setDefaultTimeout(this.properties.getDefaultTimeout());
+ setPeriodicRecoveryPeriod(this.properties.getPeriodicRecoveryPeriod());
+ setRecoveryBackoffPeriod(this.properties.getRecoveryBackoffPeriod());
+ setXaResourceOrphanFilters(this.properties.getXaResourceOrphanFilters());
+ setRecoveryModules(this.properties.getRecoveryModules());
+ setExpiryScanners(this.properties.getExpiryScanners());
+ }
+
+ private boolean isPropertiesFileAvailable() {
+ return Thread.currentThread().getContextClassLoader()
+ .getResource(JBOSSTS_PROPERTIES_FILE_NAME) != null;
+ }
+
+ private void setNodeIdentifier(String nodeIdentifier)
+ throws CoreEnvironmentBeanException {
+ getPopulator(CoreEnvironmentBean.class).setNodeIdentifier(nodeIdentifier);
+ }
+
+ private void setObjectStoreDir(String objectStoreDir) {
+ getPopulator(ObjectStoreEnvironmentBean.class).setObjectStoreDir(objectStoreDir);
+ getPopulator(ObjectStoreEnvironmentBean.class, "communicationStore")
+ .setObjectStoreDir(objectStoreDir);
+ getPopulator(ObjectStoreEnvironmentBean.class, "stateStore")
+ .setObjectStoreDir(objectStoreDir);
+ }
+
+ private void setCommitOnePhase(boolean isCommitOnePhase) {
+ getPopulator(CoordinatorEnvironmentBean.class)
+ .setCommitOnePhase(isCommitOnePhase);
+ }
+
+ private void setDefaultTimeout(int defaultTimeout) {
+ getPopulator(CoordinatorEnvironmentBean.class).setDefaultTimeout(defaultTimeout);
+ }
+
+ private void setPeriodicRecoveryPeriod(int periodicRecoveryPeriod) {
+ getPopulator(RecoveryEnvironmentBean.class)
+ .setPeriodicRecoveryPeriod(periodicRecoveryPeriod);
+ }
+
+ private void setRecoveryBackoffPeriod(int recoveryBackoffPeriod) {
+ getPopulator(RecoveryEnvironmentBean.class)
+ .setRecoveryBackoffPeriod(recoveryBackoffPeriod);
+ }
+
+ private void setXaResourceOrphanFilters(List xaResourceOrphanFilters) {
+ getPopulator(JTAEnvironmentBean.class)
+ .setXaResourceOrphanFilterClassNames(xaResourceOrphanFilters);
+ }
+
+ private void setRecoveryModules(List recoveryModules) {
+ getPopulator(RecoveryEnvironmentBean.class)
+ .setRecoveryModuleClassNames(recoveryModules);
+ }
+
+ private void setExpiryScanners(List expiryScanners) {
+ getPopulator(RecoveryEnvironmentBean.class)
+ .setExpiryScannerClassNames(expiryScanners);
+ }
+
+ private T getPopulator(Class beanClass) {
+ return BeanPopulator.getDefaultInstance(beanClass);
+ }
+
+ private T getPopulator(Class beanClass, String name) {
+ return BeanPopulator.getNamedInstance(beanClass, name);
+ }
+
+}
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..fd5c63f996
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/jta/narayana/NarayanaDataSourceBean.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2012-2016 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;
+
+import org.springframework.util.Assert;
+import org.springframework.util.ClassUtils;
+
+/**
+ * {@link DataSource} implementation wrapping {@link XADataSource} and using
+ * {@link ConnectionManager} to acquire connections.
+ *
+ * @author Gytis Trikleris
+ * @since 1.4.0
+ */
+public class NarayanaDataSourceBean implements DataSource {
+
+ private final XADataSource xaDataSource;
+
+ /**
+ * Create a new {@link NarayanaDataSourceBean} instance.
+ * @param xaDataSource the XA DataSource
+ */
+ public NarayanaDataSourceBean(XADataSource xaDataSource) {
+ Assert.notNull(xaDataSource, "XADataSource must not be null");
+ 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;
+ }
+ if (ClassUtils.isAssignableValue(iface, this.xaDataSource)) {
+ return (T) this.xaDataSource;
+ }
+ throw new SQLException(getClass() + " is not a wrapper for " + iface);
+ }
+
+ @Override
+ public boolean isWrapperFor(Class> iface) throws SQLException {
+ return iface.isAssignableFrom(getClass());
+ }
+
+}
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..730d4736cb
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/jta/narayana/NarayanaProperties.java
@@ -0,0 +1,230 @@
+/*
+ * Copyright 2012-2016 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
+ * @since 1.4.0
+ */
+@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..3bbb47356b
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/jta/narayana/NarayanaRecoveryManagerBean.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2012-2016 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;
+import org.springframework.util.Assert;
+
+/**
+ * Bean to set up Narayana recovery manager.
+ *
+ * @author Gytis Trikleris
+ * @since 1.4.0
+ */
+public class NarayanaRecoveryManagerBean implements InitializingBean, DisposableBean {
+
+ private final RecoveryManagerService recoveryManagerService;
+
+ public NarayanaRecoveryManagerBean(RecoveryManagerService recoveryManagerService) {
+ Assert.notNull(recoveryManagerService, "RecoveryManagerService must not be null");
+ 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..01bdf220c4
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/jta/narayana/NarayanaXAConnectionFactoryWrapper.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2012-2016 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;
+import org.springframework.util.Assert;
+
+/**
+ * {@link XAConnectionFactoryWrapper} that uses {@link ConnectionFactoryProxy} to wrap an
+ * {@link XAConnectionFactory}.
+ *
+ * @author Gytis Trikleris
+ * @since 1.4.0
+ */
+public class NarayanaXAConnectionFactoryWrapper implements XAConnectionFactoryWrapper {
+
+ private final TransactionManager transactionManager;
+
+ private final NarayanaRecoveryManagerBean recoveryManager;
+
+ private final NarayanaProperties properties;
+
+ /**
+ * Create a new {@link NarayanaXAConnectionFactoryWrapper} instance.
+ * @param transactionManager the underlying transaction manager
+ * @param recoveryManager the underlying recovery manager
+ * @param properties the Narayana properties
+ */
+ public NarayanaXAConnectionFactoryWrapper(TransactionManager transactionManager,
+ NarayanaRecoveryManagerBean recoveryManager, NarayanaProperties properties) {
+ Assert.notNull(transactionManager, "TransactionManager must not be null");
+ Assert.notNull(recoveryManager, "RecoveryManager must not be null");
+ Assert.notNull(properties, "Properties must not be null");
+ this.transactionManager = transactionManager;
+ this.recoveryManager = recoveryManager;
+ this.properties = properties;
+ }
+
+ @Override
+ public ConnectionFactory wrapConnectionFactory(
+ XAConnectionFactory xaConnectionFactory) {
+ XAResourceRecoveryHelper recoveryHelper = getRecoveryHelper(xaConnectionFactory);
+ this.recoveryManager.registerXAResourceRecoveryHelper(recoveryHelper);
+ return new ConnectionFactoryProxy(xaConnectionFactory,
+ new TransactionHelperImpl(this.transactionManager));
+ }
+
+ private XAResourceRecoveryHelper getRecoveryHelper(
+ XAConnectionFactory xaConnectionFactory) {
+ if (this.properties.getRecoveryJmsUser() == null
+ && this.properties.getRecoveryJmsPass() == null) {
+ return new JmsXAResourceRecoveryHelper(xaConnectionFactory);
+ }
+ return new JmsXAResourceRecoveryHelper(xaConnectionFactory,
+ this.properties.getRecoveryJmsUser(),
+ this.properties.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..e7705be90a
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/jta/narayana/NarayanaXADataSourceWrapper.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2012-2016 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;
+import org.springframework.util.Assert;
+
+/**
+ * {@link XADataSourceWrapper} that uses {@link NarayanaDataSourceBean} to wrap an
+ * {@link XADataSource}.
+ *
+ * @author Gytis Trikleris
+ * @since 1.4.0
+ */
+public class NarayanaXADataSourceWrapper implements XADataSourceWrapper {
+
+ private final NarayanaRecoveryManagerBean recoveryManager;
+
+ private final NarayanaProperties properties;
+
+ /**
+ * Create a new {@link NarayanaXADataSourceWrapper} instance.
+ * @param recoveryManager the underlying recovery manager
+ * @param properties the Narayana properties
+ */
+ public NarayanaXADataSourceWrapper(NarayanaRecoveryManagerBean recoveryManager,
+ NarayanaProperties properties) {
+ Assert.notNull(recoveryManager, "RecoveryManager must not be null");
+ Assert.notNull(properties, "Properties must not be null");
+ this.recoveryManager = recoveryManager;
+ this.properties = properties;
+ }
+
+ @Override
+ public DataSource wrapDataSource(XADataSource dataSource) {
+ XAResourceRecoveryHelper recoveryHelper = getRecoveryHelper(dataSource);
+ this.recoveryManager.registerXAResourceRecoveryHelper(recoveryHelper);
+ return new NarayanaDataSourceBean(dataSource);
+ }
+
+ private XAResourceRecoveryHelper getRecoveryHelper(XADataSource dataSource) {
+ if (this.properties.getRecoveryDbUser() == null
+ && this.properties.getRecoveryDbPass() == null) {
+ return new DataSourceXAResourceRecoveryHelper(dataSource);
+ }
+ return new DataSourceXAResourceRecoveryHelper(dataSource,
+ this.properties.getRecoveryDbUser(), this.properties.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..06eff9acdd
--- /dev/null
+++ b/spring-boot/src/test/java/org/springframework/boot/jta/narayana/DataSourceXAResourceRecoveryHelperTests.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright 2012-2016 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.given;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+/**
+ * Tests for {@link DataSourceXAResourceRecoveryHelper}.
+ *
+ * @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);
+
+ given(this.xaDataSource.getXAConnection()).willReturn(this.xaConnection);
+ given(this.xaConnection.getXAResource()).willReturn(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 {
+ given(this.xaDataSource.getXAConnection(anyString(), anyString()))
+ .willReturn(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 {
+ given(this.xaDataSource.getXAConnection())
+ .willThrow(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..07f50888c4
--- /dev/null
+++ b/spring-boot/src/test/java/org/springframework/boot/jta/narayana/NarayanaBeanFactoryPostProcessorTests.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2012-2016 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.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+/**
+ * Tests for {@link NarayanaBeanFactoryPostProcessor}.
+ *
+ * @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..a09221f8b5
--- /dev/null
+++ b/spring-boot/src/test/java/org/springframework/boot/jta/narayana/NarayanaConfigurationBeanTests.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2012-2016 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;
+
+/**
+ * Tests for {@link NarayanaConfigurationBean}.
+ *
+ * @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..fada25acca
--- /dev/null
+++ b/spring-boot/src/test/java/org/springframework/boot/jta/narayana/NarayanaDataSourceBeanTests.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2012-2016 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.given;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+/**
+ * Tests for {@link NarayanaDataSourceBean}.
+ *
+ * @author Gytis Trikleris
+ */
+public class NarayanaDataSourceBeanTests {
+
+ private XADataSource dataSource;
+
+ private NarayanaDataSourceBean dataSourceBean;
+
+ @Before
+ public void before() {
+ this.dataSource = mock(XADataSource.class);
+ this.dataSourceBean = new NarayanaDataSourceBean(this.dataSource);
+ }
+
+ @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.dataSource);
+ }
+
+ @Test
+ public void shouldGetConnectionAndCommit() throws SQLException {
+ Connection mockConnection = mock(Connection.class);
+ XAConnection mockXaConnection = mock(XAConnection.class);
+ given(mockXaConnection.getConnection()).willReturn(mockConnection);
+ given(this.dataSource.getXAConnection()).willReturn(mockXaConnection);
+
+ Properties properties = new Properties();
+ properties.put(TransactionalDriver.XADataSource, this.dataSource);
+
+ Connection connection = this.dataSourceBean.getConnection();
+ assertThat(connection).isInstanceOf(ConnectionImple.class);
+
+ connection.commit();
+
+ verify(this.dataSource, 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);
+ given(mockXaConnection.getConnection()).willReturn(mockConnection);
+ given(this.dataSource.getXAConnection(username, password))
+ .willReturn(mockXaConnection);
+
+ Properties properties = new Properties();
+ properties.put(TransactionalDriver.XADataSource, this.dataSource);
+ 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.dataSource, 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..f90425ede2
--- /dev/null
+++ b/spring-boot/src/test/java/org/springframework/boot/jta/narayana/NarayanaRecoveryManagerBeanTests.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2012-2016 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;
+
+/**
+ * Tests for {@link NarayanaRecoveryManagerBean}.
+ *
+ * @author Gytis Trikleris
+ */
+public class NarayanaRecoveryManagerBeanTests {
+
+ private RecoveryManagerService service;
+
+ private NarayanaRecoveryManagerBean recoveryManager;
+
+ @Before
+ public void before() {
+ this.service = mock(RecoveryManagerService.class);
+ this.recoveryManager = new NarayanaRecoveryManagerBean(this.service);
+ }
+
+ @Test
+ public void shouldCreateAndStartRecoveryManagerService() throws Exception {
+ this.recoveryManager.afterPropertiesSet();
+ verify(this.service, times(1)).create();
+ verify(this.service, times(1)).start();
+ }
+
+ @Test
+ public void shouldStopAndDestroyRecoveryManagerService() throws Exception {
+ this.recoveryManager.destroy();
+ verify(this.service, times(1)).stop();
+ verify(this.service, 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..e1a8143327
--- /dev/null
+++ b/spring-boot/src/test/java/org/springframework/boot/jta/narayana/NarayanaXAConnectionFactoryWrapperTests.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2012-2016 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.given;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+/**
+ * Tests for {@link NarayanaXAConnectionFactoryWrapper}.
+ *
+ * @author Gytis Trikleris
+ */
+public class NarayanaXAConnectionFactoryWrapperTests {
+
+ private XAConnectionFactory connectionFactory = mock(XAConnectionFactory.class);
+
+ private TransactionManager transactionManager = mock(TransactionManager.class);
+
+ private NarayanaRecoveryManagerBean recoveryManager = mock(
+ NarayanaRecoveryManagerBean.class);
+
+ private NarayanaProperties properties = mock(NarayanaProperties.class);
+
+ private NarayanaXAConnectionFactoryWrapper wrapper = new NarayanaXAConnectionFactoryWrapper(
+ this.transactionManager, this.recoveryManager, this.properties);
+
+ @Test
+ public void wrap() {
+ ConnectionFactory wrapped = this.wrapper
+ .wrapConnectionFactory(this.connectionFactory);
+ assertThat(wrapped).isInstanceOf(ConnectionFactoryProxy.class);
+ verify(this.recoveryManager, times(1))
+ .registerXAResourceRecoveryHelper(any(JmsXAResourceRecoveryHelper.class));
+ verify(this.properties, times(1)).getRecoveryJmsUser();
+ verify(this.properties, times(1)).getRecoveryJmsPass();
+ }
+
+ @Test
+ public void wrapWithCredentials() {
+ given(this.properties.getRecoveryJmsUser()).willReturn("userName");
+ given(this.properties.getRecoveryJmsPass()).willReturn("password");
+ ConnectionFactory wrapped = this.wrapper
+ .wrapConnectionFactory(this.connectionFactory);
+ assertThat(wrapped).isInstanceOf(ConnectionFactoryProxy.class);
+ verify(this.recoveryManager, times(1))
+ .registerXAResourceRecoveryHelper(any(JmsXAResourceRecoveryHelper.class));
+ verify(this.properties, times(2)).getRecoveryJmsUser();
+ verify(this.properties, 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..aa1ac6f4a6
--- /dev/null
+++ b/spring-boot/src/test/java/org/springframework/boot/jta/narayana/NarayanaXADataSourceWrapperTests.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2012-2016 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.given;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+/**
+ * Tests for {@link NarayanaXADataSourceWrapper}.
+ *
+ * @author Gytis Trikleris
+ */
+public class NarayanaXADataSourceWrapperTests {
+
+ private XADataSource dataSource = mock(XADataSource.class);
+
+ private NarayanaRecoveryManagerBean recoveryManager = mock(
+ NarayanaRecoveryManagerBean.class);
+
+ private NarayanaProperties properties = mock(NarayanaProperties.class);
+
+ private NarayanaXADataSourceWrapper wrapper = new NarayanaXADataSourceWrapper(
+ this.recoveryManager, this.properties);
+
+ @Test
+ public void wrap() {
+ DataSource wrapped = this.wrapper.wrapDataSource(this.dataSource);
+ assertThat(wrapped).isInstanceOf(NarayanaDataSourceBean.class);
+ verify(this.recoveryManager, times(1)).registerXAResourceRecoveryHelper(
+ any(DataSourceXAResourceRecoveryHelper.class));
+ verify(this.properties, times(1)).getRecoveryDbUser();
+ verify(this.properties, times(1)).getRecoveryDbPass();
+ }
+
+ @Test
+ public void wrapWithCredentials() {
+ given(this.properties.getRecoveryDbUser()).willReturn("userName");
+ given(this.properties.getRecoveryDbPass()).willReturn("password");
+ DataSource wrapped = this.wrapper.wrapDataSource(this.dataSource);
+ assertThat(wrapped).isInstanceOf(NarayanaDataSourceBean.class);
+ verify(this.recoveryManager, times(1)).registerXAResourceRecoveryHelper(
+ any(DataSourceXAResourceRecoveryHelper.class));
+ verify(this.properties, times(2)).getRecoveryDbUser();
+ verify(this.properties, times(1)).getRecoveryDbPass();
+ }
+
+}