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