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; } }