001/*
002 * Copyright 2006 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 */
016package org.kuali.ole.coa.document.validation.impl;
017
018import java.util.ArrayList;
019import java.util.Collection;
020import java.util.HashMap;
021import java.util.List;
022import java.util.Map;
023
024import org.apache.commons.lang.StringUtils;
025import org.kuali.ole.coa.businessobject.A21IndirectCostRecoveryAccount;
026import org.kuali.ole.coa.businessobject.A21SubAccount;
027import org.kuali.ole.coa.businessobject.IndirectCostRecoveryAccount;
028import org.kuali.ole.coa.businessobject.IndirectCostRecoveryRateDetail;
029import org.kuali.ole.coa.businessobject.SubAccount;
030import org.kuali.ole.coa.service.SubFundGroupService;
031import org.kuali.ole.sys.OLEConstants;
032import org.kuali.ole.sys.OLEKeyConstants;
033import org.kuali.ole.sys.OLEPropertyConstants;
034import org.kuali.ole.sys.context.SpringContext;
035import org.kuali.ole.sys.service.UniversityDateService;
036import org.kuali.rice.kns.document.MaintenanceDocument;
037import org.kuali.rice.kns.service.DataDictionaryService;
038import org.kuali.rice.krad.util.ObjectUtils;
039
040/**
041 * This class implements the business rules specific to the {@link SubAccount} Maintenance Document.
042 */
043public class SubAccountRule extends IndirectCostRecoveryAccountsRule {
044
045    protected static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(SubAccountRule.class);
046
047    protected SubAccount oldSubAccount;
048    protected SubAccount newSubAccount;
049
050    /**
051     * This performs rules checks on document approve
052     * <ul>
053     * <li>{@link SubAccountRule#setCgAuthorized(boolean)}</li>
054     * <li>{@link SubAccountRule#checkForPartiallyEnteredReportingFields()}</li>
055     * <li>{@link SubAccountRule#checkCgRules(MaintenanceDocument)}</li>
056     * </ul>
057     * This rule fails on business rule failures
058     * 
059     * @see org.kuali.rice.kns.maintenance.rules.MaintenanceDocumentRuleBase#processCustomApproveDocumentBusinessRules(org.kuali.rice.kns.document.MaintenanceDocument)
060     */
061    protected boolean processCustomApproveDocumentBusinessRules(MaintenanceDocument document) {
062        LOG.debug("Entering processCustomApproveDocumentBusinessRules()");
063
064        // check that all sub-objects whose keys are specified have matching objects in the db
065        boolean success = checkForPartiallyEnteredReportingFields();
066
067        // process CG rules if appropriate
068        success &= checkCgRules(document);
069
070        return success;
071    }
072
073    /**
074     * This performs rules checks on document route
075     * <ul>
076     * <li>{@link SubAccountRule#setCgAuthorized(boolean)}</li>
077     * <li>{@link SubAccountRule#checkForPartiallyEnteredReportingFields()}</li>
078     * <li>{@link SubAccountRule#checkCgRules(MaintenanceDocument)}</li>
079     * </ul>
080     * This rule fails on business rule failures
081     * 
082     * @see org.kuali.rice.kns.maintenance.rules.MaintenanceDocumentRuleBase#processCustomRouteDocumentBusinessRules(org.kuali.rice.kns.document.MaintenanceDocument)
083     */
084    protected boolean processCustomRouteDocumentBusinessRules(MaintenanceDocument document) {
085        LOG.debug("Entering processCustomRouteDocumentBusinessRules()");
086
087        boolean success = true;
088
089        // check that all sub-objects whose keys are specified have matching objects in the db
090        success &= checkForPartiallyEnteredReportingFields();
091
092        // process CG rules if appropriate
093        success &= checkCgRules(document);
094
095        success &= super.processCustomRouteDocumentBusinessRules(document);
096        return success;
097    }
098
099    /**
100     * This performs rules checks on document save
101     * <ul>
102     * <li>{@link SubAccountRule#setCgAuthorized(boolean)}</li>
103     * <li>{@link SubAccountRule#checkForPartiallyEnteredReportingFields()}</li>
104     * <li>{@link SubAccountRule#checkCgRules(MaintenanceDocument)}</li>
105     * </ul>
106     * This rule does not fail on business rule failures
107     * 
108     * @see org.kuali.rice.kns.maintenance.rules.MaintenanceDocumentRuleBase#processCustomSaveDocumentBusinessRules(org.kuali.rice.kns.document.MaintenanceDocument)
109     */
110    protected boolean processCustomSaveDocumentBusinessRules(MaintenanceDocument document) {
111
112        boolean success = true;
113
114        LOG.debug("Entering processCustomSaveDocumentBusinessRules()");
115
116        // check that all sub-objects whose keys are specified have matching objects in the db
117        success &= checkForPartiallyEnteredReportingFields();
118
119        // process CG rules if appropriate
120        success &= checkCgRules(document);
121
122        return success;
123    }
124
125    /**
126     * This method sets the convenience objects like newAccount and oldAccount, so you have short and easy handles to the new and
127     * old objects contained in the maintenance document. It also calls the BusinessObjectBase.refresh(), which will attempt to load
128     * all sub-objects from the DB by their primary keys, if available.
129     * 
130     * @param document - the maintenanceDocument being evaluated
131     */
132    public void setupConvenienceObjects() {
133
134        // setup oldAccount convenience objects, make sure all possible sub-objects are populated
135        oldSubAccount = (SubAccount) super.getOldBo();
136        refreshSubObjects(oldSubAccount);
137
138        // setup newAccount convenience objects, make sure all possible sub-objects are populated
139        newSubAccount = (SubAccount) super.getNewBo();
140        refreshSubObjects(newSubAccount);
141        
142        //icr rule checking setup
143        if (newSubAccount.getA21SubAccount() != null){
144            List<IndirectCostRecoveryAccount> icrAccountList = new ArrayList<IndirectCostRecoveryAccount>(
145                    newSubAccount.getA21SubAccount().getA21ActiveIndirectCostRecoveryAccounts());
146            setActiveIndirectCostRecoveryAccountList(icrAccountList);
147            setBoFieldPath(OLEPropertyConstants.A21INDIRECT_COST_RECOVERY_ACCOUNTS);
148        }
149    }
150    
151    /**
152     * Refreshes the references of account
153     * 
154     * @param subaccount SubAccount
155     */
156    void refreshSubObjects(SubAccount subaccount) {
157        if (subaccount != null) {
158            if (subaccount.getA21SubAccount() != null) {
159                subaccount.getA21SubAccount().refreshNonUpdateableReferences();
160                // refresh contacts
161//                if (subaccount.getA21SubAccount().getA21IndirectCostRecoveryAccounts() != null) {
162//                    for (A21IndirectCostRecoveryAccount icra : subaccount.getA21SubAccount().getA21IndirectCostRecoveryAccounts()) {
163//                        icra.refreshNonUpdateableReferences();
164//                    }
165//                }
166            }
167        }
168    }
169
170    /**
171     * This checks that the reporting fields are entered altogether or none at all
172     * 
173     * @return false if only one reporting field filled out and not all of them, true otherwise
174     */
175    protected boolean checkForPartiallyEnteredReportingFields() {
176
177        LOG.debug("Entering checkExistenceAndActive()");
178
179        boolean success = true;
180        boolean allReportingFieldsEntered = false;
181        boolean anyReportingFieldsEntered = false;
182
183        // set a flag if all three reporting fields are filled (this is separated just for readability)
184        if (StringUtils.isNotEmpty(newSubAccount.getFinancialReportChartCode()) && StringUtils.isNotEmpty(newSubAccount.getFinReportOrganizationCode()) && StringUtils.isNotEmpty(newSubAccount.getFinancialReportingCode())) {
185            allReportingFieldsEntered = true;
186        }
187
188        // set a flag if any of the three reporting fields are filled (this is separated just for readability)
189        if (StringUtils.isNotEmpty(newSubAccount.getFinancialReportChartCode()) || StringUtils.isNotEmpty(newSubAccount.getFinReportOrganizationCode()) || StringUtils.isNotEmpty(newSubAccount.getFinancialReportingCode())) {
190            anyReportingFieldsEntered = true;
191        }
192
193        // if any of the three reporting code fields are filled out, all three must be, or none
194        // if any of the three are entered
195        if (anyReportingFieldsEntered && !allReportingFieldsEntered) {
196            putGlobalError(OLEKeyConstants.ERROR_DOCUMENT_SUBACCTMAINT_RPTCODE_ALL_FIELDS_IF_ANY_FIELDS);
197            success &= false;
198        }
199
200        return success;
201    }
202
203    /**
204     * This checks to make sure that if cgAuthorized is false it succeeds immediately, otherwise it checks that all the information
205     * for CG is correctly entered and identified including:
206     * <ul>
207     * <li>If the {@link SubFundGroup} isn't for Contracts and Grants then check to make sure that the cost share and ICR fields are
208     * not empty</li>
209     * <li>If it isn't a child of CG, then the SubAccount must be of type ICR</li>
210     * </ul>
211     * 
212     * @param document
213     * @return true if the user is not authorized to change CG fields, otherwise it checks the above conditions
214     */
215    protected boolean checkCgRules(MaintenanceDocument document) {
216
217        boolean success = true;
218
219        // short circuit if the parent account is NOT part of a CG fund group
220        boolean a21SubAccountRefreshed = false;
221        if (ObjectUtils.isNotNull(newSubAccount.getAccount())) {
222            if (ObjectUtils.isNotNull(newSubAccount.getAccount().getSubFundGroup())) {
223
224                // compare them, exit if the account isn't for contracts and grants
225                if (!SpringContext.getBean(SubFundGroupService.class).isForContractsAndGrants(newSubAccount.getAccount().getSubFundGroup())) {
226
227                    // KULCOA-1116 - Check if CG CS and CG ICR are empty, if not throw an error
228                    if (checkCgCostSharingIsEmpty() == false) {
229                        putFieldError("a21SubAccount.costShareChartOfAccountCode", OLEKeyConstants.ERROR_DOCUMENT_SUBACCTMAINT_NON_FUNDED_ACCT_CS_INVALID, new String[] { SpringContext.getBean(SubFundGroupService.class).getContractsAndGrantsDenotingAttributeLabel(), SpringContext.getBean(SubFundGroupService.class).getContractsAndGrantsDenotingValueForMessage() });
230                        success = false;
231                    }
232
233                    if (checkCgIcrIsEmpty() == false) {
234                        putFieldError("a21SubAccount.indirectCostRecoveryTypeCode", OLEKeyConstants.ERROR_DOCUMENT_SUBACCTMAINT_NON_FUNDED_ACCT_ICR_INVALID, new String[] { SpringContext.getBean(SubFundGroupService.class).getContractsAndGrantsDenotingAttributeLabel(), SpringContext.getBean(SubFundGroupService.class).getContractsAndGrantsDenotingValueForMessage() });
235                        success = false;
236                    }
237
238                    // KULRNE-4660 - this isn't the child of a CG account; sub account must be ICR type
239                    if (!ObjectUtils.isNull(newSubAccount.getA21SubAccount())) {
240                        // KFSMI-798 - refresh() changed to refreshNonUpdateableReferences()
241                        // All references for A21SubAccount are non-updatable
242                        newSubAccount.getA21SubAccount().refreshNonUpdateableReferences();
243                        a21SubAccountRefreshed = true;
244                        if (StringUtils.isEmpty(newSubAccount.getA21SubAccount().getSubAccountTypeCode()) || !newSubAccount.getA21SubAccount().getSubAccountTypeCode().equals(OLEConstants.SubAccountType.EXPENSE)) {
245                            putFieldError("a21SubAccount.subAccountTypeCode", OLEKeyConstants.ERROR_DOCUMENT_SUBACCTMAINT_NON_FUNDED_ACCT_SUB_ACCT_TYPE_CODE_INVALID, new String[] { SpringContext.getBean(SubFundGroupService.class).getContractsAndGrantsDenotingAttributeLabel(), SpringContext.getBean(SubFundGroupService.class).getContractsAndGrantsDenotingValueForMessage() });
246                            success = false;
247                        }
248                    }
249
250                    return success;
251                }
252            }
253        }
254
255        A21SubAccount a21 = newSubAccount.getA21SubAccount();
256        
257        // short circuit if there is no A21SubAccount object at all (ie, null)
258        if (ObjectUtils.isNull(a21)) {
259            return success;
260        }
261
262        // FROM HERE ON IN WE CAN ASSUME THERE IS A VALID A21 SUBACCOUNT OBJECT
263
264        // KFSMI-6848 since there is a ICR Collection Account object, change refresh to perform 
265        // manually refresh the a21SubAccount object, as it wont have been
266        // refreshed by the parent, as its updateable
267        // though only refresh if we didn't refresh in the checks above
268        
269        if (!a21SubAccountRefreshed) {
270            //preserve the ICRAccounts before refresh to prevent the list from dropping
271            List<A21IndirectCostRecoveryAccount>icrAccounts =a21.getA21IndirectCostRecoveryAccounts(); 
272            a21.refresh();
273            a21.setA21IndirectCostRecoveryAccounts(icrAccounts);
274            
275        }
276
277        // C&G A21 Type field must be in the allowed values
278        if (!OLEConstants.SubAccountType.ELIGIBLE_SUB_ACCOUNT_TYPE_CODES.contains(a21.getSubAccountTypeCode())) {
279            putFieldError("a21SubAccount.subAccountTypeCode", OLEKeyConstants.ERROR_DOCUMENT_SUBACCTMAINT_INVALI_SUBACCOUNT_TYPE_CODES, OLEConstants.SubAccountType.ELIGIBLE_SUB_ACCOUNT_TYPE_CODES.toString());
280            success &= false;
281        }
282
283        // get a convenience reference to this code
284        String cgA21TypeCode = a21.getSubAccountTypeCode();
285
286        // if this is a Cost Sharing SubAccount, run the Cost Sharing rules
287        if (OLEConstants.SubAccountType.COST_SHARE.trim().equalsIgnoreCase(StringUtils.trim(cgA21TypeCode))) {
288            success &= checkCgCostSharingRules();
289        }
290
291        // if this is an ICR subaccount, run the ICR rules
292        if (OLEConstants.SubAccountType.EXPENSE.trim().equals(StringUtils.trim(cgA21TypeCode))) {
293            success &= checkCgIcrRules();
294        }
295
296        return success;
297    }
298
299    /**
300     * This checks that if the cost share information is filled out that it is valid and exists, or if fields are missing (such as
301     * the chart of accounts code and account number) an error is recorded
302     * 
303     * @return true if all cost share fields filled out correctly, false if the chart of accounts code and account number for cost
304     *         share are missing
305     */
306    protected boolean checkCgCostSharingRules() {
307
308        boolean success = true;
309        boolean allFieldsSet = false;
310
311        A21SubAccount a21 = newSubAccount.getA21SubAccount();
312
313        // check to see if all required fields are set
314        if (StringUtils.isNotEmpty(a21.getCostShareChartOfAccountCode()) && StringUtils.isNotEmpty(a21.getCostShareSourceAccountNumber())) {
315            allFieldsSet = true;
316        }
317
318        // Cost Sharing COA Code and Cost Sharing Account Number are required
319        success &= checkEmptyBOField("a21SubAccount.costShareChartOfAccountCode", a21.getCostShareChartOfAccountCode(), "Cost Share Chart of Accounts Code");
320        success &= checkEmptyBOField("a21SubAccount.costShareSourceAccountNumber", a21.getCostShareSourceAccountNumber(), "Cost Share AccountNumber");
321
322        // existence test on Cost Share Account
323        if (allFieldsSet) {
324            if (ObjectUtils.isNull(a21.getCostShareAccount())) {
325                putFieldError("a21SubAccount.costShareSourceAccountNumber", OLEKeyConstants.ERROR_EXISTENCE, getDisplayName("a21SubAccount.costShareSourceAccountNumber"));
326                success &= false;
327            }
328        }
329
330        // existence test on Cost Share SubAccount
331        if (allFieldsSet && StringUtils.isNotBlank(a21.getCostShareSourceSubAccountNumber())) {
332            if (ObjectUtils.isNull(a21.getCostShareSourceSubAccount())) {
333                putFieldError("a21SubAccount.costShareSourceSubAccountNumber", OLEKeyConstants.ERROR_EXISTENCE, getDisplayName("a21SubAccount.costShareSourceSubAccountNumber"));
334                success &= false;
335            }
336        }
337
338        // Cost Sharing Account may not be for contracts and grants
339        if (ObjectUtils.isNotNull(a21.getCostShareAccount())) {
340            if (ObjectUtils.isNotNull(a21.getCostShareAccount().getSubFundGroup())) {
341                if (a21.getCostShareAccount().isForContractsAndGrants()) {
342                    putFieldError("a21SubAccount.costShareSourceAccountNumber", OLEKeyConstants.ERROR_DOCUMENT_SUBACCTMAINT_COST_SHARE_ACCOUNT_MAY_NOT_BE_CG_FUNDGROUP, new String[] { SpringContext.getBean(SubFundGroupService.class).getContractsAndGrantsDenotingAttributeLabel(), SpringContext.getBean(SubFundGroupService.class).getContractsAndGrantsDenotingValueForMessage() });
343                    success &= false;
344                }
345            }
346        }
347
348        // The ICR fields must be empty if the sub-account type code is for cost sharing
349        if (checkCgIcrIsEmpty() == false) {
350            putFieldError("a21SubAccount.indirectCostRecoveryTypeCode", OLEKeyConstants.ERROR_DOCUMENT_SUBACCTMAINT_ICR_SECTION_INVALID, a21.getSubAccountTypeCode());
351            success &= false;
352        }
353
354        return success;
355    }
356
357    /**
358     * This checks that if the ICR information is entered that it is valid for this fiscal year and that all of its fields are valid
359     * as well (such as account)
360     * 
361     * @return true if the ICR information is filled in and it is valid
362     */
363    protected boolean checkCgIcrRules() {
364        A21SubAccount a21 = newSubAccount.getA21SubAccount();
365        if(ObjectUtils.isNull(a21)) {
366            return true;
367        }
368
369        boolean success = true;
370        
371        // existence check for ICR Type Code
372        if (StringUtils.isNotEmpty(a21.getIndirectCostRecoveryTypeCode())) {
373            if (ObjectUtils.isNull(a21.getIndirectCostRecoveryType())) {
374                putFieldError("a21SubAccount.indirectCostRecoveryTypeCode", OLEKeyConstants.ERROR_EXISTENCE, "ICR Type Code: " + a21.getIndirectCostRecoveryTypeCode());
375                success = false;
376            }
377        }
378
379        // existence check for Financial Series ID
380        if (StringUtils.isNotEmpty(a21.getFinancialIcrSeriesIdentifier())) {            
381            String fiscalYear = StringUtils.EMPTY + SpringContext.getBean(UniversityDateService.class).getCurrentFiscalYear();
382            String icrSeriesId = a21.getFinancialIcrSeriesIdentifier();
383            
384            Map<String, String> pkMap = new HashMap<String, String>();
385            pkMap.put(OLEPropertyConstants.UNIVERSITY_FISCAL_YEAR, fiscalYear);
386            pkMap.put(OLEPropertyConstants.FINANCIAL_ICR_SERIES_IDENTIFIER, icrSeriesId);
387            Collection<IndirectCostRecoveryRateDetail> icrRateDetails = getBoService().findMatching(IndirectCostRecoveryRateDetail.class, pkMap);
388            
389            if (ObjectUtils.isNull(icrRateDetails) || icrRateDetails.isEmpty()) {
390                String label = SpringContext.getBean(DataDictionaryService.class).getAttributeLabel(A21SubAccount.class, OLEPropertyConstants.FINANCIAL_ICR_SERIES_IDENTIFIER);
391                putFieldError(OLEPropertyConstants.A21_SUB_ACCOUNT + "." + OLEPropertyConstants.FINANCIAL_ICR_SERIES_IDENTIFIER, OLEKeyConstants.ERROR_EXISTENCE, label + " (" + icrSeriesId + ")");
392                success = false;
393            }
394            else {
395                for(IndirectCostRecoveryRateDetail icrRateDetail : icrRateDetails) {
396                    if(ObjectUtils.isNull(icrRateDetail.getIndirectCostRecoveryRate())){                                
397                        putFieldError(OLEPropertyConstants.A21_SUB_ACCOUNT + "." + OLEPropertyConstants.FINANCIAL_ICR_SERIES_IDENTIFIER, OLEKeyConstants.IndirectCostRecovery.ERROR_DOCUMENT_ICR_RATE_NOT_FOUND, new String[]{fiscalYear, icrSeriesId});
398                        success = false;
399                        break;
400                    }
401                }
402            }            
403        }
404
405        // existence check for ICR Account
406        for (A21IndirectCostRecoveryAccount account : a21.getA21ActiveIndirectCostRecoveryAccounts()){
407            if (StringUtils.isNotBlank(account.getIndirectCostRecoveryAccountNumber())
408                && StringUtils.isNotBlank(account.getIndirectCostRecoveryFinCoaCode())){
409                if(ObjectUtils.isNull(account.getIndirectCostRecoveryAccount())){                                
410                    putFieldError(OLEPropertyConstants.A21INDIRECT_COST_RECOVERY_ACCOUNTS, OLEKeyConstants.ERROR_EXISTENCE, "ICR Account: " + account.getIndirectCostRecoveryFinCoaCode() + "-" + account.getIndirectCostRecoveryAccountNumber());
411                    success = false;
412                    break;
413                }
414            }
415        }
416
417        // The cost sharing fields must be empty if the sub-account type code is for ICR
418        if (checkCgCostSharingIsEmpty() == false) {
419            putFieldError("a21SubAccount.costShareChartOfAccountCode", OLEKeyConstants.ERROR_DOCUMENT_SUBACCTMAINT_COST_SHARE_SECTION_INVALID, a21.getSubAccountTypeCode());
420
421            success &= false;
422        }
423
424        return success;
425    }
426
427    /**
428     * This method tests if all fields in the Cost Sharing section are empty.
429     * 
430     * @return true if the cost sharing values passed in are empty, otherwise false.
431     */
432    protected boolean checkCgCostSharingIsEmpty() {
433        boolean success = true;
434
435        A21SubAccount newA21SubAccount = newSubAccount.getA21SubAccount();
436        if (ObjectUtils.isNotNull(newA21SubAccount)) {
437            success &= StringUtils.isEmpty(newA21SubAccount.getCostShareChartOfAccountCode());
438            success &= StringUtils.isEmpty(newA21SubAccount.getCostShareSourceAccountNumber());
439            success &= StringUtils.isEmpty(newA21SubAccount.getCostShareSourceSubAccountNumber());
440        }
441
442        return success;
443    }
444
445    /**
446     * This method tests if all fields in the ICR section are empty.
447     * 
448     * @return true if the ICR values passed in are empty, otherwise false.
449     */
450    protected boolean checkCgIcrIsEmpty() {
451        boolean success = true;
452        
453        A21SubAccount newA21SubAccount = newSubAccount.getA21SubAccount();
454        if (ObjectUtils.isNotNull(newA21SubAccount)) {
455            success &= StringUtils.isEmpty(newA21SubAccount.getFinancialIcrSeriesIdentifier());
456            
457            success &= checkICRCollectionExist(false);
458            success &= StringUtils.isEmpty(newA21SubAccount.getIndirectCostRecoveryTypeCode());
459            // this is a boolean, so create any value if set to true, meaning a user checked the box, otherwise assume it's empty
460            success &= StringUtils.isEmpty(newA21SubAccount.getOffCampusCode() ? "1" : "");
461        }
462
463        return success;
464    }
465
466    /**
467     * This method tests the value entered, and if there is anything there it logs a new error, and returns false.
468     * 
469     * @param value - String value to be tested
470     * @param fieldName - name of the field being tested
471     * @return false if there is any value in value, otherwise true
472     */
473    protected boolean disallowAnyValues(String value, String fieldName) {
474        if (StringUtils.isNotEmpty(value)) {
475            putFieldError(fieldName, OLEKeyConstants.ERROR_DOCUMENT_SUBACCTMAINT_NOT_AUTHORIZED_ENTER_CG_FIELDS, getDisplayName(fieldName));
476            return false;
477        }
478        return true;
479    }
480
481    /**
482     * This method tests the two values entered, and if there is any change between the two, it logs an error, and returns false.
483     * Note that the comparison is done after trimming both leading and trailing whitespace from both strings, and then doing a
484     * case-insensitive comparison.
485     * 
486     * @param oldValue - the original String value of the field
487     * @param newValue - the new String value of the field
488     * @param fieldName - name of the field being tested
489     * @return false if there is any difference between the old and new, true otherwise
490     */
491    protected boolean disallowChangedValues(String oldValue, String newValue, String fieldName) {
492
493        if (isFieldValueChanged(oldValue, newValue)) {
494            putFieldError(fieldName, OLEKeyConstants.ERROR_DOCUMENT_SUBACCTMAINT_NOT_AUTHORIZED_CHANGE_CG_FIELDS, getDisplayName(fieldName));
495            return false;
496        }
497        return true;
498    }
499
500    /**
501     * This compares two string values to see if the newValue has changed from the oldValue
502     * 
503     * @param oldValue - original value
504     * @param newValue - new value
505     * @return true if the two fields are different from each other
506     */
507    protected boolean isFieldValueChanged(String oldValue, String newValue) {
508
509        if (StringUtils.isBlank(oldValue) && StringUtils.isBlank(newValue)) {
510            return false;
511        }
512
513        if (StringUtils.isBlank(oldValue) && StringUtils.isNotBlank(newValue)) {
514            return true;
515        }
516
517        if (StringUtils.isNotBlank(oldValue) && StringUtils.isBlank(newValue)) {
518            return true;
519        }
520
521        if (!oldValue.trim().equalsIgnoreCase(newValue.trim())) {
522            return true;
523        }
524
525        return false;
526    }
527
528
529    /**
530     * This method retrieves the label name for a specific property
531     * 
532     * @param propertyName - property to retrieve label for (from the DD)
533     * @return the label
534     */
535    protected String getDisplayName(String propertyName) {
536        return getDdService().getAttributeLabel(SubAccount.class, propertyName);
537    }
538
539}