View Javadoc

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