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