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