View Javadoc
1   /*
2    * The Kuali Financial System, a comprehensive financial management system for higher education.
3    * 
4    * Copyright 2005-2014 The Kuali Foundation
5    * 
6    * This program is free software: you can redistribute it and/or modify
7    * it under the terms of the GNU Affero General Public License as
8    * published by the Free Software Foundation, either version 3 of the
9    * License, or (at your option) any later version.
10   * 
11   * This program is distributed in the hope that it will be useful,
12   * but WITHOUT ANY WARRANTY; without even the implied warranty of
13   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14   * GNU Affero General Public License for more details.
15   * 
16   * You should have received a copy of the GNU Affero General Public License
17   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
18   */
19  package org.kuali.kfs.sys.context;
20  
21  import java.lang.reflect.Field;
22  import java.sql.Connection;
23  import java.sql.ResultSet;
24  import java.sql.Statement;
25  import java.util.ArrayList;
26  import java.util.HashMap;
27  import java.util.HashSet;
28  import java.util.List;
29  import java.util.Map;
30  import java.util.Set;
31  import java.util.TreeSet;
32  
33  import javax.sql.DataSource;
34  
35  import org.apache.commons.lang.StringUtils;
36  import org.apache.log4j.Logger;
37  import org.kuali.kfs.sys.ConfigureContext;
38  import org.kuali.kfs.sys.document.datadictionary.FinancialSystemMaintenanceDocumentEntry;
39  import org.kuali.kfs.sys.suite.AnnotationTestSuite;
40  import org.kuali.kfs.sys.suite.PreCommitSuite;
41  import org.kuali.rice.core.api.mo.common.active.MutableInactivatable;
42  import org.kuali.rice.kns.datadictionary.BusinessObjectEntry;
43  import org.kuali.rice.kns.datadictionary.InquiryDefinition;
44  import org.kuali.rice.kns.datadictionary.InquirySectionDefinition;
45  import org.kuali.rice.kns.datadictionary.LookupDefinition;
46  import org.kuali.rice.kns.datadictionary.MaintainableSectionDefinition;
47  import org.kuali.rice.krad.datadictionary.AttributeDefinition;
48  import org.kuali.rice.krad.datadictionary.DataDictionary;
49  import org.kuali.rice.krad.datadictionary.DocumentEntry;
50  import org.kuali.rice.krad.service.DataDictionaryService;
51  import org.springframework.beans.factory.NoSuchBeanDefinitionException;
52  import org.springframework.beans.factory.config.BeanDefinition;
53  import org.springframework.beans.factory.support.KualiDefaultListableBeanFactory;
54  
55  @AnnotationTestSuite(PreCommitSuite.class)
56  @ConfigureContext
57  public class DataDictionaryConfigurationTest extends KualiTestBase {
58      private static final Logger LOG = Logger.getLogger(DataDictionaryConfigurationTest.class);
59      private DataDictionary dataDictionary;
60  
61      public final static String KFS_PACKAGE_NAME_PREFIX = "org.kuali.kfs";
62      public final static String BUSINESS_OBJECT_PATH_QUALIFIER = "businessobject/datadictionary";
63      public final static String DOCUMENT_PATH_QUALIFIER = "document/datadictionary";
64      public final static String RICE_PACKAGE_NAME_PREFIX = "org.kuali.rice";
65      public final static String INACTIVATEABLE_INTERFACE_CLASS = MutableInactivatable.class.getName();
66      public final static String ACTIVE_FIELD_NAME = "active";
67  
68      public void testAllDataDictionaryDocumentTypesExistInWorkflowDocumentTypeTable() throws Exception {
69          HashSet<String> workflowDocumentTypeNames = new HashSet<String>();
70          DataSource mySource = SpringContext.getBean(DataSource.class);
71          Connection dbCon = null;
72          try {
73  
74              dbCon = mySource.getConnection();
75              Statement dbAsk = dbCon.createStatement();
76              ResultSet dbAnswer = dbAsk.executeQuery("select DOC_TYP_NM from KREW_DOC_TYP_T where CUR_IND = 1");
77              while (dbAnswer.next()) {
78                  String docName = dbAnswer.getString(1);
79                  if (StringUtils.isNotBlank(docName)) {
80                      workflowDocumentTypeNames.add(docName);
81                  }
82              }
83  
84          }
85          catch (Exception e) {
86              throw (e);
87          }
88          // Using HashSet since duplicate objects would otherwise be returned
89          HashSet<DocumentEntry> documentEntries = new HashSet<DocumentEntry>(dataDictionary.getDocumentEntries().values());
90          List<String> ddEntriesWithMissingTypes = new ArrayList<String>();
91          for (DocumentEntry documentEntry : documentEntries) {
92              String name = documentEntry.getDocumentTypeName();
93              String testName = new String(" ");
94              if (documentEntry instanceof FinancialSystemMaintenanceDocumentEntry){
95                  testName=((FinancialSystemMaintenanceDocumentEntry)documentEntry).getBusinessObjectClass().getName();
96              }else{
97                  testName=documentEntry.getDocumentClass().getName();
98              }
99              if (!workflowDocumentTypeNames.contains(name) && !"RiceUserMaintenanceDocument".equals(name) && !testName.contains("rice")) {
100                 ddEntriesWithMissingTypes.add(name);
101             }
102             else {
103                 workflowDocumentTypeNames.remove(name);
104             }
105         }
106 
107         if (workflowDocumentTypeNames.size() > 0) {
108             try{
109                 //If documents are parent docs, then they aren't superfluous.
110                 String queryString = "select distinct doc_typ_nm from KREW_DOC_TYP_T"
111                     +" where doc_typ_id in (select parnt_id from KREW_DOC_TYP_T"
112                     +" where actv_ind = 1"
113                     +" and cur_ind = 1)";
114                 Statement dbAsk = dbCon.createStatement();
115                 ResultSet dbAnswer = dbAsk.executeQuery(queryString);
116                 while (dbAnswer.next()) {
117                     String docName = dbAnswer.getString(1);
118                     if (StringUtils.isNotBlank(docName)) {
119                         workflowDocumentTypeNames.remove(docName);
120                     }
121                 }
122             }catch (Exception e){
123                 throw (e);
124             }
125 
126         System.err.print("superfluousTypesDefinedInWorkflowDatabase: " + workflowDocumentTypeNames);
127     }
128     assertEquals("documentTypesNotDefinedInWorkflowDatabase: " + ddEntriesWithMissingTypes, 0, ddEntriesWithMissingTypes.size());
129 }
130 
131     private final static List<String> INACTIVATEABLE_LOOKUP_IGNORE_CLASSES = new ArrayList<String>();
132     static {
133         // org.kuali.kfs.coa.businessobject.Account is excepted from testActiveFieldExistInLookupAndResultSection because it uses the active-derived Closed? indicator instead (KFSMI-1393)
134         INACTIVATEABLE_LOOKUP_IGNORE_CLASSES.add( "org.kuali.kfs.coa.businessobject.Account" );
135         INACTIVATEABLE_LOOKUP_IGNORE_CLASSES.add( "org.kuali.kfs.module.bc.businessobject.BudgetConstructionPosition" );
136         INACTIVATEABLE_LOOKUP_IGNORE_CLASSES.add( "org.kuali.kfs.module.bc.businessobject.PendingBudgetConstructionAppointmentFunding" );
137     }
138     private static final List<String> INACTIVATEABLE_LOOKUP_IGNORE_PACKAGES = new ArrayList<String>();
139     static {
140         INACTIVATEABLE_LOOKUP_IGNORE_PACKAGES.add( "org.kuali.kfs.pdp.businessobject" );
141         INACTIVATEABLE_LOOKUP_IGNORE_PACKAGES.add( "org.kuali.kfs.module.external.kc.businessobject" );
142     }
143 
144     public void testActiveFieldExistInLookupAndResultSection() throws Exception{
145         List<Class<?>> noActiveFieldClassList = new ArrayList<Class<?>>();
146         List<Class<?>> notImplementInactivatableList = new ArrayList<Class<?>>();
147         List<Class<?>> defaultValueWrongList = new ArrayList<Class<?>>();
148 
149         for(org.kuali.rice.krad.datadictionary.BusinessObjectEntry kradBusinessObjectEntry:dataDictionary.getBusinessObjectEntries().values()){
150             BusinessObjectEntry businessObjectEntry = (BusinessObjectEntry) kradBusinessObjectEntry;
151             if ( !businessObjectEntry.getBusinessObjectClass().getName().startsWith(RICE_PACKAGE_NAME_PREFIX)
152                     && !INACTIVATEABLE_LOOKUP_IGNORE_CLASSES.contains(businessObjectEntry.getBusinessObjectClass().getName())
153                     && !INACTIVATEABLE_LOOKUP_IGNORE_PACKAGES.contains(businessObjectEntry.getBusinessObjectClass().getPackage().getName()) ) {
154                 try {
155                     LookupDefinition lookupDefinition = businessObjectEntry.getLookupDefinition();
156                     // Class implements MutableInactivatable but active field not used on Lookup.
157                     if(Class.forName(INACTIVATEABLE_INTERFACE_CLASS).isAssignableFrom(businessObjectEntry.getBusinessObjectClass())) {
158                         if(lookupDefinition != null && !(lookupDefinition.getLookupFieldNames().contains(ACTIVE_FIELD_NAME) && lookupDefinition.getResultFieldNames().contains(ACTIVE_FIELD_NAME))){
159                             noActiveFieldClassList.add(businessObjectEntry.getBusinessObjectClass());
160                             if ( lookupDefinition.getLookupField(ACTIVE_FIELD_NAME) != null ) {
161                                 //Default must be 'Y' not 'true'
162                                 if (!StringUtils.equals(lookupDefinition.getLookupField(ACTIVE_FIELD_NAME).getDefaultValue(), "Y")) {
163                                     defaultValueWrongList.add(businessObjectEntry.getBusinessObjectClass());
164                                 }
165                             }
166                         }
167                     }else{
168                         // Lookup show active flag, but class does not implement MutableInactivatable.
169                         if(lookupDefinition != null && (lookupDefinition.getLookupFieldNames().contains(ACTIVE_FIELD_NAME) || lookupDefinition.getResultFieldNames().contains(ACTIVE_FIELD_NAME))){
170                             notImplementInactivatableList.add(businessObjectEntry.getBusinessObjectClass());
171                         }
172                     }
173                 }
174                 catch (ClassNotFoundException e) {
175                     throw(e);
176                 }
177             }
178         }
179         String errorString = "";
180         if (noActiveFieldClassList.size()!=0) errorString=errorString+"Missing Active Field: "+formatErrorStringGroupByModule(noActiveFieldClassList);
181         if (notImplementInactivatableList.size()!=0) errorString=errorString+"Inactivatable not implemented: "+formatErrorStringGroupByModule(notImplementInactivatableList);
182         if (defaultValueWrongList.size()!=0) errorString=errorString+"Wrong default value: "+formatErrorStringGroupByModule(defaultValueWrongList);
183         assertEquals(errorString, 0, noActiveFieldClassList.size()+notImplementInactivatableList.size()+defaultValueWrongList.size());
184     }
185     private String formatErrorStringGroupByModule(List<Class<?>> failedList){
186         Map<String,Set<String>> listMap = new HashMap<String, Set<String>>();
187         String module = null;
188         String itemName = null;
189         for (Class<?> item :failedList){
190             itemName=item.getName();
191             module = itemName.substring(0, itemName.lastIndexOf('.'));
192             if (!listMap.keySet().contains(module)){
193                 listMap.put(module, new HashSet<String>());
194             }
195             listMap.get(module).add(itemName.substring(itemName.lastIndexOf('.')+1));
196         }
197         String tempString="";
198         for (String moduleName : listMap.keySet()){
199             tempString = tempString+"Module :"+moduleName+"\n";
200             for (String errorClass : (Set<String>)listMap.get(moduleName)){
201                 tempString = tempString + "     "+errorClass+"\n";
202             }
203         }
204         return "\n"+tempString;
205     }
206 
207     public void testAllBusinessObjectsHaveObjectLabel() throws Exception {
208         List<Class<?>> noObjectLabelClassList = new ArrayList<Class<?>>();
209         for(org.kuali.rice.krad.datadictionary.BusinessObjectEntry kradBusinessObjectEntry:dataDictionary.getBusinessObjectEntries().values()){
210             BusinessObjectEntry businessObjectEntry = (BusinessObjectEntry) kradBusinessObjectEntry;
211             if (StringUtils.isBlank(businessObjectEntry.getObjectLabel())) {
212                 noObjectLabelClassList.add(businessObjectEntry.getBusinessObjectClass());
213             }
214         }
215         assertEquals(noObjectLabelClassList.toString(), 0, noObjectLabelClassList.size());
216     }
217 
218     public void testAllParentBeansAreAbstract() throws Exception {
219         Field f = dataDictionary.getClass().getDeclaredField("ddBeans");
220         f.setAccessible(true);
221         KualiDefaultListableBeanFactory ddBeans = (KualiDefaultListableBeanFactory)f.get(dataDictionary);
222         List<String> failingBeanNames = new ArrayList<String>();
223         for ( String beanName : ddBeans.getBeanDefinitionNames() ) {
224             BeanDefinition beanDef = ddBeans.getMergedBeanDefinition(beanName);
225             String beanClass = beanDef.getBeanClassName();
226             // skip Rice classes
227             if ( beanClass != null && beanClass.startsWith("org.kuali.rice") ) {
228                 continue;
229             }
230             if ( (beanName.endsWith("-parentBean") || beanName.endsWith("-baseBean"))
231                     && !beanDef.isAbstract() ) {
232                 failingBeanNames.add(beanName + " : " + beanDef.getResourceDescription()+"\n");
233             }
234         }
235         assertEquals( "The following parent beans are not defined as abstract:\n" + failingBeanNames, 0, failingBeanNames.size() );
236     }
237 
238     public void testBusinessObjectEntriesShouldHaveParentBeans() throws Exception {
239         somethingShouldHaveParentBeans(BusinessObjectEntry.class, new ArrayList<String>());
240     }
241 
242     public void testDocumentEntriesShouldHaveParentBeans() throws Exception {
243         somethingShouldHaveParentBeans(DocumentEntry.class, new ArrayList<String>());
244     }
245 
246     protected static final List<String> EXCLUDED_ATTRIBUTE_DEFINITIONS = new ArrayList<String>();
247     static {
248         EXCLUDED_ATTRIBUTE_DEFINITIONS.add( "Country-" );
249         EXCLUDED_ATTRIBUTE_DEFINITIONS.add( "County-" );
250         EXCLUDED_ATTRIBUTE_DEFINITIONS.add( "State-" );
251         EXCLUDED_ATTRIBUTE_DEFINITIONS.add( "PostalCode-" );
252         EXCLUDED_ATTRIBUTE_DEFINITIONS.add( "PersonImpl-" );
253         EXCLUDED_ATTRIBUTE_DEFINITIONS.add( "RoleMemberBo-" );
254         EXCLUDED_ATTRIBUTE_DEFINITIONS.add( "KimAttributes-" );
255         EXCLUDED_ATTRIBUTE_DEFINITIONS.add( "KimDocRoleMember-" );
256         EXCLUDED_ATTRIBUTE_DEFINITIONS.add( "DocRoleMember-" );
257         EXCLUDED_ATTRIBUTE_DEFINITIONS.add( "Responsibility-" );
258         EXCLUDED_ATTRIBUTE_DEFINITIONS.add( "PermissionBo-" );
259         EXCLUDED_ATTRIBUTE_DEFINITIONS.add( "PermissionImpl-" );
260         EXCLUDED_ATTRIBUTE_DEFINITIONS.add( "UberPermission-" );
261         EXCLUDED_ATTRIBUTE_DEFINITIONS.add( "ReviewResponsibility-" );
262         EXCLUDED_ATTRIBUTE_DEFINITIONS.add( "ResponsibilityImpl-" );
263         EXCLUDED_ATTRIBUTE_DEFINITIONS.add( "UberPermissionBo-" );
264         EXCLUDED_ATTRIBUTE_DEFINITIONS.add( "RuleTemplateAttribute-" );
265         EXCLUDED_ATTRIBUTE_DEFINITIONS.add( "-versionNumber" );
266     }
267 
268     public void testAttributeDefinitionsShouldHaveParentBeans() throws Exception {
269         somethingShouldHaveParentBeans(AttributeDefinition.class, EXCLUDED_ATTRIBUTE_DEFINITIONS);
270     }
271 
272     public void testMaintenanceSectionsShouldHaveParentBeans() throws Exception {
273         somethingShouldHaveParentBeans(MaintainableSectionDefinition.class, new ArrayList<String>());
274     }
275 
276     public void testInquirySectionsShouldHaveParentBeans() throws Exception {
277         somethingShouldHaveParentBeans(InquirySectionDefinition.class, new ArrayList<String>());
278     }
279 
280     public void testLookupDefinitionsShouldHaveParentBeans() throws Exception {
281         somethingShouldHaveParentBeans(LookupDefinition.class, new ArrayList<String>());
282     }
283 
284     public void testInquiryDefinitionsShouldHaveParentBeans() throws Exception {
285         somethingShouldHaveParentBeans(InquiryDefinition.class, new ArrayList<String>() );
286     }
287 
288     protected boolean doesBeanNameMatchList( String beanName, List<String> exclusions ) {
289         for ( String excl : exclusions ) {
290             if ( beanName.contains(excl) ) {
291                 return true;
292             }
293         }
294         return false;
295     }
296 
297     protected void somethingShouldHaveParentBeans( Class<?> baseClass, List<String> exclusions ) throws Exception {
298         Field f = dataDictionary.getClass().getDeclaredField("ddBeans");
299         f.setAccessible(true);
300         KualiDefaultListableBeanFactory ddBeans = (KualiDefaultListableBeanFactory)f.get(dataDictionary);
301         List<String> failingBeanNames = new ArrayList<String>();
302 
303         for ( String beanName : ddBeans.getBeanDefinitionNames() ) {
304             if ( doesBeanNameMatchList(beanName, exclusions)) {
305                 continue ;
306             }
307             BeanDefinition beanDef = ddBeans.getMergedBeanDefinition(beanName);
308             String beanClass = beanDef.getBeanClassName();
309             if ( beanClass == null ) {
310                 System.err.println( "ERROR: Bean " + beanName + " has a null class." );
311             }
312             if ( !beanDef.isAbstract()
313                     && beanClass != null
314                     && baseClass.isAssignableFrom(Class.forName(beanClass) ) ) {
315                 try {
316                     BeanDefinition parentBean = ddBeans.getBeanDefinition(beanName + "-parentBean");
317                 } catch ( NoSuchBeanDefinitionException ex ) {
318                     failingBeanNames.add(beanName + " : " + beanDef.getResourceDescription() +"\n");
319                 }
320             }
321         }
322         assertEquals( "The following " + baseClass.getSimpleName() + " beans do not have \"-parentBean\"s:\n" + failingBeanNames, 0, failingBeanNames.size() );
323     }
324 
325     private void reportErrorAttribute(Map<String, Set<String>> reports, AttributeDefinition attributeDefinition, String boClassName) {
326         Set<String> attributeSet = reports.containsKey(boClassName) ? reports.get(boClassName) : new TreeSet<String>();
327         attributeSet.add(attributeDefinition.getName());
328         reports.put(boClassName, attributeSet);
329     }
330 
331     private StringBuilder convertReportsAsText(Map<String, Set<String>> reports) {
332         StringBuilder reportsAsText = new StringBuilder();
333         for(String key : new TreeSet<String>(reports.keySet())) {
334             reportsAsText.append(key + "\n");
335             for(String value : reports.get(key)) {
336                 reportsAsText.append("\t--").append(value).append("\n");
337             }
338         }
339         return reportsAsText;
340     }
341 
342     private void printReport(Map<String, Set<String>> reports) {
343         StringBuilder reportsAsText = convertReportsAsText(reports);
344         System.out.println(reportsAsText);
345         LOG.info("\n" + reportsAsText);
346     }
347 
348     @Override
349     protected void setUp() throws Exception {
350         super.setUp();
351         dataDictionary = SpringContext.getBean(DataDictionaryService.class).getDataDictionary();
352     }
353 }