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