001/**
002 * Copyright 2005-2018 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.rice.krad.rules;
017
018import java.security.GeneralSecurityException;
019import java.util.ArrayList;
020import java.util.HashMap;
021import java.util.Iterator;
022import java.util.List;
023import java.util.Map;
024import java.util.Properties;
025import java.util.Set;
026
027import org.apache.commons.lang.StringUtils;
028import org.kuali.rice.core.api.CoreApiServiceLocator;
029import org.kuali.rice.core.api.config.property.ConfigurationService;
030import org.kuali.rice.core.api.datetime.DateTimeService;
031import org.kuali.rice.core.api.exception.RiceIllegalArgumentException;
032import org.kuali.rice.core.api.mo.common.active.MutableInactivatable;
033import org.kuali.rice.core.api.util.RiceKeyConstants;
034import org.kuali.rice.core.web.format.Formatter;
035import org.kuali.rice.kew.api.WorkflowDocument;
036import org.kuali.rice.kim.api.identity.PersonService;
037import org.kuali.rice.kim.api.role.RoleService;
038import org.kuali.rice.kim.api.services.KimApiServiceLocator;
039import org.kuali.rice.krad.bo.PersistableBusinessObjectBaseAdapter;
040import org.kuali.rice.krad.data.DataObjectService;
041import org.kuali.rice.krad.data.KradDataServiceLocator;
042import org.kuali.rice.krad.datadictionary.InactivationBlockingMetadata;
043import org.kuali.rice.krad.datadictionary.validation.ErrorLevel;
044import org.kuali.rice.krad.datadictionary.validation.result.ConstraintValidationResult;
045import org.kuali.rice.krad.datadictionary.validation.result.DictionaryValidationResult;
046import org.kuali.rice.krad.document.Document;
047import org.kuali.rice.krad.exception.ValidationException;
048import org.kuali.rice.krad.maintenance.BulkUpdateMaintainable;
049import org.kuali.rice.krad.maintenance.BulkUpdateMaintenanceDataObject;
050import org.kuali.rice.krad.maintenance.Maintainable;
051import org.kuali.rice.krad.maintenance.MaintenanceDocument;
052import org.kuali.rice.krad.maintenance.MaintenanceDocumentAuthorizer;
053import org.kuali.rice.krad.rules.rule.event.AddCollectionLineEvent;
054import org.kuali.rice.krad.rules.rule.event.ApproveDocumentEvent;
055import org.kuali.rice.krad.service.DataDictionaryService;
056import org.kuali.rice.krad.service.DataObjectAuthorizationService;
057import org.kuali.rice.krad.service.DictionaryValidationService;
058import org.kuali.rice.krad.service.InactivationBlockingDetectionService;
059import org.kuali.rice.krad.service.InactivationBlockingDisplayService;
060import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
061import org.kuali.rice.krad.service.LegacyDataAdapter;
062import org.kuali.rice.krad.util.ErrorMessage;
063import org.kuali.rice.krad.util.ForeignKeyFieldsPopulationState;
064import org.kuali.rice.krad.util.GlobalVariables;
065import org.kuali.rice.krad.util.KRADConstants;
066import org.kuali.rice.krad.util.KRADPropertyConstants;
067import org.kuali.rice.krad.util.RouteToCompletionUtil;
068import org.kuali.rice.krad.util.UrlFactory;
069import org.kuali.rice.krad.workflow.service.WorkflowDocumentService;
070import org.springframework.util.AutoPopulatingList;
071
072/**
073 * Contains all of the business rules that are common to all maintenance documents.
074 *
075 * @author Kuali Rice Team (rice.collab@kuali.org)
076 */
077public class MaintenanceDocumentRuleBase extends DocumentRuleBase implements MaintenanceDocumentRule {
078    protected static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(MaintenanceDocumentRuleBase.class);
079
080    // these two constants are used to correctly prefix errors added to
081    // the global errors
082    public static final String MAINTAINABLE_ERROR_PREFIX = KRADConstants.MAINTENANCE_NEW_MAINTAINABLE;
083    public static final String DOCUMENT_ERROR_PREFIX = "document.";
084    public static final String MAINTAINABLE_ERROR_PATH = DOCUMENT_ERROR_PREFIX + "newMaintainableObject";
085
086    private DataDictionaryService ddService;
087    private DataObjectService dataObjectService;
088    private DictionaryValidationService dictionaryValidationService;
089    private ConfigurationService configService;
090    private WorkflowDocumentService workflowDocumentService;
091    private PersonService personService;
092    private RoleService roleService;
093    private DataObjectAuthorizationService dataObjectAuthorizationService;
094
095    private Object oldDataObject;
096    private Object newDataObject;
097    private Class<?> dataObjectClass;
098
099    protected List<String> priorErrorPath;
100
101    /**
102     * Default constructor a MaintenanceDocumentRuleBase.java.
103     */
104    public MaintenanceDocumentRuleBase() {
105        priorErrorPath = new ArrayList<String>();
106    }
107
108    /**
109     * @see MaintenanceDocumentRule#processSaveDocument(org.kuali.rice.krad.document.Document)
110     */
111    @Override
112    public boolean processSaveDocument(Document document) {
113        MaintenanceDocument maintenanceDocument = (MaintenanceDocument) document;
114
115        // remove all items from the errorPath temporarily (because it may not
116        // be what we expect, or what we need)
117        clearErrorPath();
118
119        // setup convenience pointers to the old & new bo
120        setupBaseConvenienceObjects(maintenanceDocument);
121
122        // the document must be in a valid state for saving. this does not include business
123        // rules, but just enough testing that the document is populated and in a valid state
124        // to not cause exceptions when saved. if this passes, then the save will always occur,
125        // regardless of business rules.
126        if (!isDocumentValidForSave(maintenanceDocument)) {
127            resumeErrorPath();
128            return false;
129        }
130
131        // apply rules that are specific to the class of the maintenance document
132        // (if implemented). this will always succeed if not overloaded by the
133        // subclass
134        if (!processCustomSaveDocumentBusinessRules(maintenanceDocument)) {
135            resumeErrorPath();
136            return false;
137        }
138
139        // return the original set of items to the errorPath
140        resumeErrorPath();
141
142        // return the original set of items to the errorPath, to ensure no impact
143        // on other upstream or downstream items that rely on the errorPath
144        return true;
145    }
146
147    /**
148     * @see MaintenanceDocumentRule#processRouteDocument(org.kuali.rice.krad.document.Document)
149     */
150    @Override
151    public boolean processRouteDocument(Document document) {
152        LOG.info("processRouteDocument called");
153
154        MaintenanceDocument maintenanceDocument = (MaintenanceDocument) document;
155
156        boolean completeRequestPending = RouteToCompletionUtil.checkIfAtleastOneAdHocCompleteRequestExist(
157                maintenanceDocument);
158
159        // Validate the document if the header is valid and no pending completion requests
160        if (completeRequestPending) {
161            return true;
162        }
163
164        // get the documentAuthorizer for this document
165        MaintenanceDocumentAuthorizer documentAuthorizer =
166                (MaintenanceDocumentAuthorizer) getDocumentDictionaryService().getDocumentAuthorizer(document);
167
168        // remove all items from the errorPath temporarily (because it may not
169        // be what we expect, or what we need)
170        clearErrorPath();
171
172        // setup convenience pointers to the old & new bo
173        setupBaseConvenienceObjects(maintenanceDocument);
174
175        // apply rules that are common across all maintenance documents, regardless of class
176        processGlobalSaveDocumentBusinessRules(maintenanceDocument);
177
178        // from here on, it is in a default-success mode, and will route unless one of the
179        // business rules stop it.
180        boolean success = true;
181
182        WorkflowDocument workflowDocument = document.getDocumentHeader().getWorkflowDocument();
183        if (workflowDocument.isInitiated() || workflowDocument.isSaved()) {
184            try {
185                success &= documentAuthorizer.canCreateOrMaintain((MaintenanceDocument) document,
186                        GlobalVariables.getUserSession().getPerson());
187                if (success == false) {
188                    GlobalVariables.getMessageMap().putError(KRADConstants.DOCUMENT_ERRORS,
189                            RiceKeyConstants.AUTHORIZATION_ERROR_DOCUMENT,
190                            new String[]{GlobalVariables.getUserSession().getPerson().getPrincipalName(),
191                                    "Create/Maintain", getDocumentDictionaryService().getMaintenanceDocumentTypeName(
192                                    newDataObject.getClass())});
193                }
194            } catch (RiceIllegalArgumentException e) {
195                // TODO error message the right way
196                GlobalVariables.getMessageMap().putError("Unable to determine authorization due to previous errors",
197                        "Unable to determine authorization due to previous errors");
198            }
199        }
200        // apply rules that are common across all maintenance documents, regardless of class
201        success &= processGlobalRouteDocumentBusinessRules(maintenanceDocument);
202
203        // apply rules that are specific to the class of the maintenance document
204        // (if implemented). this will always succeed if not overloaded by the
205        // subclass
206        success &= processCustomRouteDocumentBusinessRules(maintenanceDocument);
207
208        success &= processInactivationBlockChecking(maintenanceDocument);
209
210        // return the original set of items to the errorPath, to ensure no impact
211        // on other upstream or downstream items that rely on the errorPath
212        resumeErrorPath();
213
214        return success;
215    }
216
217    /**
218     * Determines whether a document is inactivating the record being maintained
219     *
220     * @param maintenanceDocument
221     * @return true iff the document is inactivating the business object; false otherwise
222     */
223    protected boolean isDocumentInactivatingBusinessObject(MaintenanceDocument maintenanceDocument) {
224        if (maintenanceDocument.isEdit()) {
225            Class<?> dataObjectClass = maintenanceDocument.getNewMaintainableObject().getDataObjectClass();
226            // we can only be inactivating a business object if we're editing it
227            if (dataObjectClass != null && MutableInactivatable.class.isAssignableFrom(dataObjectClass)) {
228                MutableInactivatable oldInactivateableBO = (MutableInactivatable) oldDataObject;
229                MutableInactivatable newInactivateableBO = (MutableInactivatable) newDataObject;
230
231                return oldInactivateableBO.isActive() && !newInactivateableBO.isActive();
232            }
233        }
234        return false;
235    }
236
237    /**
238     * Determines whether this document has been inactivation blocked
239     *
240     * @param maintenanceDocument
241     * @return true iff there is NOTHING that blocks this record
242     */
243    protected boolean processInactivationBlockChecking(MaintenanceDocument maintenanceDocument) {
244        if (isDocumentInactivatingBusinessObject(maintenanceDocument)) {
245            Class<?> dataObjectClass = maintenanceDocument.getNewMaintainableObject().getDataObjectClass();
246            Set<InactivationBlockingMetadata> inactivationBlockingMetadatas =
247                    getDataDictionaryService().getAllInactivationBlockingDefinitions(dataObjectClass);
248
249            if (inactivationBlockingMetadatas != null) {
250                for (InactivationBlockingMetadata inactivationBlockingMetadata : inactivationBlockingMetadatas) {
251                    // for the purposes of maint doc validation, we only need to look for the first blocking record
252
253                    // we found a blocking record, so we return false
254                    if (!processInactivationBlockChecking(maintenanceDocument, inactivationBlockingMetadata)) {
255                        return false;
256                    }
257                }
258            }
259        }
260        return true;
261    }
262
263    /**
264     * Given a InactivationBlockingMetadata, which represents a relationship that may block inactivation of a BO, it
265     * determines whether there
266     * is a record that violates the blocking definition
267     *
268     * @param maintenanceDocument
269     * @param inactivationBlockingMetadata
270     * @return true iff, based on the InactivationBlockingMetadata, the maintenance document should be allowed to route
271     */
272    protected boolean processInactivationBlockChecking(MaintenanceDocument maintenanceDocument,
273            InactivationBlockingMetadata inactivationBlockingMetadata) {
274        String inactivationBlockingDetectionServiceBeanName =
275                inactivationBlockingMetadata.getInactivationBlockingDetectionServiceBeanName();
276        if (StringUtils.isBlank(inactivationBlockingDetectionServiceBeanName)) {
277            inactivationBlockingDetectionServiceBeanName =
278                    KRADServiceLocatorWeb.DEFAULT_INACTIVATION_BLOCKING_DETECTION_SERVICE;
279        }
280        InactivationBlockingDetectionService inactivationBlockingDetectionService =
281                KRADServiceLocatorWeb.getInactivationBlockingDetectionService(
282                        inactivationBlockingDetectionServiceBeanName);
283
284        boolean foundBlockingRecord = inactivationBlockingDetectionService.detectBlockingRecord(
285                newDataObject, inactivationBlockingMetadata);
286
287        if (foundBlockingRecord) {
288            putInactivationBlockingErrorOnPage(maintenanceDocument, inactivationBlockingMetadata);
289        }
290
291        return !foundBlockingRecord;
292    }
293
294    /**
295     * If there is a violation of an InactivationBlockingMetadata, it prints out an appropriate error into the error
296     * map
297     *
298     * @param document
299     * @param inactivationBlockingMetadata
300     */
301    protected void putInactivationBlockingErrorOnPage(MaintenanceDocument document,
302            InactivationBlockingMetadata inactivationBlockingMetadata) {
303        if (!getLegacyDataAdapter().hasPrimaryKeyFieldValues(newDataObject)) {
304            throw new RuntimeException("Maintenance document did not have all primary key values filled in.");
305        }
306
307        // Even though we found a blocking record in the passed in InactivationBlockingMetada,
308        // we need to look at all inactivationBlockingMetadata associated dataObjectClass for error display
309        Class boClass = document.getNewMaintainableObject().getDataObjectClass();
310        Set<InactivationBlockingMetadata> inactivationBlockingMetadatas =
311                getDataDictionaryService().getAllInactivationBlockingDefinitions(boClass);
312
313        StringBuffer errorMessage = new StringBuffer();
314
315        if (inactivationBlockingMetadatas != null ) {
316
317            InactivationBlockingDisplayService inactivationBlockingDisplayService = KRADServiceLocatorWeb
318                    .getInactivationBlockingDisplayService();
319
320            for (InactivationBlockingMetadata blockingMetadata : inactivationBlockingMetadatas) {
321
322                String blockingLabel = getDataDictionaryService().getDataDictionary().getDataObjectEntry(inactivationBlockingMetadata.getBlockingDataObjectClass().getName()).getObjectLabel();
323
324                String relationshipLabel = inactivationBlockingMetadata.getRelationshipLabel();
325                String displayLabel;
326
327                if (StringUtils.isEmpty(relationshipLabel)) {
328
329                    displayLabel = blockingLabel;
330                } else {
331                    displayLabel = blockingLabel + " (" + relationshipLabel + ")";
332                }
333                List<String> blockerObjectList = inactivationBlockingDisplayService.displayAllBlockingRecords(newDataObject,
334                        inactivationBlockingMetadata);
335
336                if (!blockerObjectList.isEmpty()) {
337                    errorMessage.append("<h4>"+blockingLabel+"</h4>");
338                    for(String blockerKey : blockerObjectList) {
339                        errorMessage.append("<li>");
340                        errorMessage.append(blockerKey);
341                        errorMessage.append("</li>");
342                    }
343                }
344
345                errorMessage.append("<br>");
346            }
347        }
348
349        // post an error about the locked document
350        GlobalVariables.getMessageMap().putError(KRADConstants.GLOBAL_ERRORS,
351                RiceKeyConstants.ERROR_INACTIVATION_BLOCKED, false, errorMessage.toString());
352    }
353
354    /**
355     * @see MaintenanceDocumentRule#processApproveDocument(org.kuali.rice.krad.rules.rule.event.ApproveDocumentEvent)
356     */
357    @Override
358    public boolean processApproveDocument(ApproveDocumentEvent approveEvent) {
359        MaintenanceDocument maintenanceDocument = (MaintenanceDocument) approveEvent.getDocument();
360
361        // remove all items from the errorPath temporarily (because it may not
362        // be what we expect, or what we need)
363        clearErrorPath();
364
365        // setup convenience pointers to the old & new bo
366        setupBaseConvenienceObjects(maintenanceDocument);
367
368        // apply rules that are common across all maintenance documents, regardless of class
369        processGlobalSaveDocumentBusinessRules(maintenanceDocument);
370
371        // from here on, it is in a default-success mode, and will approve unless one of the
372        // business rules stop it.
373        boolean success = true;
374
375        // apply rules that are common across all maintenance documents, regardless of class
376        success &= processGlobalApproveDocumentBusinessRules(maintenanceDocument);
377
378        // apply rules that are specific to the class of the maintenance document
379        // (if implemented). this will always succeed if not overloaded by the
380        // subclass
381        success &= processCustomApproveDocumentBusinessRules(maintenanceDocument);
382
383        // return the original set of items to the errorPath, to ensure no impact
384        // on other upstream or downstream items that rely on the errorPath
385        resumeErrorPath();
386
387        return success;
388    }
389
390    /**
391     * {@inheritDoc}
392     *
393     * This implementation additionally performs existence and duplicate checks.
394     */
395    @Override
396    public boolean processAddCollectionLine(AddCollectionLineEvent addEvent) {
397        MaintenanceDocument maintenanceDocument = (MaintenanceDocument) addEvent.getDocument();
398        String collectionName = addEvent.getCollectionName();
399        Object addLine = addEvent.getAddLine();
400
401        // setup convenience pointers to the old & new bo
402        setupBaseConvenienceObjects(maintenanceDocument);
403
404        // from here on, it is in a default-success mode, and will add the line unless one of the
405        // business rules stop it.
406        boolean success = true;
407
408        // TODO: Will be covered by KULRICE-7666
409        /*
410        // apply rules that check whether child objects that cannot be hooked up by normal means exist and are active   */
411
412        success &= getDictionaryValidationService().validateDefaultExistenceChecksForNewCollectionItem(
413                maintenanceDocument.getNewMaintainableObject().getDataObject(), addLine, collectionName);
414
415        // apply rules that are specific to the class of the maintenance document (if implemented)
416        success &= processCustomAddCollectionLineBusinessRules(maintenanceDocument, collectionName, addLine);
417
418        return success;
419    }
420
421    /**
422     * This method is a convenience method to easily add a Document level error (ie, one not tied to a specific field,
423     * but
424     * applicable to the whole document).
425     *
426     * @param errorConstant - Error Constant that can be mapped to a resource for the actual text message.
427     */
428    protected void putGlobalError(String errorConstant) {
429        if (!errorAlreadyExists(KRADConstants.DOCUMENT_ERRORS, errorConstant)) {
430            GlobalVariables.getMessageMap().putErrorWithoutFullErrorPath(KRADConstants.DOCUMENT_ERRORS, errorConstant);
431        }
432    }
433
434    /**
435     * This method is a convenience method to easily add a Document level error (ie, one not tied to a specific field,
436     * but
437     * applicable to the whole document).
438     *
439     * @param errorConstant - Error Constant that can be mapped to a resource for the actual text message.
440     * @param parameter - Replacement value for part of the error message.
441     */
442    protected void putGlobalError(String errorConstant, String parameter) {
443        if (!errorAlreadyExists(KRADConstants.DOCUMENT_ERRORS, errorConstant)) {
444            GlobalVariables.getMessageMap().putErrorWithoutFullErrorPath(KRADConstants.DOCUMENT_ERRORS, errorConstant,
445                    parameter);
446        }
447    }
448
449    /**
450     * This method is a convenience method to easily add a Document level error (ie, one not tied to a specific field,
451     * but
452     * applicable to the whole document).
453     *
454     * @param errorConstant - Error Constant that can be mapped to a resource for the actual text message.
455     * @param parameters - Array of replacement values for part of the error message.
456     */
457    protected void putGlobalError(String errorConstant, String[] parameters) {
458        if (!errorAlreadyExists(KRADConstants.DOCUMENT_ERRORS, errorConstant)) {
459            GlobalVariables.getMessageMap().putErrorWithoutFullErrorPath(KRADConstants.DOCUMENT_ERRORS, errorConstant,
460                    parameters);
461        }
462    }
463
464    /**
465     * This method is a convenience method to add a property-specific error to the global errors list. This method
466     * makes
467     * sure that
468     * the correct prefix is added to the property name so that it will display correctly on maintenance documents.
469     *
470     * @param propertyName - Property name of the element that is associated with the error. Used to mark the field as
471     * errored in
472     * the UI.
473     * @param errorConstant - Error Constant that can be mapped to a resource for the actual text message.
474     */
475    protected void putFieldError(String propertyName, String errorConstant) {
476        if (!errorAlreadyExists(MAINTAINABLE_ERROR_PREFIX + propertyName, errorConstant)) {
477            GlobalVariables.getMessageMap().putErrorWithoutFullErrorPath(MAINTAINABLE_ERROR_PREFIX + propertyName,
478                    errorConstant);
479        }
480    }
481
482    /**
483     * This method is a convenience method to add a property-specific error to the global errors list. This method
484     * makes
485     * sure that
486     * the correct prefix is added to the property name so that it will display correctly on maintenance documents.
487     *
488     * @param propertyName - Property name of the element that is associated with the error. Used to mark the field as
489     * errored in
490     * the UI.
491     * @param errorConstant - Error Constant that can be mapped to a resource for the actual text message.
492     * @param parameter - Single parameter value that can be used in the message so that you can display specific
493     * values
494     * to the
495     * user.
496     */
497    protected void putFieldError(String propertyName, String errorConstant, String parameter) {
498        if (!errorAlreadyExists(MAINTAINABLE_ERROR_PREFIX + propertyName, errorConstant)) {
499            GlobalVariables.getMessageMap().putErrorWithoutFullErrorPath(MAINTAINABLE_ERROR_PREFIX + propertyName,
500                    errorConstant, parameter);
501        }
502    }
503
504    /**
505     * This method is a convenience method to add a property-specific error to the global errors list. This method
506     * makes
507     * sure that
508     * the correct prefix is added to the property name so that it will display correctly on maintenance documents.
509     *
510     * @param propertyName - Property name of the element that is associated with the error. Used to mark the field as
511     * errored in
512     * the UI.
513     * @param errorConstant - Error Constant that can be mapped to a resource for the actual text message.
514     * @param parameters - Array of strings holding values that can be used in the message so that you can display
515     * specific values
516     * to the user.
517     */
518    protected void putFieldError(String propertyName, String errorConstant, String[] parameters) {
519        if (!errorAlreadyExists(MAINTAINABLE_ERROR_PREFIX + propertyName, errorConstant)) {
520            GlobalVariables.getMessageMap().putErrorWithoutFullErrorPath(MAINTAINABLE_ERROR_PREFIX + propertyName,
521                    errorConstant, parameters);
522        }
523    }
524
525    /**
526     * Adds a property-specific error to the global errors list, with the DD short label as the single argument.
527     *
528     * @param propertyName - Property name of the element that is associated with the error. Used to mark the field as
529     * errored in
530     * the UI.
531     * @param errorConstant - Error Constant that can be mapped to a resource for the actual text message.
532     */
533    protected void putFieldErrorWithShortLabel(String propertyName, String errorConstant) {
534        String shortLabel = getDataDictionaryService().getAttributeShortLabel(dataObjectClass, propertyName);
535        putFieldError(propertyName, errorConstant, shortLabel);
536    }
537
538    /**
539     * This method is a convenience method to add a property-specific document error to the global errors list. This
540     * method makes
541     * sure that the correct prefix is added to the property name so that it will display correctly on maintenance
542     * documents.
543     *
544     * @param propertyName - Property name of the element that is associated with the error. Used to mark the field as
545     * errored in
546     * the UI.
547     * @param errorConstant - Error Constant that can be mapped to a resource for the actual text message.
548     * @param parameter - Single parameter value that can be used in the message so that you can display specific
549     * values
550     * to the
551     * user.
552     */
553    protected void putDocumentError(String propertyName, String errorConstant, String parameter) {
554        if (!errorAlreadyExists(DOCUMENT_ERROR_PREFIX + propertyName, errorConstant)) {
555            GlobalVariables.getMessageMap().putError(DOCUMENT_ERROR_PREFIX + propertyName, errorConstant, parameter);
556        }
557    }
558
559    /**
560     * This method is a convenience method to add a property-specific document error to the global errors list. This
561     * method makes
562     * sure that the correct prefix is added to the property name so that it will display correctly on maintenance
563     * documents.
564     *
565     * @param propertyName - Property name of the element that is associated with the error. Used to mark the field as
566     * errored in
567     * the UI.
568     * @param errorConstant - Error Constant that can be mapped to a resource for the actual text message.
569     * @param parameters - Array of String parameters that can be used in the message so that you can display specific
570     * values to the
571     * user.
572     */
573    protected void putDocumentError(String propertyName, String errorConstant, String[] parameters) {
574        GlobalVariables.getMessageMap().putError(DOCUMENT_ERROR_PREFIX + propertyName, errorConstant, parameters);
575    }
576
577    /**
578     * Convenience method to determine whether the field already has the message indicated.
579     *
580     * This is useful if you want to suppress duplicate error messages on the same field.
581     *
582     * @param propertyName - propertyName you want to test on
583     * @param errorConstant - errorConstant you want to test
584     * @return returns True if the propertyName indicated already has the errorConstant indicated, false otherwise
585     */
586    protected boolean errorAlreadyExists(String propertyName, String errorConstant) {
587        if (GlobalVariables.getMessageMap().fieldHasMessage(propertyName, errorConstant)) {
588            return true;
589        } else {
590            return false;
591        }
592    }
593
594    /**
595     * This method specifically doesn't put any prefixes before the error so that the developer can do things specific
596     * to the
597     * globals errors (like newDelegateChangeDocument errors)
598     *
599     * @param propertyName
600     * @param errorConstant
601     */
602    protected void putGlobalsError(String propertyName, String errorConstant) {
603        if (!errorAlreadyExists(propertyName, errorConstant)) {
604            GlobalVariables.getMessageMap().putErrorWithoutFullErrorPath(propertyName, errorConstant);
605        }
606    }
607
608    /**
609     * This method specifically doesn't put any prefixes before the error so that the developer can do things specific
610     * to the
611     * globals errors (like newDelegateChangeDocument errors)
612     *
613     * @param propertyName
614     * @param errorConstant
615     * @param parameter
616     */
617    protected void putGlobalsError(String propertyName, String errorConstant, String parameter) {
618        if (!errorAlreadyExists(propertyName, errorConstant)) {
619            GlobalVariables.getMessageMap().putErrorWithoutFullErrorPath(propertyName, errorConstant, parameter);
620        }
621    }
622
623    /**
624     * This method is used to deal with error paths that are not what we expect them to be. This method, along with
625     * resumeErrorPath() are used to temporarily clear the errorPath, and then return it to the original state after
626     * the
627     * rule is
628     * executed.
629     *
630     * This method is called at the very beginning of rule enforcement and pulls a copy of the contents of the
631     * errorPath
632     * ArrayList
633     * to a local arrayList for temporary storage.
634     */
635    protected void clearErrorPath() {
636        // add all the items from the global list to the local list
637        priorErrorPath.addAll(GlobalVariables.getMessageMap().getErrorPath());
638
639        // clear the global list
640        GlobalVariables.getMessageMap().getErrorPath().clear();
641    }
642
643    /**
644     * This method is used to deal with error paths that are not what we expect them to be. This method, along with
645     * clearErrorPath()
646     * are used to temporarily clear the errorPath, and then return it to the original state after the rule is
647     * executed.
648     *
649     * This method is called at the very end of the rule enforcement, and returns the temporarily stored copy of the
650     * errorPath to
651     * the global errorPath, so that no other classes are interrupted.
652     */
653    protected void resumeErrorPath() {
654        // revert the global errorPath back to what it was when we entered this
655        // class
656        GlobalVariables.getMessageMap().getErrorPath().addAll(priorErrorPath);
657    }
658
659    /**
660     * Executes the DataDictionary Validation against the document.
661     *
662     * @param document
663     * @return true if it passes DD validation, false otherwise
664     */
665    protected boolean dataDictionaryValidate(MaintenanceDocument document) {
666        LOG.debug("MaintenanceDocument validation beginning");
667        boolean success = true;
668        // explicitly put the errorPath that the dictionaryValidationService
669        // requires
670        GlobalVariables.getMessageMap().addToErrorPath("document.newMaintainableObject");
671
672        // document must have a newMaintainable object
673        Maintainable newMaintainable = document.getNewMaintainableObject();
674        if (newMaintainable == null) {
675            GlobalVariables.getMessageMap().removeFromErrorPath("document.newMaintainableObject");
676            throw new ValidationException(
677                    "Maintainable object from Maintenance Document '" + document.getDocumentTitle() +
678                            "' is null, unable to proceed.");
679        }
680
681        // document's newMaintainable must contain an object (ie, not null)
682        Object dataObject = newMaintainable.getDataObject();
683        if (dataObject == null) {
684            GlobalVariables.getMessageMap().removeFromErrorPath("document.newMaintainableObject.");
685            throw new ValidationException("Maintainable's component business object is null.");
686        }
687
688        // check if there are errors in validating the business object
689        GlobalVariables.getMessageMap().addToErrorPath("dataObject");
690        DictionaryValidationResult dictionaryValidationResult = getDictionaryValidationService().validate(
691                newDataObject);
692        if (dictionaryValidationResult.getNumberOfErrors() > 0) {
693            for (ConstraintValidationResult cvr : dictionaryValidationResult) {
694                if (cvr.getStatus() == ErrorLevel.ERROR) {
695                    GlobalVariables.getMessageMap().putError(cvr.getAttributePath(), cvr.getErrorKey());
696                }
697            }
698        }
699
700        // validate default existence checks
701        // TODO: Default existence checks need support for general data objects, see KULRICE-7666
702        success &= getDictionaryValidationService().validateDefaultExistenceChecks(dataObject);
703        GlobalVariables.getMessageMap().removeFromErrorPath("dataObject");
704
705        // explicitly remove the errorPath we've added
706        GlobalVariables.getMessageMap().removeFromErrorPath("document.newMaintainableObject");
707
708        LOG.debug("MaintenanceDocument validation ending");
709        return success;
710    }
711
712    /**
713     * This method checks the two major cases that may violate primary key integrity.
714     *
715     * 1. Disallow changing of the primary keys on an EDIT maintenance document. Other fields can be changed, but once
716     * the primary
717     * keys have been set, they are permanent.
718     *
719     * 2. Disallow creating a new object whose primary key values are already present in the system on a CREATE NEW
720     * maintenance
721     * document.
722     *
723     * This method also will add new Errors to the Global Error Map.
724     *
725     * @param document - The Maintenance Document being tested.
726     * @return Returns false if either test failed, otherwise returns true.
727     */
728    protected boolean primaryKeyCheck(MaintenanceDocument document) {
729        // default to success if no failures
730        boolean success = true;
731        Class<?> dataObjectClass = document.getNewMaintainableObject().getDataObjectClass();
732
733        Object oldBo = document.getOldMaintainableObject().getDataObject();
734        Object newDataObject = document.getNewMaintainableObject().getDataObject();
735
736        // We don't do primaryKeyChecks on Bulk Update Data Object maintenance documents. This is
737        // because it doesn't really make any sense to do so, given the behavior of Bulk Update. When a
738        // Bulk Update Document completes, it will update or create a new record for each BO in the list.
739        // As a result, there's no problem with having existing BO records in the system, they will
740        // simply get updated.
741        if (newDataObject instanceof BulkUpdateMaintenanceDataObject) {
742            return success;
743        }
744
745        // fail and complain if the person has changed the primary keys on
746        // an EDIT maintenance document.
747        if (document.isEdit()) {
748            if (!getLegacyDataAdapter().equalsByPrimaryKeys(oldBo, newDataObject)) {
749                // add a complaint to the errors
750                putDocumentError(KRADConstants.DOCUMENT_ERRORS,
751                        RiceKeyConstants.ERROR_DOCUMENT_MAINTENANCE_PRIMARY_KEYS_CHANGED_ON_EDIT,
752                        getHumanReadablePrimaryKeyFieldNames(dataObjectClass));
753                success &= false;
754            }
755        }
756
757        // fail and complain if the person has selected a new object with keys that already exist
758        // in the DB.
759        else if (document.isNew()) {
760
761            // TODO: when/if we have standard support for DO retrieval, do this check for DO's
762            if (newDataObject instanceof PersistableBusinessObjectBaseAdapter) {
763
764                // get a map of the pk field names and values
765                Map<String, ?> newPkFields = getLegacyDataAdapter().getPrimaryKeyFieldValuesDOMDS(newDataObject);
766
767                //Remove any parts of the pk that has a null value as JPA will throw an error
768                Map<String, Object> filteredPkFields = new HashMap<String, Object>();
769                filteredPkFields.putAll(newPkFields);
770
771                Iterator it = newPkFields.entrySet().iterator();
772                while (it.hasNext()) {
773                    Map.Entry pairs = (Map.Entry)it.next();
774                    if(pairs.getValue() == null){
775                       filteredPkFields.remove(pairs.getKey());
776                    }
777                }
778
779                if(!filteredPkFields.isEmpty()){
780                    // attempt to do a lookup, see if this object already exists by these Primary Keys
781                    Object testBo = getLegacyDataAdapter().findByPrimaryKey(dataObjectClass, filteredPkFields);
782
783                    // if the retrieve was successful, then this object already exists, and we need
784                    // to complain
785                    if (testBo != null) {
786                        putDocumentError(KRADConstants.DOCUMENT_ERRORS,
787                                RiceKeyConstants.ERROR_DOCUMENT_MAINTENANCE_KEYS_ALREADY_EXIST_ON_CREATE_NEW,
788                                getHumanReadablePrimaryKeyFieldNames(dataObjectClass));
789                        success &= false;
790                    }
791                }
792
793            }
794        }
795
796        return success;
797    }
798
799    /**
800     * This method creates a human-readable string of the class' primary key field names, as designated by the
801     * DataDictionary.
802     *
803     * @param dataObjectClass
804     * @return human-readable string representation of the primary key field names
805     */
806    protected String getHumanReadablePrimaryKeyFieldNames(Class<?> dataObjectClass) {
807        String delim = "";
808        StringBuilder pkFieldNames = new StringBuilder();
809
810        // get a list of all the primary key field names, walk through them
811        List<String> pkFields = getLegacyDataAdapter().listPrimaryKeyFieldNames(dataObjectClass);
812        for (Iterator<String> iter = pkFields.iterator(); iter.hasNext(); ) {
813            String pkFieldName = (String) iter.next();
814
815            // TODO should this be getting labels from the view dictionary
816            // use the DataDictionary service to translate field name into human-readable label
817            String humanReadableFieldName = getDataDictionaryService().getAttributeLabel(dataObjectClass, pkFieldName);
818
819            // append the next field
820            pkFieldNames.append(delim + humanReadableFieldName);
821
822            // separate names with commas after the first one
823            if (delim.equalsIgnoreCase("")) {
824                delim = ", ";
825            }
826        }
827
828        return pkFieldNames.toString();
829    }
830
831    /**
832     * This method enforces all business rules that are common to all maintenance documents which must be tested before
833     * doing an
834     * approval.
835     *
836     * It can be overloaded in special cases where a MaintenanceDocument has very special needs that would be contrary
837     * to what is
838     * enforced here.
839     *
840     * @param document - a populated MaintenanceDocument instance
841     * @return true if the document can be approved, false if not
842     */
843    protected boolean processGlobalApproveDocumentBusinessRules(MaintenanceDocument document) {
844        return true;
845    }
846
847    /**
848     * This method enforces all business rules that are common to all maintenance documents which must be tested before
849     * doing a
850     * route.
851     *
852     * It can be overloaded in special cases where a MaintenanceDocument has very special needs that would be contrary
853     * to what is
854     * enforced here.
855     *
856     * @param document - a populated MaintenanceDocument instance
857     * @return true if the document can be routed, false if not
858     */
859    protected boolean processGlobalRouteDocumentBusinessRules(MaintenanceDocument document) {
860        boolean success = true;
861
862        // require a document description field
863        success &= checkEmptyDocumentField(
864                KRADPropertyConstants.DOCUMENT_HEADER + "." + KRADPropertyConstants.DOCUMENT_DESCRIPTION,
865                document.getDocumentHeader().getDocumentDescription(), "Description");
866
867        return success;
868    }
869
870    /**
871     * This method enforces all business rules that are common to all maintenance documents which must be tested before
872     * doing a
873     * save.
874     *
875     * It can be overloaded in special cases where a MaintenanceDocument has very special needs that would be contrary
876     * to what is
877     * enforced here.
878     *
879     * Note that although this method returns a true or false to indicate whether the save should happen or not, this
880     * result may not
881     * be followed by the calling method. In other words, the boolean result will likely be ignored, and the document
882     * saved,
883     * regardless.
884     *
885     * @param document - a populated MaintenanceDocument instance
886     * @return true if all business rules succeed, false if not
887     */
888    protected boolean processGlobalSaveDocumentBusinessRules(MaintenanceDocument document) {
889        // default to success
890        boolean success = true;
891
892        // do generic checks that impact primary key violations
893        success &= primaryKeyCheck(document);
894
895        // this is happening only on the processSave, since a Save happens in both the
896        // Route and Save events.
897        success &= this.dataDictionaryValidate(document);
898
899        return success;
900    }
901
902    /**
903     * This method should be overridden to provide custom rules for processing document saving
904     *
905     * @param document
906     * @return boolean
907     */
908    protected boolean processCustomSaveDocumentBusinessRules(MaintenanceDocument document) {
909        return true;
910    }
911
912    /**
913     * This method should be overridden to provide custom rules for processing document routing
914     *
915     * @param document
916     * @return boolean
917     */
918    protected boolean processCustomRouteDocumentBusinessRules(MaintenanceDocument document) {
919        return true;
920    }
921
922    /**
923     * This method should be overridden to provide custom rules for processing document approval.
924     *
925     * @param document
926     * @return boolean
927     */
928    protected boolean processCustomApproveDocumentBusinessRules(MaintenanceDocument document) {
929        return true;
930    }
931
932    /**
933     * This method should be overridden to provide custom rules for processing adding collection lines.
934     *
935     * @param document
936     * @return boolean
937     */
938    protected boolean processCustomAddCollectionLineBusinessRules(MaintenanceDocument document, String collectionName, Object line) {
939        return true;
940    }
941
942    // Document Validation Helper Methods
943
944    /**
945     * This method checks to see if the document is in a state that it can be saved without causing exceptions.
946     *
947     * Note that Business Rules are NOT enforced here, only validity checks.
948     *
949     * This method will only return false if the document is in such a state that routing it will cause
950     * RunTimeExceptions.
951     *
952     * @param maintenanceDocument - a populated MaintenaceDocument instance.
953     * @return boolean - returns true unless the object is in an invalid state.
954     */
955    protected boolean isDocumentValidForSave(MaintenanceDocument maintenanceDocument) {
956
957        boolean success = true;
958
959        success &= super.isDocumentOverviewValid(maintenanceDocument);
960        success &= validateDocumentStructure((Document) maintenanceDocument);
961        success &= validateMaintenanceDocument(maintenanceDocument);
962        success &= validateBulkUpdateMaintenanceDocument(maintenanceDocument);
963        return success;
964    }
965
966    /**
967     * This method makes sure the document itself is valid, and has the necessary fields populated to be routable.
968     *
969     * This is not a business rules test, rather its a structure test to make sure that the document will not cause
970     * exceptions
971     * before routing.
972     *
973     * @param document - document to be tested
974     * @return false if the document is missing key values, true otherwise
975     */
976    protected boolean validateDocumentStructure(Document document) {
977        boolean success = true;
978
979        // document must have a populated documentNumber
980        String documentHeaderId = document.getDocumentNumber();
981        if (documentHeaderId == null || StringUtils.isEmpty(documentHeaderId)) {
982            throw new ValidationException("Document has no document number, unable to proceed.");
983        }
984
985        return success;
986    }
987
988    /**
989     * This method checks to make sure the document is a valid maintenanceDocument, and has the necessary values
990     * populated such that it will not cause exceptions in later routing or business rules testing.
991     *
992     * This is not a business rules test.
993     *
994     * @param maintenanceDocument - document to be tested
995     * @return whether maintenance doc passes
996     * @throws ValidationException
997     */
998    protected boolean validateMaintenanceDocument(MaintenanceDocument maintenanceDocument) {
999        boolean success = true;
1000        Maintainable newMaintainable = maintenanceDocument.getNewMaintainableObject();
1001
1002        // document must have a newMaintainable object
1003        if (newMaintainable == null) {
1004            throw new ValidationException(
1005                    "Maintainable object from Maintenance Document '" + maintenanceDocument.getDocumentTitle() +
1006                            "' is null, unable to proceed.");
1007        }
1008
1009        // document's newMaintainable must contain an object (ie, not null)
1010        if (newMaintainable.getDataObject() == null) {
1011            throw new ValidationException("Maintainable's component data object is null.");
1012        }
1013
1014        return success;
1015    }
1016
1017    /**
1018     * This method checks whether this maintenance document represents a bulk update maintenance document, and if so,
1019     * whether the data object is in a persistable state.
1020     *
1021     * @param maintenanceDocument- document to be tested
1022     * @return false for bulk update maintenance doc that are not persistable, true otherwise (incl. non-bulkupdate
1023     *         maintenance documents)
1024     */
1025    protected boolean validateBulkUpdateMaintenanceDocument(MaintenanceDocument maintenanceDocument) {
1026        boolean success = true;
1027        Maintainable newMaintainable = maintenanceDocument.getNewMaintainableObject();
1028
1029        if (newMaintainable instanceof BulkUpdateMaintainable) {
1030            success = ((BulkUpdateMaintainable) newMaintainable).isPersistable();
1031        }
1032
1033        return success;
1034    }
1035
1036    /**
1037     * This method tests to make sure the MaintenanceDocument passed in is based on the class you are expecting.
1038     *
1039     * It does this based on the NewMaintainableObject of the MaintenanceDocument.
1040     *
1041     * @param document - MaintenanceDocument instance you want to test
1042     * @param clazz - class you are expecting the MaintenanceDocument to be based on
1043     * @return true if they match, false if not
1044     */
1045    protected boolean isCorrectMaintenanceClass(MaintenanceDocument document, Class clazz) {
1046        // disallow null arguments
1047        if (document == null || clazz == null) {
1048            throw new IllegalArgumentException("Null arguments were passed in.");
1049        }
1050
1051        // compare the class names
1052        if (clazz.toString().equals(document.getNewMaintainableObject().getDataObjectClass().toString())) {
1053            return true;
1054        } else {
1055            return false;
1056        }
1057    }
1058
1059    /**
1060     * This method accepts an object, and attempts to determine whether it is empty by this method's definition.
1061     *
1062     * OBJECT RESULT null false empty-string false whitespace false otherwise true
1063     *
1064     * If the result is false, it will add an object field error to the Global Errors.
1065     *
1066     * @param valueToTest - any object to test, usually a String
1067     * @param propertyName - the name of the property being tested
1068     * @return true or false, by the description above
1069     */
1070    protected boolean checkEmptyBOField(String propertyName, Object valueToTest, String parameter) {
1071        boolean success = true;
1072
1073        success = checkEmptyValue(valueToTest);
1074
1075        // if failed, then add a field error
1076        if (!success) {
1077            putFieldError(propertyName, RiceKeyConstants.ERROR_REQUIRED, parameter);
1078        }
1079
1080        return success;
1081    }
1082
1083    /**
1084     * This method accepts document field (such as , and attempts to determine whether it is empty by this method's
1085     * definition.
1086     *
1087     * OBJECT RESULT null false empty-string false whitespace false otherwise true
1088     *
1089     * If the result is false, it will add document field error to the Global Errors.
1090     *
1091     * @param valueToTest - any object to test, usually a String
1092     * @param propertyName - the name of the property being tested
1093     * @return true or false, by the description above
1094     */
1095    protected boolean checkEmptyDocumentField(String propertyName, Object valueToTest, String parameter) {
1096        boolean success = true;
1097        success = checkEmptyValue(valueToTest);
1098        if (!success) {
1099            putDocumentError(propertyName, RiceKeyConstants.ERROR_REQUIRED, parameter);
1100        }
1101        return success;
1102    }
1103
1104    /**
1105     * This method accepts document field (such as , and attempts to determine whether it is empty by this method's
1106     * definition.
1107     *
1108     * OBJECT RESULT null false empty-string false whitespace false otherwise true
1109     *
1110     * It will the result as a boolean
1111     *
1112     * @param valueToTest - any object to test, usually a String
1113     */
1114    protected boolean checkEmptyValue(Object valueToTest) {
1115        boolean success = true;
1116
1117        // if its not a string, only fail if its a null object
1118        if (valueToTest == null) {
1119            success = false;
1120        } else {
1121            // test for null, empty-string, or whitespace if its a string
1122            if (valueToTest instanceof String) {
1123                if (StringUtils.isBlank((String) valueToTest)) {
1124                    success = false;
1125                }
1126            }
1127        }
1128
1129        return success;
1130    }
1131
1132    /**
1133     * This method is used during debugging to dump the contents of the error map, including the key names. It is not
1134     * used by the
1135     * application in normal circumstances at all.
1136     */
1137    protected void showErrorMap() {
1138        if (GlobalVariables.getMessageMap().hasNoErrors()) {
1139            return;
1140        }
1141
1142        for (Iterator i = GlobalVariables.getMessageMap().getAllPropertiesAndErrors().iterator(); i.hasNext(); ) {
1143            Map.Entry e = (Map.Entry) i.next();
1144
1145            AutoPopulatingList errorList = (AutoPopulatingList) e.getValue();
1146            for (Iterator j = errorList.iterator(); j.hasNext(); ) {
1147                ErrorMessage em = (ErrorMessage) j.next();
1148
1149                if (em.getMessageParameters() == null) {
1150                    LOG.error(e.getKey().toString() + " = " + em.getErrorKey());
1151                } else {
1152                    LOG.error(e.getKey().toString() + " = " + em.getErrorKey() + " : " +
1153                            em.getMessageParameters().toString());
1154                }
1155            }
1156        }
1157    }
1158
1159    /**
1160     * @see MaintenanceDocumentRule#setupBaseConvenienceObjects(org.kuali.rice.krad.maintenance.MaintenanceDocument)
1161     */
1162    @Override
1163    public void setupBaseConvenienceObjects(MaintenanceDocument document) {
1164        // setup oldAccount convenience objects, make sure all possible sub-objects are populated
1165        oldDataObject = document.getOldMaintainableObject().getDataObject();
1166        if (oldDataObject != null && oldDataObject instanceof PersistableBusinessObjectBaseAdapter) {
1167            ((PersistableBusinessObjectBaseAdapter) oldDataObject).refreshNonUpdateableReferences();
1168        }
1169
1170        // setup newAccount convenience objects, make sure all possible sub-objects are populated
1171        newDataObject = document.getNewMaintainableObject().getDataObject();
1172        if (newDataObject instanceof PersistableBusinessObjectBaseAdapter) {
1173            ((PersistableBusinessObjectBaseAdapter) newDataObject).refreshNonUpdateableReferences();
1174        }
1175
1176        dataObjectClass = document.getNewMaintainableObject().getDataObjectClass();
1177
1178        // call the setupConvenienceObjects in the subclass, if a subclass exists
1179        setupConvenienceObjects();
1180    }
1181
1182    @Override
1183    public void setupConvenienceObjects() {
1184        // should always be overriden by subclass
1185    }
1186
1187    /**
1188     * This method checks to make sure that if the foreign-key fields for the given reference attributes have any
1189     * fields filled out,that all fields are filled out.
1190     *
1191     * If any are filled out, but all are not, it will return false and add a global error message about the problem.
1192     *
1193     * @param referenceName - The name of the reference object, whose foreign-key fields must be all-or-none filled
1194     * out.
1195     * @return true if this is the case, false if not
1196     */
1197    protected boolean checkForPartiallyFilledOutReferenceForeignKeys(String referenceName) {
1198        boolean success = true;
1199
1200        ForeignKeyFieldsPopulationState fkFieldsState = getLegacyDataAdapter().getForeignKeyFieldsPopulationState( newDataObject, referenceName);
1201
1202        // determine result
1203        if (fkFieldsState.isAnyFieldsPopulated() && !fkFieldsState.isAllFieldsPopulated()) {
1204            success = false;
1205
1206            // add errors if appropriate
1207
1208            // get the full set of foreign-keys
1209            List fKeys = new ArrayList(getLegacyDataAdapter().getForeignKeysForReference(
1210                    newDataObject.getClass(), referenceName).keySet());
1211            String fKeysReadable = consolidateFieldNames(fKeys, ", ").toString();
1212
1213            // walk through the missing fields
1214            for (Iterator iter = fkFieldsState.getUnpopulatedFieldNames().iterator(); iter.hasNext(); ) {
1215                String fieldName = (String) iter.next();
1216
1217                // get the human-readable name
1218                String fieldNameReadable = getDataDictionaryService().getAttributeLabel(newDataObject.getClass(),
1219                        fieldName);
1220
1221                // add a field error
1222                putFieldError(fieldName, RiceKeyConstants.ERROR_DOCUMENT_MAINTENANCE_PARTIALLY_FILLED_OUT_REF_FKEYS,
1223                        new String[]{fieldNameReadable, fKeysReadable});
1224            }
1225        }
1226
1227        return success;
1228    }
1229
1230    /**
1231     * This method turns a list of field property names, into a delimited string of the human-readable names.
1232     *
1233     * @param fieldNames - List of fieldNames
1234     * @return A filled StringBuffer ready to go in an error message
1235     */
1236    protected StringBuilder consolidateFieldNames(List<String> fieldNames, String delimiter) {
1237        StringBuilder sb = new StringBuilder();
1238
1239        // setup some vars
1240        boolean firstPass = true;
1241        String delim = "";
1242
1243        // walk through the list
1244        for (Iterator<String> iter = fieldNames.iterator(); iter.hasNext(); ) {
1245            String fieldName = (String) iter.next();
1246
1247            // get the human-readable name
1248            // add the new one, with the appropriate delimiter
1249            sb.append(delim + getDataDictionaryService().getAttributeLabel(newDataObject.getClass(), fieldName));
1250
1251            // after the first item, start using a delimiter
1252            if (firstPass) {
1253                delim = delimiter;
1254                firstPass = false;
1255            }
1256        }
1257
1258        return sb;
1259    }
1260
1261    /**
1262     * This method translates the passed in field name into a human-readable attribute label.
1263     *
1264     * It assumes the existing newDataObject's class as the class to examine the fieldName for.
1265     *
1266     * @param fieldName The fieldName you want a human-readable label for.
1267     * @return A human-readable label, pulled from the DataDictionary.
1268     */
1269    protected String getFieldLabel(String fieldName) {
1270        return getDataDictionaryService().getAttributeLabel(newDataObject.getClass(), fieldName) + "(" +
1271                getDataDictionaryService().getAttributeShortLabel(newDataObject.getClass(), fieldName) + ")";
1272    }
1273
1274    /**
1275     * This method translates the passed in field name into a human-readable attribute label.
1276     *
1277     * It assumes the existing newDataObject's class as the class to examine the fieldName for.
1278     *
1279     * @param dataObjectClass The class to use in combination with the fieldName.
1280     * @param fieldName The fieldName you want a human-readable label for.
1281     * @return A human-readable label, pulled from the DataDictionary.
1282     */
1283    protected String getFieldLabel(Class<?> dataObjectClass, String fieldName) {
1284        return getDataDictionaryService().getAttributeLabel(dataObjectClass, fieldName) + "(" +
1285                getDataDictionaryService().getAttributeShortLabel(dataObjectClass, fieldName) + ")";
1286    }
1287
1288    /**
1289     * Gets the newDataObject attribute.
1290     *
1291     * @return Returns the newDataObject.
1292     */
1293    protected final Object getNewDataObject() {
1294        return newDataObject;
1295    }
1296
1297    protected void setNewDataObject(Object newDataObject) {
1298        this.newDataObject = newDataObject;
1299    }
1300
1301    /**
1302     * Gets the oldDataObject attribute.
1303     *
1304     * @return Returns the oldDataObject.
1305     */
1306    protected final Object getOldDataObject() {
1307        return oldDataObject;
1308    }
1309
1310    protected final ConfigurationService getConfigService() {
1311        if (configService == null) {
1312            this.configService = CoreApiServiceLocator.getKualiConfigurationService();
1313        }
1314        return configService;
1315    }
1316
1317    public final void setConfigService(ConfigurationService configService) {
1318        this.configService = configService;
1319    }
1320
1321    protected final DataDictionaryService getDdService() {
1322        if (ddService == null) {
1323            this.ddService = KRADServiceLocatorWeb.getDataDictionaryService();
1324        }
1325        return ddService;
1326    }
1327
1328    public final void setDdService(DataDictionaryService ddService) {
1329        this.ddService = ddService;
1330    }
1331
1332    @Override
1333    protected final DictionaryValidationService getDictionaryValidationService() {
1334        if (dictionaryValidationService == null) {
1335            this.dictionaryValidationService = KRADServiceLocatorWeb.getDictionaryValidationService();
1336        }
1337        return dictionaryValidationService;
1338    }
1339
1340    public final void setDictionaryValidationService(DictionaryValidationService dictionaryValidationService) {
1341        this.dictionaryValidationService = dictionaryValidationService;
1342    }
1343
1344    @Override
1345    public PersonService getPersonService() {
1346        if (personService == null) {
1347            this.personService = KimApiServiceLocator.getPersonService();
1348        }
1349        return personService;
1350    }
1351
1352    public void setPersonService(PersonService personService) {
1353        this.personService = personService;
1354    }
1355
1356    public DateTimeService getDateTimeService() {
1357        return CoreApiServiceLocator.getDateTimeService();
1358    }
1359
1360    protected RoleService getRoleService() {
1361        if (this.roleService == null) {
1362            this.roleService = KimApiServiceLocator.getRoleService();
1363        }
1364        return this.roleService;
1365    }
1366
1367    public WorkflowDocumentService getWorkflowDocumentService() {
1368        if (workflowDocumentService == null) {
1369            this.workflowDocumentService = KRADServiceLocatorWeb.getWorkflowDocumentService();
1370        }
1371        return workflowDocumentService;
1372    }
1373
1374    public void setWorkflowDocumentService(WorkflowDocumentService workflowDocumentService) {
1375        this.workflowDocumentService = workflowDocumentService;
1376    }
1377
1378    public DataObjectAuthorizationService getDataObjectAuthorizationService() {
1379        if (dataObjectAuthorizationService == null) {
1380            this.dataObjectAuthorizationService = KRADServiceLocatorWeb.getDataObjectAuthorizationService();
1381        }
1382        return dataObjectAuthorizationService;
1383    }
1384
1385    public void setDataObjectAuthorizationService(DataObjectAuthorizationService dataObjectAuthorizationService) {
1386        this.dataObjectAuthorizationService = dataObjectAuthorizationService;
1387    }
1388
1389    private LegacyDataAdapter getLegacyDataAdapter() {
1390        return KRADServiceLocatorWeb.getLegacyDataAdapter();
1391    }
1392
1393    public DataObjectService getDataObjectService() {
1394        if ( dataObjectService == null ) {
1395            dataObjectService = KradDataServiceLocator.getDataObjectService();
1396        }
1397        return dataObjectService;
1398    }
1399
1400}
1401