001    /*
002     * Copyright 2011 The Kuali Foundation.
003     * 
004     * Licensed under the Educational Community License, Version 2.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/ecl2.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     */
016    package org.kuali.kfs.coa.service.impl;
017    
018    import java.util.Collections;
019    import java.util.HashMap;
020    import java.util.HashSet;
021    import java.util.Iterator;
022    import java.util.List;
023    import java.util.Map;
024    import java.util.Set;
025    
026    import org.apache.commons.lang.StringUtils;
027    import org.kuali.kfs.coa.businessobject.Account;
028    import org.kuali.kfs.coa.service.AccountPersistenceStructureService;
029    import org.kuali.kfs.sys.KFSPropertyConstants;
030    import org.kuali.rice.kns.bo.PersistableBusinessObject;
031    import org.kuali.rice.kns.service.MaintenanceDocumentDictionaryService;
032    import org.kuali.rice.kns.service.impl.PersistenceStructureServiceImpl;
033    import org.kuali.rice.kns.util.KNSConstants;
034    
035    public class AccountPersistenceStructureServiceImpl extends PersistenceStructureServiceImpl implements AccountPersistenceStructureService {
036                    
037        /* 
038         * The following list is commented out as it's not used in code anymore, but still can server as a reference for testing. 
039         * The list causes problems when referencing AwardAccount.class, a class in optional module;
040         * 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.
041         * Instead of using "if (ACCOUNT_CLASSES.contains(clazz))" to judge whether whether a class is account-related,  
042         * we now judge by whether the PKs of the class contain chartOfAccountsCode-accountNumber.
043         * 
044         *      
045        // List of account-related BO classes (and all their subclasses) which have chartOfAccountsCode and accountNumber as (part of) the primary keys,
046        // i.e. the complete list of all possible referenced BO classes with chart code and account number as (part of) the foreign keys. 
047        protected static final HashSet<Class<? extends BusinessObject>> ACCOUNT_CLASSES = new HashSet<Class<? extends BusinessObject>>();    
048        static {
049            ACCOUNT_CLASSES.add(Account.class);
050            ACCOUNT_CLASSES.add(SubAccount.class);
051            ACCOUNT_CLASSES.add(A21SubAccount.class);
052            ACCOUNT_CLASSES.add(AwardAccount.class); // this class can't be referenced by core code
053            ACCOUNT_CLASSES.add(IndirectCostRecoveryExclusionAccount.class);
054            ACCOUNT_CLASSES.add(PriorYearAccount.class);
055            ACCOUNT_CLASSES.add(AccountDelegate.class);
056            ACCOUNT_CLASSES.add(AccountDescription.class);
057            ACCOUNT_CLASSES.add(AccountGlobalDetail.class);
058            ACCOUNT_CLASSES.add(AccountGuideline.class);
059            ACCOUNT_CLASSES.add(SubObjectCode.class);
060            ACCOUNT_CLASSES.add(SubObjectCodeCurrent.class);
061        }     
062        */
063        
064        public boolean isAccountRelatedClass(Class clazz) {
065            List<String> pks = listPrimaryKeyFieldNames(clazz);
066            
067            if (pks.contains(KFSPropertyConstants.CHART_OF_ACCOUNTS_CODE) && pks.contains(KFSPropertyConstants.ACCOUNT_NUMBER )) {
068                return true;
069            }
070            else {
071                return false;
072            }
073        }
074        
075        private MaintenanceDocumentDictionaryService maintenanceDocumentDictionaryService;
076    
077        public void setMaintenanceDocumentDictionaryService(MaintenanceDocumentDictionaryService maintenanceDocumentDictionaryService) {
078            this.maintenanceDocumentDictionaryService = maintenanceDocumentDictionaryService;
079        }
080    
081        @SuppressWarnings("rawtypes")
082        public Map<String, Class> listCollectionAccountFields(PersistableBusinessObject bo) {
083            Map<String, Class> accountFields = new HashMap<String, Class>(); 
084            Iterator<Map.Entry<String, Class>> collObjs = listCollectionObjectTypes(bo).entrySet().iterator();
085            
086            while (collObjs.hasNext()) {
087                Map.Entry<String, Class> entry = (Map.Entry<String, Class>)collObjs.next();
088                String accountCollName = entry.getKey();
089                Class accountCollType = entry.getValue();
090                
091                // if the reference object is of Account or Account-involved BO class (including all subclasses) 
092                if (isAccountRelatedClass(accountCollType)) {
093                    // exclude non-maintainable account collection
094                    String docTypeName = maintenanceDocumentDictionaryService.getDocumentTypeName(bo.getClass());
095                    if (maintenanceDocumentDictionaryService.getMaintainableCollection(docTypeName, accountCollName) == null)
096                        continue;
097                    
098                    // otherwise include the account field
099                    accountFields.put(accountCollName, accountCollType);                
100                }
101            }
102            
103            return accountFields;
104        }
105        
106        @SuppressWarnings("rawtypes")
107        public Set<String> listCollectionChartOfAccountsCodeNames(PersistableBusinessObject bo) {
108            Set<String> coaCodeNames = new HashSet<String>();
109            String docTypeName = maintenanceDocumentDictionaryService.getDocumentTypeName(bo.getClass());
110            Iterator<Map.Entry<String, Class>> collObjs = listCollectionObjectTypes(bo).entrySet().iterator();
111            
112            while (collObjs.hasNext()) {
113                Map.Entry<String, Class> entry = (Map.Entry<String, Class>)collObjs.next();
114                String accountCollName = entry.getKey();
115                Class accountCollType = entry.getValue();
116                
117                // if the reference object is of Account or Account-involved BO class (including all subclasses) 
118                if (isAccountRelatedClass(accountCollType)) {
119                    // exclude non-maintainable account collection
120                    if (maintenanceDocumentDictionaryService.getMaintainableCollection(docTypeName, accountCollName) == null)
121                        continue;
122    
123                    // otherwise include the account field
124                    String coaCodeName = KNSConstants.ADD_PREFIX + "." + accountCollName + "." + KFSPropertyConstants.CHART_OF_ACCOUNTS_CODE;
125                    coaCodeNames.add(coaCodeName);
126                }
127            }
128    
129            return coaCodeNames;
130        }
131        
132        @SuppressWarnings("rawtypes")
133        public Map<String, Class> listReferenceAccountFields(PersistableBusinessObject bo) {
134            Map<String, Class> accountFields = new HashMap<String, Class>();       
135            Iterator<Map.Entry<String, Class>> refObjs = listReferenceObjectFields(bo).entrySet().iterator();
136            
137            while (refObjs.hasNext()) {
138                Map.Entry<String, Class> entry = (Map.Entry<String, Class>)refObjs.next();
139                String accountName = entry.getKey();
140                Class accountType = entry.getValue();
141                
142                // if the reference object is of Account or Account-involved BO class (including all subclasses)            
143                if (isAccountRelatedClass(accountType)) {
144                    String coaCodeName = getForeignKeyFieldName(bo.getClass(), accountName, KFSPropertyConstants.CHART_OF_ACCOUNTS_CODE);
145                    String acctNumName = getForeignKeyFieldName(bo.getClass(), accountName, KFSPropertyConstants.ACCOUNT_NUMBER);
146                    
147                    // exclude the case when chartOfAccountsCode-accountNumber don't exist as foreign keys in the BO:
148                    // for ex, in SubAccount, a21SubAccount is a reference object but its PKs don't exist as FKs in SubAccount;
149                    // rather, A21SubAccount has a nested reference account - costShareAccount, 
150                    // whose PKs exists in A21SubAccount as FKs, and are used in SubAccount maint doc as nested reference;
151                    // special treatment outside this method is needed for this case
152                    if (StringUtils.isEmpty(coaCodeName) || StringUtils.isEmpty(acctNumName)) 
153                        continue;
154                    
155                    // in general we do want to have chartOfAccountsCode fields readOnly/auto-populated even when they are part of PKs,  
156                    // (such as in SubAccount), as the associated account shall only be chosen from existing accounts; 
157                    // however, when the BO is Account itself, we don't want to make the PK chartOfAccountsCode field readOnly, 
158                    // as it shall be editable when a new Account is being created; so we shall exclude such case 
159                    List<String> pks = listPrimaryKeyFieldNames(bo.getClass());
160                    if (bo instanceof Account && pks.contains(coaCodeName) && pks.contains(acctNumName )) 
161                        continue;                
162                    
163                    // exclude non-maintainable account field
164                    String docTypeName = maintenanceDocumentDictionaryService.getDocumentTypeName(bo.getClass());
165                    if (maintenanceDocumentDictionaryService.getMaintainableField(docTypeName, coaCodeName) == null ||
166                        maintenanceDocumentDictionaryService.getMaintainableField(docTypeName, acctNumName) == null)
167                        continue;
168                    
169                    // otherwise include the account field
170                    accountFields.put(accountName, accountType);                
171                }
172            }
173            
174            return accountFields;
175        }
176        
177        @SuppressWarnings("rawtypes")
178        public Map<String, String> listChartCodeAccountNumberPairs(PersistableBusinessObject bo) {
179            Map<String, String> chartAccountPairs = new HashMap<String, String>();       
180            Iterator<Map.Entry<String, Class>> refObjs = listReferenceObjectFields(bo).entrySet().iterator();
181            
182            while (refObjs.hasNext()) {
183                Map.Entry<String, Class> entry = (Map.Entry<String, Class>)refObjs.next();
184                String accountName = entry.getKey();
185                Class accountType = entry.getValue();
186                
187                // if the reference object is of Account or Account-involved BO class (including all subclasses)            
188                if (isAccountRelatedClass(accountType)) {
189                    String coaCodeName = getForeignKeyFieldName(bo.getClass(), accountName, KFSPropertyConstants.CHART_OF_ACCOUNTS_CODE);
190                    String acctNumName = getForeignKeyFieldName(bo.getClass(), accountName, KFSPropertyConstants.ACCOUNT_NUMBER);
191                    
192                    // exclude the case when chartOfAccountsCode-accountNumber don't exist as foreign keys in the BO:
193                    // for ex, in SubAccount, a21SubAccount is a reference object but its PKs don't exist as FKs in SubAccount;
194                    // rather, A21SubAccount has a nested reference account - costShareAccount, 
195                    // whose PKs exists in A21SubAccount as FKs, and are used in SubAccount maint doc as nested reference
196                    // special treatment outside this method is needed for this case
197                    if (StringUtils.isEmpty(coaCodeName) || StringUtils.isEmpty(acctNumName)) 
198                        continue;
199                    
200                    // in general we do want to have chartOfAccountsCode fields readOnly/auto-populated even when they are part of PKs,  
201                    // (such as in SubAccount), as the associated account shall only be chosen from existing accounts; 
202                    // however, when the BO is Account itself, we don't want to make the PK chartOfAccountsCode field readOnly, 
203                    // as it shall be editable when a new Account is being created; so we shall exclude such case 
204                    List<String> pks = listPrimaryKeyFieldNames(bo.getClass());
205                    if (bo instanceof Account && pks.contains(coaCodeName) && pks.contains(acctNumName )) 
206                        continue;                
207                                    
208                    // exclude non-maintainable account field
209                    String docTypeName = maintenanceDocumentDictionaryService.getDocumentTypeName(bo.getClass());
210                    if (maintenanceDocumentDictionaryService.getMaintainableField(docTypeName, coaCodeName) == null ||
211                        maintenanceDocumentDictionaryService.getMaintainableField(docTypeName, acctNumName) == null)
212                        continue;
213                    
214                    // otherwise include the account field PKs
215                    chartAccountPairs.put(coaCodeName, acctNumName);                
216                }
217            }
218            
219            return chartAccountPairs;
220        }
221        
222        @SuppressWarnings("rawtypes")
223        public Map<String, String> listAccountNumberChartCodePairs(PersistableBusinessObject bo) {
224            Map<String, String> accountChartPairs = new HashMap<String, String>(); 
225            Iterator<Map.Entry<String, Class>> refObjs = listReferenceObjectFields(bo).entrySet().iterator();
226            
227            while (refObjs.hasNext()) {
228                Map.Entry<String, Class> entry = (Map.Entry<String, Class>)refObjs.next();
229                String accountName = entry.getKey();
230                Class accountType = entry.getValue();
231                
232                // if the reference object is of Account or Account-involved BO class (including all subclasses)            
233                if (isAccountRelatedClass(accountType)) {
234                    String coaCodeName = getForeignKeyFieldName(bo.getClass(), accountName, KFSPropertyConstants.CHART_OF_ACCOUNTS_CODE);
235                    String acctNumName = getForeignKeyFieldName(bo.getClass(), accountName, KFSPropertyConstants.ACCOUNT_NUMBER);
236                    
237                    // exclude the case when chartOfAccountsCode-accountNumber don't exist as foreign keys in the BO:
238                    // for ex, in SubAccount, a21SubAccount is a reference object but its PKs don't exist as FKs in SubAccount;
239                    // rather, A21SubAccount has a nested reference account - costShareAccount, 
240                    // whose PKs exists in A21SubAccount as FKs, and are used in SubAccount maint doc as nested reference
241                    // special treatment outside this method is needed for this case
242                    if (StringUtils.isEmpty(coaCodeName) || StringUtils.isEmpty(acctNumName)) 
243                        continue;
244                    
245                    // in general we do want to have chartOfAccountsCode fields readOnly/auto-populated even when they are part of PKs,  
246                    // (such as in SubAccount), as the associated account shall only be chosen from existing accounts; 
247                    // however, when the BO is Account itself, we don't want to make the PK chartOfAccountsCode field readOnly, 
248                    // as it shall be editable when a new Account is being created; so we shall exclude such case 
249                    List<String> pks = listPrimaryKeyFieldNames(bo.getClass());
250                    if (bo instanceof Account && pks.contains(coaCodeName) && pks.contains(acctNumName )) 
251                        continue;                
252                    
253                    // exclude non-maintainable account field
254                    String docTypeName = maintenanceDocumentDictionaryService.getDocumentTypeName(bo.getClass());
255                    if (maintenanceDocumentDictionaryService.getMaintainableField(docTypeName, coaCodeName) == null ||
256                        maintenanceDocumentDictionaryService.getMaintainableField(docTypeName, acctNumName) == null)
257                        continue;
258                    
259                    // otherwise include the account field PKs
260                    accountChartPairs.put(acctNumName, coaCodeName);
261                }
262            }
263            
264            return accountChartPairs;
265        }
266    
267        public Set<String> listChartOfAccountsCodeNames(PersistableBusinessObject bo) {;
268            return listChartCodeAccountNumberPairs(bo).keySet();        
269        }
270    
271        public Set<String> listAccountNumberNames(PersistableBusinessObject bo) {
272            return listAccountNumberChartCodePairs(bo).keySet();     
273        }
274        
275        public String getChartOfAccountsCodeName(PersistableBusinessObject bo, String accountNumberName) {
276            return listAccountNumberChartCodePairs(bo).get(accountNumberName);        
277        }
278        
279        public String getAccountNumberName(PersistableBusinessObject bo, String chartOfAccountsCodeName) {
280            return listChartCodeAccountNumberPairs(bo).get(chartOfAccountsCodeName);        
281        }
282        
283        /** 
284         * Need to stop this method from running for objects which are not bound into the ORM layer (OJB),
285         * for ex. OrgReviewRole is not persistable. In this case, we can just return an empty list.
286         * 
287         * @see org.kuali.rice.kns.service.impl.PersistenceStructureServiceImpl#listReferenceObjectFields(org.kuali.rice.kns.bo.PersistableBusinessObject)
288         */
289        @SuppressWarnings("rawtypes")
290        @Override
291        public Map<String, Class> listReferenceObjectFields(PersistableBusinessObject bo) {
292            if ( isPersistable(bo.getClass() ) ) {
293                return super.listReferenceObjectFields(bo);
294            }
295            return Collections.emptyMap();
296        }
297        
298    }