View Javadoc
1   /**
2    * Copyright 2005-2016 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.krms.impl.ui;
17  
18  import org.apache.commons.collections.CollectionUtils;
19  import org.apache.commons.lang.StringUtils;
20  import org.kuali.rice.core.api.uif.RemotableAttributeError;
21  import org.kuali.rice.core.api.util.KeyValue;
22  import org.kuali.rice.core.api.util.tree.Node;
23  import org.kuali.rice.krad.maintenance.MaintenanceDocument;
24  import org.kuali.rice.krad.service.KRADServiceLocator;
25  import org.kuali.rice.krad.service.SequenceAccessorService;
26  import org.kuali.rice.krad.uif.UifParameters;
27  import org.kuali.rice.krad.util.GlobalVariables;
28  import org.kuali.rice.krad.util.ObjectUtils;
29  import org.kuali.rice.krad.web.controller.MaintenanceDocumentController;
30  import org.kuali.rice.krad.web.form.DocumentFormBase;
31  import org.kuali.rice.krad.web.form.MaintenanceDocumentForm;
32  import org.kuali.rice.krad.web.form.UifFormBase;
33  import org.kuali.rice.krms.api.KrmsApiServiceLocator;
34  import org.kuali.rice.krms.api.engine.expression.ComparisonOperatorService;
35  import org.kuali.rice.krms.api.repository.LogicalOperator;
36  import org.kuali.rice.krms.api.repository.operator.CustomOperator;
37  import org.kuali.rice.krms.api.repository.proposition.PropositionParameterType;
38  import org.kuali.rice.krms.api.repository.proposition.PropositionType;
39  import org.kuali.rice.krms.api.repository.rule.RuleDefinition;
40  import org.kuali.rice.krms.api.repository.term.TermDefinition;
41  import org.kuali.rice.krms.api.repository.term.TermResolverDefinition;
42  import org.kuali.rice.krms.api.repository.term.TermSpecificationDefinition;
43  import org.kuali.rice.krms.api.repository.type.KrmsAttributeDefinition;
44  import org.kuali.rice.krms.impl.repository.ActionBo;
45  import org.kuali.rice.krms.impl.repository.AgendaBo;
46  import org.kuali.rice.krms.impl.repository.AgendaItemBo;
47  import org.kuali.rice.krms.impl.repository.ContextBoService;
48  import org.kuali.rice.krms.impl.repository.FunctionBoService;
49  import org.kuali.rice.krms.impl.repository.KrmsAttributeDefinitionService;
50  import org.kuali.rice.krms.impl.repository.KrmsRepositoryServiceLocator;
51  import org.kuali.rice.krms.impl.repository.PropositionBo;
52  import org.kuali.rice.krms.impl.repository.PropositionParameterBo;
53  import org.kuali.rice.krms.impl.repository.RuleBo;
54  import org.kuali.rice.krms.impl.repository.RuleBoService;
55  import org.kuali.rice.krms.impl.repository.TermBo;
56  import org.kuali.rice.krms.impl.rule.AgendaEditorBusRule;
57  import org.kuali.rice.krms.impl.util.KRMSPropertyConstants;
58  import org.kuali.rice.krms.impl.util.KrmsImplConstants;
59  import org.kuali.rice.krms.impl.util.KrmsServiceLocatorInternal;
60  import org.springframework.stereotype.Controller;
61  import org.springframework.validation.BindingResult;
62  import org.springframework.web.bind.annotation.ModelAttribute;
63  import org.springframework.web.bind.annotation.RequestMapping;
64  import org.springframework.web.bind.annotation.RequestMethod;
65  import org.springframework.web.bind.annotation.RequestParam;
66  import org.springframework.web.bind.annotation.ResponseBody;
67  import org.springframework.web.servlet.ModelAndView;
68  
69  import javax.servlet.http.HttpServletRequest;
70  import javax.servlet.http.HttpServletResponse;
71  import java.util.ArrayList;
72  import java.util.Collection;
73  import java.util.Collections;
74  import java.util.Enumeration;
75  import java.util.HashMap;
76  import java.util.List;
77  import java.util.Map;
78  
79  /**
80   * Controller for the Test UI Page
81   * @author Kuali Rice Team (rice.collab@kuali.org)
82   */
83  @Controller
84  @RequestMapping(value = org.kuali.rice.krms.impl.util.KrmsImplConstants.WebPaths.AGENDA_EDITOR_PATH)
85  public class AgendaEditorController extends MaintenanceDocumentController {
86  
87      private SequenceAccessorService sequenceAccessorService;
88  
89      /**
90       * Override route to set the setSelectedAgendaItemId to empty and disable all the buttons
91       *
92       * @see org.kuali.rice.krad.web.controller.MaintenanceDocumentController#route
93       *     (DocumentFormBase, BindingResult, HttpServletRequest, HttpServletResponse)
94       */
95      @Override
96      @RequestMapping(params = "methodToCall=route")
97      public ModelAndView route(@ModelAttribute("KualiForm") DocumentFormBase form, BindingResult result,
98              HttpServletRequest request, HttpServletResponse response) {
99  
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 }