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