/* * 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.coa.service.impl; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.commons.lang.StringUtils; import org.kuali.kfs.coa.businessobject.Account; import org.kuali.kfs.coa.service.AccountPersistenceStructureService; import org.kuali.kfs.sys.KFSPropertyConstants; import org.kuali.rice.kns.service.MaintenanceDocumentDictionaryService; import org.kuali.rice.krad.bo.PersistableBusinessObject; import org.kuali.rice.krad.service.impl.PersistenceStructureServiceImpl; import org.kuali.rice.krad.util.KRADConstants; import org.springframework.beans.factory.InitializingBean; public class AccountPersistenceStructureServiceImpl extends PersistenceStructureServiceImpl implements AccountPersistenceStructureService, InitializingBean { protected List<AccountReferencePersistenceExemption> accountReferencePersistenceExemptions; protected Map<Class<?>, List<AccountReferencePersistenceExemption>> accountReferencePersistenceExemptionsMap; /* * The following list is commented out as it's not used in code anymore, but still can server as a reference for testing. * The list causes problems when referencing AwardAccount.class, a class in optional module; * 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. * Instead of using "if (ACCOUNT_CLASSES.contains(clazz))" to judge whether whether a class is account-related, * we now judge by whether the PKs of the class contain chartOfAccountsCode-accountNumber. * * // List of account-related BO classes (and all their subclasses) which have chartOfAccountsCode and accountNumber as (part of) the primary keys, // i.e. the complete list of all possible referenced BO classes with chart code and account number as (part of) the foreign keys. protected static final HashSet<Class<? extends BusinessObject>> ACCOUNT_CLASSES = new HashSet<Class<? extends BusinessObject>>(); static { ACCOUNT_CLASSES.add(Account.class); ACCOUNT_CLASSES.add(SubAccount.class); ACCOUNT_CLASSES.add(A21SubAccount.class); ACCOUNT_CLASSES.add(AwardAccount.class); // this class can't be referenced by core code ACCOUNT_CLASSES.add(IndirectCostRecoveryExclusionAccount.class); ACCOUNT_CLASSES.add(PriorYearAccount.class); ACCOUNT_CLASSES.add(AccountDelegate.class); ACCOUNT_CLASSES.add(AccountDescription.class); ACCOUNT_CLASSES.add(AccountGlobalDetail.class); ACCOUNT_CLASSES.add(AccountGuideline.class); ACCOUNT_CLASSES.add(SubObjectCode.class); ACCOUNT_CLASSES.add(SubObjectCodeCurrent.class); } */ public boolean isAccountRelatedClass(Class clazz) { List<String> pks = listPrimaryKeyFieldNames(clazz); if (pks.contains(KFSPropertyConstants.CHART_OF_ACCOUNTS_CODE) && pks.contains(KFSPropertyConstants.ACCOUNT_NUMBER )) { return true; } else { return false; } } private MaintenanceDocumentDictionaryService maintenanceDocumentDictionaryService; public void setMaintenanceDocumentDictionaryService(MaintenanceDocumentDictionaryService maintenanceDocumentDictionaryService) { this.maintenanceDocumentDictionaryService = maintenanceDocumentDictionaryService; } @SuppressWarnings("rawtypes") public Map<String, Class> listCollectionAccountFields(PersistableBusinessObject bo) { Map<String, Class> accountFields = new HashMap<String, Class>(); Iterator<Map.Entry<String, Class>> collObjs = listCollectionObjectTypes(bo).entrySet().iterator(); while (collObjs.hasNext()) { Map.Entry<String, Class> entry = (Map.Entry<String, Class>)collObjs.next(); String accountCollName = entry.getKey(); Class accountCollType = entry.getValue(); // if the reference object is of Account or Account-involved BO class (including all subclasses) if (isAccountRelatedClass(accountCollType)) { // exclude non-maintainable account collection String docTypeName = maintenanceDocumentDictionaryService.getDocumentTypeName(bo.getClass()); if (maintenanceDocumentDictionaryService.getMaintainableCollection(docTypeName, accountCollName) == null) continue; // otherwise include the account field accountFields.put(accountCollName, accountCollType); } } return accountFields; } @SuppressWarnings("rawtypes") public Set<String> listCollectionChartOfAccountsCodeNames(PersistableBusinessObject bo) { Set<String> coaCodeNames = new HashSet<String>(); String docTypeName = maintenanceDocumentDictionaryService.getDocumentTypeName(bo.getClass()); Iterator<Map.Entry<String, Class>> collObjs = listCollectionObjectTypes(bo).entrySet().iterator(); while (collObjs.hasNext()) { Map.Entry<String, Class> entry = (Map.Entry<String, Class>)collObjs.next(); String accountCollName = entry.getKey(); Class accountCollType = entry.getValue(); // if the reference object is of Account or Account-involved BO class (including all subclasses) if (isAccountRelatedClass(accountCollType)) { // exclude non-maintainable account collection if (maintenanceDocumentDictionaryService.getMaintainableCollection(docTypeName, accountCollName) == null) continue; // otherwise include the account field String coaCodeName = KRADConstants.ADD_PREFIX + "." + accountCollName + "." + KFSPropertyConstants.CHART_OF_ACCOUNTS_CODE; coaCodeNames.add(coaCodeName); } } return coaCodeNames; } @SuppressWarnings("rawtypes") public Map<String, Class> listReferenceAccountFields(PersistableBusinessObject bo) { Map<String, Class> accountFields = new HashMap<String, Class>(); Iterator<Map.Entry<String, Class>> refObjs = listReferenceObjectFields(bo).entrySet().iterator(); while (refObjs.hasNext()) { Map.Entry<String, Class> entry = (Map.Entry<String, Class>)refObjs.next(); String accountName = entry.getKey(); Class accountType = entry.getValue(); // if the reference object is of Account or Account-involved BO class (including all subclasses) if (isAccountRelatedClass(accountType)) { String coaCodeName = getForeignKeyFieldName(bo.getClass(), accountName, KFSPropertyConstants.CHART_OF_ACCOUNTS_CODE); String acctNumName = getForeignKeyFieldName(bo.getClass(), accountName, KFSPropertyConstants.ACCOUNT_NUMBER); // exclude the case when chartOfAccountsCode-accountNumber don't exist as foreign keys in the BO: // for ex, in SubAccount, a21SubAccount is a reference object but its PKs don't exist as FKs in SubAccount; // rather, A21SubAccount has a nested reference account - costShareAccount, // whose PKs exists in A21SubAccount as FKs, and are used in SubAccount maint doc as nested reference; // special treatment outside this method is needed for this case if (StringUtils.isEmpty(coaCodeName) || StringUtils.isEmpty(acctNumName)) continue; // in general we do want to have chartOfAccountsCode fields readOnly/auto-populated even when they are part of PKs, // (such as in SubAccount), as the associated account shall only be chosen from existing accounts; // however, when the BO is Account itself, we don't want to make the PK chartOfAccountsCode field readOnly, // as it shall be editable when a new Account is being created; so we shall exclude such case List<String> pks = listPrimaryKeyFieldNames(bo.getClass()); if (bo instanceof Account && pks.contains(coaCodeName) && pks.contains(acctNumName )) continue; // exclude non-maintainable account field String docTypeName = maintenanceDocumentDictionaryService.getDocumentTypeName(bo.getClass()); if (maintenanceDocumentDictionaryService.getMaintainableField(docTypeName, coaCodeName) == null || maintenanceDocumentDictionaryService.getMaintainableField(docTypeName, acctNumName) == null) continue; // otherwise include the account field accountFields.put(accountName, accountType); } } return accountFields; } @SuppressWarnings("rawtypes") public Map<String, String> listChartCodeAccountNumberPairs(PersistableBusinessObject bo) { Map<String, String> chartAccountPairs = new HashMap<String, String>(); Iterator<Map.Entry<String, Class>> refObjs = listReferenceObjectFields(bo).entrySet().iterator(); while (refObjs.hasNext()) { Map.Entry<String, Class> entry = (Map.Entry<String, Class>)refObjs.next(); String accountName = entry.getKey(); Class accountType = entry.getValue(); // if the reference object is of Account or Account-involved BO class (including all subclasses) if (isAccountRelatedClass(accountType)) { String coaCodeName = getForeignKeyFieldName(bo.getClass(), accountName, KFSPropertyConstants.CHART_OF_ACCOUNTS_CODE); String acctNumName = getForeignKeyFieldName(bo.getClass(), accountName, KFSPropertyConstants.ACCOUNT_NUMBER); // exclude the case when chartOfAccountsCode-accountNumber don't exist as foreign keys in the BO: // for ex, in SubAccount, a21SubAccount is a reference object but its PKs don't exist as FKs in SubAccount; // rather, A21SubAccount has a nested reference account - costShareAccount, // whose PKs exists in A21SubAccount as FKs, and are used in SubAccount maint doc as nested reference // special treatment outside this method is needed for this case if (StringUtils.isEmpty(coaCodeName) || StringUtils.isEmpty(acctNumName)) continue; // in general we do want to have chartOfAccountsCode fields readOnly/auto-populated even when they are part of PKs, // (such as in SubAccount), as the associated account shall only be chosen from existing accounts; // however, when the BO is Account itself, we don't want to make the PK chartOfAccountsCode field readOnly, // as it shall be editable when a new Account is being created; so we shall exclude such case List<String> pks = listPrimaryKeyFieldNames(bo.getClass()); if (bo instanceof Account && pks.contains(coaCodeName) && pks.contains(acctNumName )) continue; // if this relationship is specifically exempted then exempt it if (isExemptedFromAccountsCannotCrossChartsRules(bo.getClass(), coaCodeName, acctNumName)) { continue; } // exclude non-maintainable account field String docTypeName = maintenanceDocumentDictionaryService.getDocumentTypeName(bo.getClass()); if (maintenanceDocumentDictionaryService.getMaintainableField(docTypeName, coaCodeName) == null || maintenanceDocumentDictionaryService.getMaintainableField(docTypeName, acctNumName) == null) continue; // otherwise include the account field PKs chartAccountPairs.put(coaCodeName, acctNumName); } } return chartAccountPairs; } @SuppressWarnings("rawtypes") public Map<String, String> listAccountNumberChartCodePairs(PersistableBusinessObject bo) { Map<String, String> accountChartPairs = new HashMap<String, String>(); Iterator<Map.Entry<String, Class>> refObjs = listReferenceObjectFields(bo).entrySet().iterator(); while (refObjs.hasNext()) { Map.Entry<String, Class> entry = (Map.Entry<String, Class>)refObjs.next(); String accountName = entry.getKey(); Class accountType = entry.getValue(); // if the reference object is of Account or Account-involved BO class (including all subclasses) if (isAccountRelatedClass(accountType)) { String coaCodeName = getForeignKeyFieldName(bo.getClass(), accountName, KFSPropertyConstants.CHART_OF_ACCOUNTS_CODE); String acctNumName = getForeignKeyFieldName(bo.getClass(), accountName, KFSPropertyConstants.ACCOUNT_NUMBER); // exclude the case when chartOfAccountsCode-accountNumber don't exist as foreign keys in the BO: // for ex, in SubAccount, a21SubAccount is a reference object but its PKs don't exist as FKs in SubAccount; // rather, A21SubAccount has a nested reference account - costShareAccount, // whose PKs exists in A21SubAccount as FKs, and are used in SubAccount maint doc as nested reference // special treatment outside this method is needed for this case if (StringUtils.isEmpty(coaCodeName) || StringUtils.isEmpty(acctNumName)) continue; // in general we do want to have chartOfAccountsCode fields readOnly/auto-populated even when they are part of PKs, // (such as in SubAccount), as the associated account shall only be chosen from existing accounts; // however, when the BO is Account itself, we don't want to make the PK chartOfAccountsCode field readOnly, // as it shall be editable when a new Account is being created; so we shall exclude such case List<String> pks = listPrimaryKeyFieldNames(bo.getClass()); if (bo instanceof Account && pks.contains(coaCodeName) && pks.contains(acctNumName )) continue; // if this relationship is specifically exempted then exempt it if (isExemptedFromAccountsCannotCrossChartsRules(bo.getClass(), coaCodeName, acctNumName)) { continue; } // exclude non-maintainable account field String docTypeName = maintenanceDocumentDictionaryService.getDocumentTypeName(bo.getClass()); if (maintenanceDocumentDictionaryService.getMaintainableField(docTypeName, coaCodeName) == null || maintenanceDocumentDictionaryService.getMaintainableField(docTypeName, acctNumName) == null) continue; // otherwise include the account field PKs accountChartPairs.put(acctNumName, coaCodeName); } } return accountChartPairs; } public Set<String> listChartOfAccountsCodeNames(PersistableBusinessObject bo) {; return listChartCodeAccountNumberPairs(bo).keySet(); } public Set<String> listAccountNumberNames(PersistableBusinessObject bo) { return listAccountNumberChartCodePairs(bo).keySet(); } public String getChartOfAccountsCodeName(PersistableBusinessObject bo, String accountNumberName) { return listAccountNumberChartCodePairs(bo).get(accountNumberName); } public String getAccountNumberName(PersistableBusinessObject bo, String chartOfAccountsCodeName) { return listChartCodeAccountNumberPairs(bo).get(chartOfAccountsCodeName); } /** * Need to stop this method from running for objects which are not bound into the ORM layer (OJB), * for ex. OrgReviewRole is not persistable. In this case, we can just return an empty list. * * @see org.kuali.rice.krad.service.impl.PersistenceStructureServiceImpl#listReferenceObjectFields(org.kuali.rice.krad.bo.PersistableBusinessObject) */ @SuppressWarnings("rawtypes") @Override public Map<String, Class> listReferenceObjectFields(PersistableBusinessObject bo) { if ( isPersistable(bo.getClass() ) ) { return super.listReferenceObjectFields(bo); } return Collections.emptyMap(); } /** * Determines if the relationship to an Account or Account-like business object, with keys of chartOfAccountsCodePropertyName and accountNumberPropertyName, * is exempted from accounts cannot cross charts roles * @param relationshipOwningClass the business object which possibly has an exempted relationship to Account * @param chartOfAccountsCodePropertyName the property name of the relationshipOwningClass which represents the chart of accounts code part of the foreign key * @param accountNumberPropertyName the property name of the relationshipOwningClass which represents the account number part of the foreign key * @return true if the relationship is exempted, false otherwise */ public boolean isExemptedFromAccountsCannotCrossChartsRules(Class<?> relationshipOwningClass, String chartOfAccountsCodePropertyName, String accountNumberPropertyName) { final List<AccountReferencePersistenceExemption> exemptionList = accountReferencePersistenceExemptionsMap.get(relationshipOwningClass); if (exemptionList != null) { for (AccountReferencePersistenceExemption exemption : exemptionList) { if (exemption.matches(chartOfAccountsCodePropertyName, accountNumberPropertyName)) { return true; } } } return false; } /** * Sets the list of classes and relationships which are exempted from the accounts can't cross charts rules * @param accountReferencePersistenceExemptions the list of classes and relationships which are exempted from the accounts can't cross charts rules */ public void setAccountReferencePersistenceExemptions(List<AccountReferencePersistenceExemption> accountReferencePersistenceExemptions) { this.accountReferencePersistenceExemptions = accountReferencePersistenceExemptions; } /** * Implemented to build the AccountReferencePersistenceExemptionsMap from the AccoutnReferencePersistenceExemptions List after intialization * @throws Exception well, we're not going to throw an exception */ @Override public void afterPropertiesSet() throws Exception { accountReferencePersistenceExemptionsMap = new HashMap<Class<?>, List<AccountReferencePersistenceExemption>>(); if (accountReferencePersistenceExemptions != null) { for (AccountReferencePersistenceExemption exemption : accountReferencePersistenceExemptions) { List<AccountReferencePersistenceExemption> exemptionList = accountReferencePersistenceExemptionsMap.get(exemption.getParentBusinessObjectClass()); if (exemptionList == null) { exemptionList = new ArrayList<AccountReferencePersistenceExemption>(); } exemptionList.add(exemption); accountReferencePersistenceExemptionsMap.put(exemption.getParentBusinessObjectClass(), exemptionList); } } } }