001/** 002 * Copyright 2005-2017 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.ui; 017 018import java.util.ArrayList; 019import java.util.Collections; 020import java.util.HashMap; 021import java.util.List; 022import java.util.Map; 023 024import javax.servlet.http.HttpServletRequest; 025import javax.servlet.http.HttpServletResponse; 026 027import org.apache.commons.collections.CollectionUtils; 028import org.apache.commons.lang.StringUtils; 029import org.kuali.rice.core.api.criteria.QueryByCriteria; 030import org.kuali.rice.core.api.criteria.QueryResults; 031import org.kuali.rice.core.api.uif.RemotableAttributeError; 032import org.kuali.rice.core.api.util.KeyValue; 033import org.kuali.rice.core.api.util.tree.Node; 034import org.kuali.rice.krad.data.KradDataServiceLocator; 035import org.kuali.rice.krad.maintenance.MaintenanceDocument; 036import org.kuali.rice.krad.maintenance.MaintenanceDocumentController; 037import org.kuali.rice.krad.service.KRADServiceLocator; 038import org.kuali.rice.krad.uif.UifParameters; 039import org.kuali.rice.krad.util.GlobalVariables; 040import org.kuali.rice.krad.util.KRADUtils; 041import org.kuali.rice.krad.web.form.DocumentFormBase; 042import org.kuali.rice.krad.web.form.MaintenanceDocumentForm; 043import org.kuali.rice.krad.web.form.UifFormBase; 044import org.kuali.rice.krms.api.KrmsApiServiceLocator; 045import org.kuali.rice.krms.api.engine.expression.ComparisonOperatorService; 046import org.kuali.rice.krms.api.repository.LogicalOperator; 047import org.kuali.rice.krms.api.repository.operator.CustomOperator; 048import org.kuali.rice.krms.api.repository.proposition.PropositionParameterType; 049import org.kuali.rice.krms.api.repository.proposition.PropositionType; 050import org.kuali.rice.krms.api.repository.rule.RuleDefinition; 051import org.kuali.rice.krms.api.repository.term.TermDefinition; 052import org.kuali.rice.krms.api.repository.term.TermResolverDefinition; 053import org.kuali.rice.krms.api.repository.term.TermSpecificationDefinition; 054import org.kuali.rice.krms.api.repository.type.KrmsAttributeDefinition; 055import org.kuali.rice.krms.impl.repository.ActionBo; 056import org.kuali.rice.krms.impl.repository.AgendaBo; 057import org.kuali.rice.krms.impl.repository.AgendaItemBo; 058import org.kuali.rice.krms.impl.repository.ContextBoService; 059import org.kuali.rice.krms.impl.repository.FunctionBoService; 060import org.kuali.rice.krms.impl.repository.KrmsAttributeDefinitionService; 061import org.kuali.rice.krms.impl.repository.KrmsRepositoryServiceLocator; 062import org.kuali.rice.krms.impl.repository.PropositionBo; 063import org.kuali.rice.krms.impl.repository.PropositionParameterBo; 064import org.kuali.rice.krms.impl.repository.RepositoryBoIncrementer; 065import org.kuali.rice.krms.impl.repository.RuleBo; 066import org.kuali.rice.krms.impl.repository.RuleBoService; 067import org.kuali.rice.krms.impl.repository.TermBo; 068import org.kuali.rice.krms.impl.rule.AgendaEditorBusRule; 069import org.kuali.rice.krms.impl.util.KRMSPropertyConstants; 070import org.kuali.rice.krms.impl.util.KrmsImplConstants; 071import org.kuali.rice.krms.impl.util.KrmsServiceLocatorInternal; 072import org.springframework.stereotype.Controller; 073import org.springframework.validation.BindingResult; 074import org.springframework.web.bind.annotation.ModelAttribute; 075import org.springframework.web.bind.annotation.RequestMapping; 076import org.springframework.web.bind.annotation.RequestMethod; 077import org.springframework.web.bind.annotation.RequestParam; 078import org.springframework.web.bind.annotation.ResponseBody; 079import org.springframework.web.servlet.ModelAndView; 080 081/** 082 * Controller for the Test UI Page 083 * @author Kuali Rice Team (rice.collab@kuali.org) 084 */ 085@Controller 086@RequestMapping(value = org.kuali.rice.krms.impl.util.KrmsImplConstants.WebPaths.AGENDA_EDITOR_PATH) 087public class AgendaEditorController extends MaintenanceDocumentController { 088 089 private static final RepositoryBoIncrementer agendaIdIncrementer = new RepositoryBoIncrementer(AgendaBo.AGENDA_SEQ_NAME); 090 private static final RepositoryBoIncrementer agendaItemIdIncrementer = new RepositoryBoIncrementer(AgendaItemBo.AGENDA_ITEM_SEQ_NAME); 091 private static final RepositoryBoIncrementer ruleIdIncrementer = new RepositoryBoIncrementer(RuleBo.RULE_SEQ_NAME); 092 093 /** 094 * Override route to set the setSelectedAgendaItemId to empty and disable all the buttons 095 * 096 * @see org.kuali.rice.krad.maintenance.MaintenanceDocumentController#route 097 * (DocumentFormBase, BindingResult, HttpServletRequest, HttpServletResponse) 098 */ 099 @Override 100 @RequestMapping(params = "methodToCall=route") 101 public ModelAndView route(DocumentFormBase form) { 102 103 ModelAndView modelAndView; 104 MaintenanceDocumentForm maintenanceForm = (MaintenanceDocumentForm) form; 105 AgendaEditor agendaEditor = ((AgendaEditor) maintenanceForm.getDocument().getNewMaintainableObject().getDataObject()); 106 agendaEditor.setSelectedAgendaItemId(""); 107 agendaEditor.setDisableButtons(true); 108 modelAndView = super.route(form); 109 110 return modelAndView; 111 } 112 113 /** 114 * This overridden method does extra work on refresh to update the namespace when the context has been changed. 115 * 116 * {@inheritDoc} 117 */ 118 @RequestMapping(params = "methodToCall=" + "refresh") 119 @Override 120 public ModelAndView refresh(UifFormBase form) { 121 ModelAndView modelAndView = super.refresh(form); 122 123 // handle return from context lookup 124 MaintenanceDocumentForm maintenanceForm = (MaintenanceDocumentForm) form; 125 AgendaEditor agendaEditor = ((AgendaEditor) maintenanceForm.getDocument().getNewMaintainableObject().getDataObject()); 126 AgendaEditorBusRule rule = new AgendaEditorBusRule(); 127 if (rule.validContext(agendaEditor) && rule.validAgendaName(agendaEditor)) { 128 // update the namespace on all agenda related objects if the contest has been changed 129 if (!StringUtils.equals(agendaEditor.getOldContextId(), agendaEditor.getAgenda().getContextId())) { 130 agendaEditor.setOldContextId(agendaEditor.getAgenda().getContextId()); 131 132 String namespace = ""; 133 if (!StringUtils.isBlank(agendaEditor.getAgenda().getContextId())) { 134 namespace = getContextBoService().getContextByContextId(agendaEditor.getAgenda().getContextId()).getNamespace(); 135 } 136 137 for (AgendaItemBo agendaItem : agendaEditor.getAgenda().getItems()) { 138 agendaItem.getRule().setNamespace(namespace); 139 for (ActionBo action : agendaItem.getRule().getActions()) { 140 action.setNamespace(namespace); 141 } 142 } 143 } 144 } 145 return modelAndView; 146 } 147 148 @Override 149 public ModelAndView setupMaintenanceEdit(MaintenanceDocumentForm form) { 150 151 // Reset the page Id so that bread crumbs can come back to the default page on EditAgenda 152 form.setPageId(null); 153 return super.setupMaintenanceEdit(form); 154 } 155 156 /** 157 * This method updates the existing rule in the agenda. 158 */ 159 @RequestMapping(params = "methodToCall=" + "goToAddRule") 160 public ModelAndView goToAddRule(UifFormBase form) throws Exception { 161 AgendaEditor agendaEditorForBusRuleChecks = getAgendaEditor(form); 162 AgendaEditorBusRule rule = new AgendaEditorBusRule(); 163 if (rule.validContext(agendaEditorForBusRuleChecks) && rule.validAgendaName(agendaEditorForBusRuleChecks)) { 164 setAgendaItemLine(form, null); 165 AgendaEditor agendaEditor = getAgendaEditor(form); 166 agendaEditor.setAddRuleInProgress(true); 167 form.getActionParameters().put(UifParameters.NAVIGATE_TO_PAGE_ID, "AgendaEditorView-AddRule-Page"); 168 169 return super.navigate(form); 170 } 171 172 return super.navigate(form); 173 } 174 175 /** 176 * This method sets the agendaItemLine for adding/editing AgendaItems. 177 * The agendaItemLine is a copy of the agendaItem so that changes are not applied when 178 * they are abandoned. If the agendaItem is null a new empty agendaItemLine is created. 179 * 180 * @param form 181 * @param agendaItem 182 */ 183 private void setAgendaItemLine(UifFormBase form, AgendaItemBo agendaItem) { 184 AgendaEditor agendaEditor = getAgendaEditor(form); 185 if (agendaItem == null) { 186 RuleBo rule = new RuleBo(); 187 rule.setId(ruleIdIncrementer.getNewId()); 188 if (StringUtils.isBlank(agendaEditor.getAgenda().getContextId())) { 189 rule.setNamespace(""); 190 } else { 191 rule.setNamespace(getContextBoService().getContextByContextId(agendaEditor.getAgenda().getContextId()).getNamespace()); 192 } 193 agendaItem = new AgendaItemBo(); 194 agendaItem.setRule(rule); 195 agendaEditor.setAgendaItemLine(agendaItem); 196 } else { 197 agendaEditor.setAgendaItemLine(KradDataServiceLocator.getDataObjectService().copyInstance(agendaItem)); 198 } 199 200 201 if (agendaItem.getRule().getActions().isEmpty()) { 202 ActionBo actionBo = new ActionBo(); 203 actionBo.setTypeId(""); 204 actionBo.setNamespace(agendaItem.getRule().getNamespace()); 205 actionBo.setRule(agendaItem.getRule()); 206 actionBo.setSequenceNumber(1); 207 agendaEditor.setAgendaItemLineRuleAction(actionBo); 208 } else { 209 agendaEditor.setAgendaItemLineRuleAction(agendaItem.getRule().getActions().get(0)); 210 } 211 212 agendaEditor.setCustomRuleActionAttributesMap(agendaEditor.getAgendaItemLineRuleAction().getAttributes()); 213 agendaEditor.setCustomRuleAttributesMap(agendaEditor.getAgendaItemLine().getRule().getAttributes()); 214 } 215 216 /** 217 * This method returns the id of the selected agendaItem. 218 * 219 * @param form 220 * @return selectedAgendaItemId 221 */ 222 private String getSelectedAgendaItemId(UifFormBase form) { 223 AgendaEditor agendaEditor = getAgendaEditor(form); 224 return agendaEditor.getSelectedAgendaItemId(); 225 } 226 227 /** 228 * This method sets the id of the cut agendaItem. 229 * 230 * @param form 231 * @param cutAgendaItemId 232 */ 233 private void setCutAgendaItemId(UifFormBase form, String cutAgendaItemId) { 234 AgendaEditor agendaEditor = getAgendaEditor(form); 235 agendaEditor.setCutAgendaItemId(cutAgendaItemId); 236 } 237 238 /** 239 * This method returns the id of the cut agendaItem. 240 * 241 * @param form 242 * @return cutAgendaItemId 243 */ 244 private String getCutAgendaItemId(UifFormBase form) { 245 AgendaEditor agendaEditor = getAgendaEditor(form); 246 return agendaEditor.getCutAgendaItemId(); 247 } 248 249 /** 250 * This method updates the existing rule in the agenda. 251 */ 252 @RequestMapping(params = "methodToCall=" + "goToEditRule") 253 public ModelAndView goToEditRule(UifFormBase form) throws Exception { 254 255 AgendaEditor agendaEditor = getAgendaEditor(form); 256 AgendaEditorBusRule rule = new AgendaEditorBusRule(); 257 258 // clear the deleted list for the previous edited rule before editing another one 259 agendaEditor.clearDeletedPropositionIdsFromRule(); 260 261 if (rule.validContext(agendaEditor) && rule.validAgendaName(agendaEditor)) { 262 agendaEditor.setAddRuleInProgress(false); 263 // this is the root of the tree: 264 AgendaItemBo firstItem = getFirstAgendaItem(agendaEditor.getAgenda()); 265 String selectedItemId = agendaEditor.getSelectedAgendaItemId(); 266 AgendaItemBo node = getAgendaItemById(firstItem, selectedItemId); 267 268 preprocessCustomOperators(node.getRule().getProposition(), getCustomOperatorValueMap(form)); 269 270 setAgendaItemLine(form, node); 271 272 form.getActionParameters().put(UifParameters.NAVIGATE_TO_PAGE_ID, "AgendaEditorView-EditRule-Page"); 273 274 return super.navigate(form); 275 } 276 277 return super.navigate(form); 278 } 279 280 /** 281 * Gets a map from function ID to custom operator key. 282 * 283 * <p>The key for a custom operator uses a special prefix and format.</p> 284 * 285 * @param form the form containing the agenda editor 286 * @return the map from function id to custom operator key 287 */ 288 protected Map<String,String> getCustomOperatorValueMap(UifFormBase form) { 289 List<KeyValue> allPropositionOpCodes = new PropositionOpCodeValuesFinder().getKeyValues(form); 290 291 // filter to just custom operators 292 Map<String, String> functionIdToCustomOpCodeMap = new HashMap<String, String>(); 293 for (KeyValue opCode : allPropositionOpCodes) { 294 if (opCode.getKey().startsWith(KrmsImplConstants.CUSTOM_OPERATOR_PREFIX)) { 295 CustomOperator customOperator = getCustomOperatorUiTranslator().getCustomOperator(opCode.getKey()); 296 functionIdToCustomOpCodeMap.put(customOperator.getOperatorFunctionDefinition().getId(), opCode.getKey()); 297 } 298 } 299 300 return functionIdToCustomOpCodeMap; 301 } 302 303 /** 304 * Looks for any custom function calls within simple propositions and attempts to convert them to custom 305 * operator keys. 306 * 307 * @param proposition the proposition to search within and convert 308 * @param customOperatorValuesMap a map from function ID to custom operator key, used for the conversion 309 */ 310 protected void preprocessCustomOperators(PropositionBo proposition, Map<String, String> customOperatorValuesMap) { 311 if (proposition == null) { return; } 312 313 if (proposition.getParameters() != null && proposition.getParameters().size() > 0) { 314 for (PropositionParameterBo param : proposition.getParameters()) { 315 if (PropositionParameterType.FUNCTION.getCode().equals(param.getParameterType())) { 316 // convert to our convention of customOperator:<functionNamespace>:<functionName> 317 String convertedValue = customOperatorValuesMap.get(param.getValue()); 318 if (!StringUtils.isEmpty(convertedValue)) { 319 param.setValue(convertedValue); 320 } 321 } 322 } 323 } else if (proposition.getCompoundComponents() != null && proposition.getCompoundComponents().size() > 0) { 324 for (PropositionBo childProposition : proposition.getCompoundComponents()) { 325 // recurse 326 preprocessCustomOperators(childProposition, customOperatorValuesMap); 327 } 328 } 329 } 330 331 /** 332 * This method adds the newly create rule to the agenda. 333 */ 334 @RequestMapping(params = "methodToCall=" + "addRule") 335 public ModelAndView addRule(UifFormBase form) throws Exception { 336 337 AgendaEditor agendaEditor = getAgendaEditor(form); 338 AgendaBo agenda = agendaEditor.getAgenda(); 339 AgendaItemBo newAgendaItem = agendaEditor.getAgendaItemLine(); 340 341 if (!validateProposition(newAgendaItem.getRule().getProposition(), newAgendaItem.getRule().getNamespace())) { 342 form.getActionParameters().put(UifParameters.NAVIGATE_TO_PAGE_ID, "AgendaEditorView-AddRule-Page"); 343 // NOTICE short circuit method on invalid proposition 344 return super.navigate(form); 345 } 346 347 newAgendaItem.getRule().setAttributes(agendaEditor.getCustomRuleAttributesMap()); 348 349 // Check to see if the Rule Id already exists. Essentially the user clicked on AddRule without selection 350 // of copyRule 351 RuleDefinition existingRule = getRuleBoService().getRuleByRuleId( newAgendaItem.getRuleId() ); 352 353 if (existingRule != null) { 354 GlobalVariables.getMessageMap().putError("AgendaEditorView-AddRule-Page", 355 "error.rule.unsavedCopyRule"); 356 return super.navigate(form); 357 } 358 359 updateRuleAction(agendaEditor); 360 361 if (agenda.getItems() == null) { 362 agenda.setItems(new ArrayList<AgendaItemBo>()); 363 } 364 365 AgendaEditorBusRule rule = new AgendaEditorBusRule(); 366 MaintenanceDocumentForm maintenanceForm = (MaintenanceDocumentForm) form; 367 MaintenanceDocument document = maintenanceForm.getDocument(); 368 369 if (rule.processAgendaItemBusinessRules(document)) { 370 newAgendaItem.setId(agendaItemIdIncrementer.getNewId()); 371 newAgendaItem.setAgendaId(getCreateAgendaId(agenda)); 372 373 if (agenda.getFirstItemId() == null) { 374 agenda.setFirstItem(newAgendaItem); 375 agenda.setFirstItemId(newAgendaItem.getId()); 376 } else { 377 // insert agenda in tree 378 String selectedAgendaItemId = getSelectedAgendaItemId(form); 379 380 if (StringUtils.isBlank(selectedAgendaItemId)) { 381 // add after the last root node 382 AgendaItemBo node = getFirstAgendaItem(agenda); 383 while (node.getAlways() != null) { 384 node = node.getAlways(); 385 } 386 node.setAlwaysId(newAgendaItem.getId()); 387 node.setAlways(newAgendaItem); 388 } else { 389 // add after selected node 390 AgendaItemBo firstItem = getFirstAgendaItem(agenda); 391 AgendaItemBo node = getAgendaItemById(firstItem, selectedAgendaItemId); 392 newAgendaItem.setAlwaysId(node.getAlwaysId()); 393 newAgendaItem.setAlways(node.getAlways()); 394 node.setAlwaysId(newAgendaItem.getId()); 395 node.setAlways(newAgendaItem); 396 } 397 } 398 // add it to the collection on the agenda too 399 agenda.getItems().add(newAgendaItem); 400 agendaEditor.setAddRuleInProgress(false); 401 form.getActionParameters().put(UifParameters.NAVIGATE_TO_PAGE_ID, "AgendaEditorView-Agenda-Page"); 402 } else { 403 form.getActionParameters().put(UifParameters.NAVIGATE_TO_PAGE_ID, "AgendaEditorView-AddRule-Page"); 404 } 405 406 return super.navigate(form); 407 } 408 409 /** 410 * Validate the given proposition and its children. Note that this method is side-effecting, 411 * when errors are detected with the proposition, errors are added to the error map. 412 * @param proposition the proposition to validate 413 * @param namespace the namespace of the parent rule 414 * @return true if the proposition and its children (if any) are considered valid 415 */ 416 // TODO also wire up to proposition for faster feedback to the user 417 private boolean validateProposition(PropositionBo proposition, String namespace) { 418 boolean result = true; 419 420 if (proposition != null) { // Null props are allowed. 421 422 if (StringUtils.isBlank(proposition.getDescription())) { 423 GlobalVariables.getMessageMap().putError(KRMSPropertyConstants.Rule.PROPOSITION_TREE_GROUP_ID, 424 "error.rule.proposition.missingDescription"); 425 result &= false; 426 } 427 428 if (StringUtils.isBlank(proposition.getCompoundOpCode())) { 429 // then this is a simple proposition, validate accordingly 430 431 result &= validateSimpleProposition(proposition, namespace); 432 433 } else { 434 // this is a compound proposition (or it should be) 435 List<PropositionBo> compoundComponents = proposition.getCompoundComponents(); 436 437 if (!CollectionUtils.isEmpty(proposition.getParameters())) { 438 GlobalVariables.getMessageMap().putError(KRMSPropertyConstants.Rule.PROPOSITION_TREE_GROUP_ID, 439 "error.rule.proposition.compound.invalidParameter", proposition.getDescription()); 440 result &= false; 441 } 442 443 // recurse 444 if (!CollectionUtils.isEmpty(compoundComponents)) { 445 for (PropositionBo childProp : compoundComponents) { 446 result &= validateProposition(childProp, namespace); 447 } 448 } 449 } 450 } 451 452 return result; 453 } 454 455 /** 456 * Validate the given simple proposition. Note that this method is side-effecting, 457 * when errors are detected with the proposition, errors are added to the error map. 458 * @param proposition the proposition to validate 459 * @param namespace the namespace of the parent rule 460 * @return true if the proposition is considered valid 461 */ 462 private boolean validateSimpleProposition(PropositionBo proposition, String namespace) { 463 boolean result = true; 464 465 if (!CollectionUtils.isEmpty(proposition.getCompoundComponents())) { 466 GlobalVariables.getMessageMap().putError(KRMSPropertyConstants.Rule.PROPOSITION_TREE_GROUP_ID, 467 "error.rule.proposition.simple.hasChildren", proposition.getDescription()); 468 result &= false; // simple prop should not have compound components 469 } 470 471 if (CollectionUtils.isEmpty(proposition.getParameters())) { 472 result &= false; 473 474 return result; 475 } 476 477 String propConstant = null; 478 if (proposition.getParameters().get(1) != null) { 479 propConstant = proposition.getParameters().get(1).getValue(); 480 } 481 String operatorCode = null; 482 if (proposition.getParameters().get(2) != null) { 483 operatorCode = proposition.getParameters().get(2).getValue(); 484 } 485 486 String termId = null; 487 if (proposition.getParameters().get(0) != null) { 488 termId = proposition.getParameters().get(0).getValue(); 489 } 490 491 // Simple proposition requires all of propConstant, termId and operator to be specified 492 if (StringUtils.isBlank(termId)) { 493 GlobalVariables.getMessageMap().putErrorWithoutFullErrorPath(KRMSPropertyConstants.Rule.PROPOSITION_TREE_GROUP_ID, 494 "error.rule.proposition.simple.blankField", proposition.getDescription(), "Term"); 495 result &= false; 496 } else { 497 result = validateTerm(proposition, namespace); 498 } 499 500 if (StringUtils.isBlank(operatorCode)) { 501 GlobalVariables.getMessageMap().putErrorWithoutFullErrorPath(KRMSPropertyConstants.Rule.PROPOSITION_TREE_GROUP_ID, 502 "error.rule.proposition.simple.blankField", proposition.getDescription(), "Operator"); 503 result &= false; 504 505 // The remaining checks depend on a non-blank operator, so we'll short circuit to avoid possible NPEs 506 return result; 507 } 508 509 if (StringUtils.isBlank(propConstant) && !operatorCode.endsWith("null")) { // ==null and !=null operators have blank values. 510 GlobalVariables.getMessageMap().putErrorForSectionId(KRMSPropertyConstants.Rule.PROPOSITION_TREE_GROUP_ID, 511 "error.rule.proposition.simple.blankField", proposition.getDescription(), "Value"); 512 result &= false; 513 } else if (operatorCode.endsWith("null")) { // ==null and !=null operators have blank values. 514 if (propConstant != null) { 515 proposition.getParameters().get(1).setValue(null); 516 } 517 } else if (!StringUtils.isBlank(termId)) { 518 // validate that the constant value is comparable against the term 519 String termType = lookupTermType(termId); 520 521 if (operatorCode.startsWith(KrmsImplConstants.CUSTOM_OPERATOR_PREFIX)) { 522 CustomOperator customOperator = getCustomOperatorUiTranslator().getCustomOperator(operatorCode); 523 List<RemotableAttributeError> errors = customOperator.validateOperandClasses(termType, String.class.getName()); 524 525 if (!CollectionUtils.isEmpty(errors)) { 526 for (RemotableAttributeError error : errors) { 527 GlobalVariables.getMessageMap().putError(KRMSPropertyConstants.Rule.PROPOSITION_TREE_GROUP_ID, 528 error.getMessage(), proposition.getDescription(), termType); 529 } 530 531 result &= false; 532 } 533 } else { 534 ComparisonOperatorService comparisonOperatorService = KrmsApiServiceLocator.getComparisonOperatorService(); 535 if (comparisonOperatorService.canCoerce(termType, propConstant)) { 536 if (comparisonOperatorService.coerce(termType, propConstant) == null) { 537 GlobalVariables.getMessageMap().putError(KRMSPropertyConstants.Rule.PROPOSITION_TREE_GROUP_ID, 538 "error.rule.proposition.simple.invalidValue", proposition.getDescription(), propConstant); 539 result &= false; 540 } 541 } 542 } 543 } 544 545 return result; 546 } 547 548 /** 549 * Validate the term in the given simple proposition. Note that this method is side-effecting, 550 * when errors are detected with the proposition, errors are added to the error map. 551 * @param proposition the proposition with the term to validate 552 * @param namespace the namespace of the parent rule 553 * @return true if the proposition's term is considered valid 554 */ 555 private boolean validateTerm(PropositionBo proposition, String namespace) { 556 boolean result = true; 557 558 String termId = proposition.getParameters().get(0).getValue(); 559 if (termId.startsWith(KrmsImplConstants.PARAMETERIZED_TERM_PREFIX)) { 560 // validate parameterized term 561 562 // is the term name non-blank 563 if (StringUtils.isBlank(proposition.getNewTermDescription())) { 564 GlobalVariables.getMessageMap().putErrorWithoutFullErrorPath(KRMSPropertyConstants.Rule.PROPOSITION_TREE_GROUP_ID, 565 "error.rule.proposition.simple.emptyTermName", proposition.getDescription()); 566 result &= false; 567 } else { // check if the term name is unique 568 569 Map<String, String> critMap = new HashMap<String, String>(); 570 571 critMap.put("description", proposition.getNewTermDescription()); 572 critMap.put("specification.namespace", namespace); 573 QueryByCriteria criteria = QueryByCriteria.Builder.andAttributes(critMap).build(); 574 575 QueryResults<TermBo> matchingTerms = 576 KRADServiceLocator.getDataObjectService().findMatching(TermBo.class, criteria); 577 578 if (!CollectionUtils.isEmpty(matchingTerms.getResults())) { 579 // this is a Warning -- maybe it should be an error? 580 GlobalVariables.getMessageMap().putWarningWithoutFullErrorPath(KRMSPropertyConstants.Rule.PROPOSITION_TREE_GROUP_ID, 581 "warning.rule.proposition.simple.duplicateTermName", proposition.getDescription()); 582 } 583 } 584 585 String termSpecificationId = termId.substring(KrmsImplConstants.PARAMETERIZED_TERM_PREFIX.length()); 586 587 TermResolverDefinition termResolverDefinition = 588 AgendaEditorMaintainable.getSimplestTermResolver(termSpecificationId, namespace); 589 590 if (termResolverDefinition == null) { 591 GlobalVariables.getMessageMap().putErrorWithoutFullErrorPath(KRMSPropertyConstants.Rule.PROPOSITION_TREE_GROUP_ID, 592 "error.rule.proposition.simple.invalidTerm", proposition.getDescription()); 593 result &= false; 594 } else { 595 List<String> parameterNames = new ArrayList<String>(termResolverDefinition.getParameterNames()); 596 Collections.sort(parameterNames); 597 for (String parameterName : parameterNames) { 598 if (!proposition.getTermParameters().containsKey(parameterName) || 599 StringUtils.isBlank(proposition.getTermParameters().get(parameterName))) { 600 GlobalVariables.getMessageMap().putErrorWithoutFullErrorPath(KRMSPropertyConstants.Rule.PROPOSITION_TREE_GROUP_ID, 601 "error.rule.proposition.simple.missingTermParameter", proposition.getDescription()); 602 result &= false; 603 break; 604 } 605 } 606 } 607 608 } else { 609 //validate normal term 610 TermDefinition termDefinition = KrmsRepositoryServiceLocator.getTermBoService().getTerm(termId); 611 if (termDefinition == null) { 612 GlobalVariables.getMessageMap().putErrorWithoutFullErrorPath(KRMSPropertyConstants.Rule.PROPOSITION_TREE_GROUP_ID, 613 "error.rule.proposition.simple.invalidTerm", proposition.getDescription()); 614 } else if (!namespace.equals(termDefinition.getSpecification().getNamespace())) { 615 GlobalVariables.getMessageMap().putErrorWithoutFullErrorPath(KRMSPropertyConstants.Rule.PROPOSITION_TREE_GROUP_ID, 616 "error.rule.proposition.simple.invalidTerm", proposition.getDescription()); 617 } 618 } 619 return result; 620 } 621 622 /** 623 * Lookup the {@link org.kuali.rice.krms.api.repository.term.TermSpecificationDefinitionContract} type. 624 * @param key krms_term_t key 625 * @return String the krms_term_spec_t TYP for the given krms_term_t key given 626 */ 627 private String lookupTermType(String key) { 628 TermSpecificationDefinition termSpec = null; 629 if (key.startsWith(KrmsImplConstants.PARAMETERIZED_TERM_PREFIX)) { 630 String termSpecificationId = key.substring(KrmsImplConstants.PARAMETERIZED_TERM_PREFIX.length()); 631 termSpec = KrmsRepositoryServiceLocator.getTermBoService().getTermSpecificationById(termSpecificationId); 632 } else { 633 TermDefinition term = KrmsRepositoryServiceLocator.getTermBoService().getTerm(key); 634 if (term != null) { 635 termSpec = term.getSpecification(); 636 } 637 } 638 if (termSpec != null) { 639 return termSpec.getType(); 640 } else { 641 return null; 642 } 643 } 644 645 646 /** 647 * This method returns the agendaId of the given agenda. If the agendaId is null a new id will be created. 648 */ 649 private String getCreateAgendaId(AgendaBo agenda) { 650 if (agenda.getId() == null) { 651 agenda.setId(agendaIdIncrementer.getNewId()); 652 } 653 654 return agenda.getId(); 655 } 656 657 private void updateRuleAction(AgendaEditor agendaEditor) { 658 agendaEditor.getAgendaItemLine().getRule().setActions(new ArrayList<ActionBo>()); 659 if (StringUtils.isNotBlank(agendaEditor.getAgendaItemLineRuleAction().getTypeId())) { 660 agendaEditor.getAgendaItemLineRuleAction().setAttributes(agendaEditor.getCustomRuleActionAttributesMap()); 661 agendaEditor.getAgendaItemLine().getRule().getActions().add(agendaEditor.getAgendaItemLineRuleAction()); 662 } 663 } 664 665 /** 666 * Build a map from attribute name to attribute definition from all the defined attribute definitions for the 667 * specified rule action type 668 * @param actionTypeId 669 * @return 670 */ 671 private Map<String, KrmsAttributeDefinition> buildAttributeDefinitionMap(String actionTypeId) { 672 KrmsAttributeDefinitionService attributeDefinitionService = 673 KrmsRepositoryServiceLocator.getKrmsAttributeDefinitionService(); 674 675 // build a map from attribute name to definition 676 Map<String, KrmsAttributeDefinition> attributeDefinitionMap = new HashMap<String, KrmsAttributeDefinition>(); 677 678 List<KrmsAttributeDefinition> attributeDefinitions = 679 attributeDefinitionService.findAttributeDefinitionsByType(actionTypeId); 680 681 for (KrmsAttributeDefinition attributeDefinition : attributeDefinitions) { 682 attributeDefinitionMap.put(attributeDefinition.getName(), attributeDefinition); 683 } 684 return attributeDefinitionMap; 685 } 686 687 /** 688 * This method updates the existing rule in the agenda. 689 */ 690 @RequestMapping(params = "methodToCall=" + "editRule") 691 public ModelAndView editRule(UifFormBase form) throws Exception { 692 AgendaEditor agendaEditor = getAgendaEditor(form); 693 // this is the root of the tree: 694 AgendaItemBo firstItem = getFirstAgendaItem(agendaEditor.getAgenda()); 695 AgendaItemBo node = getAgendaItemById(firstItem, getSelectedAgendaItemId(form)); 696 AgendaItemBo agendaItemLine = agendaEditor.getAgendaItemLine(); 697 698 if (!validateProposition(agendaItemLine.getRule().getProposition(), agendaItemLine.getRule().getNamespace())) { 699 form.getActionParameters().put(UifParameters.NAVIGATE_TO_PAGE_ID, "AgendaEditorView-EditRule-Page"); 700 // NOTICE short circuit method on invalid proposition 701 return super.navigate(form); 702 } 703 704 agendaItemLine.getRule().setAttributes(agendaEditor.getCustomRuleAttributesMap()); 705 updateRuleAction(agendaEditor); 706 707 AgendaEditorBusRule rule = new AgendaEditorBusRule(); 708 MaintenanceDocumentForm maintenanceForm = (MaintenanceDocumentForm) form; 709 MaintenanceDocument document = maintenanceForm.getDocument(); 710 if (rule.processAgendaItemBusinessRules(document)) { 711 node.setRule(agendaItemLine.getRule()); 712 form.getActionParameters().put(UifParameters.NAVIGATE_TO_PAGE_ID, "AgendaEditorView-Agenda-Page"); 713 } else { 714 form.getActionParameters().put(UifParameters.NAVIGATE_TO_PAGE_ID, "AgendaEditorView-EditRule-Page"); 715 } 716 717 // apply deleted item IDs 718 agendaEditor.applyDeletedPropositionIdsFromRule(); 719 720 return super.navigate(form); 721 } 722 723 /** 724 * @return the ALWAYS {@link AgendaItemInstanceChildAccessor} for the last ALWAYS child of the instance accessed by the parameter. 725 * It will by definition refer to null. If the instanceAccessor parameter refers to null, then it will be returned. This is useful 726 * for adding a youngest child to a sibling group. 727 */ 728 private AgendaItemInstanceChildAccessor getLastChildsAlwaysAccessor(AgendaItemInstanceChildAccessor instanceAccessor) { 729 AgendaItemBo next = instanceAccessor.getChild(); 730 if (next == null) { 731 return instanceAccessor; 732 } 733 while (next.getAlways() != null) { next = next.getAlways(); }; 734 return new AgendaItemInstanceChildAccessor(AgendaItemChildAccessor.always, next); 735 } 736 737 /** 738 * @return the accessor to the child with the given agendaItemId under the given parent. This method will search both When TRUE and 739 * When FALSE sibling groups. If the instance with the given id is not found, null is returned. 740 * @see AgendaItemChildAccessor for nomenclature explanation 741 */ 742 private AgendaItemInstanceChildAccessor getInstanceAccessorToChild(AgendaItemBo parent, String agendaItemId) { 743 744 // first try When TRUE, then When FALSE via AgendaItemChildAccessor.levelOrderChildren 745 for (AgendaItemChildAccessor levelOrderChildAccessor : AgendaItemChildAccessor.children) { 746 747 AgendaItemBo next = levelOrderChildAccessor.getChild(parent); 748 749 // if the first item matches, return the accessor from the parent 750 if (next != null && agendaItemId.equals(next.getId())) { 751 return new AgendaItemInstanceChildAccessor(levelOrderChildAccessor, parent); 752 } 753 754 // otherwise walk the children 755 while (next != null && next.getAlwaysId() != null) { 756 if (next.getAlwaysId().equals(agendaItemId)) { 757 return new AgendaItemInstanceChildAccessor(AgendaItemChildAccessor.always, next); 758 } 759 // move down 760 next = next.getAlways(); 761 } 762 } 763 764 return null; 765 } 766 767 @RequestMapping(params = "methodToCall=" + "ajaxRefresh") 768 public ModelAndView ajaxRefresh(@ModelAttribute("KualiForm") UifFormBase form, BindingResult result, 769 HttpServletRequest request, HttpServletResponse response) 770 throws Exception { 771 // call the super method to avoid the agenda tree being reloaded from the db 772 return getModelAndView(form); 773 } 774 775 @RequestMapping(params = "methodToCall=" + "ajaxMoveUp") 776 public ModelAndView ajaxMoveUp(@ModelAttribute("KualiForm") UifFormBase form, BindingResult result, 777 HttpServletRequest request, HttpServletResponse response) 778 throws Exception { 779 moveSelectedSubtreeUp(form); 780 781 // call the super method to avoid the agenda tree being reloaded from the db 782 return getModelAndView(form); 783 } 784 785 /** 786 * Exposes Ajax callback to UI to validate entered rule name to copy 787 * @param name the copyRuleName 788 * @param namespace the rule namespace 789 * @return true or false 790 */ 791 @RequestMapping(params = "methodToCall=" + "ajaxValidRuleName", method=RequestMethod.GET) 792 public @ResponseBody boolean ajaxValidRuleName(@RequestParam String name, @RequestParam String namespace) { 793 return (getRuleBoService().getRuleByNameAndNamespace(name, namespace) != null); 794 } 795 796 /** 797 * 798 * @param form 799 * @see AgendaItemChildAccessor for nomenclature explanation 800 */ 801 private void moveSelectedSubtreeUp(UifFormBase form) { 802 803 /* Rough algorithm for moving a node up. This is a "level order" move. Note that in this tree, 804 * level order means something a bit funky. We are defining a level as it would be displayed in the browser, 805 * so only the traversal of When FALSE or When TRUE links increments the level, since ALWAYS linked nodes are 806 * considered siblings. 807 * 808 * find the following: 809 * node := the selected node 810 * parent := the selected node's parent, its containing node (via when true or when false relationship) 811 * parentsOlderCousin := the parent's level-order predecessor (sibling or cousin) 812 * 813 * if (node is first child in sibling group) 814 * if (node is in When FALSE group) 815 * move node to last position in When TRUE group 816 * else 817 * find youngest child of parentsOlderCousin and put node after it 818 * else 819 * move node up within its sibling group 820 */ 821 822 AgendaEditor agendaEditor = getAgendaEditor(form); 823 // this is the root of the tree: 824 AgendaItemBo firstItem = getFirstAgendaItem(agendaEditor.getAgenda()); 825 826 String selectedItemId = agendaEditor.getSelectedAgendaItemId(); 827 AgendaItemBo node = getAgendaItemById(firstItem, selectedItemId); 828 AgendaItemBo parent = getParent(firstItem, selectedItemId); 829 AgendaItemBo parentsOlderCousin = (parent == null) ? null : getNextOldestOfSameGeneration(firstItem, parent); 830 831 StringBuilder ruleEditorMessage = new StringBuilder(); 832 AgendaItemChildAccessor childAccessor = getOldestChildAccessor(node, parent); 833 if (childAccessor != null) { // node is first child in sibling group 834 if (childAccessor == AgendaItemChildAccessor.whenFalse) { 835 // move node to last position in When TRUE group 836 AgendaItemInstanceChildAccessor youngestWhenTrueSiblingInsertionPoint = 837 getLastChildsAlwaysAccessor(new AgendaItemInstanceChildAccessor(AgendaItemChildAccessor.whenTrue, parent)); 838 youngestWhenTrueSiblingInsertionPoint.setChild(node); 839 AgendaItemChildAccessor.whenFalse.setChild(parent, node.getAlways()); 840 AgendaItemChildAccessor.always.setChild(node, null); 841 842 ruleEditorMessage.append("Moved ").append(node.getRule().getName()).append(" up "); 843 ruleEditorMessage.append("to last position in When TRUE group of ").append(parent.getRule().getName()); 844 } else if (parentsOlderCousin != null) { 845 // find youngest child of parentsOlderCousin and put node after it 846 AgendaItemInstanceChildAccessor youngestWhenFalseSiblingInsertionPoint = 847 getLastChildsAlwaysAccessor(new AgendaItemInstanceChildAccessor(AgendaItemChildAccessor.whenFalse, parentsOlderCousin)); 848 youngestWhenFalseSiblingInsertionPoint.setChild(node); 849 AgendaItemChildAccessor.whenTrue.setChild(parent, node.getAlways()); 850 AgendaItemChildAccessor.always.setChild(node, null); 851 ruleEditorMessage.append("Moved ").append(node.getRule().getName()).append(" up "); 852 ruleEditorMessage.append("to When FALSE group of ").append(parentsOlderCousin.getRule().getName()); 853 } 854 } else if (!selectedItemId.equals(firstItem.getId())) { // conditional to miss special case of first node 855 856 AgendaItemBo bogusRootNode = null; 857 if (parent == null) { 858 // special case, this is a top level sibling. rig up special parent node 859 bogusRootNode = new AgendaItemBo(); 860 AgendaItemChildAccessor.whenTrue.setChild(bogusRootNode, firstItem); 861 parent = bogusRootNode; 862 } 863 864 // move node up within its sibling group 865 AgendaItemInstanceChildAccessor accessorToSelectedNode = getInstanceAccessorToChild(parent, node.getId()); 866 AgendaItemBo olderSibling = accessorToSelectedNode.getInstance(); 867 AgendaItemInstanceChildAccessor accessorToOlderSibling = getInstanceAccessorToChild(parent, olderSibling.getId()); 868 869 accessorToOlderSibling.setChild(node); 870 accessorToSelectedNode.setChild(node.getAlways()); 871 AgendaItemChildAccessor.always.setChild(node, olderSibling); 872 873 ruleEditorMessage.append("Moved ").append(node.getRule().getName()).append(" up "); 874 875 if (bogusRootNode != null) { 876 // clean up special case with bogus root node 877 agendaEditor.getAgenda().setFirstItemId(bogusRootNode.getWhenTrueId()); 878 agendaEditor.getAgenda().setFirstItem(bogusRootNode.getWhenTrue()); 879 ruleEditorMessage.append(" to ").append(getFirstAgendaItem(agendaEditor.getAgenda()).getRule().getName()).append(" When TRUE group"); 880 } else { 881 ruleEditorMessage.append(" within its sibling group, above " + olderSibling.getRule().getName()); 882 } 883 } 884 agendaEditor.setRuleEditorMessage(ruleEditorMessage.toString()); 885 } 886 887 @RequestMapping(params = "methodToCall=" + "ajaxMoveDown") 888 public ModelAndView ajaxMoveDown(@ModelAttribute("KualiForm") UifFormBase form, BindingResult result, 889 HttpServletRequest request, HttpServletResponse response) 890 throws Exception { 891 moveSelectedSubtreeDown(form); 892 893 // call the super method to avoid the agenda tree being reloaded from the db 894 return getModelAndView(form); 895 } 896 897 /** 898 * 899 * @param form 900 * @see AgendaItemChildAccessor for nomenclature explanation 901 */ 902 private void moveSelectedSubtreeDown(UifFormBase form) { 903 904 /* Rough algorithm for moving a node down. This is a "level order" move. Note that in this tree, 905 * level order means something a bit funky. We are defining a level as it would be displayed in the browser, 906 * so only the traversal of When FALSE or When TRUE links increments the level, since ALWAYS linked nodes are 907 * considered siblings. 908 * 909 * find the following: 910 * node := the selected node 911 * parent := the selected node's parent, its containing node (via when true or when false relationship) 912 * parentsYoungerCousin := the parent's level-order successor (sibling or cousin) 913 * 914 * if (node is last child in sibling group) 915 * if (node is in When TRUE group) 916 * move node to first position in When FALSE group 917 * else 918 * move to first child of parentsYoungerCousin 919 * else 920 * move node down within its sibling group 921 */ 922 923 AgendaEditor agendaEditor = getAgendaEditor(form); 924 // this is the root of the tree: 925 AgendaItemBo firstItem = getFirstAgendaItem(agendaEditor.getAgenda()); 926 927 String selectedItemId = agendaEditor.getSelectedAgendaItemId(); 928 AgendaItemBo node = getAgendaItemById(firstItem, selectedItemId); 929 AgendaItemBo parent = getParent(firstItem, selectedItemId); 930 AgendaItemBo parentsYoungerCousin = (parent == null) ? null : getNextYoungestOfSameGeneration(firstItem, parent); 931 932 StringBuilder ruleEditorMessage = new StringBuilder(); 933 if (node.getAlways() == null && parent != null) { // node is last child in sibling group 934 // set link to selected node to null 935 if (parent.getWhenTrue() != null && isSiblings(parent.getWhenTrue(), node)) { // node is in When TRUE group 936 // move node to first child under When FALSE 937 AgendaItemInstanceChildAccessor accessorToSelectedNode = getInstanceAccessorToChild(parent, node.getId()); 938 accessorToSelectedNode.setChild(null); 939 940 AgendaItemBo parentsFirstChild = parent.getWhenFalse(); 941 AgendaItemChildAccessor.whenFalse.setChild(parent, node); 942 AgendaItemChildAccessor.always.setChild(node, parentsFirstChild); 943 944 ruleEditorMessage.append("Moved ").append(node.getRule().getName()).append(" down "); 945 ruleEditorMessage.append("to first child under When FALSE group of ").append(parent.getRule().getName()); 946 } else if (parentsYoungerCousin != null) { // node is in the When FALSE group 947 // move to first child of parentsYoungerCousin under When TRUE 948 AgendaItemInstanceChildAccessor accessorToSelectedNode = getInstanceAccessorToChild(parent, node.getId()); 949 accessorToSelectedNode.setChild(null); 950 951 AgendaItemBo parentsYoungerCousinsFirstChild = parentsYoungerCousin.getWhenTrue(); 952 AgendaItemChildAccessor.whenTrue.setChild(parentsYoungerCousin, node); 953 AgendaItemChildAccessor.always.setChild(node, parentsYoungerCousinsFirstChild); 954 955 ruleEditorMessage.append("Moved ").append(node.getRule().getName()).append(" down "); 956 ruleEditorMessage.append("to first child under When TRUE group of ").append(parentsYoungerCousin.getRule().getName()); 957 } 958 } else if (node.getAlways() != null) { // move node down within its sibling group 959 960 AgendaItemBo bogusRootNode = null; 961 if (parent == null) { 962 // special case, this is a top level sibling. rig up special parent node 963 964 bogusRootNode = new AgendaItemBo(); 965 AgendaItemChildAccessor.whenFalse.setChild(bogusRootNode, firstItem); 966 parent = bogusRootNode; 967 } 968 969 // move node down within its sibling group 970 AgendaItemInstanceChildAccessor accessorToSelectedNode = getInstanceAccessorToChild(parent, node.getId()); 971 AgendaItemBo youngerSibling = node.getAlways(); 972 accessorToSelectedNode.setChild(youngerSibling); 973 AgendaItemChildAccessor.always.setChild(node, youngerSibling.getAlways()); 974 AgendaItemChildAccessor.always.setChild(youngerSibling, node); 975 976 if (bogusRootNode != null) { 977 // clean up special case with bogus root node 978 agendaEditor.getAgenda().setFirstItemId(bogusRootNode.getWhenFalseId()); 979 agendaEditor.getAgenda().setFirstItem(bogusRootNode.getWhenFalse()); 980 } 981 ruleEditorMessage.append("Moved ").append(node.getRule().getName()).append(" down "); 982 ruleEditorMessage.append(" within its sibling group, below ").append(youngerSibling.getRule().getName()); 983 } // falls through if already bottom-most 984 agendaEditor.setRuleEditorMessage(ruleEditorMessage.toString()); 985 } 986 987 @RequestMapping(params = "methodToCall=" + "ajaxMoveLeft") 988 public ModelAndView ajaxMoveLeft(@ModelAttribute("KualiForm") UifFormBase form, BindingResult result, 989 HttpServletRequest request, HttpServletResponse response) 990 throws Exception { 991 992 moveSelectedSubtreeLeft(form); 993 994 // call the super method to avoid the agenda tree being reloaded from the db 995 return getModelAndView(form); 996 } 997 998 /** 999 * 1000 * @param form 1001 * @see AgendaItemChildAccessor for nomenclature explanation 1002 */ 1003 private void moveSelectedSubtreeLeft(UifFormBase form) { 1004 1005 /* 1006 * Move left means make it a younger sibling of it's parent. 1007 */ 1008 1009 AgendaEditor agendaEditor = getAgendaEditor(form); 1010 // this is the root of the tree: 1011 AgendaItemBo firstItem = getFirstAgendaItem(agendaEditor.getAgenda()); 1012 1013 String selectedItemId = agendaEditor.getSelectedAgendaItemId(); 1014 AgendaItemBo node = getAgendaItemById(firstItem, selectedItemId); 1015 AgendaItemBo parent = getParent(firstItem, selectedItemId); 1016 1017 if (parent != null) { 1018 AgendaItemInstanceChildAccessor accessorToSelectedNode = getInstanceAccessorToChild(parent, node.getId()); 1019 accessorToSelectedNode.setChild(node.getAlways()); 1020 AgendaItemChildAccessor.always.setChild(node, parent.getAlways()); 1021 AgendaItemChildAccessor.always.setChild(parent, node); 1022 1023 StringBuilder ruleEditorMessage = new StringBuilder(); 1024 ruleEditorMessage.append("Moved ").append(node.getRule().getName()).append(" left to be a sibling of its parent ").append(parent.getRule().getName()); 1025 agendaEditor.setRuleEditorMessage(ruleEditorMessage.toString()); 1026 } 1027 } 1028 1029 @RequestMapping(params = "methodToCall=" + "ajaxMoveRight") 1030 public ModelAndView ajaxMoveRight(@ModelAttribute("KualiForm") UifFormBase form, BindingResult result, 1031 HttpServletRequest request, HttpServletResponse response) 1032 throws Exception { 1033 1034 moveSelectedSubtreeRight(form); 1035 1036 // call the super method to avoid the agenda tree being reloaded from the db 1037 return getModelAndView(form); 1038 } 1039 1040 /** 1041 * 1042 * @param form 1043 * @see AgendaItemChildAccessor for nomenclature explanation 1044 */ 1045 private void moveSelectedSubtreeRight(UifFormBase form) { 1046 1047 /* 1048 * Move right prefers moving to bottom of upper sibling's When FALSE branch 1049 * ... otherwise .. 1050 * moves to top of lower sibling's When TRUE branch 1051 */ 1052 1053 AgendaEditor agendaEditor = getAgendaEditor(form); 1054 // this is the root of the tree: 1055 AgendaItemBo firstItem = getFirstAgendaItem(agendaEditor.getAgenda()); 1056 1057 String selectedItemId = agendaEditor.getSelectedAgendaItemId(); 1058 AgendaItemBo node = getAgendaItemById(firstItem, selectedItemId); 1059 AgendaItemBo parent = getParent(firstItem, selectedItemId); 1060 1061 AgendaItemBo bogusRootNode = null; 1062 if (parent == null) { 1063 // special case, this is a top level sibling. rig up special parent node 1064 bogusRootNode = new AgendaItemBo(); 1065 AgendaItemChildAccessor.whenFalse.setChild(bogusRootNode, firstItem); 1066 parent = bogusRootNode; 1067 } 1068 1069 AgendaItemInstanceChildAccessor accessorToSelectedNode = getInstanceAccessorToChild(parent, node.getId()); 1070 AgendaItemBo olderSibling = (accessorToSelectedNode.getInstance() == parent) ? null : accessorToSelectedNode.getInstance(); 1071 1072 StringBuilder ruleEditorMessage = new StringBuilder(); 1073 if (olderSibling != null) { 1074 accessorToSelectedNode.setChild(node.getAlways()); 1075 AgendaItemInstanceChildAccessor yougestWhenFalseSiblingInsertionPoint = 1076 getLastChildsAlwaysAccessor(new AgendaItemInstanceChildAccessor(AgendaItemChildAccessor.whenFalse, olderSibling)); 1077 yougestWhenFalseSiblingInsertionPoint.setChild(node); 1078 AgendaItemChildAccessor.always.setChild(node, null); 1079 1080 ruleEditorMessage.append("Moved ").append(node.getRule().getName()).append(" right to "); 1081 ruleEditorMessage.append(olderSibling.getRule().getName()).append(" When FALSE group."); 1082 } else if (node.getAlways() != null) { // has younger sibling 1083 accessorToSelectedNode.setChild(node.getAlways()); 1084 AgendaItemBo childsWhenTrue = node.getAlways().getWhenTrue(); 1085 AgendaItemChildAccessor.whenTrue.setChild(node.getAlways(), node); 1086 AgendaItemChildAccessor.always.setChild(node, childsWhenTrue); 1087 1088 ruleEditorMessage.append("Moved ").append(node.getRule().getName()).append(" right to "); 1089 if (childsWhenTrue != null) { // childsWhenTrue is null if the topmost rule is moved right see bogusRootNode below 1090 ruleEditorMessage.append(childsWhenTrue.getRule().getName()).append(" When TRUE group"); 1091 } 1092 } // falls through if node is already the rightmost. 1093 1094 if (bogusRootNode != null) { 1095 // clean up special case with bogus root node 1096 agendaEditor.getAgenda().setFirstItemId(bogusRootNode.getWhenFalseId()); 1097 agendaEditor.getAgenda().setFirstItem(bogusRootNode.getWhenFalse()); 1098 ruleEditorMessage.append(getFirstAgendaItem(agendaEditor.getAgenda()).getRule().getName()).append(" When TRUE group"); 1099 } 1100 agendaEditor.setRuleEditorMessage(ruleEditorMessage.toString()); 1101 } 1102 1103 /** 1104 * 1105 * @param cousin1 1106 * @param cousin2 1107 * @return 1108 * @see AgendaItemChildAccessor for nomenclature explanation 1109 */ 1110 private boolean isSiblings(AgendaItemBo cousin1, AgendaItemBo cousin2) { 1111 if (cousin1.equals(cousin2)) 1112 { 1113 return true; // this is a bit abusive 1114 } 1115 1116 // can you walk to c1 from ALWAYS links of c2? 1117 AgendaItemBo candidate = cousin2; 1118 while (null != (candidate = candidate.getAlways())) { 1119 if (candidate.equals(cousin1)) { 1120 return true; 1121 } 1122 } 1123 // can you walk to c2 from ALWAYS links of c1? 1124 candidate = cousin1; 1125 while (null != (candidate = candidate.getAlways())) { 1126 if (candidate.equals(cousin2)) { 1127 return true; 1128 } 1129 } 1130 return false; 1131 } 1132 1133 /** 1134 * This method returns the level order accessor (getWhenTrue or getWhenFalse) that relates the parent directly 1135 * to the child. If the two nodes don't have such a relationship, null is returned. 1136 * Note that this only finds accessors for oldest children, not younger siblings. 1137 * @see AgendaItemChildAccessor for nomenclature explanation 1138 */ 1139 private AgendaItemChildAccessor getOldestChildAccessor( 1140 AgendaItemBo child, AgendaItemBo parent) { 1141 AgendaItemChildAccessor levelOrderChildAccessor = null; 1142 1143 if (parent != null) { 1144 for (AgendaItemChildAccessor childAccessor : AgendaItemChildAccessor.children) { 1145 if (child.equals(childAccessor.getChild(parent))) { 1146 levelOrderChildAccessor = childAccessor; 1147 break; 1148 } 1149 } 1150 } 1151 return levelOrderChildAccessor; 1152 } 1153 1154 /** 1155 * This method finds and returns the first agenda item in the agenda, or null if there are no items presently 1156 * 1157 * @param agenda 1158 * @return 1159 */ 1160 private AgendaItemBo getFirstAgendaItem(AgendaBo agenda) { 1161 AgendaItemBo firstItem = null; 1162 if (agenda != null && agenda.getItems() != null) { 1163 for (AgendaItemBo agendaItem : agenda.getItems()) { 1164 if (agenda.getFirstItemId().equals(agendaItem.getId())) { 1165 firstItem = agendaItem; 1166 break; 1167 } 1168 } 1169 } 1170 return firstItem; 1171 } 1172 1173 /** 1174 * @return the closest younger sibling of the agenda item with the given ID, and if there is no such sibling, the closest younger cousin. 1175 * If there is no such cousin either, then null is returned. 1176 * @see AgendaItemChildAccessor for nomenclature explanation 1177 */ 1178 private AgendaItemBo getNextYoungestOfSameGeneration(AgendaItemBo root, AgendaItemBo agendaItem) { 1179 1180 int genNumber = getAgendaItemGenerationNumber(0, root, agendaItem.getId()); 1181 List<AgendaItemBo> genList = new ArrayList<AgendaItemBo>(); 1182 buildAgendaItemGenerationList(genList, root, 0, genNumber); 1183 1184 int itemIndex = genList.indexOf(agendaItem); 1185 if (genList.size() > itemIndex + 1) { 1186 return genList.get(itemIndex + 1); 1187 } 1188 1189 return null; 1190 } 1191 1192 /** 1193 * 1194 * @param currentLevel 1195 * @param node 1196 * @param agendaItemId 1197 * @return 1198 * @see AgendaItemChildAccessor for nomenclature explanation 1199 */ 1200 private int getAgendaItemGenerationNumber(int currentLevel, AgendaItemBo node, String agendaItemId) { 1201 int result = -1; 1202 if (agendaItemId.equals(node.getId())) { 1203 result = currentLevel; 1204 } else { 1205 for (AgendaItemChildAccessor childAccessor : AgendaItemChildAccessor.linkedNodes) { 1206 AgendaItemBo child = childAccessor.getChild(node); 1207 if (child != null) { 1208 int nextLevel = currentLevel; 1209 // we don't change the level order parent when we traverse ALWAYS links 1210 if (childAccessor != AgendaItemChildAccessor.always) { 1211 nextLevel = currentLevel +1; 1212 } 1213 result = getAgendaItemGenerationNumber(nextLevel, child, agendaItemId); 1214 if (result != -1) { 1215 break; 1216 } 1217 } 1218 } 1219 } 1220 return result; 1221 } 1222 1223 /** 1224 * 1225 * @param genList 1226 * @param node 1227 * @param currentLevel 1228 * @param generation 1229 * @see AgendaItemChildAccessor for nomenclature explanation 1230 */ 1231 private void buildAgendaItemGenerationList(List<AgendaItemBo> genList, AgendaItemBo node, int currentLevel, int generation) { 1232 if (currentLevel == generation) { 1233 genList.add(node); 1234 } 1235 1236 if (currentLevel > generation) { 1237 return; 1238 } 1239 1240 for (AgendaItemChildAccessor childAccessor : AgendaItemChildAccessor.linkedNodes) { 1241 AgendaItemBo child = childAccessor.getChild(node); 1242 if (child != null) { 1243 int nextLevel = currentLevel; 1244 // we don't change the level order parent when we traverse ALWAYS links 1245 if (childAccessor != AgendaItemChildAccessor.always) { 1246 nextLevel = currentLevel +1; 1247 } 1248 buildAgendaItemGenerationList(genList, child, nextLevel, generation); 1249 } 1250 } 1251 } 1252 1253 /** 1254 * @return the closest older sibling of the agenda item with the given ID, and if there is no such sibling, the closest older cousin. 1255 * If there is no such cousin either, then null is returned. 1256 * @see AgendaItemChildAccessor for nomenclature explanation 1257 */ 1258 private AgendaItemBo getNextOldestOfSameGeneration(AgendaItemBo root, AgendaItemBo agendaItem) { 1259 1260 int genNumber = getAgendaItemGenerationNumber(0, root, agendaItem.getId()); 1261 List<AgendaItemBo> genList = new ArrayList<AgendaItemBo>(); 1262 buildAgendaItemGenerationList(genList, root, 0, genNumber); 1263 1264 int itemIndex = genList.indexOf(agendaItem); 1265 if (itemIndex >= 1) { 1266 return genList.get(itemIndex - 1); 1267 } 1268 1269 return null; 1270 } 1271 1272 1273 /** 1274 * returns the parent of the item with the passed in id. Note that {@link AgendaItemBo}s related by ALWAYS relationships are considered siblings. 1275 * @see AgendaItemChildAccessor for nomenclature explanation 1276 */ 1277 private AgendaItemBo getParent(AgendaItemBo root, String agendaItemId) { 1278 return getParentHelper(root, null, agendaItemId); 1279 } 1280 1281 /** 1282 * 1283 * @param node 1284 * @param levelOrderParent 1285 * @param agendaItemId 1286 * @return 1287 * @see AgendaItemChildAccessor for nomenclature explanation 1288 */ 1289 private AgendaItemBo getParentHelper(AgendaItemBo node, AgendaItemBo levelOrderParent, String agendaItemId) { 1290 AgendaItemBo result = null; 1291 if (agendaItemId.equals(node.getId())) { 1292 result = levelOrderParent; 1293 } else { 1294 for (AgendaItemChildAccessor childAccessor : AgendaItemChildAccessor.linkedNodes) { 1295 AgendaItemBo child = childAccessor.getChild(node); 1296 if (child != null) { 1297 // we don't change the level order parent when we traverse ALWAYS links 1298 AgendaItemBo lop = (childAccessor == AgendaItemChildAccessor.always) ? levelOrderParent : node; 1299 result = getParentHelper(child, lop, agendaItemId); 1300 if (result != null) { 1301 break; 1302 } 1303 } 1304 } 1305 } 1306 return result; 1307 } 1308 1309 /** 1310 * Search the tree for the agenda item with the given id. 1311 */ 1312 private AgendaItemBo getAgendaItemById(AgendaItemBo node, String agendaItemId) { 1313 if (node == null) { 1314 throw new IllegalArgumentException("node must be non-null"); 1315 } 1316 1317 AgendaItemBo result = null; 1318 1319 if (agendaItemId.equals(node.getId())) { 1320 result = node; 1321 } else { 1322 for (AgendaItemChildAccessor childAccessor : AgendaItemChildAccessor.linkedNodes) { 1323 AgendaItemBo child = childAccessor.getChild(node); 1324 if (child != null) { 1325 result = getAgendaItemById(child, agendaItemId); 1326 if (result != null) { 1327 break; 1328 } 1329 } 1330 } 1331 } 1332 return result; 1333 } 1334 1335 /** 1336 * @param form 1337 * @return the {@link AgendaEditor} from the form 1338 */ 1339 private AgendaEditor getAgendaEditor(UifFormBase form) { 1340 MaintenanceDocumentForm maintenanceForm = (MaintenanceDocumentForm) form; 1341 return ((AgendaEditor)maintenanceForm.getDocument().getDocumentDataObject()); 1342 } 1343 1344 @RequestMapping(params = "methodToCall=" + "ajaxDelete") 1345 public ModelAndView ajaxDelete(@ModelAttribute("KualiForm") UifFormBase form, BindingResult result, 1346 HttpServletRequest request, HttpServletResponse response) 1347 throws Exception { 1348 1349 deleteSelectedSubtree(form); 1350 1351 // call the super method to avoid the agenda tree being reloaded from the db 1352 return getModelAndView(form); 1353 } 1354 1355 private void deleteSelectedSubtree(UifFormBase form) { 1356 AgendaEditor agendaEditor = getAgendaEditor(form); 1357 AgendaItemBo firstItem = getFirstAgendaItem(agendaEditor.getAgenda()); 1358 1359 if (firstItem != null) { 1360 String agendaItemSelected = agendaEditor.getSelectedAgendaItemId(); 1361 AgendaItemBo selectedItem = getAgendaItemById(firstItem, agendaItemSelected); 1362 1363 // need to handle the first item here, our recursive method won't handle it. 1364 if (agendaItemSelected.equals(firstItem.getId())) { 1365 agendaEditor.getAgenda().setFirstItemId(firstItem.getAlwaysId()); 1366 agendaEditor.getAgenda().setFirstItem(firstItem.getAlways()); 1367 } else { 1368 deleteAgendaItem(firstItem, agendaItemSelected); 1369 } 1370 1371 StringBuilder ruleEditorMessage = new StringBuilder(); 1372 ruleEditorMessage.append("Deleted ").append(selectedItem.getRule().getName()); 1373 // remove agenda item and its whenTrue & whenFalse children from the list of agendaItems of the agenda 1374 if (selectedItem.getWhenTrue() != null) { 1375 removeAgendaItem(agendaEditor.getAgenda().getItems(), selectedItem.getWhenTrue()); 1376 ruleEditorMessage.append(" and its When TRUE ").append(selectedItem.getWhenTrue().getRule().getName()); 1377 } 1378 if (selectedItem.getWhenFalse() != null) { 1379 removeAgendaItem(agendaEditor.getAgenda().getItems(), selectedItem.getWhenFalse()); 1380 ruleEditorMessage.append(" and its When FALSE ").append(selectedItem.getWhenFalse().getRule().getName()); 1381 } 1382 agendaEditor.getAgenda().getItems().remove(selectedItem); 1383 agendaEditor.setRuleEditorMessage(ruleEditorMessage.toString()); 1384 } 1385 } 1386 1387 private void deleteAgendaItem(AgendaItemBo root, String agendaItemIdToDelete) { 1388 if (deleteAgendaItem(root, AgendaItemChildAccessor.whenTrue, agendaItemIdToDelete) || 1389 deleteAgendaItem(root, AgendaItemChildAccessor.whenFalse, agendaItemIdToDelete) || 1390 deleteAgendaItem(root, AgendaItemChildAccessor.always, agendaItemIdToDelete)) 1391 { 1392 ; // TODO: this is confusing, refactor 1393 } 1394 } 1395 1396 private boolean deleteAgendaItem(AgendaItemBo agendaItem, AgendaItemChildAccessor childAccessor, String agendaItemIdToDelete) { 1397 if (agendaItem == null || childAccessor.getChild(agendaItem) == null) { 1398 return false; 1399 } 1400 if (agendaItemIdToDelete.equals(childAccessor.getChild(agendaItem).getId())) { 1401 // delete the child in such a way that any ALWAYS children don't get lost from the tree 1402 AgendaItemBo grandchildToKeep = childAccessor.getChild(agendaItem).getAlways(); 1403 childAccessor.setChild(agendaItem, grandchildToKeep); 1404 return true; 1405 } else { 1406 AgendaItemBo child = childAccessor.getChild(agendaItem); 1407 // recurse 1408 for (AgendaItemChildAccessor nextChildAccessor : AgendaItemChildAccessor.linkedNodes) { 1409 if (deleteAgendaItem(child, nextChildAccessor, agendaItemIdToDelete)) { 1410 return true; 1411 } 1412 } 1413 } 1414 return false; 1415 } 1416 1417 /** 1418 * Recursively delete the agendaItem and its children from the agendaItemBo list. 1419 * @param items, the list of agendaItemBo that the agenda holds 1420 * @param removeAgendaItem, the agendaItemBo to be removed 1421 */ 1422 private void removeAgendaItem(List<AgendaItemBo> items, AgendaItemBo removeAgendaItem) { 1423 if (removeAgendaItem.getWhenTrue() != null) { 1424 removeAgendaItem(items, removeAgendaItem.getWhenTrue()); 1425 } 1426 if (removeAgendaItem.getWhenFalse() != null) { 1427 removeAgendaItem(items, removeAgendaItem.getWhenFalse()); 1428 } 1429 if (removeAgendaItem.getAlways() != null) { 1430 removeAgendaItem(items, removeAgendaItem.getAlways()); 1431 } 1432 items.remove(removeAgendaItem); 1433 } 1434 1435 @RequestMapping(params = "methodToCall=" + "ajaxCut") 1436 public ModelAndView ajaxCut(@ModelAttribute("KualiForm") UifFormBase form, BindingResult result, 1437 HttpServletRequest request, HttpServletResponse response) throws Exception { 1438 1439 AgendaEditor agendaEditor = getAgendaEditor(form); 1440 // this is the root of the tree: 1441 AgendaItemBo firstItem = getFirstAgendaItem(agendaEditor.getAgenda()); 1442 String selectedItemId = agendaEditor.getSelectedAgendaItemId(); 1443 1444 AgendaItemBo selectedAgendaItem = getAgendaItemById(firstItem, selectedItemId); 1445 setCutAgendaItemId(form, selectedItemId); 1446 1447 StringBuilder ruleEditorMessage = new StringBuilder(); 1448 ruleEditorMessage.append("Marked ").append(selectedAgendaItem.getRule().getName()).append(" for cutting."); 1449 agendaEditor.setRuleEditorMessage(ruleEditorMessage.toString()); 1450 // call the super method to avoid the agenda tree being reloaded from the db 1451 return getModelAndView(form); 1452 } 1453 1454 @RequestMapping(params = "methodToCall=" + "ajaxPaste") 1455 public ModelAndView ajaxPaste(@ModelAttribute("KualiForm") UifFormBase form, BindingResult result, 1456 HttpServletRequest request, HttpServletResponse response) throws Exception { 1457 1458 AgendaEditor agendaEditor = getAgendaEditor(form); 1459 // this is the root of the tree: 1460 AgendaItemBo firstItem = getFirstAgendaItem(agendaEditor.getAgenda()); 1461 String selectedItemId = agendaEditor.getSelectedAgendaItemId(); 1462 1463 String agendaItemId = getCutAgendaItemId(form); 1464 if (StringUtils.isNotBlank(selectedItemId) && StringUtils.isNotBlank(agendaItemId)) { 1465 StringBuilder ruleEditorMessage = new StringBuilder(); 1466 AgendaItemBo node = getAgendaItemById(firstItem, agendaItemId); 1467 AgendaItemBo orgRefNode = getReferringNode(firstItem, agendaItemId); 1468 AgendaItemBo newRefNode = getAgendaItemById(firstItem, selectedItemId); 1469 1470 if (isSameOrChildNode(node, newRefNode)) { 1471 // note if the cut agenda item is not cleared, then the javascript on the AgendaEditorView will need to be 1472 // updated to deal with a paste that doesn't paste. As the ui disables the paste button after it is clicked 1473 ruleEditorMessage.append("Cannot paste ").append(node.getRule().getName()).append(" to itself."); 1474 } else { 1475 // remove node 1476 if (orgRefNode == null) { 1477 agendaEditor.getAgenda().setFirstItemId(node.getAlwaysId()); 1478 agendaEditor.getAgenda().setFirstItem(node.getAlways()); 1479 } else { 1480 // determine if true, false or always 1481 // do appropriate operation 1482 if (node.getId().equals(orgRefNode.getWhenTrueId())) { 1483 orgRefNode.setWhenTrueId(node.getAlwaysId()); 1484 orgRefNode.setWhenTrue(node.getAlways()); 1485 } else if(node.getId().equals(orgRefNode.getWhenFalseId())) { 1486 orgRefNode.setWhenFalseId(node.getAlwaysId()); 1487 orgRefNode.setWhenFalse(node.getAlways()); 1488 } else { 1489 orgRefNode.setAlwaysId(node.getAlwaysId()); 1490 orgRefNode.setAlways(node.getAlways()); 1491 } 1492 } 1493 1494 // insert node 1495 node.setAlwaysId(newRefNode.getAlwaysId()); 1496 node.setAlways(newRefNode.getAlways()); 1497 newRefNode.setAlwaysId(node.getId()); 1498 newRefNode.setAlways(node); 1499 1500 ruleEditorMessage.append(" Pasted ").append(node.getRule().getName()); 1501 ruleEditorMessage.append(" to ").append(newRefNode.getRule().getName()); 1502 agendaEditor.setRuleEditorMessage(ruleEditorMessage.toString()); 1503 1504 } 1505 setCutAgendaItemId(form, null); 1506 } 1507 1508 1509 // call the super method to avoid the agenda tree being reloaded from the db 1510 return getModelAndView(form); 1511 } 1512 1513 /** 1514 * This method checks if the node is the same as the new parent node or a when-true/when-fase 1515 * child of the new parent node. 1516 * 1517 * @param node - the node to be checked if it's the same or a child 1518 * @param newParent - the parent node to check against 1519 * @return true if same or child, false otherwise 1520 * @see AgendaItemChildAccessor for nomenclature explanation 1521 */ 1522 private boolean isSameOrChildNode(AgendaItemBo node, AgendaItemBo newParent) { 1523 return isSameOrChildNodeHelper(node, newParent, AgendaItemChildAccessor.children); 1524 } 1525 1526 private boolean isSameOrChildNodeHelper(AgendaItemBo node, AgendaItemBo newParent, AgendaItemChildAccessor[] childAccessors) { 1527 boolean result = false; 1528 if (newParent == null || node == null) { 1529 return false; 1530 } 1531 if (StringUtils.equals(node.getId(), newParent.getId())) { 1532 result = true; 1533 } else { 1534 for (AgendaItemChildAccessor childAccessor : childAccessors) { 1535 AgendaItemBo child = childAccessor.getChild(node); 1536 if (child != null) { 1537 result = isSameOrChildNodeHelper(child, newParent, AgendaItemChildAccessor.linkedNodes); 1538 if (result == true) { 1539 break; 1540 } 1541 } 1542 } 1543 } 1544 return result; 1545 } 1546 1547 /** 1548 * This method returns the node that points to the specified agendaItemId. 1549 * (returns the next older sibling or the parent if no older sibling exists) 1550 * 1551 * @param root - the first agenda item of the agenda 1552 * @param agendaItemId - agenda item id of the agenda item whose referring node is to be returned 1553 * @return AgendaItemBo that points to the specified agenda item 1554 * @see AgendaItemChildAccessor for nomenclature explanation 1555 */ 1556 private AgendaItemBo getReferringNode(AgendaItemBo root, String agendaItemId) { 1557 return getReferringNodeHelper(root, null, agendaItemId); 1558 } 1559 1560 private AgendaItemBo getReferringNodeHelper(AgendaItemBo node, AgendaItemBo referringNode, String agendaItemId) { 1561 AgendaItemBo result = null; 1562 if (agendaItemId.equals(node.getId())) { 1563 result = referringNode; 1564 } else { 1565 for (AgendaItemChildAccessor childAccessor : AgendaItemChildAccessor.linkedNodes) { 1566 AgendaItemBo child = childAccessor.getChild(node); 1567 if (child != null) { 1568 result = getReferringNodeHelper(child, node, agendaItemId); 1569 if (result != null) { 1570 break; 1571 } 1572 } 1573 } 1574 } 1575 return result; 1576 } 1577 1578 private FunctionBoService getFunctionBoService() { 1579 return KrmsRepositoryServiceLocator.getFunctionBoService(); 1580 } 1581 1582 /** 1583 * return the contextBoService 1584 */ 1585 private ContextBoService getContextBoService() { 1586 return KrmsRepositoryServiceLocator.getContextBoService(); 1587 } 1588 1589 /** 1590 * return the contextBoService 1591 */ 1592 private RuleBoService getRuleBoService() { 1593 return KrmsRepositoryServiceLocator.getRuleBoService(); 1594 } 1595 1596 private CustomOperatorUiTranslator getCustomOperatorUiTranslator() { 1597 return KrmsServiceLocatorInternal.getCustomOperatorUiTranslator(); 1598 } 1599 1600 /** 1601 * binds a child accessor to an AgendaItemBo instance. An {@link AgendaItemInstanceChildAccessor} allows you to 1602 * get and set the referent 1603 */ 1604 private static class AgendaItemInstanceChildAccessor { 1605 1606 private final AgendaItemChildAccessor accessor; 1607 private final AgendaItemBo instance; 1608 1609 public AgendaItemInstanceChildAccessor(AgendaItemChildAccessor accessor, AgendaItemBo instance) { 1610 this.accessor = accessor; 1611 this.instance = instance; 1612 } 1613 1614 public void setChild(AgendaItemBo child) { 1615 accessor.setChild(instance, child); 1616 } 1617 1618 public AgendaItemBo getChild() { 1619 return accessor.getChild(instance); 1620 } 1621 1622 public AgendaItemBo getInstance() { return instance; } 1623 } 1624 1625 /** 1626 * <p>This class abstracts getting and setting a child of an AgendaItemBo, making some recursive operations 1627 * require less boiler plate.</p> 1628 * 1629 * <p>The word 'child' in AgendaItemChildAccessor means child in the strict data structures sense, in that the 1630 * instance passed in holds a reference to some other node (or null). However, when discussing the agenda tree 1631 * and algorithms for manipulating it, the meaning of 'child' is somewhat different, and there are notions of 1632 * 'sibling' and 'cousin' that are tossed about too. It's probably worth explaining that somewhat here:</p> 1633 * 1634 * <p>General principals of relationships when talking about the agenda tree: 1635 * <ul> 1636 * <li>Generation boundaries (parent to child) are across 'When TRUE' and 'When FALSE' references.</li> 1637 * <li>"Age" among siblings & cousins goes from top (oldest) to bottom (youngest).</li> 1638 * <li>siblings are related by 'Always' references.</li> 1639 * </ul> 1640 * </p> 1641 * <p>This diagram of an agenda tree and the following examples seek to illustrate these principals:</p> 1642 * <img src="doc-files/AgendaEditorController-1.png" alt="Example Agenda Items"/> 1643 * <p>Examples: 1644 * <ul> 1645 * <li>A is the parent of B, C, & D</li> 1646 * <li>E is the younger sibling of A</li> 1647 * <li>B is the older cousin of C</li> 1648 * <li>C is the older sibling of D</li> 1649 * <li>F is the younger cousin of D</li> 1650 * </ul> 1651 * </p> 1652 */ 1653 protected static class AgendaItemChildAccessor { 1654 1655 private enum Child { WHEN_TRUE, WHEN_FALSE, ALWAYS }; 1656 1657 private static final AgendaItemChildAccessor whenTrue = new AgendaItemChildAccessor(Child.WHEN_TRUE); 1658 private static final AgendaItemChildAccessor whenFalse = new AgendaItemChildAccessor(Child.WHEN_FALSE); 1659 private static final AgendaItemChildAccessor always = new AgendaItemChildAccessor(Child.ALWAYS); 1660 1661 /** 1662 * Accessors for all linked items 1663 */ 1664 private static final AgendaItemChildAccessor [] linkedNodes = { whenTrue, whenFalse, always }; 1665 1666 /** 1667 * Accessors for children (so ALWAYS is omitted); 1668 */ 1669 private static final AgendaItemChildAccessor [] children = { whenTrue, whenFalse }; 1670 1671 private final Child whichChild; 1672 1673 private AgendaItemChildAccessor(Child whichChild) { 1674 if (whichChild == null) { 1675 throw new IllegalArgumentException("whichChild must be non-null"); 1676 } 1677 this.whichChild = whichChild; 1678 } 1679 1680 /** 1681 * @return the referenced child 1682 */ 1683 public AgendaItemBo getChild(AgendaItemBo parent) { 1684 switch (whichChild) { 1685 case WHEN_TRUE: return parent.getWhenTrue(); 1686 case WHEN_FALSE: return parent.getWhenFalse(); 1687 case ALWAYS: return parent.getAlways(); 1688 default: throw new IllegalStateException(); 1689 } 1690 } 1691 1692 /** 1693 * Sets the child reference and the child id 1694 */ 1695 public void setChild(AgendaItemBo parent, AgendaItemBo child) { 1696 switch (whichChild) { 1697 case WHEN_TRUE: 1698 parent.setWhenTrue(child); 1699 parent.setWhenTrueId(child == null ? null : child.getId()); 1700 break; 1701 case WHEN_FALSE: 1702 parent.setWhenFalse(child); 1703 parent.setWhenFalseId(child == null ? null : child.getId()); 1704 break; 1705 case ALWAYS: 1706 parent.setAlways(child); 1707 parent.setAlwaysId(child == null ? null : child.getId()); 1708 break; 1709 default: throw new IllegalStateException(); 1710 } 1711 } 1712 } 1713 // 1714 // Rule Editor Controller methods 1715 // 1716 @RequestMapping(params = "methodToCall=" + "copyRule") 1717 public ModelAndView copyRule(@ModelAttribute("KualiForm") UifFormBase form, BindingResult result, 1718 HttpServletRequest request, HttpServletResponse response) throws Exception { 1719 1720 AgendaEditor agendaEditor = getAgendaEditor(form); 1721 String name = agendaEditor.getCopyRuleName(); 1722 String namespace = agendaEditor.getNamespace(); 1723 // fetch existing rule and copy fields to new rule 1724 1725 final String copyRuleNameErrorPropertyName = "AgendaEditorView-AddRule-Page"; //"copyRuleName", 1726 if (StringUtils.isBlank(name)) { 1727 GlobalVariables.getMessageMap().putError(copyRuleNameErrorPropertyName, "error.rule.missingCopyRuleName"); 1728 return super.refresh(form); 1729 } 1730 1731 RuleDefinition oldRuleDefinition = getRuleBoService().getRuleByNameAndNamespace(name, namespace); 1732 1733 if (oldRuleDefinition == null) { 1734 GlobalVariables.getMessageMap().putError(copyRuleNameErrorPropertyName, "error.rule.invalidCopyRuleName", namespace + ":" + name); 1735 return super.refresh(form); 1736 } 1737 1738 RuleBo oldRule = RuleBo.from(oldRuleDefinition); 1739 RuleBo newRule = RuleBo.copyRule(oldRule); 1740 agendaEditor.getAgendaItemLine().setRule( newRule ); 1741 // hack to set ui action object to first action in the list 1742 if (!newRule.getActions().isEmpty()) { 1743 agendaEditor.setAgendaItemLineRuleAction( newRule.getActions().get(0)); 1744 } 1745 return super.refresh(form); 1746 } 1747 1748 1749 /** 1750 * This method starts an edit proposition. 1751 */ 1752 @RequestMapping(params = "methodToCall=" + "goToEditProposition") 1753 public ModelAndView goToEditProposition(@ModelAttribute("KualiForm") UifFormBase form, BindingResult result, 1754 HttpServletRequest request, HttpServletResponse response) throws Exception { 1755 1756 // open the selected node for editing 1757 AgendaEditor agendaEditor = getAgendaEditor(form); 1758 RuleBo rule = agendaEditor.getAgendaItemLine().getRule(); 1759 String selectedPropId = agendaEditor.getSelectedPropositionId(); 1760 1761 Node<RuleTreeNode,String> root = rule.getPropositionTree().getRootElement(); 1762 PropositionBo propositionToToggleEdit = null; 1763 boolean newEditMode = true; 1764 1765 // find parent 1766 Node<RuleTreeNode,String> parent = findParentPropositionNode( root, selectedPropId); 1767 if (parent != null){ 1768 List<Node<RuleTreeNode,String>> children = parent.getChildren(); 1769 for( int index=0; index< children.size(); index++){ 1770 Node<RuleTreeNode,String> child = children.get(index); 1771 if (propIdMatches(child, selectedPropId)){ 1772 PropositionBo prop = child.getData().getProposition(); 1773 propositionToToggleEdit = prop; 1774 newEditMode = !prop.getEditMode(); 1775 break; 1776 } else { 1777 child.getData().getProposition().setEditMode(false); 1778 } 1779 } 1780 } 1781 1782 resetEditModeOnPropositionTree(root); 1783 if (propositionToToggleEdit != null) { 1784 propositionToToggleEdit.setEditMode(newEditMode); 1785 //refresh the tree 1786 rule.refreshPropositionTree(null); 1787 } 1788 1789 return getModelAndView(form); 1790 } 1791 1792 /** 1793 * This method returns the last simple node in the topmost branch. 1794 * @param grandChildren 1795 * @return 1796 */ 1797 protected Node<RuleTreeNode,String> getLastSimpleNode(List<Node<RuleTreeNode,String>> grandChildren) { 1798 int lastIndex = grandChildren.size() - 1; 1799 Node<RuleTreeNode,String> lastSimpleNode = grandChildren.get(lastIndex); 1800 1801 // search until you find the first simple proposition since some nodes are operators. 1802 while (!(SimplePropositionNode.NODE_TYPE.equalsIgnoreCase(lastSimpleNode.getNodeType()) 1803 || SimplePropositionEditNode.NODE_TYPE.equalsIgnoreCase(lastSimpleNode.getNodeType()) 1804 ) && lastIndex >= 0) { 1805 lastSimpleNode = grandChildren.get(lastIndex); 1806 lastIndex--; 1807 } 1808 1809 return lastSimpleNode; 1810 } 1811 1812 /** 1813 * 1814 * This method gets the last propostion in the topmost branch. 1815 * 1816 * @param root 1817 * @return 1818 */ 1819 protected String getDefaultAddLocationPropositionId(Node<RuleTreeNode, String> root) { 1820 List<Node<RuleTreeNode,String>> children = root.getChildren(); 1821 String selectedId = ""; 1822 1823 // The root usually has only one child. 1824 //This child either has multiple grandchildren when there is more than one proposition or 1825 //is a simple proposition with no grandchildren. 1826 if (children.size() != 0) { 1827 Node<RuleTreeNode,String> child = children.get(0); 1828 List<Node<RuleTreeNode,String>> grandChildren = child.getChildren(); 1829 1830 // if there are grandchildren it means multiple propositions have been added. 1831 if (grandChildren.size() != 0) { 1832 Node<RuleTreeNode,String> lastSimpleNode = getLastSimpleNode(grandChildren); 1833 selectedId = lastSimpleNode.getData().getProposition().getId(); 1834 } else { 1835 // if there are no grandchildren, it means only a single simpleProposition 1836 // has been added. 1837 selectedId = child.getData().getProposition().getId(); 1838 } 1839 } 1840 1841 return selectedId; 1842 } 1843 1844 @RequestMapping(params = "methodToCall=" + "addProposition") 1845 public ModelAndView addProposition(@ModelAttribute("KualiForm") UifFormBase form, BindingResult result, 1846 HttpServletRequest request, HttpServletResponse response) throws Exception { 1847 1848 AgendaEditor agendaEditor = getAgendaEditor(form); 1849 RuleBo rule = agendaEditor.getAgendaItemLine().getRule(); 1850 String selectedPropId = agendaEditor.getSelectedPropositionId(); 1851 1852 // find parent 1853 Node<RuleTreeNode,String> root = agendaEditor.getAgendaItemLine().getRule().getPropositionTree().getRootElement(); 1854 1855 // if a proposition is not selected, get the last one in the topmost 1856 // branch 1857 if (StringUtils.isEmpty(selectedPropId)) { 1858 selectedPropId = getDefaultAddLocationPropositionId(root); 1859 } 1860 1861 // parent is the proposition user selected 1862 Node<RuleTreeNode,String> parent = findParentPropositionNode( root, selectedPropId); 1863 1864 resetEditModeOnPropositionTree(root); 1865 1866 // add new child at appropriate spot 1867 if (parent != null){ 1868 List<Node<RuleTreeNode,String>> children = parent.getChildren(); 1869 for( int index=0; index< children.size(); index++){ 1870 Node<RuleTreeNode,String> child = children.get(index); 1871 1872 // if our selected node is a simple proposition, add a new one after 1873 if (propIdMatches(child, selectedPropId)){ 1874 // handle special case of adding to a lone simple proposition. 1875 // in this case, we need to change the root level proposition to a compound proposition 1876 // move the existing simple proposition as the first compound component, 1877 // then add a new blank simple prop as the second compound component. 1878 if (parent == root && 1879 (SimplePropositionNode.NODE_TYPE.equalsIgnoreCase(child.getNodeType()) || 1880 SimplePropositionEditNode.NODE_TYPE.equalsIgnoreCase(child.getNodeType()))){ 1881 1882 // create a new compound proposition 1883 PropositionBo compound = PropositionBo.createCompoundPropositionBoStub(child.getData().getProposition(), true); 1884 compound.setDescription("New Compound Proposition"); 1885 // don't set compound.setEditMode(true) as the Simple Prop in the compound prop is the only prop in edit mode 1886 rule.setProposition(compound); 1887 rule.refreshPropositionTree(null); 1888 } 1889 // handle regular case of adding a simple prop to an existing compound prop 1890 else if(SimplePropositionNode.NODE_TYPE.equalsIgnoreCase(child.getNodeType()) || 1891 SimplePropositionEditNode.NODE_TYPE.equalsIgnoreCase(child.getNodeType())){ 1892 1893 // build new Blank Proposition 1894 PropositionBo blank = PropositionBo.createSimplePropositionBoStub(child.getData().getProposition(),PropositionType.SIMPLE.getCode()); 1895 //add it to the parent 1896 PropositionBo parentProp = parent.getData().getProposition(); 1897 parentProp.getCompoundComponents().add(((index/2)+1), blank); 1898 1899 rule.refreshPropositionTree(true); 1900 } 1901 1902 break; 1903 } 1904 } 1905 } else { 1906 // special case, if root has no children, add a new simple proposition 1907 // todo: how to add compound proposition. - just add another to the firs simple 1908 if (root.getChildren().isEmpty()){ 1909 PropositionBo blank = PropositionBo.createSimplePropositionBoStub(null,PropositionType.SIMPLE.getCode()); 1910 blank.setRuleId(rule.getId()); 1911 rule.setProposition(blank); 1912 rule.setProposition(blank); 1913 rule.refreshPropositionTree(true); 1914 } 1915 } 1916 return getModelAndView(form); 1917 } 1918 1919 /** 1920 * 1921 * This method adds an opCode Node to separate components in a compound proposition. 1922 * 1923 * @param currentNode 1924 * @param prop 1925 * @return 1926 */ 1927 private void addOpCodeNode(Node currentNode, PropositionBo prop, int index){ 1928 String opCodeLabel = ""; 1929 1930 if (LogicalOperator.AND.getCode().equalsIgnoreCase(prop.getCompoundOpCode())){ 1931 opCodeLabel = "AND"; 1932 } else if (LogicalOperator.OR.getCode().equalsIgnoreCase(prop.getCompoundOpCode())){ 1933 opCodeLabel = "OR"; 1934 } 1935 Node<RuleTreeNode, String> aNode = new Node<RuleTreeNode, String>(); 1936 aNode.setNodeLabel(""); 1937 aNode.setNodeType("ruleTreeNode compoundOpCodeNode"); 1938 aNode.setData(new CompoundOpCodeNode(prop)); 1939 currentNode.insertChildAt(index, aNode); 1940 } 1941 1942 1943 private boolean propIdMatches(Node<RuleTreeNode, String> node, String propId){ 1944 if (propId!=null && node != null && node.getData() != null && propId.equalsIgnoreCase(node.getData().getProposition().getId())) { 1945 return true; 1946 } 1947 return false; 1948 } 1949 1950 /** 1951 * disable edit mode for all Nodes beneath and including the passed in Node 1952 * @param currentNode 1953 */ 1954 private void resetEditModeOnPropositionTree(Node<RuleTreeNode, String> currentNode){ 1955 if (currentNode.getData() != null){ 1956 RuleTreeNode dataNode = currentNode.getData(); 1957 dataNode.getProposition().setEditMode(false); 1958 } 1959 List<Node<RuleTreeNode,String>> children = currentNode.getChildren(); 1960 for( Node<RuleTreeNode,String> child : children){ 1961 resetEditModeOnPropositionTree(child); 1962 } 1963 } 1964 1965 private Node<RuleTreeNode, String> findPropositionTreeNode(Node<RuleTreeNode, String> currentNode, String selectedPropId){ 1966 Node<RuleTreeNode,String> bingo = null; 1967 if (currentNode.getData() != null){ 1968 RuleTreeNode dataNode = currentNode.getData(); 1969 if (selectedPropId.equalsIgnoreCase(dataNode.getProposition().getId())){ 1970 return currentNode; 1971 } 1972 } 1973 List<Node<RuleTreeNode,String>> children = currentNode.getChildren(); 1974 for( Node<RuleTreeNode,String> child : children){ 1975 bingo = findPropositionTreeNode(child, selectedPropId); 1976 if (bingo != null) { 1977 break; 1978 } 1979 } 1980 return bingo; 1981 } 1982 1983 private Node<RuleTreeNode, String> findParentPropositionNode(Node<RuleTreeNode, String> currentNode, String selectedPropId){ 1984 Node<RuleTreeNode,String> bingo = null; 1985 if (selectedPropId != null) { 1986 // if it's in children, we have the parent 1987 List<Node<RuleTreeNode,String>> children = currentNode.getChildren(); 1988 for( Node<RuleTreeNode,String> child : children){ 1989 RuleTreeNode dataNode = child.getData(); 1990 if (selectedPropId.equalsIgnoreCase(dataNode.getProposition().getId())) { 1991 return currentNode; 1992 } 1993 } 1994 1995 // if not found check grandchildren 1996 for( Node<RuleTreeNode,String> kid : children){ 1997 bingo = findParentPropositionNode(kid, selectedPropId); 1998 if (bingo != null) { 1999 break; 2000 } 2001 } 2002 } 2003 return bingo; 2004 } 2005 2006 /** 2007 * This method return the index of the position of the child that matches the id 2008 * @param parent 2009 * @param propId 2010 * @return index if found, -1 if not found 2011 */ 2012 private int findChildIndex(Node<RuleTreeNode,String> parent, String propId){ 2013 int index; 2014 List<Node<RuleTreeNode,String>> children = parent.getChildren(); 2015 for(index=0; index< children.size(); index++){ 2016 Node<RuleTreeNode,String> child = children.get(index); 2017 // if our selected node is a simple proposition, add a new one after 2018 if (propIdMatches(child, propId)){ 2019 return index; 2020 } 2021 } 2022 return -1; 2023 } 2024 2025 @RequestMapping(params = "methodToCall=" + "movePropositionUp") 2026 public ModelAndView movePropositionUp(@ModelAttribute("KualiForm") UifFormBase form, BindingResult result, 2027 HttpServletRequest request, HttpServletResponse response) 2028 throws Exception { 2029 moveSelectedProposition(form, true); 2030 2031 return getModelAndView(form); 2032 } 2033 2034 @RequestMapping(params = "methodToCall=" + "movePropositionDown") 2035 public ModelAndView movePropositionDown(@ModelAttribute("KualiForm") UifFormBase form, BindingResult result, 2036 HttpServletRequest request, HttpServletResponse response) 2037 throws Exception { 2038 moveSelectedProposition(form, false); 2039 2040 return getModelAndView(form); 2041 } 2042 2043 private void moveSelectedProposition(UifFormBase form, boolean up) { 2044 2045 /* Rough algorithm for moving a node up. 2046 * 2047 * find the following: 2048 * node := the selected node 2049 * parent := the selected node's parent, its containing node (via when true or when false relationship) 2050 * parentsOlderCousin := the parent's level-order predecessor (sibling or cousin) 2051 * 2052 */ 2053 AgendaEditor agendaEditor = getAgendaEditor(form); 2054 RuleBo rule = agendaEditor.getAgendaItemLine().getRule(); 2055 String selectedPropId = agendaEditor.getSelectedPropositionId(); 2056 2057 // find parent 2058 Node<RuleTreeNode,String> parent = findParentPropositionNode(rule.getPropositionTree().getRootElement(), selectedPropId); 2059 2060 // add new child at appropriate spot 2061 if (parent != null){ 2062 List<Node<RuleTreeNode,String>> children = parent.getChildren(); 2063 for( int index=0; index< children.size(); index++){ 2064 Node<RuleTreeNode,String> child = children.get(index); 2065 // if our selected node is a simple proposition, add a new one after 2066 if (propIdMatches(child, selectedPropId)){ 2067 if(SimplePropositionNode.NODE_TYPE.equalsIgnoreCase(child.getNodeType()) || 2068 SimplePropositionEditNode.NODE_TYPE.equalsIgnoreCase(child.getNodeType()) || 2069 RuleTreeNode.COMPOUND_NODE_TYPE.equalsIgnoreCase(child.getNodeType()) ){ 2070 2071 if (((index > 0) && up) || ((index <(children.size() - 1)&& !up))){ 2072 //remove it from its current spot 2073 PropositionBo parentProp = parent.getData().getProposition(); 2074 PropositionBo workingProp = parentProp.getCompoundComponents().remove(index/2); 2075 if (up){ 2076 parentProp.getCompoundComponents().add((index/2)-1, workingProp); 2077 }else{ 2078 parentProp.getCompoundComponents().add((index/2)+1, workingProp); 2079 } 2080 2081 // insert it in the new spot 2082 // redisplay the tree (editMode = true) 2083 boolean editMode = (SimplePropositionEditNode.NODE_TYPE.equalsIgnoreCase(child.getNodeType())); 2084 rule.refreshPropositionTree(editMode); 2085 } 2086 } 2087 2088 break; 2089 } 2090 } 2091 } 2092 } 2093 2094 @RequestMapping(params = "methodToCall=" + "movePropositionLeft") 2095 public ModelAndView movePropositionLeft(@ModelAttribute("KualiForm") UifFormBase form, BindingResult result, 2096 HttpServletRequest request, HttpServletResponse response) 2097 throws Exception { 2098 2099 /* Rough algorithm for moving a node up. 2100 * 2101 * find the following: 2102 * node := the selected node 2103 * parent := the selected node's parent, its containing node (via when true or when false relationship) 2104 * parentsOlderCousin := the parent's level-order predecessor (sibling or cousin) 2105 * 2106 */ 2107 AgendaEditor agendaEditor = getAgendaEditor(form); 2108 RuleBo rule = agendaEditor.getAgendaItemLine().getRule(); 2109 String selectedPropId = agendaEditor.getSelectedPropositionId(); 2110 2111 // find agendaEditor.getAgendaItemLine().getRule().getPropositionTree().getRootElement()parent 2112 Node<RuleTreeNode,String> root = rule.getPropositionTree().getRootElement(); 2113 Node<RuleTreeNode,String> parent = findParentPropositionNode(root, selectedPropId); 2114 if ((parent != null) && (RuleTreeNode.COMPOUND_NODE_TYPE.equalsIgnoreCase(parent.getNodeType()))){ 2115 Node<RuleTreeNode,String> granny = findParentPropositionNode(root,parent.getData().getProposition().getId()); 2116 if (granny != root){ 2117 int oldIndex = findChildIndex(parent, selectedPropId); 2118 int newIndex = findChildIndex(granny, parent.getData().getProposition().getId()); 2119 if (oldIndex >= 0 && newIndex >= 0){ 2120 PropositionBo prop = parent.getData().getProposition().getCompoundComponents().remove(oldIndex/2); 2121 granny.getData().getProposition().getCompoundComponents().add((newIndex/2)+1, prop); 2122 rule.refreshPropositionTree(false); 2123 } 2124 } else { 2125 // TODO: do we allow moving up to the root? 2126 // we could add a new top level compound node, with current root as 1st child, 2127 // and move the node to the second child. 2128 } 2129 } 2130 return getModelAndView(form); 2131 } 2132 2133 @RequestMapping(params = "methodToCall=" + "movePropositionRight") 2134 public ModelAndView movePropositionRight(@ModelAttribute("KualiForm") UifFormBase form, BindingResult result, 2135 HttpServletRequest request, HttpServletResponse response) 2136 throws Exception { 2137 /* Rough algorithm for moving a node Right 2138 * if the selected node is above a compound proposition, move it into the compound proposition as the first child 2139 * if the node is above a simple proposition, do nothing. 2140 * find the following: 2141 * node := the selected node 2142 * parent := the selected node's parent, its containing node 2143 * nextSibling := the node after the selected node 2144 * 2145 */ 2146 AgendaEditor agendaEditor = getAgendaEditor(form); 2147 RuleBo rule = agendaEditor.getAgendaItemLine().getRule(); 2148 String selectedPropId = agendaEditor.getSelectedPropositionId(); 2149 2150 // find parent 2151 Node<RuleTreeNode,String> parent = findParentPropositionNode( 2152 rule.getPropositionTree().getRootElement(), selectedPropId); 2153 if (parent != null){ 2154 int index = findChildIndex(parent, selectedPropId); 2155 // if we are the last child, do nothing, otherwise 2156 if (index >= 0 && index+1 < parent.getChildren().size()){ 2157 Node<RuleTreeNode,String> child = parent.getChildren().get(index); 2158 Node<RuleTreeNode,String> nextSibling = parent.getChildren().get(index+2); 2159 // if selected node above a compound node, move it into it as first child 2160 if(RuleTreeNode.COMPOUND_NODE_TYPE.equalsIgnoreCase(nextSibling.getNodeType()) ){ 2161 // remove selected node from it's current spot 2162 PropositionBo prop = parent.getData().getProposition().getCompoundComponents().remove(index/2); 2163 // add it to it's siblings children 2164 nextSibling.getData().getProposition().getCompoundComponents().add(0, prop); 2165 rule.refreshPropositionTree(false); 2166 } 2167 } 2168 } 2169 return getModelAndView(form); 2170 } 2171 2172 /** 2173 * introduces a new compound proposition between the selected proposition and its parent. 2174 * Additionally, it puts a new blank simple proposition underneath the compound proposition 2175 * as a sibling to the selected proposition. 2176 */ 2177 @RequestMapping(params = "methodToCall=" + "togglePropositionSimpleCompound") 2178 public ModelAndView togglePropositionSimpleCompound(@ModelAttribute("KualiForm") UifFormBase form, BindingResult result, 2179 HttpServletRequest request, HttpServletResponse response) 2180 throws Exception { 2181 2182 AgendaEditor agendaEditor = getAgendaEditor(form); 2183 RuleBo rule = agendaEditor.getAgendaItemLine().getRule(); 2184 String selectedPropId = agendaEditor.getSelectedPropositionId(); 2185 2186 resetEditModeOnPropositionTree(rule.getPropositionTree().getRootElement()); 2187 2188 if (!StringUtils.isBlank(selectedPropId)) { 2189 // find parent 2190 Node<RuleTreeNode,String> parent = findParentPropositionNode( 2191 rule.getPropositionTree().getRootElement(), selectedPropId); 2192 if (parent != null){ 2193 2194 int index = findChildIndex(parent, selectedPropId); 2195 2196 PropositionBo propBo = parent.getChildren().get(index).getData().getProposition(); 2197 2198 // create a new compound proposition 2199 PropositionBo compound = PropositionBo.createCompoundPropositionBoStub(propBo, true); 2200 compound.setDescription("New Compound Proposition"); 2201 compound.setEditMode(false); 2202 2203 if (parent.getData() == null) { // SPECIAL CASE: this is the only proposition in the tree 2204 rule.setProposition(compound); 2205 } else { 2206 PropositionBo parentBo = parent.getData().getProposition(); 2207 List<PropositionBo> siblings = parentBo.getCompoundComponents(); 2208 2209 int propIndex = -1; 2210 for (int i=0; i<siblings.size(); i++) { 2211 if (propBo.getId().equals(siblings.get(i).getId())) { 2212 propIndex = i; 2213 break; 2214 } 2215 } 2216 2217 parentBo.getCompoundComponents().set(propIndex, compound); 2218 } 2219 } 2220 } 2221 2222 agendaEditor.getAgendaItemLine().getRule().refreshPropositionTree(true); 2223 return getModelAndView(form); 2224 } 2225 2226 @RequestMapping(params = "methodToCall=" + "cutProposition") 2227 public ModelAndView cutProposition(@ModelAttribute("KualiForm") UifFormBase form, BindingResult result, 2228 HttpServletRequest request, HttpServletResponse response) 2229 throws Exception { 2230 2231 AgendaEditor agendaEditor = getAgendaEditor(form); 2232 String selectedPropId = agendaEditor.getSelectedPropositionId(); 2233 agendaEditor.setCutPropositionId(selectedPropId); 2234 2235 return getModelAndView(form); 2236 } 2237 2238 @RequestMapping(params = "methodToCall=" + "pasteProposition") 2239 public ModelAndView pasteProposition(@ModelAttribute("KualiForm") UifFormBase form, BindingResult result, 2240 HttpServletRequest request, HttpServletResponse response) 2241 throws Exception { 2242 2243 AgendaEditor agendaEditor = getAgendaEditor(form); 2244 RuleBo rule = agendaEditor.getAgendaItemLine().getRule(); 2245 2246 // get selected id 2247 String cutPropId = agendaEditor.getCutPropositionId(); 2248 String selectedPropId = agendaEditor.getSelectedPropositionId(); 2249 2250 if (StringUtils.isNotBlank(selectedPropId) && selectedPropId.equals(cutPropId)) { 2251 // do nothing; can't paste to itself 2252 } else { 2253 2254 // proposition tree root 2255 Node<RuleTreeNode, String> root = rule.getPropositionTree().getRootElement(); 2256 2257 if (StringUtils.isNotBlank(selectedPropId) && StringUtils.isNotBlank(cutPropId)) { 2258 Node<RuleTreeNode,String> parentNode = findParentPropositionNode(root, selectedPropId); 2259 PropositionBo newParent; 2260 if (parentNode == root){ 2261 // special case 2262 // build new top level compound proposition, 2263 // add existing as first child 2264 // then paste cut node as 2nd child 2265 newParent = PropositionBo.createCompoundPropositionBoStub2( 2266 root.getChildren().get(0).getData().getProposition()); 2267 newParent.setEditMode(true); 2268 rule.setProposition(newParent); 2269 } else { 2270 newParent = parentNode.getData().getProposition(); 2271 } 2272 PropositionBo oldParent = findParentPropositionNode(root, cutPropId).getData().getProposition(); 2273 2274 PropositionBo workingProp = null; 2275 // cut from old 2276 if (oldParent != null){ 2277 List <PropositionBo> children = oldParent.getCompoundComponents(); 2278 for( int index=0; index< children.size(); index++){ 2279 if (cutPropId.equalsIgnoreCase(children.get(index).getId())){ 2280 workingProp = oldParent.getCompoundComponents().remove(index); 2281 break; 2282 } 2283 } 2284 } 2285 2286 // add to new 2287 if (newParent != null && workingProp != null){ 2288 List <PropositionBo> children = newParent.getCompoundComponents(); 2289 for( int index=0; index< children.size(); index++){ 2290 if (selectedPropId.equalsIgnoreCase(children.get(index).getId())){ 2291 children.add(index+1, workingProp); 2292 break; 2293 } 2294 } 2295 } 2296 // TODO: determine edit mode. 2297// boolean editMode = (SimplePropositionEditNode.NODE_TYPE.equalsIgnoreCase(child.getNodeType())); 2298 rule.refreshPropositionTree(false); 2299 } 2300 } 2301 agendaEditor.setCutPropositionId(null); 2302 // call the super method to avoid the agenda tree being reloaded from the db 2303 return getModelAndView(form); 2304 } 2305 2306 @RequestMapping(params = "methodToCall=" + "deleteProposition") 2307 public ModelAndView deleteProposition(@ModelAttribute("KualiForm") UifFormBase form, BindingResult result, 2308 HttpServletRequest request, HttpServletResponse response) 2309 throws Exception { 2310 AgendaEditor agendaEditor = getAgendaEditor(form); 2311 String selectedPropId = agendaEditor.getSelectedPropositionId(); 2312 Node<RuleTreeNode, String> root = agendaEditor.getAgendaItemLine().getRule().getPropositionTree().getRootElement(); 2313 2314 Node<RuleTreeNode, String> parentNode = findParentPropositionNode(root, selectedPropId); 2315 2316 // what if it is the root? 2317 if (parentNode != null && parentNode.getData() != null) { // it is not the root as there is a parent w/ a prop 2318 PropositionBo parent = parentNode.getData().getProposition(); 2319 if (parent != null){ 2320 List <PropositionBo> children = parent.getCompoundComponents(); 2321 for( int index=0; index< children.size(); index++){ 2322 if (selectedPropId.equalsIgnoreCase(children.get(index).getId())){ 2323 parent.getCompoundComponents().remove(index); 2324 break; 2325 } 2326 } 2327 } 2328 } else { // no parent, it is the root 2329 if (KRADUtils.isNotNull(parentNode)) { 2330 parentNode.getChildren().clear(); 2331 agendaEditor.getAgendaItemLine().getRule().getPropositionTree().setRootElement(null); 2332 agendaEditor.getAgendaItemLine().getRule().setProposition(null); 2333 } else { 2334 GlobalVariables.getMessageMap().putError(KRMSPropertyConstants.Rule.PROPOSITION_TREE_GROUP_ID, 2335 "error.rule.proposition.noneHighlighted"); 2336 } 2337 } 2338 2339 agendaEditor.getDeletedPropositionIdsFromRule().add(selectedPropId); 2340 agendaEditor.getAgendaItemLine().getRule().refreshPropositionTree(false); 2341 2342 return getModelAndView(form); 2343 } 2344 2345 @RequestMapping(params = "methodToCall=" + "updateCompoundOperator") 2346 public ModelAndView updateCompoundOperator(@ModelAttribute("KualiForm") UifFormBase form, BindingResult result, 2347 HttpServletRequest request, HttpServletResponse response) 2348 throws Exception { 2349 2350 AgendaEditor agendaEditor = getAgendaEditor(form); 2351 RuleBo rule = agendaEditor.getAgendaItemLine().getRule(); 2352 rule.refreshPropositionTree(false); 2353 2354 return getModelAndView(form); 2355 } 2356 2357}