From a2adc5a1304f2eb24127bf60e6ad7dd50643fc7c Mon Sep 17 00:00:00 2001 From: Gytis Trikleris Date: Mon, 18 Jan 2016 14:56:27 +0100 Subject: [PATCH] Add Narayana JTA support Add support for JBoss Narayana. Fixes gh-5552 --- spring-boot-autoconfigure/pom.xml | 10 + .../transaction/jta/JtaAutoConfiguration.java | 2 +- .../jta/NarayanaJtaConfiguration.java | 131 +++++++++++ spring-boot-dependencies/pom.xml | 32 +++ .../appendix-application-properties.adoc | 13 ++ spring-boot-samples/pom.xml | 1 + .../spring-boot-sample-jta-narayana/pom.xml | 57 +++++ .../main/java/sample/narayana/Account.java | 43 ++++ .../sample/narayana/AccountRepository.java | 23 ++ .../java/sample/narayana/AccountService.java | 47 ++++ .../main/java/sample/narayana/Messages.java | 30 +++ .../narayana/SampleNarayanaApplication.java | 45 ++++ .../narayana/SampleRuntimeException.java | 12 + .../src/main/resources/application.properties | 5 + .../SampleNarayanaApplicationTests.java | 66 ++++++ spring-boot-starters/pom.xml | 1 + .../spring-boot-starter-jta-narayana/pom.xml | 48 ++++ .../main/resources/META-INF/spring.provides | 1 + spring-boot/pom.xml | 25 ++ .../DataSourceXAResourceRecoveryHelper.java | 169 ++++++++++++++ .../NarayanaBeanFactoryPostProcessor.java | 77 ++++++ .../narayana/NarayanaConfigurationBean.java | 106 +++++++++ .../jta/narayana/NarayanaDataSourceBean.java | 110 +++++++++ .../boot/jta/narayana/NarayanaProperties.java | 221 ++++++++++++++++++ .../narayana/NarayanaRecoveryManagerBean.java | 67 ++++++ .../NarayanaXAConnectionFactoryWrapper.java | 66 ++++++ .../narayana/NarayanaXADataSourceWrapper.java | 59 +++++ .../boot/jta/narayana/package-info.java | 20 ++ ...taSourceXAResourceRecoveryHelperTests.java | 182 +++++++++++++++ ...NarayanaBeanFactoryPostProcessorTests.java | 87 +++++++ .../NarayanaConfigurationBeanTests.java | 111 +++++++++ .../narayana/NarayanaDataSourceBeanTests.java | 119 ++++++++++ .../NarayanaRecoveryManagerBeanTests.java | 59 +++++ ...rayanaXAConnectionFactoryWrapperTests.java | 73 ++++++ .../NarayanaXADataSourceWrapperTests.java | 69 ++++++ 35 files changed, 2186 insertions(+), 1 deletion(-) create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/jta/NarayanaJtaConfiguration.java create mode 100644 spring-boot-samples/spring-boot-sample-jta-narayana/pom.xml create mode 100644 spring-boot-samples/spring-boot-sample-jta-narayana/src/main/java/sample/narayana/Account.java create mode 100644 spring-boot-samples/spring-boot-sample-jta-narayana/src/main/java/sample/narayana/AccountRepository.java create mode 100644 spring-boot-samples/spring-boot-sample-jta-narayana/src/main/java/sample/narayana/AccountService.java create mode 100644 spring-boot-samples/spring-boot-sample-jta-narayana/src/main/java/sample/narayana/Messages.java create mode 100644 spring-boot-samples/spring-boot-sample-jta-narayana/src/main/java/sample/narayana/SampleNarayanaApplication.java create mode 100644 spring-boot-samples/spring-boot-sample-jta-narayana/src/main/java/sample/narayana/SampleRuntimeException.java create mode 100644 spring-boot-samples/spring-boot-sample-jta-narayana/src/main/resources/application.properties create mode 100644 spring-boot-samples/spring-boot-sample-jta-narayana/src/test/java/sample/narayana/SampleNarayanaApplicationTests.java create mode 100644 spring-boot-starters/spring-boot-starter-jta-narayana/pom.xml create mode 100644 spring-boot-starters/spring-boot-starter-jta-narayana/src/main/resources/META-INF/spring.provides create mode 100644 spring-boot/src/main/java/org/springframework/boot/jta/narayana/DataSourceXAResourceRecoveryHelper.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/jta/narayana/NarayanaBeanFactoryPostProcessor.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/jta/narayana/NarayanaConfigurationBean.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/jta/narayana/NarayanaDataSourceBean.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/jta/narayana/NarayanaProperties.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/jta/narayana/NarayanaRecoveryManagerBean.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/jta/narayana/NarayanaXAConnectionFactoryWrapper.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/jta/narayana/NarayanaXADataSourceWrapper.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/jta/narayana/package-info.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/jta/narayana/DataSourceXAResourceRecoveryHelperTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/jta/narayana/NarayanaBeanFactoryPostProcessorTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/jta/narayana/NarayanaConfigurationBeanTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/jta/narayana/NarayanaDataSourceBeanTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/jta/narayana/NarayanaRecoveryManagerBeanTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/jta/narayana/NarayanaXAConnectionFactoryWrapperTests.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/jta/narayana/NarayanaXADataSourceWrapperTests.java 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 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(); + } + +}