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.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.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.rule.RuleDefinition; 031import org.kuali.rice.krms.api.repository.type.KrmsTypeDefinition; 032import org.kuali.rice.krms.api.repository.type.KrmsTypeRepositoryService; 033import org.kuali.rice.krms.framework.type.ActionTypeService; 034import org.kuali.rice.krms.framework.type.AgendaTypeService; 035import org.kuali.rice.krms.impl.authorization.AgendaAuthorizationService; 036import org.kuali.rice.krms.impl.repository.ActionBo; 037import org.kuali.rice.krms.impl.repository.AgendaBo; 038import org.kuali.rice.krms.impl.repository.AgendaBoService; 039import org.kuali.rice.krms.impl.repository.AgendaItemBo; 040import org.kuali.rice.krms.impl.repository.ContextBoService; 041import org.kuali.rice.krms.impl.repository.KrmsRepositoryServiceLocator; 042import org.kuali.rice.krms.impl.repository.RuleBo; 043import org.kuali.rice.krms.impl.repository.RuleBoService; 044import org.kuali.rice.krms.impl.ui.AgendaEditor; 045import org.kuali.rice.krms.impl.util.KRMSPropertyConstants; 046 047import java.util.List; 048import java.util.Map; 049 050import static org.kuali.rice.krms.impl.repository.BusinessObjectServiceMigrationUtils.findSingleMatching; 051 052/** 053 * This class contains the rules for the AgendaEditor. 054 */ 055public class AgendaEditorBusRule extends MaintenanceDocumentRuleBase { 056 057 @Override 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 (!KRADServiceLocatorWeb.getLegacyDataAdapter().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 = KRADServiceLocatorWeb.getLegacyDataAdapter().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 = findSingleMatching(getDataObjectService(), 105 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 @Override 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 if (StringUtils.isNotBlank(agendaEditor.getNamespace()) && 172 getContextBoService().getContextByNameAndNamespace(agendaEditor.getContextName(), agendaEditor.getNamespace()) != null) { 173 return true; 174 } else { 175 this.putFieldError(KRMSPropertyConstants.Context.NAMESPACE, "error.context.invalidNamespace"); 176 return false; 177 } 178 } 179 180 private boolean validAgendaTypeAndAttributes( AgendaEditor oldAgendaEditor, AgendaEditor newAgendaEditor) { 181 if (validAgendaType(newAgendaEditor.getAgenda().getTypeId(), newAgendaEditor.getAgenda().getContextId())) { 182 return validAgendaAttributes(oldAgendaEditor, newAgendaEditor); 183 } else { 184 return false; 185 } 186 } 187 private boolean validAgendaType(String typeId, String contextId) { 188 boolean isValid = true; 189 190 if (!StringUtils.isBlank(typeId) && !StringUtils.isBlank(contextId)) { 191 if (getKrmsTypeRepositoryService().getAgendaTypeByAgendaTypeIdAndContextId(typeId, contextId) != null) { 192 return true; 193 } else { 194 this.putFieldError(KRMSPropertyConstants.Agenda.TYPE, "error.agenda.invalidType"); 195 return false; 196 } 197 } 198 199 return isValid; 200 } 201 202 private boolean validAgendaAttributes(AgendaEditor oldAgendaEditor, AgendaEditor newAgendaEditor) { 203 boolean isValid = true; 204 205 String typeId = newAgendaEditor.getAgenda().getTypeId(); 206 207 if (!StringUtils.isEmpty(typeId)) { 208 KrmsTypeDefinition typeDefinition = getKrmsTypeRepositoryService().getTypeById(typeId); 209 210 if (typeDefinition == null) { 211 throw new IllegalStateException("agenda typeId must match the id of a valid krms type"); 212 } else if (StringUtils.isBlank(typeDefinition.getServiceName())) { 213 throw new IllegalStateException("agenda type definition must have a non-blank service name"); 214 } else { 215 AgendaTypeService agendaTypeService = 216 (AgendaTypeService)KrmsRepositoryServiceLocator.getService(typeDefinition.getServiceName()); 217 218 if (agendaTypeService == null) { 219 throw new IllegalStateException("typeDefinition must have a valid serviceName"); 220 } else { 221 222 List<RemotableAttributeError> errors; 223 if (oldAgendaEditor == null) { 224 errors = agendaTypeService.validateAttributes(typeId, newAgendaEditor.getCustomAttributesMap()); 225 } else { 226 errors = agendaTypeService.validateAttributesAgainstExisting(typeId, newAgendaEditor.getCustomAttributesMap(), oldAgendaEditor.getCustomAttributesMap()); 227 } 228 229 if (!CollectionUtils.isEmpty(errors)) { 230 isValid = false; 231 for (RemotableAttributeError error : errors) { 232 for (String errorStr : error.getErrors()) { 233 this.putFieldError( 234 KRMSPropertyConstants.AgendaEditor.CUSTOM_ATTRIBUTES_MAP + 235 "['" + error.getAttributeName() + "']", 236 errorStr 237 ); 238 } 239 } 240 } 241 } 242 } 243 } 244 return isValid; 245 } 246 247 /** 248 * Check if an agenda with that name exists already in the context. 249 * @param agendaEditor 250 * @return true if agenda name is unique, false otherwise 251 */ 252 public boolean validAgendaName(AgendaEditor agendaEditor) { 253 try { 254 AgendaDefinition agendaFromDataBase = getAgendaBoService().getAgendaByNameAndContextId( 255 agendaEditor.getAgenda().getName(), agendaEditor.getAgenda().getContextId()); 256 if ((agendaFromDataBase != null) && !StringUtils.equals(agendaFromDataBase.getId(), agendaEditor.getAgenda().getId())) { 257 this.putFieldError(KRMSPropertyConstants.Agenda.NAME, "error.agenda.duplicateName"); 258 return false; 259 } 260 } 261 catch (IllegalArgumentException e) { 262 this.putFieldError(KRMSPropertyConstants.Agenda.NAME, "error.agenda.invalidName"); 263 return false; 264 } 265 return true; 266 } 267 268 /** 269 * Check if a agenda item is valid. 270 * 271 * @param document, the Agenda document of the added/edited agenda item 272 * @return true if agenda item is valid, false otherwise 273 */ 274 public boolean processAgendaItemBusinessRules(MaintenanceDocument document) { 275 boolean isValid = true; 276 277 AgendaEditor newAgendaEditor = (AgendaEditor) document.getNewMaintainableObject().getDataObject(); 278 AgendaEditor oldAgendaEditor = (AgendaEditor) document.getOldMaintainableObject().getDataObject(); 279 RuleBo rule = newAgendaEditor.getAgendaItemLine().getRule(); 280 isValid &= validateRuleName(rule, newAgendaEditor.getAgenda()); 281 isValid &= validRuleType(rule.getTypeId(), newAgendaEditor.getAgenda().getContextId()); 282 isValid &= validateRuleAction(oldAgendaEditor, newAgendaEditor); 283 284 return isValid; 285 } 286 287 /** 288 * Check if a rule with that name exists already in the namespace. 289 * @param rule 290 * @parm agenda 291 * @return true if rule name is unique, false otherwise 292 */ 293 private boolean validateRuleName(RuleBo rule, AgendaBo agenda) { 294 if (StringUtils.isBlank(rule.getName())) { 295 this.putFieldError(KRMSPropertyConstants.Rule.NAME, "error.rule.invalidName"); 296 return false; 297 } 298 // check current bo for rules (including ones that aren't persisted to the database) 299 for (AgendaItemBo agendaItem : agenda.getItems()) { 300 if (!StringUtils.equals(agendaItem.getRule().getId(), rule.getId()) && StringUtils.equals(agendaItem.getRule().getName(), rule.getName()) 301 && StringUtils.equals(agendaItem.getRule().getNamespace(), rule.getNamespace())) { 302 this.putFieldError(KRMSPropertyConstants.Rule.NAME, "error.rule.duplicateName"); 303 return false; 304 } 305 } 306 307 // check database for rules used with other agendas - the namespace might not yet be specified on new agendas. 308 if (StringUtils.isNotBlank(rule.getNamespace())) { 309 RuleDefinition ruleFromDatabase = getRuleBoService().getRuleByNameAndNamespace(rule.getName(), rule.getNamespace()); 310 try { 311 if ((ruleFromDatabase != null) && !StringUtils.equals(ruleFromDatabase.getId(), rule.getId())) { 312 this.putFieldError(KRMSPropertyConstants.Rule.NAME, "error.rule.duplicateName"); 313 return false; 314 } 315 } 316 catch (IllegalArgumentException e) { 317 this.putFieldError(KRMSPropertyConstants.Rule.NAME, "error.rule.invalidName"); 318 return false; 319 } 320 } 321 return true; 322 } 323 324 /** 325 * Check that the rule type is valid when specified. 326 * @param ruleTypeId, the type id 327 * @param contextId, the contextId the action needs to belong to. 328 * @return true if valid, false otherwise. 329 */ 330 private boolean validRuleType(String ruleTypeId, String contextId) { 331 if (StringUtils.isBlank(ruleTypeId)) { 332 return true; 333 } 334 335 if (getKrmsTypeRepositoryService().getRuleTypeByRuleTypeIdAndContextId(ruleTypeId, contextId) != null) { 336 return true; 337 } else { 338 this.putFieldError(KRMSPropertyConstants.Rule.TYPE, "error.rule.invalidType"); 339 return false; 340 } 341 } 342 343 private boolean validateRuleAction(AgendaEditor oldAgendaEditor, AgendaEditor newAgendaEditor) { 344 boolean isValid = true; 345 ActionBo newActionBo = newAgendaEditor.getAgendaItemLineRuleAction(); 346 347 isValid &= validRuleActionType(newActionBo.getTypeId(), newAgendaEditor.getAgenda().getContextId()); 348 if (isValid && StringUtils.isNotBlank(newActionBo.getTypeId())) { 349 isValid &= validRuleActionName(newActionBo.getName()); 350 isValid &= validRuleActionAttributes(oldAgendaEditor, newAgendaEditor); 351 } 352 return isValid; 353 } 354 355 /** 356 * Check that the rule action type is valid when specified. 357 * @param typeId, the action type id 358 * @parm contextId, the contextId the action needs to belong to. 359 * @return true if valid, false otherwise. 360 */ 361 private boolean validRuleActionType(String typeId, String contextId) { 362 if (StringUtils.isBlank(typeId)) { 363 return true; 364 } 365 366 if (getKrmsTypeRepositoryService().getActionTypeByActionTypeIdAndContextId(typeId, contextId) != null) { 367 return true; 368 } else { 369 this.putFieldError(KRMSPropertyConstants.Action.TYPE, "error.action.invalidType"); 370 return false; 371 } 372 } 373 374 /** 375 * Check that a action name is specified. 376 */ 377 private boolean validRuleActionName(String name) { 378 if (StringUtils.isNotBlank(name)) { 379 return true; 380 } else { 381 this.putFieldError(KRMSPropertyConstants.Action.NAME, "error.action.missingName"); 382 return false; 383 } 384 } 385 386 private boolean validRuleActionAttributes(AgendaEditor oldAgendaEditor, AgendaEditor newAgendaEditor) { 387 boolean isValid = true; 388 389 String typeId = newAgendaEditor.getAgendaItemLineRuleAction().getTypeId(); 390 391 if (!StringUtils.isBlank(typeId)) { 392 KrmsTypeDefinition typeDefinition = getKrmsTypeRepositoryService().getTypeById(typeId); 393 394 if (typeDefinition == null) { 395 throw new IllegalStateException("rule action typeId must match the id of a valid krms type"); 396 } else if (StringUtils.isBlank(typeDefinition.getServiceName())) { 397 throw new IllegalStateException("rule action type definition must have a non-blank service name"); 398 } else { 399 ActionTypeService actionTypeService = getActionTypeService(typeDefinition.getServiceName()); 400 401 if (actionTypeService == null) { 402 throw new IllegalStateException("typeDefinition must have a valid serviceName"); 403 } else { 404 405 List<RemotableAttributeError> errors; 406 if (oldAgendaEditor == null) { 407 errors = actionTypeService.validateAttributes(typeId, 408 newAgendaEditor.getCustomRuleActionAttributesMap()); 409 } else { 410 errors = actionTypeService.validateAttributesAgainstExisting(typeId, 411 newAgendaEditor.getCustomRuleActionAttributesMap(), oldAgendaEditor.getCustomRuleActionAttributesMap()); 412 } 413 414 if (!CollectionUtils.isEmpty(errors)) { 415 isValid = false; 416 for (RemotableAttributeError error : errors) { 417 for (String errorStr : error.getErrors()) { 418 this.putFieldError( 419 KRMSPropertyConstants.AgendaEditor.CUSTOM_RULE_ACTION_ATTRIBUTES_MAP + 420 "['" + error.getAttributeName() + "']", 421 errorStr 422 ); 423 } 424 } 425 } 426 } 427 } 428 } 429 return isValid; 430 } 431 432 public ContextBoService getContextBoService() { 433 return KrmsRepositoryServiceLocator.getContextBoService(); 434 } 435 436 public AgendaBoService getAgendaBoService() { 437 return KrmsRepositoryServiceLocator.getAgendaBoService(); 438 } 439 440 public RuleBoService getRuleBoService() { 441 return KrmsRepositoryServiceLocator.getRuleBoService(); 442 } 443 444 public KrmsTypeRepositoryService getKrmsTypeRepositoryService() { 445 return KrmsRepositoryServiceLocator.getKrmsTypeRepositoryService(); 446 } 447 448 public ActionTypeService getActionTypeService(String serviceName) { 449 return (ActionTypeService)KrmsRepositoryServiceLocator.getService(serviceName); 450 } 451 452 public AgendaAuthorizationService getAgendaAuthorizationService() { 453 return KrmsRepositoryServiceLocator.getAgendaAuthorizationService(); 454 } 455} 456