View Javadoc

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