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