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