001/* 002 * Copyright 2010 The Kuali Foundation. 003 * 004 * Licensed under the Educational Community License, Version 1.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.opensource.org/licenses/ecl1.php 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.kuali.ole.coa.service.impl; 017 018import java.util.ArrayList; 019import java.util.Collections; 020import java.util.HashMap; 021import java.util.HashSet; 022import java.util.Iterator; 023import java.util.List; 024import java.util.Map; 025import java.util.Set; 026 027import org.apache.commons.lang.StringUtils; 028import org.kuali.ole.coa.businessobject.Account; 029import org.kuali.ole.coa.service.AccountPersistenceStructureService; 030import org.kuali.ole.sys.OLEPropertyConstants; 031import org.kuali.rice.kns.service.MaintenanceDocumentDictionaryService; 032import org.kuali.rice.krad.bo.PersistableBusinessObject; 033import org.kuali.rice.krad.service.impl.PersistenceStructureServiceImpl; 034import org.kuali.rice.krad.util.KRADConstants; 035import org.springframework.beans.factory.InitializingBean; 036 037public class AccountPersistenceStructureServiceImpl extends PersistenceStructureServiceImpl implements AccountPersistenceStructureService, InitializingBean { 038 039 protected List<AccountReferencePersistenceExemption> accountReferencePersistenceExemptions; 040 protected Map<Class<?>, List<AccountReferencePersistenceExemption>> accountReferencePersistenceExemptionsMap; 041 042 /* 043 * The following list is commented out as it's not used in code anymore, but still can server as a reference for testing. 044 * The list causes problems when referencing AwardAccount.class, a class in optional module; 045 * also it's not a good practice to hard-code the list as it may have to be expanded when new sub/classes are added. 046 * Instead of using "if (ACCOUNT_CLASSES.contains(clazz))" to judge whether whether a class is account-related, 047 * we now judge by whether the PKs of the class contain chartOfAccountsCode-accountNumber. 048 * 049 * 050 // List of account-related BO classes (and all their subclasses) which have chartOfAccountsCode and accountNumber as (part of) the primary keys, 051 // i.e. the complete list of all possible referenced BO classes with chart code and account number as (part of) the foreign keys. 052 protected static final HashSet<Class<? extends BusinessObject>> ACCOUNT_CLASSES = new HashSet<Class<? extends BusinessObject>>(); 053 static { 054 ACCOUNT_CLASSES.add(Account.class); 055 ACCOUNT_CLASSES.add(SubAccount.class); 056 ACCOUNT_CLASSES.add(A21SubAccount.class); 057 ACCOUNT_CLASSES.add(AwardAccount.class); // this class can't be referenced by core code 058 ACCOUNT_CLASSES.add(IndirectCostRecoveryExclusionAccount.class); 059 ACCOUNT_CLASSES.add(PriorYearAccount.class); 060 ACCOUNT_CLASSES.add(AccountDelegate.class); 061 ACCOUNT_CLASSES.add(AccountDescription.class); 062 ACCOUNT_CLASSES.add(AccountGlobalDetail.class); 063 ACCOUNT_CLASSES.add(AccountGuideline.class); 064 ACCOUNT_CLASSES.add(SubObjectCode.class); 065 ACCOUNT_CLASSES.add(SubObjectCodeCurrent.class); 066 } 067 */ 068 069 public boolean isAccountRelatedClass(Class clazz) { 070 List<String> pks = listPrimaryKeyFieldNames(clazz); 071 072 if (pks.contains(OLEPropertyConstants.CHART_OF_ACCOUNTS_CODE) && pks.contains(OLEPropertyConstants.ACCOUNT_NUMBER )) { 073 return true; 074 } 075 else { 076 return false; 077 } 078 } 079 080 private MaintenanceDocumentDictionaryService maintenanceDocumentDictionaryService; 081 082 public void setMaintenanceDocumentDictionaryService(MaintenanceDocumentDictionaryService maintenanceDocumentDictionaryService) { 083 this.maintenanceDocumentDictionaryService = maintenanceDocumentDictionaryService; 084 } 085 086 @SuppressWarnings("rawtypes") 087 public Map<String, Class> listCollectionAccountFields(PersistableBusinessObject bo) { 088 Map<String, Class> accountFields = new HashMap<String, Class>(); 089 Iterator<Map.Entry<String, Class>> collObjs = listCollectionObjectTypes(bo).entrySet().iterator(); 090 091 while (collObjs.hasNext()) { 092 Map.Entry<String, Class> entry = (Map.Entry<String, Class>)collObjs.next(); 093 String accountCollName = entry.getKey(); 094 Class accountCollType = entry.getValue(); 095 096 // if the reference object is of Account or Account-involved BO class (including all subclasses) 097 if (isAccountRelatedClass(accountCollType)) { 098 // exclude non-maintainable account collection 099 String docTypeName = maintenanceDocumentDictionaryService.getDocumentTypeName(bo.getClass()); 100 if (maintenanceDocumentDictionaryService.getMaintainableCollection(docTypeName, accountCollName) == null) 101 continue; 102 103 // otherwise include the account field 104 accountFields.put(accountCollName, accountCollType); 105 } 106 } 107 108 return accountFields; 109 } 110 111 @SuppressWarnings("rawtypes") 112 public Set<String> listCollectionChartOfAccountsCodeNames(PersistableBusinessObject bo) { 113 Set<String> coaCodeNames = new HashSet<String>(); 114 String docTypeName = maintenanceDocumentDictionaryService.getDocumentTypeName(bo.getClass()); 115 Iterator<Map.Entry<String, Class>> collObjs = listCollectionObjectTypes(bo).entrySet().iterator(); 116 117 while (collObjs.hasNext()) { 118 Map.Entry<String, Class> entry = (Map.Entry<String, Class>)collObjs.next(); 119 String accountCollName = entry.getKey(); 120 Class accountCollType = entry.getValue(); 121 122 // if the reference object is of Account or Account-involved BO class (including all subclasses) 123 if (isAccountRelatedClass(accountCollType)) { 124 // exclude non-maintainable account collection 125 if (maintenanceDocumentDictionaryService.getMaintainableCollection(docTypeName, accountCollName) == null) 126 continue; 127 128 // otherwise include the account field 129 String coaCodeName = KRADConstants.ADD_PREFIX + "." + accountCollName + "." + OLEPropertyConstants.CHART_OF_ACCOUNTS_CODE; 130 coaCodeNames.add(coaCodeName); 131 } 132 } 133 134 return coaCodeNames; 135 } 136 137 @SuppressWarnings("rawtypes") 138 public Map<String, Class> listReferenceAccountFields(PersistableBusinessObject bo) { 139 Map<String, Class> accountFields = new HashMap<String, Class>(); 140 Iterator<Map.Entry<String, Class>> refObjs = listReferenceObjectFields(bo).entrySet().iterator(); 141 142 while (refObjs.hasNext()) { 143 Map.Entry<String, Class> entry = (Map.Entry<String, Class>)refObjs.next(); 144 String accountName = entry.getKey(); 145 Class accountType = entry.getValue(); 146 147 // if the reference object is of Account or Account-involved BO class (including all subclasses) 148 if (isAccountRelatedClass(accountType)) { 149 String coaCodeName = getForeignKeyFieldName(bo.getClass(), accountName, OLEPropertyConstants.CHART_OF_ACCOUNTS_CODE); 150 String acctNumName = getForeignKeyFieldName(bo.getClass(), accountName, OLEPropertyConstants.ACCOUNT_NUMBER); 151 152 // exclude the case when chartOfAccountsCode-accountNumber don't exist as foreign keys in the BO: 153 // for ex, in SubAccount, a21SubAccount is a reference object but its PKs don't exist as FKs in SubAccount; 154 // rather, A21SubAccount has a nested reference account - costShareAccount, 155 // whose PKs exists in A21SubAccount as FKs, and are used in SubAccount maint doc as nested reference; 156 // special treatment outside this method is needed for this case 157 if (StringUtils.isEmpty(coaCodeName) || StringUtils.isEmpty(acctNumName)) 158 continue; 159 160 // in general we do want to have chartOfAccountsCode fields readOnly/auto-populated even when they are part of PKs, 161 // (such as in SubAccount), as the associated account shall only be chosen from existing accounts; 162 // however, when the BO is Account itself, we don't want to make the PK chartOfAccountsCode field readOnly, 163 // as it shall be editable when a new Account is being created; so we shall exclude such case 164 List<String> pks = listPrimaryKeyFieldNames(bo.getClass()); 165 if (bo instanceof Account && pks.contains(coaCodeName) && pks.contains(acctNumName )) 166 continue; 167 168 // exclude non-maintainable account field 169 String docTypeName = maintenanceDocumentDictionaryService.getDocumentTypeName(bo.getClass()); 170 if (maintenanceDocumentDictionaryService.getMaintainableField(docTypeName, coaCodeName) == null || 171 maintenanceDocumentDictionaryService.getMaintainableField(docTypeName, acctNumName) == null) 172 continue; 173 174 // otherwise include the account field 175 accountFields.put(accountName, accountType); 176 } 177 } 178 179 return accountFields; 180 } 181 182 @SuppressWarnings("rawtypes") 183 public Map<String, String> listChartCodeAccountNumberPairs(PersistableBusinessObject bo) { 184 Map<String, String> chartAccountPairs = new HashMap<String, String>(); 185 Iterator<Map.Entry<String, Class>> refObjs = listReferenceObjectFields(bo).entrySet().iterator(); 186 187 while (refObjs.hasNext()) { 188 Map.Entry<String, Class> entry = (Map.Entry<String, Class>)refObjs.next(); 189 String accountName = entry.getKey(); 190 Class accountType = entry.getValue(); 191 192 // if the reference object is of Account or Account-involved BO class (including all subclasses) 193 if (isAccountRelatedClass(accountType)) { 194 String coaCodeName = getForeignKeyFieldName(bo.getClass(), accountName, OLEPropertyConstants.CHART_OF_ACCOUNTS_CODE); 195 String acctNumName = getForeignKeyFieldName(bo.getClass(), accountName, OLEPropertyConstants.ACCOUNT_NUMBER); 196 197 // exclude the case when chartOfAccountsCode-accountNumber don't exist as foreign keys in the BO: 198 // for ex, in SubAccount, a21SubAccount is a reference object but its PKs don't exist as FKs in SubAccount; 199 // rather, A21SubAccount has a nested reference account - costShareAccount, 200 // whose PKs exists in A21SubAccount as FKs, and are used in SubAccount maint doc as nested reference 201 // special treatment outside this method is needed for this case 202 if (StringUtils.isEmpty(coaCodeName) || StringUtils.isEmpty(acctNumName)) 203 continue; 204 205 // in general we do want to have chartOfAccountsCode fields readOnly/auto-populated even when they are part of PKs, 206 // (such as in SubAccount), as the associated account shall only be chosen from existing accounts; 207 // however, when the BO is Account itself, we don't want to make the PK chartOfAccountsCode field readOnly, 208 // as it shall be editable when a new Account is being created; so we shall exclude such case 209 List<String> pks = listPrimaryKeyFieldNames(bo.getClass()); 210 if (bo instanceof Account && pks.contains(coaCodeName) && pks.contains(acctNumName )) 211 continue; 212 213 // if this relationship is specifically exempted then exempt it 214 if (isExemptedFromAccountsCannotCrossChartsRules(bo.getClass(), coaCodeName, acctNumName)) { 215 continue; 216 } 217 218 // exclude non-maintainable account field 219 String docTypeName = maintenanceDocumentDictionaryService.getDocumentTypeName(bo.getClass()); 220 if (maintenanceDocumentDictionaryService.getMaintainableField(docTypeName, coaCodeName) == null || 221 maintenanceDocumentDictionaryService.getMaintainableField(docTypeName, acctNumName) == null) 222 continue; 223 224 // otherwise include the account field PKs 225 chartAccountPairs.put(coaCodeName, acctNumName); 226 } 227 } 228 229 return chartAccountPairs; 230 } 231 232 @SuppressWarnings("rawtypes") 233 public Map<String, String> listAccountNumberChartCodePairs(PersistableBusinessObject bo) { 234 Map<String, String> accountChartPairs = new HashMap<String, String>(); 235 Iterator<Map.Entry<String, Class>> refObjs = listReferenceObjectFields(bo).entrySet().iterator(); 236 237 while (refObjs.hasNext()) { 238 Map.Entry<String, Class> entry = (Map.Entry<String, Class>)refObjs.next(); 239 String accountName = entry.getKey(); 240 Class accountType = entry.getValue(); 241 242 // if the reference object is of Account or Account-involved BO class (including all subclasses) 243 if (isAccountRelatedClass(accountType)) { 244 String coaCodeName = getForeignKeyFieldName(bo.getClass(), accountName, OLEPropertyConstants.CHART_OF_ACCOUNTS_CODE); 245 String acctNumName = getForeignKeyFieldName(bo.getClass(), accountName, OLEPropertyConstants.ACCOUNT_NUMBER); 246 247 // exclude the case when chartOfAccountsCode-accountNumber don't exist as foreign keys in the BO: 248 // for ex, in SubAccount, a21SubAccount is a reference object but its PKs don't exist as FKs in SubAccount; 249 // rather, A21SubAccount has a nested reference account - costShareAccount, 250 // whose PKs exists in A21SubAccount as FKs, and are used in SubAccount maint doc as nested reference 251 // special treatment outside this method is needed for this case 252 if (StringUtils.isEmpty(coaCodeName) || StringUtils.isEmpty(acctNumName)) 253 continue; 254 255 // in general we do want to have chartOfAccountsCode fields readOnly/auto-populated even when they are part of PKs, 256 // (such as in SubAccount), as the associated account shall only be chosen from existing accounts; 257 // however, when the BO is Account itself, we don't want to make the PK chartOfAccountsCode field readOnly, 258 // as it shall be editable when a new Account is being created; so we shall exclude such case 259 List<String> pks = listPrimaryKeyFieldNames(bo.getClass()); 260 if (bo instanceof Account && pks.contains(coaCodeName) && pks.contains(acctNumName )) 261 continue; 262 263 // if this relationship is specifically exempted then exempt it 264 if (isExemptedFromAccountsCannotCrossChartsRules(bo.getClass(), coaCodeName, acctNumName)) { 265 continue; 266 } 267 268 // exclude non-maintainable account field 269 String docTypeName = maintenanceDocumentDictionaryService.getDocumentTypeName(bo.getClass()); 270 if (maintenanceDocumentDictionaryService.getMaintainableField(docTypeName, coaCodeName) == null || 271 maintenanceDocumentDictionaryService.getMaintainableField(docTypeName, acctNumName) == null) 272 continue; 273 274 // otherwise include the account field PKs 275 accountChartPairs.put(acctNumName, coaCodeName); 276 } 277 } 278 279 return accountChartPairs; 280 } 281 282 public Set<String> listChartOfAccountsCodeNames(PersistableBusinessObject bo) {; 283 return listChartCodeAccountNumberPairs(bo).keySet(); 284 } 285 286 public Set<String> listAccountNumberNames(PersistableBusinessObject bo) { 287 return listAccountNumberChartCodePairs(bo).keySet(); 288 } 289 290 public String getChartOfAccountsCodeName(PersistableBusinessObject bo, String accountNumberName) { 291 return listAccountNumberChartCodePairs(bo).get(accountNumberName); 292 } 293 294 public String getAccountNumberName(PersistableBusinessObject bo, String chartOfAccountsCodeName) { 295 return listChartCodeAccountNumberPairs(bo).get(chartOfAccountsCodeName); 296 } 297 298 /** 299 * Need to stop this method from running for objects which are not bound into the ORM layer (OJB), 300 * for ex. OrgReviewRole is not persistable. In this case, we can just return an empty list. 301 * 302 * @see org.kuali.rice.krad.service.impl.PersistenceStructureServiceImpl#listReferenceObjectFields(org.kuali.rice.krad.bo.PersistableBusinessObject) 303 */ 304 @SuppressWarnings("rawtypes") 305 @Override 306 public Map<String, Class> listReferenceObjectFields(PersistableBusinessObject bo) { 307 if ( isPersistable(bo.getClass() ) ) { 308 return super.listReferenceObjectFields(bo); 309 } 310 return Collections.emptyMap(); 311 } 312 313 /** 314 * Determines if the relationship to an Account or Account-like business object, with keys of chartOfAccountsCodePropertyName and accountNumberPropertyName, 315 * is exempted from accounts cannot cross charts roles 316 * @param relationshipOwningClass the business object which possibly has an exempted relationship to Account 317 * @param chartOfAccountsCodePropertyName the property name of the relationshipOwningClass which represents the chart of accounts code part of the foreign key 318 * @param accountNumberPropertyName the property name of the relationshipOwningClass which represents the account number part of the foreign key 319 * @return true if the relationship is exempted, false otherwise 320 */ 321 public boolean isExemptedFromAccountsCannotCrossChartsRules(Class<?> relationshipOwningClass, String chartOfAccountsCodePropertyName, String accountNumberPropertyName) { 322 final List<AccountReferencePersistenceExemption> exemptionList = accountReferencePersistenceExemptionsMap.get(relationshipOwningClass); 323 if (exemptionList != null) { 324 for (AccountReferencePersistenceExemption exemption : exemptionList) { 325 if (exemption.matches(chartOfAccountsCodePropertyName, accountNumberPropertyName)) { 326 return true; 327 } 328 } 329 } 330 return false; 331 } 332 333 /** 334 * Sets the list of classes and relationships which are exempted from the accounts can't cross charts rules 335 * @param accountReferencePersistenceExemptions the list of classes and relationships which are exempted from the accounts can't cross charts rules 336 */ 337 public void setAccountReferencePersistenceExemptions(List<AccountReferencePersistenceExemption> accountReferencePersistenceExemptions) { 338 this.accountReferencePersistenceExemptions = accountReferencePersistenceExemptions; 339 } 340 341 /** 342 * Implemented to build the AccountReferencePersistenceExemptionsMap from the AccoutnReferencePersistenceExemptions List after intialization 343 * @throws Exception well, we're not going to throw an exception 344 */ 345 @Override 346 public void afterPropertiesSet() throws Exception { 347 accountReferencePersistenceExemptionsMap = new HashMap<Class<?>, List<AccountReferencePersistenceExemption>>(); 348 if (accountReferencePersistenceExemptions != null) { 349 for (AccountReferencePersistenceExemption exemption : accountReferencePersistenceExemptions) { 350 List<AccountReferencePersistenceExemption> exemptionList = accountReferencePersistenceExemptionsMap.get(exemption.getParentBusinessObjectClass()); 351 if (exemptionList == null) { 352 exemptionList = new ArrayList<AccountReferencePersistenceExemption>(); 353 } 354 exemptionList.add(exemption); 355 accountReferencePersistenceExemptionsMap.put(exemption.getParentBusinessObjectClass(), exemptionList); 356 } 357 } 358 } 359 360}