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