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