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