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(); + } + +}