/**
* Copyright 2014-2016 yangming.liu<bytefox@126.com>.
*
* This copyrighted material is made available to anyone wishing to use, modify,
* copy, or redistribute it subject to the terms and conditions of the GNU
* Lesser General Public License, as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
* for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this distribution; if not, see <http://www.gnu.org/licenses/>.
*/
package org.bytesoft.bytetcc.supports.spring;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import org.bytesoft.compensable.Compensable;
import org.bytesoft.compensable.CompensableCancel;
import org.bytesoft.compensable.CompensableConfirm;
import org.bytesoft.compensable.RemotingException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.FatalBeanException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
public class CompensableAnnotationValidator implements BeanFactoryPostProcessor {
static final Logger logger = LoggerFactory.getLogger(CompensableAnnotationValidator.class);
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
Map<String, Class<?>> otherServiceMap = new HashMap<String, Class<?>>();
Map<String, Compensable> compensables = new HashMap<String, Compensable>();
ClassLoader cl = Thread.currentThread().getContextClassLoader();
String[] beanNameArray = beanFactory.getBeanDefinitionNames();
for (int i = 0; beanNameArray != null && i < beanNameArray.length; i++) {
String beanName = beanNameArray[i];
BeanDefinition beanDef = beanFactory.getBeanDefinition(beanName);
String className = beanDef.getBeanClassName();
Class<?> clazz = null;
try {
clazz = cl.loadClass(className);
} catch (Exception ex) {
logger.debug("Cannot load class {}, beanId= {}!", className, beanName, ex);
continue;
}
try {
Compensable compensable = clazz.getAnnotation(Compensable.class);
if (compensable == null) {
otherServiceMap.put(beanName, clazz);
continue;
} else {
compensables.put(beanName, compensable);
}
Class<?> interfaceClass = compensable.interfaceClass();
if (interfaceClass.isInterface() == false) {
throw new IllegalStateException("Compensable's interfaceClass must be a interface.");
}
Method[] methodArray = interfaceClass.getDeclaredMethods();
for (int j = 0; j < methodArray.length; j++) {
Method interfaceMethod = methodArray[j];
Method method = clazz.getMethod(interfaceMethod.getName(), interfaceMethod.getParameterTypes());
this.validateSimplifiedCompensable(method, clazz);
this.validateDeclaredRemotingException(method, clazz);
this.validateTransactionalPropagation(method, clazz);
}
} catch (IllegalStateException ex) {
throw new FatalBeanException(ex.getMessage(), ex);
} catch (NoSuchMethodException ex) {
throw new FatalBeanException(ex.getMessage(), ex);
} catch (SecurityException ex) {
throw new FatalBeanException(ex.getMessage(), ex);
}
}
Iterator<Map.Entry<String, Compensable>> itr = compensables.entrySet().iterator();
while (itr.hasNext()) {
Map.Entry<String, Compensable> entry = itr.next();
Compensable compensable = entry.getValue();
Class<?> interfaceClass = compensable.interfaceClass();
String confirmableKey = compensable.confirmableKey();
String cancellableKey = compensable.cancellableKey();
if (StringUtils.isNotBlank(confirmableKey)) {
if (compensables.containsKey(confirmableKey)) {
throw new FatalBeanException(
String.format("The confirm bean(id= %s) cannot be a compensable service!", confirmableKey));
}
Class<?> clazz = otherServiceMap.get(confirmableKey);
if (clazz == null) {
throw new IllegalStateException(String.format("The confirm bean(id= %s) is not exists!", confirmableKey));
}
try {
Method[] methodArray = interfaceClass.getDeclaredMethods();
for (int j = 0; j < methodArray.length; j++) {
Method interfaceMethod = methodArray[j];
Method method = clazz.getMethod(interfaceMethod.getName(), interfaceMethod.getParameterTypes());
this.validateDeclaredRemotingException(method, clazz);
this.validateTransactionalPropagation(method, clazz);
this.validateTransactionalRollbackFor(method, clazz, confirmableKey);
}
} catch (IllegalStateException ex) {
throw new FatalBeanException(ex.getMessage(), ex);
} catch (NoSuchMethodException ex) {
throw new FatalBeanException(ex.getMessage(), ex);
} catch (SecurityException ex) {
throw new FatalBeanException(ex.getMessage(), ex);
}
}
if (StringUtils.isNotBlank(cancellableKey)) {
if (compensables.containsKey(cancellableKey)) {
throw new FatalBeanException(
String.format("The cancel bean(id= %s) cannot be a compensable service!", confirmableKey));
}
Class<?> clazz = otherServiceMap.get(cancellableKey);
if (clazz == null) {
throw new IllegalStateException(String.format("The cancel bean(id= %s) is not exists!", cancellableKey));
}
try {
Method[] methodArray = interfaceClass.getDeclaredMethods();
for (int j = 0; j < methodArray.length; j++) {
Method interfaceMethod = methodArray[j];
Method method = clazz.getMethod(interfaceMethod.getName(), interfaceMethod.getParameterTypes());
this.validateDeclaredRemotingException(method, clazz);
this.validateTransactionalPropagation(method, clazz);
this.validateTransactionalRollbackFor(method, clazz, cancellableKey);
}
} catch (IllegalStateException ex) {
throw new FatalBeanException(ex.getMessage(), ex);
} catch (NoSuchMethodException ex) {
throw new FatalBeanException(ex.getMessage(), ex);
} catch (SecurityException ex) {
throw new FatalBeanException(ex.getMessage(), ex);
}
}
}
}
private void validateSimplifiedCompensable(Method method, Class<?> clazz) throws IllegalStateException {
Compensable compensable = clazz.getAnnotation(Compensable.class);
Class<?> interfaceClass = compensable.interfaceClass();
Method[] methods = interfaceClass.getDeclaredMethods();
if (compensable.simplified() == false) {
return;
} else if (method.getAnnotation(CompensableConfirm.class) != null) {
throw new FatalBeanException(
String.format("The try method(%s) can not be the same as the confirm method!", method));
} else if (method.getAnnotation(CompensableCancel.class) != null) {
throw new FatalBeanException(String.format("The try method(%s) can not be the same as the cancel method!", method));
} else if (methods != null && methods.length > 1) {
throw new FatalBeanException(String.format(
"The interface bound by @Compensable(simplified= true) supports only one method, class= %s!", clazz));
}
Class<?>[] parameterTypes = method.getParameterTypes();
Method[] methodArray = clazz.getDeclaredMethods();
CompensableConfirm confirmable = null;
CompensableCancel cancellable = null;
for (int i = 0; i < methodArray.length; i++) {
Method element = methodArray[i];
Class<?>[] paramTypes = element.getParameterTypes();
CompensableConfirm confirm = element.getAnnotation(CompensableConfirm.class);
CompensableCancel cancel = element.getAnnotation(CompensableCancel.class);
if (confirm == null && cancel == null) {
continue;
} else if (Arrays.equals(parameterTypes, paramTypes) == false) {
throw new FatalBeanException(
String.format("The parameter types of confirm/cancel method({}) is different from the try method({})!",
element, method));
} else if (confirm != null) {
if (confirmable != null) {
throw new FatalBeanException(
String.format("There are more than one confirm method specified, class= %s!", clazz));
} else {
confirmable = confirm;
}
} else if (cancel != null) {
if (cancellable != null) {
throw new FatalBeanException(
String.format("There are more than one cancel method specified, class= %s!", clazz));
} else {
cancellable = cancel;
}
}
}
}
private void validateDeclaredRemotingException(Method method, Class<?> clazz) throws IllegalStateException {
Class<?>[] exceptionTypeArray = method.getExceptionTypes();
boolean located = false;
for (int i = 0; i < exceptionTypeArray.length; i++) {
Class<?> exceptionType = exceptionTypeArray[i];
if (RemotingException.class.isAssignableFrom(exceptionType)) {
located = true;
break;
}
}
if (located) {
throw new FatalBeanException(String.format(
"The method(%s) shouldn't be declared to throw a remote exception: org.bytesoft.compensable.RemotingException!",
method));
}
}
private void validateTransactionalPropagation(Method method, Class<?> clazz) throws IllegalStateException {
Transactional transactional = method.getAnnotation(Transactional.class);
if (transactional == null) {
Class<?> declaringClass = method.getDeclaringClass();
transactional = declaringClass.getAnnotation(Transactional.class);
}
if (transactional == null) {
throw new IllegalStateException(String.format("Method(%s) must be specificed a Transactional annotation!", method));
}
Propagation propagation = transactional.propagation();
if (Propagation.REQUIRED.equals(propagation) == false //
&& Propagation.MANDATORY.equals(propagation) == false //
&& Propagation.REQUIRES_NEW.equals(propagation) == false) {
throw new IllegalStateException(
String.format("Method(%s) not support propagation level: %s!", method, propagation.name()));
}
}
private void validateTransactionalRollbackFor(Method method, Class<?> clazz, String beanName) throws IllegalStateException {
Transactional transactional = method.getAnnotation(Transactional.class);
if (transactional == null) {
Class<?> declaringClass = method.getDeclaringClass();
transactional = declaringClass.getAnnotation(Transactional.class);
}
if (transactional == null) {
throw new IllegalStateException(String.format("Method(%s) must be specificed a Transactional annotation!", method));
}
String[] rollbackForClassNameArray = transactional.rollbackForClassName();
if (rollbackForClassNameArray != null && rollbackForClassNameArray.length > 0) {
throw new IllegalStateException(String.format(
"The transactional annotation on the confirm/cancel class does not support the property rollbackForClassName yet(beanId= %s)!",
beanName));
}
Class<?>[] rollErrorArray = transactional.rollbackFor();
Class<?>[] errorTypeArray = method.getExceptionTypes();
for (int j = 0; errorTypeArray != null && j < errorTypeArray.length; j++) {
Class<?> errorType = errorTypeArray[j];
if (RuntimeException.class.isAssignableFrom(errorType)) {
continue;
}
boolean matched = false;
for (int k = 0; rollErrorArray != null && k < rollErrorArray.length; k++) {
Class<?> rollbackError = rollErrorArray[k];
if (rollbackError.isAssignableFrom(errorType)) {
matched = true;
break;
}
}
if (matched == false) {
throw new IllegalStateException(
String.format("The value of Transactional.rollbackFor annotated on method(%s) must includes %s!",
method, errorType.getName()));
}
}
}
}