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