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 */ 016package org.kuali.rice.krms.impl.rule; 017 018import org.apache.commons.collections.CollectionUtils; 019import org.apache.commons.lang.StringUtils; 020import org.kuali.rice.core.api.uif.RemotableAttributeError; 021import org.kuali.rice.core.api.util.RiceKeyConstants; 022import org.kuali.rice.krad.bo.PersistableBusinessObject; 023import org.kuali.rice.krad.maintenance.BulkUpdateMaintenanceDataObject; 024import org.kuali.rice.krad.maintenance.MaintenanceDocument; 025import org.kuali.rice.krad.rules.MaintenanceDocumentRuleBase; 026import org.kuali.rice.krad.service.KRADServiceLocatorWeb; 027import org.kuali.rice.krad.util.KRADConstants; 028import org.kuali.rice.krms.api.KrmsConstants; 029import org.kuali.rice.krms.api.repository.agenda.AgendaDefinition; 030import org.kuali.rice.krms.api.repository.context.ContextDefinition; 031import org.kuali.rice.krms.api.repository.rule.RuleDefinition; 032import org.kuali.rice.krms.api.repository.type.KrmsTypeDefinition; 033import org.kuali.rice.krms.api.repository.type.KrmsTypeRepositoryService; 034import org.kuali.rice.krms.framework.type.ActionTypeService; 035import org.kuali.rice.krms.framework.type.AgendaTypeService; 036import org.kuali.rice.krms.impl.authorization.AgendaAuthorizationService; 037import org.kuali.rice.krms.impl.repository.ActionBo; 038import org.kuali.rice.krms.impl.repository.AgendaBo; 039import org.kuali.rice.krms.impl.repository.AgendaBoService; 040import org.kuali.rice.krms.impl.repository.AgendaItemBo; 041import org.kuali.rice.krms.impl.repository.ContextBoService; 042import org.kuali.rice.krms.impl.repository.KrmsRepositoryServiceLocator; 043import org.kuali.rice.krms.impl.repository.RuleBo; 044import org.kuali.rice.krms.impl.repository.RuleBoService; 045import org.kuali.rice.krms.impl.ui.AgendaEditor; 046import org.kuali.rice.krms.impl.util.KRMSPropertyConstants; 047 048import java.util.List; 049import java.util.Map; 050 051import static org.kuali.rice.krms.impl.repository.BusinessObjectServiceMigrationUtils.findSingleMatching; 052 053/** 054 * This class contains the rules for the AgendaEditor. 055 */ 056public class AgendaEditorBusRule extends MaintenanceDocumentRuleBase { 057 058 @Override 059 protected boolean primaryKeyCheck(MaintenanceDocument document) { 060 // default to success if no failures 061 boolean success = true; 062 Class<?> dataObjectClass = document.getNewMaintainableObject().getDataObjectClass(); 063 064 // Since the dataObject is a wrapper class we need to return the agendaBo instead. 065 Object oldBo = ((AgendaEditor) document.getOldMaintainableObject().getDataObject()).getAgenda(); 066 Object newDataObject = ((AgendaEditor) document.getNewMaintainableObject().getDataObject()).getAgenda(); 067 068 // We dont do primaryKeyChecks on Global Business Object maintenance documents. This is 069 // because it doesnt really make any sense to do so, given the behavior of Globals. When a 070 // Global Document completes, it will update or create a new record for each BO in the list. 071 // As a result, there's no problem with having existing BO records in the system, they will 072 // simply get updated. 073 if (newDataObject instanceof BulkUpdateMaintenanceDataObject) { 074 return success; 075 } 076 077 // fail and complain if the person has changed the primary keys on 078 // an EDIT maintenance document. 079 if (document.isEdit()) { 080 if (!KRADServiceLocatorWeb.getLegacyDataAdapter().equalsByPrimaryKeys(oldBo, newDataObject)) { 081 // add a complaint to the errors 082 putDocumentError(KRADConstants.DOCUMENT_ERRORS, 083 RiceKeyConstants.ERROR_DOCUMENT_MAINTENANCE_PRIMARY_KEYS_CHANGED_ON_EDIT, 084 getHumanReadablePrimaryKeyFieldNames(dataObjectClass)); 085 success &= false; 086 } 087 } 088 089 // fail and complain if the person has selected a new object with keys that already exist 090 // in the DB. 091 else if (document.isNew()) { 092 093 // TODO: when/if we have standard support for DO retrieval, do this check for DO's 094 if (newDataObject instanceof PersistableBusinessObject) { 095 096 // get a map of the pk field names and values 097 Map<String, ?> newPkFields = KRADServiceLocatorWeb.getLegacyDataAdapter().getPrimaryKeyFieldValues(newDataObject); 098 099 // TODO: Good suggestion from Aaron, dont bother checking the DB, if all of the 100 // objects PK fields dont have values. If any are null or empty, then 101 // we're done. The current way wont fail, but it will make a wasteful 102 // DB call that may not be necessary, and we want to minimize these. 103 104 // attempt to do a lookup, see if this object already exists by these Primary Keys 105 PersistableBusinessObject testBo = findSingleMatching(getDataObjectService(), 106 dataObjectClass.asSubclass(PersistableBusinessObject.class), newPkFields); 107 108 // if the retrieve was successful, then this object already exists, and we need 109 // to complain 110 if (testBo != null) { 111 putDocumentError(KRADConstants.DOCUMENT_ERRORS, 112 RiceKeyConstants.ERROR_DOCUMENT_MAINTENANCE_KEYS_ALREADY_EXIST_ON_CREATE_NEW, 113 getHumanReadablePrimaryKeyFieldNames(dataObjectClass)); 114 success &= false; 115 } 116 } 117 } 118 119 return success; 120 } 121 122 123 124 @Override 125 protected boolean processCustomSaveDocumentBusinessRules(MaintenanceDocument document) { 126 boolean isValid = true; 127 128 AgendaEditor agendaEditor = (AgendaEditor) document.getNewMaintainableObject().getDataObject(); 129 AgendaEditor oldAgendaEditor = (AgendaEditor) document.getOldMaintainableObject().getDataObject(); 130 isValid &= validContext(agendaEditor); 131 isValid &= validAgendaName(agendaEditor); 132 isValid &= validContextAgendaNamespace(agendaEditor); 133 isValid &= validAgendaTypeAndAttributes(oldAgendaEditor, agendaEditor); 134 135 return isValid; 136 } 137 138 /** 139 * Check if the context exists and if user has authorization to edit agendas under this context. 140 * @param agendaEditor 141 * @return true if the context exist and has authorization, false otherwise 142 */ 143 public boolean validContext(AgendaEditor agendaEditor) { 144 boolean isValid = true; 145 146 try { 147 if (agendaEditor.getAgenda().getContextId() == null) { 148 ContextDefinition context = getContextBoService().getContextByNameAndNamespace( 149 agendaEditor.getContextName(), agendaEditor.getNamespace()); 150 151 if (context != null) { 152 agendaEditor.getAgenda().setContextId(context.getId()); 153 } 154 } 155 156 if (getContextBoService().getContextByContextId(agendaEditor.getAgenda().getContextId()) == null) { 157 this.putFieldError(KRMSPropertyConstants.Agenda.CONTEXT, "error.agenda.invalidContext"); 158 isValid = false; 159 } else { 160 if (!getAgendaAuthorizationService().isAuthorized(KrmsConstants.MAINTAIN_KRMS_AGENDA, 161 agendaEditor.getAgenda().getContextId())) { 162 this.putFieldError(KRMSPropertyConstants.Agenda.CONTEXT, "error.agenda.unauthorizedContext"); 163 isValid = false; 164 } 165 } 166 } 167 catch (IllegalArgumentException e) { 168 this.putFieldError(KRMSPropertyConstants.Agenda.CONTEXT, "error.agenda.invalidContext"); 169 isValid = false; 170 } 171 172 return isValid; 173 } 174 175 /** 176 * Check if for namespace. 177 * @param agendaEditor 178 * @return 179 */ 180 public boolean validContextAgendaNamespace(AgendaEditor agendaEditor) { 181 if (StringUtils.isNotBlank(agendaEditor.getNamespace()) && 182 getContextBoService().getContextByNameAndNamespace(agendaEditor.getContextName(), agendaEditor.getNamespace()) != null) { 183 return true; 184 } else { 185 this.putFieldError(KRMSPropertyConstants.Context.NAMESPACE, "error.context.invalidNamespace"); 186 return false; 187 } 188 } 189 190 private boolean validAgendaTypeAndAttributes( AgendaEditor oldAgendaEditor, AgendaEditor newAgendaEditor) { 191 if (validAgendaType(newAgendaEditor.getAgenda().getTypeId(), newAgendaEditor.getAgenda().getContextId())) { 192 return validAgendaAttributes(oldAgendaEditor, newAgendaEditor); 193 } else { 194 return false; 195 } 196 } 197 private boolean validAgendaType(String typeId, String contextId) { 198 boolean isValid = true; 199 200 if (!StringUtils.isBlank(typeId) && !StringUtils.isBlank(contextId)) { 201 if (getKrmsTypeRepositoryService().getAgendaTypeByAgendaTypeIdAndContextId(typeId, contextId) != null) { 202 return true; 203 } else { 204 this.putFieldError(KRMSPropertyConstants.Agenda.TYPE, "error.agenda.invalidType"); 205 return false; 206 } 207 } 208 209 return isValid; 210 } 211 212 private boolean validAgendaAttributes(AgendaEditor oldAgendaEditor, AgendaEditor newAgendaEditor) { 213 boolean isValid = true; 214 215 String typeId = newAgendaEditor.getAgenda().getTypeId(); 216 217 if (!StringUtils.isEmpty(typeId)) { 218 KrmsTypeDefinition typeDefinition = getKrmsTypeRepositoryService().getTypeById(typeId); 219 220 if (typeDefinition == null) { 221 throw new IllegalStateException("agenda typeId must match the id of a valid krms type"); 222 } else if (StringUtils.isBlank(typeDefinition.getServiceName())) { 223 throw new IllegalStateException("agenda type definition must have a non-blank service name"); 224 } else { 225 AgendaTypeService agendaTypeService = 226 (AgendaTypeService)KrmsRepositoryServiceLocator.getService(typeDefinition.getServiceName()); 227 228 if (agendaTypeService == null) { 229 throw new IllegalStateException("typeDefinition must have a valid serviceName"); 230 } else { 231 232 List<RemotableAttributeError> errors; 233 if (oldAgendaEditor == null) { 234 errors = agendaTypeService.validateAttributes(typeId, newAgendaEditor.getCustomAttributesMap()); 235 } else { 236 errors = agendaTypeService.validateAttributesAgainstExisting(typeId, newAgendaEditor.getCustomAttributesMap(), oldAgendaEditor.getCustomAttributesMap()); 237 } 238 239 if (!CollectionUtils.isEmpty(errors)) { 240 isValid = false; 241 for (RemotableAttributeError error : errors) { 242 for (String errorStr : error.getErrors()) { 243 this.putFieldError( 244 KRMSPropertyConstants.AgendaEditor.CUSTOM_ATTRIBUTES_MAP + 245 "['" + error.getAttributeName() + "']", 246 errorStr 247 ); 248 } 249 } 250 } 251 } 252 } 253 } 254 return isValid; 255 } 256 257 /** 258 * Check if an agenda with that name exists already in the context. 259 * @param agendaEditor 260 * @return true if agenda name is unique, false otherwise 261 */ 262 public boolean validAgendaName(AgendaEditor agendaEditor) { 263 try { 264 AgendaDefinition agendaFromDataBase = getAgendaBoService().getAgendaByNameAndContextId( 265 agendaEditor.getAgenda().getName(), agendaEditor.getAgenda().getContextId()); 266 if ((agendaFromDataBase != null) && !StringUtils.equals(agendaFromDataBase.getId(), agendaEditor.getAgenda().getId())) { 267 this.putFieldError(KRMSPropertyConstants.Agenda.NAME, "error.agenda.duplicateName"); 268 return false; 269 } 270 } 271 catch (IllegalArgumentException e) { 272 this.putFieldError(KRMSPropertyConstants.Agenda.NAME, "error.agenda.invalidName"); 273 return false; 274 } 275 return true; 276 } 277 278 /** 279 * Check if a agenda item is valid. 280 * 281 * @param document, the Agenda document of the added/edited agenda item 282 * @return true if agenda item is valid, false otherwise 283 */ 284 public boolean processAgendaItemBusinessRules(MaintenanceDocument document) { 285 boolean isValid = true; 286 287 AgendaEditor newAgendaEditor = (AgendaEditor) document.getNewMaintainableObject().getDataObject(); 288 AgendaEditor oldAgendaEditor = (AgendaEditor) document.getOldMaintainableObject().getDataObject(); 289 RuleBo rule = newAgendaEditor.getAgendaItemLine().getRule(); 290 isValid &= validateRuleName(rule, newAgendaEditor.getAgenda()); 291 isValid &= validRuleType(rule.getTypeId(), newAgendaEditor.getAgenda().getContextId()); 292 isValid &= validateRuleAction(oldAgendaEditor, newAgendaEditor); 293 294 return isValid; 295 } 296 297 /** 298 * Check if a rule with that name exists already in the namespace. 299 * @param rule 300 * @parm agenda 301 * @return true if rule name is unique, false otherwise 302 */ 303 private boolean validateRuleName(RuleBo rule, AgendaBo agenda) { 304 if (StringUtils.isBlank(rule.getName())) { 305 this.putFieldError(KRMSPropertyConstants.Rule.NAME, "error.rule.invalidName"); 306 return false; 307 } 308 // check current bo for rules (including ones that aren't persisted to the database) 309 for (AgendaItemBo agendaItem : agenda.getItems()) { 310 if (!StringUtils.equals(agendaItem.getRule().getId(), rule.getId()) && StringUtils.equals(agendaItem.getRule().getName(), rule.getName()) 311 && StringUtils.equals(agendaItem.getRule().getNamespace(), rule.getNamespace())) { 312 this.putFieldError(KRMSPropertyConstants.Rule.NAME, "error.rule.duplicateName"); 313 return false; 314 } 315 } 316 317 // check database for rules used with other agendas - the namespace might not yet be specified on new agendas. 318 if (StringUtils.isNotBlank(rule.getNamespace())) { 319 RuleDefinition ruleFromDatabase = getRuleBoService().getRuleByNameAndNamespace(rule.getName(), rule.getNamespace()); 320 try { 321 if ((ruleFromDatabase != null) && !StringUtils.equals(ruleFromDatabase.getId(), rule.getId())) { 322 this.putFieldError(KRMSPropertyConstants.Rule.NAME, "error.rule.duplicateName"); 323 return false; 324 } 325 } 326 catch (IllegalArgumentException e) { 327 this.putFieldError(KRMSPropertyConstants.Rule.NAME, "error.rule.invalidName"); 328 return false; 329 } 330 } 331 return true; 332 } 333 334 /** 335 * Check that the rule type is valid when specified. 336 * @param ruleTypeId, the type id 337 * @param contextId, the contextId the action needs to belong to. 338 * @return true if valid, false otherwise. 339 */ 340 private boolean validRuleType(String ruleTypeId, String contextId) { 341 if (StringUtils.isBlank(ruleTypeId)) { 342 return true; 343 } 344 345 if (getKrmsTypeRepositoryService().getRuleTypeByRuleTypeIdAndContextId(ruleTypeId, contextId) != null) { 346 return true; 347 } else { 348 this.putFieldError(KRMSPropertyConstants.Rule.TYPE, "error.rule.invalidType"); 349 return false; 350 } 351 } 352 353 private boolean validateRuleAction(AgendaEditor oldAgendaEditor, AgendaEditor newAgendaEditor) { 354 boolean isValid = true; 355 ActionBo newActionBo = newAgendaEditor.getAgendaItemLineRuleAction(); 356 357 isValid &= validRuleActionType(newActionBo.getTypeId(), newAgendaEditor.getAgenda().getContextId()); 358 if (isValid && StringUtils.isNotBlank(newActionBo.getTypeId())) { 359 isValid &= validRuleActionName(newActionBo.getName()); 360 isValid &= validRuleActionAttributes(oldAgendaEditor, newAgendaEditor); 361 } 362 return isValid; 363 } 364 365 /** 366 * Check that the rule action type is valid when specified. 367 * @param typeId, the action type id 368 * @parm contextId, the contextId the action needs to belong to. 369 * @return true if valid, false otherwise. 370 */ 371 private boolean validRuleActionType(String typeId, String contextId) { 372 if (StringUtils.isBlank(typeId)) { 373 return true; 374 } 375 376 if (getKrmsTypeRepositoryService().getActionTypeByActionTypeIdAndContextId(typeId, contextId) != null) { 377 return true; 378 } else { 379 this.putFieldError(KRMSPropertyConstants.Action.TYPE, "error.action.invalidType"); 380 return false; 381 } 382 } 383 384 /** 385 * Check that a action name is specified. 386 */ 387 private boolean validRuleActionName(String name) { 388 if (StringUtils.isNotBlank(name)) { 389 return true; 390 } else { 391 this.putFieldError(KRMSPropertyConstants.Action.NAME, "error.action.missingName"); 392 return false; 393 } 394 } 395 396 private boolean validRuleActionAttributes(AgendaEditor oldAgendaEditor, AgendaEditor newAgendaEditor) { 397 boolean isValid = true; 398 399 String typeId = newAgendaEditor.getAgendaItemLineRuleAction().getTypeId(); 400 401 if (!StringUtils.isBlank(typeId)) { 402 KrmsTypeDefinition typeDefinition = getKrmsTypeRepositoryService().getTypeById(typeId); 403 404 if (typeDefinition == null) { 405 throw new IllegalStateException("rule action typeId must match the id of a valid krms type"); 406 } else if (StringUtils.isBlank(typeDefinition.getServiceName())) { 407 throw new IllegalStateException("rule action type definition must have a non-blank service name"); 408 } else { 409 ActionTypeService actionTypeService = getActionTypeService(typeDefinition.getServiceName()); 410 411 if (actionTypeService == null) { 412 throw new IllegalStateException("typeDefinition must have a valid serviceName"); 413 } else { 414 415 List<RemotableAttributeError> errors; 416 if (oldAgendaEditor == null) { 417 errors = actionTypeService.validateAttributes(typeId, 418 newAgendaEditor.getCustomRuleActionAttributesMap()); 419 } else { 420 errors = actionTypeService.validateAttributesAgainstExisting(typeId, 421 newAgendaEditor.getCustomRuleActionAttributesMap(), oldAgendaEditor.getCustomRuleActionAttributesMap()); 422 } 423 424 if (!CollectionUtils.isEmpty(errors)) { 425 isValid = false; 426 for (RemotableAttributeError error : errors) { 427 for (String errorStr : error.getErrors()) { 428 this.putFieldError( 429 KRMSPropertyConstants.AgendaEditor.CUSTOM_RULE_ACTION_ATTRIBUTES_MAP + 430 "['" + error.getAttributeName() + "']", 431 errorStr 432 ); 433 } 434 } 435 } 436 } 437 } 438 } 439 return isValid; 440 } 441 442 public ContextBoService getContextBoService() { 443 return KrmsRepositoryServiceLocator.getContextBoService(); 444 } 445 446 public AgendaBoService getAgendaBoService() { 447 return KrmsRepositoryServiceLocator.getAgendaBoService(); 448 } 449 450 public RuleBoService getRuleBoService() { 451 return KrmsRepositoryServiceLocator.getRuleBoService(); 452 } 453 454 public KrmsTypeRepositoryService getKrmsTypeRepositoryService() { 455 return KrmsRepositoryServiceLocator.getKrmsTypeRepositoryService(); 456 } 457 458 public ActionTypeService getActionTypeService(String serviceName) { 459 return (ActionTypeService)KrmsRepositoryServiceLocator.getService(serviceName); 460 } 461 462 public AgendaAuthorizationService getAgendaAuthorizationService() { 463 return KrmsRepositoryServiceLocator.getAgendaAuthorizationService(); 464 } 465} 466