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}