View Javadoc
1   /*
2    * Copyright 2010 The Kuali Foundation.
3    * 
4    * Licensed under the Educational Community License, Version 1.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * 
8    * http://www.opensource.org/licenses/ecl1.php
9    * 
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.ole.coa.service.impl;
17  
18  import java.util.ArrayList;
19  import java.util.Collections;
20  import java.util.HashMap;
21  import java.util.HashSet;
22  import java.util.Iterator;
23  import java.util.List;
24  import java.util.Map;
25  import java.util.Set;
26  
27  import org.apache.commons.lang.StringUtils;
28  import org.kuali.ole.coa.businessobject.Account;
29  import org.kuali.ole.coa.service.AccountPersistenceStructureService;
30  import org.kuali.ole.sys.OLEPropertyConstants;
31  import org.kuali.rice.kns.service.MaintenanceDocumentDictionaryService;
32  import org.kuali.rice.krad.bo.PersistableBusinessObject;
33  import org.kuali.rice.krad.service.impl.PersistenceStructureServiceImpl;
34  import org.kuali.rice.krad.util.KRADConstants;
35  import org.springframework.beans.factory.InitializingBean;
36  
37  public class AccountPersistenceStructureServiceImpl extends PersistenceStructureServiceImpl implements AccountPersistenceStructureService, InitializingBean {
38      
39      protected List<AccountReferencePersistenceExemption> accountReferencePersistenceExemptions;
40      protected Map<Class<?>, List<AccountReferencePersistenceExemption>> accountReferencePersistenceExemptionsMap;
41                  
42      /* 
43       * The following list is commented out as it's not used in code anymore, but still can server as a reference for testing. 
44       * The list causes problems when referencing AwardAccount.class, a class in optional module;
45       * 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.
46       * Instead of using "if (ACCOUNT_CLASSES.contains(clazz))" to judge whether whether a class is account-related,  
47       * we now judge by whether the PKs of the class contain chartOfAccountsCode-accountNumber.
48       * 
49       *      
50      // List of account-related BO classes (and all their subclasses) which have chartOfAccountsCode and accountNumber as (part of) the primary keys,
51      // i.e. the complete list of all possible referenced BO classes with chart code and account number as (part of) the foreign keys. 
52      protected static final HashSet<Class<? extends BusinessObject>> ACCOUNT_CLASSES = new HashSet<Class<? extends BusinessObject>>();    
53      static {
54          ACCOUNT_CLASSES.add(Account.class);
55          ACCOUNT_CLASSES.add(SubAccount.class);
56          ACCOUNT_CLASSES.add(A21SubAccount.class);
57          ACCOUNT_CLASSES.add(AwardAccount.class); // this class can't be referenced by core code
58          ACCOUNT_CLASSES.add(IndirectCostRecoveryExclusionAccount.class);
59          ACCOUNT_CLASSES.add(PriorYearAccount.class);
60          ACCOUNT_CLASSES.add(AccountDelegate.class);
61          ACCOUNT_CLASSES.add(AccountDescription.class);
62          ACCOUNT_CLASSES.add(AccountGlobalDetail.class);
63          ACCOUNT_CLASSES.add(AccountGuideline.class);
64          ACCOUNT_CLASSES.add(SubObjectCode.class);
65          ACCOUNT_CLASSES.add(SubObjectCodeCurrent.class);
66      }     
67      */
68      
69      public boolean isAccountRelatedClass(Class clazz) {
70          List<String> pks = listPrimaryKeyFieldNames(clazz);
71          
72          if (pks.contains(OLEPropertyConstants.CHART_OF_ACCOUNTS_CODE) && pks.contains(OLEPropertyConstants.ACCOUNT_NUMBER )) {
73              return true;
74          }
75          else {
76              return false;
77          }
78      }
79      
80      private MaintenanceDocumentDictionaryService maintenanceDocumentDictionaryService;
81  
82      public void setMaintenanceDocumentDictionaryService(MaintenanceDocumentDictionaryService maintenanceDocumentDictionaryService) {
83          this.maintenanceDocumentDictionaryService = maintenanceDocumentDictionaryService;
84      }
85  
86      @SuppressWarnings("rawtypes")
87      public Map<String, Class> listCollectionAccountFields(PersistableBusinessObject bo) {
88          Map<String, Class> accountFields = new HashMap<String, Class>(); 
89          Iterator<Map.Entry<String, Class>> collObjs = listCollectionObjectTypes(bo).entrySet().iterator();
90          
91          while (collObjs.hasNext()) {
92              Map.Entry<String, Class> entry = (Map.Entry<String, Class>)collObjs.next();
93              String accountCollName = entry.getKey();
94              Class accountCollType = entry.getValue();
95              
96              // if the reference object is of Account or Account-involved BO class (including all subclasses) 
97              if (isAccountRelatedClass(accountCollType)) {
98                  // exclude non-maintainable account collection
99                  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 }