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