001/**
002 * Copyright 2005-2016 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 */
016package org.kuali.rice.krms.impl.ui;
017
018import org.apache.commons.collections.CollectionUtils;
019import org.apache.commons.lang.StringUtils;
020import org.kuali.rice.core.api.util.tree.Node;
021import org.kuali.rice.krad.maintenance.MaintenanceDocument;
022import org.kuali.rice.krad.service.KRADServiceLocator;
023import org.kuali.rice.krad.service.SequenceAccessorService;
024import org.kuali.rice.krad.uif.UifParameters;
025import org.kuali.rice.krad.util.GlobalVariables;
026import org.kuali.rice.krad.util.ObjectUtils;
027import org.kuali.rice.krad.web.controller.MaintenanceDocumentController;
028import org.kuali.rice.krad.web.form.MaintenanceForm;
029import org.kuali.rice.krad.web.form.UifFormBase;
030import org.kuali.rice.krms.api.KrmsApiServiceLocator;
031import org.kuali.rice.krms.api.engine.expression.ComparisonOperatorService;
032import org.kuali.rice.krms.api.repository.rule.RuleDefinition;
033import org.kuali.rice.krms.api.repository.term.TermDefinition;
034import org.kuali.rice.krms.api.repository.term.TermResolverDefinition;
035import org.kuali.rice.krms.api.repository.term.TermSpecificationDefinition;
036import org.kuali.rice.krms.api.repository.type.KrmsAttributeDefinition;
037import org.kuali.rice.krms.impl.repository.ActionBo;
038import org.kuali.rice.krms.api.repository.LogicalOperator;
039import org.kuali.rice.krms.api.repository.proposition.PropositionType;
040import org.kuali.rice.krms.impl.repository.AgendaBo;
041import org.kuali.rice.krms.impl.repository.AgendaItemBo;
042import org.kuali.rice.krms.impl.repository.ContextBoService;
043import org.kuali.rice.krms.impl.repository.KrmsAttributeDefinitionService;
044import org.kuali.rice.krms.impl.repository.KrmsRepositoryServiceLocator;
045import org.kuali.rice.krms.impl.repository.PropositionBo;
046import org.kuali.rice.krms.impl.repository.RuleBo;
047import org.kuali.rice.krms.impl.repository.RuleBoService;
048import org.kuali.rice.krms.impl.repository.TermBo;
049import org.kuali.rice.krms.impl.rule.AgendaEditorBusRule;
050import org.kuali.rice.krms.impl.util.KRMSPropertyConstants;
051import org.kuali.rice.krms.impl.util.KrmsImplConstants;
052import org.springframework.stereotype.Controller;
053import org.springframework.validation.BindingResult;
054import org.springframework.web.bind.annotation.ModelAttribute;
055import org.springframework.web.bind.annotation.RequestMapping;
056import org.springframework.web.bind.annotation.RequestMethod;
057import org.springframework.web.bind.annotation.RequestParam;
058import org.springframework.web.bind.annotation.ResponseBody;
059import org.springframework.web.servlet.ModelAndView;
060
061import javax.jws.WebParam;
062import javax.servlet.http.HttpServletRequest;
063import javax.servlet.http.HttpServletResponse;
064import java.util.ArrayList;
065import java.util.Collection;
066import java.util.Collections;
067import java.util.Enumeration;
068import java.util.HashMap;
069import java.util.List;
070import java.util.Map;
071import 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)
079public 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}