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