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