/** * Licensed to Apereo under one or more contributor license agreements. See the NOTICE file * distributed with this work for additional information regarding copyright ownership. Apereo * licenses this file to you 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 the * following location: * * <p>http://www.apache.org/licenses/LICENSE-2.0 * * <p>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.apereo.portal.spring.tx; import com.google.common.collect.MapMaker; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import org.apereo.portal.hibernate.DelegatingHibernateIntegrator.HibernateConfiguration; import org.apereo.portal.hibernate.HibernateConfigurationAware; import org.hibernate.dialect.Dialect; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.TransactionException; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.interceptor.TransactionAttribute; import org.springframework.transaction.interceptor.TransactionAttributeSource; import org.springframework.transaction.support.DefaultTransactionStatus; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; public class DialectAwareTransactionInterceptor extends TransactionManagerCachingTransactionInterceptor implements HibernateConfigurationAware { private static final Logger LOGGER = LoggerFactory.getLogger(DialectAwareTransactionInterceptor.class); private static final long serialVersionUID = 1L; /** Returned as the TX manager when skipping the TX. Doesn't actually do any db level TX work */ private static final PlatformTransactionManager NOOP_TRANSACTION_MANAGER = new PlatformTransactionManager() { private Map<TransactionDefinition, DefaultTransactionStatus> statusCache = new MapMaker().weakKeys().weakValues().makeMap(); protected final Logger logger = LoggerFactory.getLogger( DialectAwareTransactionInterceptor.class.getPackage().getName() + ".NoopTransactionManager"); @Override public TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException { DefaultTransactionStatus status = statusCache.get(definition); if (status == null) { logger.debug( "Creating new NOOP transaction with name [{}]: {}", definition.getName(), definition); status = new DefaultTransactionStatus( definition, true, false, definition.isReadOnly(), logger.isDebugEnabled(), null); statusCache.put(definition, status); } else { logger.debug( "Using existing NOOP transaction with name [{}]: {}", definition.getName(), definition); } return status; } @Override public void commit(TransactionStatus status) throws TransactionException { if (status instanceof DefaultTransactionStatus) { final TransactionDefinition definition = (TransactionDefinition) ((DefaultTransactionStatus) status).getTransaction(); if (statusCache.remove(definition) != null) { logger.debug( "Closing NOOP transaction with name [{}] after commit: {}", definition.getName(), definition); } else { logger.debug( "Can't commit NOOP transaction with name [{}], already closed: {}", definition.getName(), definition); } } else { logger.warn( "TransactionStatus {} is not a DefaultTransactionStatus, no NOOP commit done: {}", status, status.getClass()); } } @Override public void rollback(TransactionStatus status) throws TransactionException { if (status instanceof DefaultTransactionStatus) { final TransactionDefinition definition = (TransactionDefinition) ((DefaultTransactionStatus) status).getTransaction(); if (statusCache.remove(definition) != null) { logger.debug( "Closing NOOP transaction with name [{}] after rollback: {}", definition.getName(), definition); } else { logger.debug( "Can't rollback NOOP transaction with name [{}], already closed: {}", definition.getName(), definition); } } else { logger.warn( "TransactionStatus {} is not a DefaultTransactionStatus, no NOOP rollback done: {}", status, status.getClass()); } } }; private final Map<String, Class<? extends Dialect>> dialects = new ConcurrentHashMap<String, Class<? extends Dialect>>(); private TransactionAttributeSource wrappedTransactionAttributeSource; @Override public boolean supports(String persistenceUnit) { return true; } @Override public void setConfiguration( String persistenceUnit, HibernateConfiguration hibernateConfiguration) { final SessionFactoryImplementor sessionFactory = hibernateConfiguration.getSessionFactory(); this.dialects.put(persistenceUnit, sessionFactory.getDialect().getClass()); } @Override public TransactionAttributeSource getTransactionAttributeSource() { TransactionAttributeSource tas = this.wrappedTransactionAttributeSource; if (tas == null) { final TransactionAttributeSource transactionAttributeSource = super.getTransactionAttributeSource(); if (this.dialects.isEmpty()) { return transactionAttributeSource; } tas = new TransactionAttributeSourceWrapper(this.dialects, transactionAttributeSource); this.wrappedTransactionAttributeSource = tas; } return tas; } protected PlatformTransactionManager determineTransactionManager(TransactionAttribute txAttr) { if (txAttr instanceof SkipTransactionAttribute) { return NOOP_TRANSACTION_MANAGER; } return super.determineTransactionManager(txAttr); } private static final class TransactionAttributeSourceWrapper implements TransactionAttributeSource { private final Map<String, Class<? extends Dialect>> dialects; private final TransactionAttributeSource transactionAttributeSource; public TransactionAttributeSourceWrapper( Map<String, Class<? extends Dialect>> dialects, TransactionAttributeSource transactionAttributeSource) { this.dialects = dialects; this.transactionAttributeSource = transactionAttributeSource; } @Override public TransactionAttribute getTransactionAttribute(Method method, Class<?> targetClass) { final TransactionAttribute transactionAttribute = transactionAttributeSource.getTransactionAttribute(method, targetClass); //No dialect ignore support if transactionAttribute is null if (transactionAttribute == null) { return transactionAttribute; } final DialectAwareTransactional ann = getDialectAwareTransactionalAnnotation(method, targetClass); //No DialectAwareTransactional annotation, just return the original transactionAttribute if (ann == null) { return transactionAttribute; } //Check if a TX is needed for the Dialect final Class<? extends Dialect> dialect = determineDialect(method, transactionAttribute); final boolean ignored = isDialectIgnored(dialect, ann); //Dialect is ignored if (!ignored) { return transactionAttribute; } //Determine interfaces to proxy @SuppressWarnings("rawtypes") final Set<Class> interfaces = ClassUtils.getAllInterfacesAsSet(transactionAttribute); interfaces.add(SkipTransactionAttribute.class); //Proxy the existing transactionAttribute to mix in our SkipTransactionAttribute interface return (TransactionAttribute) Proxy.newProxyInstance( DialectAwareTransactionInterceptor.class.getClassLoader(), interfaces.toArray(new Class<?>[interfaces.size()]), new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { return method.invoke(transactionAttribute, args); } }); } private boolean isDialectIgnored( Class<? extends Dialect> dialect, DialectAwareTransactional ann) { if (dialect == null) { return false; } boolean ignored = !ann.exclude(); for (final Class<? extends Dialect> ignoredDialect : ann.value()) { if (ignoredDialect.isAssignableFrom(dialect)) { ignored = !ignored; break; } } return ignored; } private Class<? extends Dialect> determineDialect( Method method, TransactionAttribute transactionAttribute) { final Class<? extends Dialect> dialect; final String qualifier = transactionAttribute.getQualifier(); if (StringUtils.hasLength(qualifier)) { dialect = this.dialects.get(qualifier); } else if (this.dialects.size() == 1) { dialect = this.dialects.values().iterator().next(); } else if (!this.dialects.isEmpty()) { LOGGER.debug( "No qualifier specified for @Transactional on {} and multiple Dialects are configured: {}", method, this.dialects.keySet()); return null; } else { return null; } return dialect; } private DialectAwareTransactional getDialectAwareTransactionalAnnotation( Method method, Class<?> targetClass) { // Ignore CGLIB subclasses - introspect the actual user class. Class<?> userClass = ClassUtils.getUserClass(targetClass); // The method may be on an interface, but we need attributes from the target class. // If the target class is null, the method will be unchanged. Method specificMethod = ClassUtils.getMostSpecificMethod(method, userClass); DialectAwareTransactional ann = AnnotationUtils.getAnnotation(specificMethod, DialectAwareTransactional.class); if (ann == null) { ann = AnnotationUtils.getAnnotation(targetClass, DialectAwareTransactional.class); } return ann; } } private interface SkipTransactionAttribute extends TransactionAttribute {} }