/* * The Kuali Financial System, a comprehensive financial management system for higher education. * * Copyright 2005-2014 The Kuali Foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.kuali.kfs.sys.context; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.TreeSet; import org.kuali.kfs.sys.ConfigureContext; import org.kuali.kfs.sys.suite.AnnotationTestSuite; import org.kuali.kfs.sys.suite.PreCommitSuite; import org.springframework.aop.support.AopUtils; import org.springframework.beans.factory.BeanIsAbstractException; @AnnotationTestSuite(PreCommitSuite.class) @ConfigureContext /** * This test checks that services are properly annotated as either Transactional * or @link NonTransactional. The first test is a superset of the subsequent * test. The first test will always fail if one of the subsequent test fails. * Acceptable annotations are either at the class level or on each of the public * methods, but not both. */ public class TransactionalAnnotationTest extends KualiTestBase { private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(TransactionalAnnotationTest.class); Map<Class<? extends Object>, Boolean> seenClasses = new HashMap<Class<? extends Object>, Boolean>(); List<String> excludedClasses = new ArrayList<String>(); Map<String, String> doubleAnnotatedTransactionalServices; Map<String, String> nonAnnotatedTransactionalServices; Map<String, Class<? extends Object>> incorrectlyAnnotatedTransactionalServices; { excludedClasses.add( "org.kuali.kfs.coa.service.impl.SubFundGroupServiceImpl" ); excludedClasses.add( "org.kuali.kfs.module.purap.service.impl.SensitiveDataServiceImpl" ); } @Override public void setUp() throws Exception { super.setUp(); } public void testTransactionAnnotations() { getNonAnnotatedTransactionalServices(); for (String beanName : new TreeSet<String>(incorrectlyAnnotatedTransactionalServices.keySet())) { LOG.error(String.format("Service Bean improperly annotated: %s <%s>\n", beanName, incorrectlyAnnotatedTransactionalServices.get(beanName).getName())); } int count = incorrectlyAnnotatedTransactionalServices.size(); StringBuffer failureMessage = new StringBuffer("Transaction support for ").append(count).append(count == 1 ? " Service" : " Services").append(" improperly annotated: "); for (String serviceName : incorrectlyAnnotatedTransactionalServices.keySet()) { failureMessage.append("\t").append(serviceName).append(": ").append(incorrectlyAnnotatedTransactionalServices.get(serviceName)); } assertTrue(failureMessage.toString(), incorrectlyAnnotatedTransactionalServices.isEmpty()); } public void testNoTransactionAnnotations() { getNonAnnotatedTransactionalServices(); for (String beanName : new TreeSet<String>(nonAnnotatedTransactionalServices.keySet())) { LOG.error(String.format("Service Bean not annotated: %s <%s>\n", beanName, nonAnnotatedTransactionalServices.get(beanName))); } int count = nonAnnotatedTransactionalServices.size(); StringBuffer failureMessage = new StringBuffer("Transaction support for ").append(count).append(count == 1 ? " Service" : " Services").append(" not annotated: "); for (String serviceName : nonAnnotatedTransactionalServices.keySet()) { failureMessage.append("\t").append(serviceName).append(": ").append(nonAnnotatedTransactionalServices.get(serviceName)); } assertTrue(failureMessage.toString(), nonAnnotatedTransactionalServices.isEmpty()); } public void testDoubleTransactionAnnotations() { getNonAnnotatedTransactionalServices(); for (String beanName : new TreeSet<String>(doubleAnnotatedTransactionalServices.keySet())) { LOG.error(String.format("Service Bean improperly annotated: %s <%s>\n", beanName, doubleAnnotatedTransactionalServices.get(beanName))); } int count = doubleAnnotatedTransactionalServices.size(); StringBuffer failureMessage = new StringBuffer("Transaction support for ").append(count).append(count == 1 ? " Service" : " Services").append(" double annotated: "); for (String serviceName : doubleAnnotatedTransactionalServices.keySet()) { failureMessage.append("\t").append(serviceName).append(": ").append(doubleAnnotatedTransactionalServices.get(serviceName)); } assertTrue(failureMessage.toString(), doubleAnnotatedTransactionalServices.isEmpty()); } @SuppressWarnings("deprecation") public void getNonAnnotatedTransactionalServices() { /* We only want to run getNonAnnotatedTransactionalSerivces once. * The tests actually just read the Maps that are generated here. */ if (incorrectlyAnnotatedTransactionalServices != null) { return; } incorrectlyAnnotatedTransactionalServices = new HashMap<String, Class<? extends Object>>(); nonAnnotatedTransactionalServices = new HashMap<String, String>(); doubleAnnotatedTransactionalServices = new HashMap<String, String>(); String[] beanNames = SpringContext.getBeanNames(); for (String beanName : beanNames) { if ( beanName.endsWith( "-parentBean" ) ) { continue; } Object bean = null; try { bean = SpringContext.getBean(beanName); } catch ( BeanIsAbstractException ex ) { // do nothing, ignore } catch (Exception e) { LOG.warn("Caught exception while trying to obtain service: " + beanName); LOG.warn(e.getClass().getName() + " : " + e.getMessage(), e ); } if (bean != null) { Class<? extends Object> beanClass = bean.getClass(); if (beanClass.getName().matches(".*\\$Proxy.*")) { beanClass = AopUtils.getTargetClass(bean); } if (beanClass.getName().startsWith("org.kuali") && !Modifier.isAbstract(beanClass.getModifiers()) && !beanClass.getName().endsWith("DaoOjb") && !beanClass.getName().endsWith("DaoJdbc") && !beanClass.getName().endsWith("Factory") && !beanClass.getName().contains("Lookupable") && !isClassAnnotated(beanName, beanClass)) { incorrectlyAnnotatedTransactionalServices.put(beanName, beanClass); } } } return; } private boolean isExcludedClass( Class<? extends Object> beanClass ) { return beanClass.getName().startsWith("org.kuali.rice") || excludedClasses.contains(beanClass.getName()); } private boolean isClassAnnotated(String beanName, Class<? extends Object> beanClass) { boolean hasClassAnnotation = false; if (shouldHaveTransaction(beanClass)&& !isExcludedClass(beanClass)){ if (beanClass.getAnnotation(org.springframework.transaction.annotation.Transactional.class) != null) { hasClassAnnotation = true; } if (beanClass.getAnnotation(org.kuali.kfs.sys.service.NonTransactional.class) != null){ hasClassAnnotation = true; } boolean hasMethodAnnotation; for( Method beanMethod : beanClass.getDeclaredMethods()){ if (Modifier.isPublic(beanMethod.getModifiers())){ hasMethodAnnotation = false; if (beanMethod.getAnnotation(org.springframework.transaction.annotation.Transactional.class) != null) { hasMethodAnnotation = true; } if (beanMethod.getAnnotation(org.kuali.kfs.sys.service.NonTransactional.class) != null) { hasMethodAnnotation = true; } if (hasMethodAnnotation == false && hasClassAnnotation == false) { nonAnnotatedTransactionalServices.put(beanName, beanClass.getName() + "." + beanMethod.getName()); return false; } if (hasMethodAnnotation == true && hasClassAnnotation == true){ doubleAnnotatedTransactionalServices.put(beanName, beanClass.getName() + "." + beanMethod.getName()); return false; } } } return true; } return true; } /* * Recursively seek evidence that a Transaction is necessary by examining the fields of the given class and recursively * investigating its superclass. */ private boolean shouldHaveTransaction(Class<? extends Object> beanClass) { Boolean result = seenClasses.get(beanClass); if (result != null) { return result; } if (seenClasses.containsKey(beanClass)) { return false; } seenClasses.put(beanClass, null); // placeholder to avoid recursive problems result = Boolean.FALSE; for (Field field : beanClass.getDeclaredFields()) { String name = field.getType().getName(); String fieldTypeName = field.getType().getName(); if (fieldTypeName.startsWith("org.apache.ojb")) { if (!fieldTypeName.equals("org.apache.ojb.broker.metadata.DescriptorRepository")) { result = Boolean.TRUE; } } if (name.startsWith("org.kuali")) { if (name.contains("Dao")) { result = Boolean.TRUE; } if (result == false) { result = shouldHaveTransaction(field.getType()); } } } if (result == false) { if (beanClass.getSuperclass() != null && beanClass.getSuperclass().getName().startsWith("org.kuali")) { result = shouldHaveTransaction(beanClass.getSuperclass()); } } seenClasses.put(beanClass, result); return result; } }