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