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