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