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