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