package org.grails.transaction;
import grails.config.Config;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.grails.config.PropertySourcesConfig;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.ConstructorArgumentValues;
import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.beans.factory.support.ManagedList;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.core.Ordered;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.jta.JtaTransactionManager;
import org.springframework.util.ClassUtils;
/**
* A {@link BeanDefinitionRegistryPostProcessor} for using the "Best Effort 1 Phase Commit" (BE1PC) in Grails
* applications when there are multiple data sources.
*
* When the context contains multiple transactionManager beans, the bean with the name "transactionManager"
* will be renamed to "$primaryTransactionManager" and a new ChainedTransactionManager bean will be added with the name
* "transactionManager". All transactionManager beans will be registered in the ChainedTransactionManager bean.
*
* The post processor checks if the previous transactionManager bean is an instance of {@link JtaTransactionManager}.
* In that case it will not do anything since it's assumed that JTA/XA is handling transactions spanning multiple datasources.
*
* For performance reasons an additional dataSource can be marked as non-transactional by adding a property 'transactional = false' in
* it's dataSource configuration. This will leave the dataSource out of the transactions initiated by Grails transactions.
* This is the default behaviour in Grails versions before Grails 2.3.6 .
*
*
* @author Lari Hotari
* @since 2.3.6
*
*/
public class ChainedTransactionManagerPostProcessor implements BeanDefinitionRegistryPostProcessor, Ordered {
private static final String TRANSACTIONAL = "transactional";
private static final String DEFAULT_TRANSACTION_MANAGER_BEAN_NAME_WHITELIST_PATTERN = "(?i).*transactionManager(_.+)?";
private static final String DEFAULT_TRANSACTION_MANAGER_INTERNAL_BEAN_NAME_BLACKLIST_PATTERN = "(?i)chainedTransactionManagerPostProcessor|transactionManagerPostProcessor|.*PostProcessor";
public static final String DATA_SOURCE_SETTING = "dataSource";
public static final String DATA_SOURCES_SETTING = "dataSources";
public static final String DATA_SOURCES_PREFIX = "dataSources.";
private String beanNameWhitelistPattern = DEFAULT_TRANSACTION_MANAGER_BEAN_NAME_WHITELIST_PATTERN;
private String beanNameBlacklistPattern = null;
private String beanNameInternalBlacklistPattern = DEFAULT_TRANSACTION_MANAGER_INTERNAL_BEAN_NAME_BLACKLIST_PATTERN;
private static final Pattern SUFFIX_PATTERN = Pattern.compile("^transactionManager_(.+)$");
private static final String PRIMARY_TRANSACTION_MANAGER = "$primaryTransactionManager";
private static final String TRANSACTION_MANAGER = "transactionManager";
private static final String READONLY = "readOnly";
private Config config;
private Map<String, Map> dsConfigs;
private static String[] transactionManagerBeanNames = null;
public ChainedTransactionManagerPostProcessor(Config config) {
this(config, null, null);
}
public ChainedTransactionManagerPostProcessor(Config config, String whitelistPattern, String blacklistPattern) {
transactionManagerBeanNames = null;
this.config = config;
if (whitelistPattern != null) {
beanNameWhitelistPattern = whitelistPattern;
}
if (blacklistPattern != null) {
beanNameBlacklistPattern = blacklistPattern;
}
}
public ChainedTransactionManagerPostProcessor() {
this(new PropertySourcesConfig(), null, null);
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
}
protected void registerAdditionalTransactionManagers(BeanDefinitionRegistry registry, BeanDefinition chainedTransactionManagerBeanDefinition, ManagedList<RuntimeBeanReference> transactionManagerRefs) {
String[] allBeanNames = getTransactionManagerBeanNames(registry);
for (String beanName : allBeanNames) {
BeanDefinition beanDefinition = registry.getBeanDefinition(beanName);
if(!TRANSACTION_MANAGER.equals(beanName) && !PRIMARY_TRANSACTION_MANAGER.equals(beanName) && isValidTransactionManagerBeanDefinition(beanName, beanDefinition)) {
String suffix = resolveDataSourceSuffix(beanName);
if (!isNotTransactional(suffix)) {
transactionManagerRefs.add(new RuntimeBeanReference(beanName));
}
}
}
}
protected static String[] getTransactionManagerBeanNames(BeanDefinitionRegistry registry) {
if(transactionManagerBeanNames == null) {
if(registry instanceof ListableBeanFactory) {
transactionManagerBeanNames = ((ListableBeanFactory)registry).getBeanNamesForType(PlatformTransactionManager.class, false, false);
}
else {
transactionManagerBeanNames = registry.getBeanDefinitionNames();
}
}
return transactionManagerBeanNames;
}
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
if (registry.containsBeanDefinition(TRANSACTION_MANAGER) && countChainableTransactionManagerBeans(registry) > 1 && !hasJtaOrChainedTransactionManager(registry)) {
BeanDefinition chainedTransactionManagerBeanDefinition = addChainedTransactionManager(registry);
ManagedList<RuntimeBeanReference> transactionManagerRefs = createTransactionManagerBeanReferences(chainedTransactionManagerBeanDefinition);
registerAdditionalTransactionManagers(registry, chainedTransactionManagerBeanDefinition, transactionManagerRefs);
}
}
protected ManagedList<RuntimeBeanReference> createTransactionManagerBeanReferences(
BeanDefinition chainedTransactionManagerBeanDefinition) {
ManagedList<RuntimeBeanReference> transactionManagerRefs = new ManagedList<RuntimeBeanReference>();
ConstructorArgumentValues constructorValues=chainedTransactionManagerBeanDefinition.getConstructorArgumentValues();
constructorValues.addIndexedArgumentValue(0, transactionManagerRefs);
transactionManagerRefs.add(new RuntimeBeanReference(PRIMARY_TRANSACTION_MANAGER));
return transactionManagerRefs;
}
protected BeanDefinition addChainedTransactionManager(BeanDefinitionRegistry registry) {
renameBean(TRANSACTION_MANAGER, PRIMARY_TRANSACTION_MANAGER, registry);
BeanDefinition beanDefinition = new RootBeanDefinition(ChainedTransactionManager.class);
registry.registerBeanDefinition(TRANSACTION_MANAGER, beanDefinition);
return beanDefinition;
}
protected boolean hasJtaOrChainedTransactionManager(BeanDefinitionRegistry registry) {
Class<?> transactionManagerBeanClass = resolveTransactionManagerClass(registry);
if (transactionManagerBeanClass == null) {
return false;
}
boolean isJtaTransactionManager = JtaTransactionManager.class.isAssignableFrom(transactionManagerBeanClass);
boolean isChainedTransactionManager = ChainedTransactionManager.class.isAssignableFrom(transactionManagerBeanClass);
return isJtaTransactionManager || isChainedTransactionManager;
}
protected Class<?> resolveTransactionManagerClass(BeanDefinitionRegistry registry) {
if(!registry.containsBeanDefinition(TRANSACTION_MANAGER)) {
return null;
}
BeanDefinition transactionManagerBeanDefinition = registry.getBeanDefinition(TRANSACTION_MANAGER);
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
Class<?> transactionManagerBeanClass = ClassUtils.resolveClassName(transactionManagerBeanDefinition.getBeanClassName(), classLoader);
return transactionManagerBeanClass;
}
protected int countChainableTransactionManagerBeans(BeanDefinitionRegistry registry) {
int transactionManagerBeanCount=0;
for (String beanName : getTransactionManagerBeanNames(registry)) {
BeanDefinition beanDefinition = registry.getBeanDefinition(beanName);
if(isValidTransactionManagerBeanDefinition(beanName, beanDefinition)) {
String suffix = resolveDataSourceSuffix(beanName);
if (beanName.equals(TRANSACTION_MANAGER) || !isNotTransactional(suffix)) {
transactionManagerBeanCount++;
}
}
}
return transactionManagerBeanCount;
}
protected boolean isValidTransactionManagerBeanDefinition(String beanName, BeanDefinition beanDefinition) {
return beanName.matches(beanNameWhitelistPattern) && (beanNameBlacklistPattern==null || !beanName.matches(beanNameBlacklistPattern)) && !beanName.matches(beanNameInternalBlacklistPattern);
}
protected boolean isNotTransactional(String suffix) {
if (suffix == null || config == null) {
return false;
}
Boolean transactional = config.getProperty(DATA_SOURCES_PREFIX + suffix + "." + TRANSACTIONAL, Boolean.class, null);
if(transactional == null) {
Boolean isReadOnly = config.getProperty(DATA_SOURCES_PREFIX + suffix + "." + READONLY, Boolean.class, null);
if (isReadOnly != null && isReadOnly == true) {
transactional = false;
}
}
if(transactional != null){
return !transactional;
}
else {
return false;
}
}
protected String resolveDataSourceSuffix(String transactionManagerBeanName) {
if(TRANSACTION_MANAGER.equals(transactionManagerBeanName)) {
return "";
} else {
Matcher matcher=SUFFIX_PATTERN.matcher(transactionManagerBeanName);
if(matcher.matches()) {
return matcher.group(1);
}
}
return null;
}
private static boolean renameBean(String oldName, String newName, BeanDefinitionRegistry registry) {
if(!registry.containsBeanDefinition(oldName)) {
return false;
}
// remove link to child beans
Set<String> previousChildBeans = new LinkedHashSet<String>();
for (String bdName : getTransactionManagerBeanNames(registry)) {
if (!oldName.equals(bdName)) {
BeanDefinition bd = registry.getBeanDefinition(bdName);
if (oldName.equals(bd.getParentName())) {
bd.setParentName(null);
previousChildBeans.add(bdName);
}
}
}
BeanDefinition oldBeanDefinition = registry.getBeanDefinition(oldName);
registry.removeBeanDefinition(oldName);
registry.registerBeanDefinition(newName, oldBeanDefinition);
// re-link possible child beans to new parent name
for(String bdName : previousChildBeans) {
BeanDefinition bd = registry.getBeanDefinition(bdName);
bd.setParentName(newName);
}
return true;
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
public void setConfig(Config config) {
this.config = config;
}
public String getBeanNameWhitelistPattern() {
return beanNameWhitelistPattern;
}
public void setBeanNameWhitelistPattern(String beanNameWhitelistPattern) {
this.beanNameWhitelistPattern = beanNameWhitelistPattern;
}
public String getBeanNameBlacklistPattern() {
return beanNameBlacklistPattern;
}
public void setBeanNameBlacklistPattern(String beanNameBlacklistPattern) {
this.beanNameBlacklistPattern = beanNameBlacklistPattern;
}
public String getBeanNameInternalBlacklistPattern() {
return beanNameInternalBlacklistPattern;
}
public void setBeanNameInternalBlacklistPattern(String beanNameInternalBlacklistPattern) {
this.beanNameInternalBlacklistPattern = beanNameInternalBlacklistPattern;
}
}