View Javadoc

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