View Javadoc

1   /*
2    * Copyright 2007 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 1.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl1.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.krms.impl.ui;
17  
18  import java.util.ArrayList;
19  import java.util.Collections;
20  import java.util.HashMap;
21  import java.util.List;
22  import java.util.Map;
23  
24  import javax.servlet.http.HttpServletRequest;
25  import javax.servlet.http.HttpServletResponse;
26  
27  import org.apache.commons.lang.StringUtils;
28  import org.apache.commons.collections.MapUtils;
29  import org.kuali.rice.krad.service.KRADServiceLocator;
30  import org.kuali.rice.krad.service.SequenceAccessorService;
31  import org.kuali.rice.krad.uif.UifParameters;
32  import org.kuali.rice.krad.util.ObjectUtils;
33  import org.kuali.rice.krad.web.controller.MaintenanceDocumentController;
34  import org.kuali.rice.krad.web.form.MaintenanceForm;
35  import org.kuali.rice.krad.web.form.UifFormBase;
36  import org.kuali.rice.krms.impl.repository.AgendaBo;
37  import org.kuali.rice.krms.impl.repository.AgendaItemBo;
38  import org.kuali.rice.krms.impl.repository.ContextBo;
39  import org.springframework.stereotype.Controller;
40  import org.springframework.validation.BindingResult;
41  import org.springframework.web.bind.annotation.ModelAttribute;
42  import org.springframework.web.bind.annotation.RequestMapping;
43  import org.springframework.web.bind.annotation.RequestMethod;
44  import org.springframework.web.servlet.ModelAndView;
45  
46  /**
47   * Controller for the Test UI Page
48   * 
49   * @author Kuali Rice Team (rice.collab@kuali.org)
50   */
51  @Controller
52  @RequestMapping(value = "/krmsRuleEditor")
53  public class RuleEditorController extends MaintenanceDocumentController {
54  
55      private static final String AGENDA_ITEM_SELECTED = "agenda_item_selected";
56  
57      private SequenceAccessorService sequenceAccessorService;
58  
59      @Override
60      public MaintenanceForm createInitialForm(HttpServletRequest request) {
61          return new MaintenanceForm();
62      }
63      
64      /**
65       * This overridden method does extra work on refresh to populate the context and agenda
66       * 
67       * @see org.kuali.rice.krad.web.spring.controller.UifControllerBase#refresh(org.kuali.rice.krad.web.spring.form.UifFormBase, org.springframework.validation.BindingResult, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
68       */
69      @RequestMapping(params = "methodToCall=" + "refresh")
70      @Override
71      public ModelAndView refresh(@ModelAttribute("KualiForm") UifFormBase form, BindingResult result,
72              HttpServletRequest request, HttpServletResponse response)
73              throws Exception {
74          
75          MapUtils.verbosePrint(System.out, "actionParameters", form.getActionParameters());
76          MapUtils.verbosePrint(System.out, "requestParameters", request.getParameterMap());
77          
78          String agendaId = null;
79  
80          MaintenanceForm maintenanceForm = (MaintenanceForm) form;
81          String conversionFields = maintenanceForm.getActionParameters().get("conversionFields");
82          String refreshCaller = request.getParameter("refreshCaller");
83  
84          // handle return from agenda lookup
85          // TODO: this condition is sloppy 
86          if (refreshCaller != null && refreshCaller.contains("Agenda") && refreshCaller.contains("LookupView") &&
87                  conversionFields != null &&
88                  // TODO: this is sloppy
89                  conversionFields.contains("agenda.id")) {
90              AgendaEditor editorDocument =
91                      ((AgendaEditor) maintenanceForm.getDocument().getNewMaintainableObject().getDataObject());
92              agendaId = editorDocument.getAgenda().getId();
93              AgendaBo agenda = getBusinessObjectService().findBySinglePrimaryKey(AgendaBo.class, agendaId);
94              editorDocument.setAgenda(agenda);
95  
96              if (agenda.getContextId() != null) {
97                  ContextBo context = getBusinessObjectService().findBySinglePrimaryKey(ContextBo.class, agenda.getContextId());
98                  editorDocument.setContext(context);
99              }
100         }
101         
102         return super.refresh(form, result, request, response);
103     }
104 
105     /**
106      * This override is used to populate the agenda from the agenda name and context selection of the user.
107      * It is triggered by the refreshWhenChanged property of the MaintenanceView.
108      */
109     @RequestMapping(method = RequestMethod.POST, params = "methodToCall=updateComponent")
110     @Override
111     public ModelAndView updateComponent(@ModelAttribute("KualiForm") UifFormBase form, BindingResult result,
112             HttpServletRequest request, HttpServletResponse response) {
113 
114         MaintenanceForm maintenanceForm = (MaintenanceForm) form;
115         AgendaEditor editorDocument =
116                 ((AgendaEditor) maintenanceForm.getDocument().getNewMaintainableObject().getDataObject());
117         final Map<String, Object> map = new HashMap<String, Object>();
118         map.put("name", editorDocument.getAgenda().getName());
119         map.put("contextId", editorDocument.getContext().getId());
120 
121         AgendaBo agenda = getBusinessObjectService().findByPrimaryKey(AgendaBo.class, Collections.unmodifiableMap(map));
122         editorDocument.setAgenda(agenda);
123 
124         return super.updateComponent(form, result, request, response);
125     }
126 
127     /**
128      * This method updates the existing rule in the agenda.
129      */
130     @RequestMapping(params = "methodToCall=" + "goToAddRule")
131     public ModelAndView goToAddRule(@ModelAttribute("KualiForm") UifFormBase form, BindingResult result,
132             HttpServletRequest request, HttpServletResponse response) throws Exception {
133         AgendaBo agenda = getAgenda(form, request);
134         // this is the root of the tree:
135         AgendaItemBo firstItem = getFirstAgendaItem(agenda);
136         String selectedItemId = request.getParameter(AGENDA_ITEM_SELECTED);
137 
138         if (selectedItemId == null) {
139             setSelectedAgendaItemId(form, null);
140         } else {
141             setSelectedAgendaItemId(form, selectedItemId);
142         }
143         setAgendaItemLine(form, null);
144 
145         form.getActionParameters().put(UifParameters.NAVIGATE_TO_PAGE_ID, "AgendaEditorView-AddRule-Page");
146         return super.navigate(form, result, request, response);
147     }
148 
149     /**
150      * This method sets the agendaItemLine for adding/editing AgendaItems.
151      * The agendaItemLine is a copy of the agendaItem so that changes are not applied when
152      * they are abandoned.  If the agendaItem is null a new empty agendaItemLine is created.
153      *
154      * @param form
155      * @param agendaItem
156      */
157     private void setAgendaItemLine(UifFormBase form, AgendaItemBo agendaItem) {
158         MaintenanceForm maintenanceForm = (MaintenanceForm) form;
159         AgendaEditor editorDocument = ((AgendaEditor)maintenanceForm.getDocument().getDocumentDataObject());
160         if (agendaItem == null) {
161             editorDocument.setAgendaItemLine(new AgendaItemBo());
162         } else {
163             // TODO: Add a copy not the reference
164             editorDocument.setAgendaItemLine((AgendaItemBo) ObjectUtils.deepCopy(agendaItem));
165         }
166     }
167 
168     /**
169      * This method returns the agendaItemLine from adding/editing AgendaItems.
170      *
171      * @param form
172      * @return agendaItem
173      */
174     private AgendaItemBo getAgendaItemLine(UifFormBase form) {
175         MaintenanceForm maintenanceForm = (MaintenanceForm) form;
176         AgendaEditor editorDocument = ((AgendaEditor)maintenanceForm.getDocument().getDocumentDataObject());
177         return editorDocument.getAgendaItemLine();
178     }
179 
180     /**
181      * This method sets the id of the selected agendaItem.
182      *
183      * @param form
184      * @param selectedItemId
185      */
186     private void setSelectedAgendaItemId(UifFormBase form, String selectedAgendaItemId) {
187         MaintenanceForm maintenanceForm = (MaintenanceForm) form;
188         AgendaEditor editorDocument = ((AgendaEditor)maintenanceForm.getDocument().getDocumentDataObject());
189         editorDocument.setSelectedAgendaItemId(selectedAgendaItemId);
190     }
191 
192     /**
193      * This method returns the id of the selected agendaItem.
194      *
195      * @param form
196      * @return selectedAgendaItemId
197      */
198     private String getSelectedAgendaItemId(UifFormBase form) {
199         MaintenanceForm maintenanceForm = (MaintenanceForm) form;
200         AgendaEditor editorDocument = ((AgendaEditor)maintenanceForm.getDocument().getDocumentDataObject());
201         return editorDocument.getSelectedAgendaItemId();
202     }
203 
204     /**
205      * This method updates the existing rule in the agenda.
206      */
207     @RequestMapping(params = "methodToCall=" + "goToEditRule")
208     public ModelAndView goToEditRule(@ModelAttribute("KualiForm") UifFormBase form, BindingResult result,
209             HttpServletRequest request, HttpServletResponse response) throws Exception {
210 
211         AgendaBo agenda = getAgenda(form, request);
212         // this is the root of the tree:
213         AgendaItemBo firstItem = getFirstAgendaItem(agenda);
214         String selectedItemId = request.getParameter(AGENDA_ITEM_SELECTED);
215         AgendaItemBo node = getAgendaItemById(firstItem, selectedItemId);
216 
217         setSelectedAgendaItemId(form, selectedItemId);
218         setAgendaItemLine(form, node);
219 
220         form.getActionParameters().put(UifParameters.NAVIGATE_TO_PAGE_ID, "AgendaEditorView-EditRule-Page");
221         return super.navigate(form, result, request, response);
222     }
223 
224     /**
225      *  This method adds the newly create rule to the agenda.
226      */
227     @RequestMapping(params = "methodToCall=" + "addRule")
228     public ModelAndView addRule(@ModelAttribute("KualiForm") UifFormBase form, BindingResult result,
229             HttpServletRequest request, HttpServletResponse response) throws Exception {
230         MaintenanceForm maintenanceForm = (MaintenanceForm) form;
231         AgendaEditor editorDocument =
232                 ((AgendaEditor) maintenanceForm.getDocument().getNewMaintainableObject().getDataObject());
233         AgendaBo agenda = editorDocument.getAgenda();
234         AgendaItemBo newAgendaItem = editorDocument.getAgendaItemLine();
235         newAgendaItem.setId(getSequenceAccessorService().getNextAvailableSequenceNumber("KRMS_AGENDA_ITM_S").toString());
236         newAgendaItem.setAgendaId(agenda.getId());
237         if (agenda.getItems() == null) {
238             agenda.setItems(new ArrayList<AgendaItemBo>());
239         }
240         if (agenda.getFirstItemId() == null) {
241             agenda.setFirstItemId(newAgendaItem.getId());
242             agenda.getItems().add(newAgendaItem);
243         } else {
244             // insert agenda in tree
245             String selectedAgendaItemId = getSelectedAgendaItemId(form);
246             if (StringUtils.isBlank(selectedAgendaItemId)) {
247                 // add after the last root node
248                 AgendaItemBo node = getFirstAgendaItem(agenda);
249                 while (node.getAlways() != null) {
250                     node = node.getAlways();
251                 }
252                 node.setAlwaysId(newAgendaItem.getId());
253                 node.setAlways(newAgendaItem);
254             } else {
255                 // add after selected node
256                 AgendaItemBo firstItem = getFirstAgendaItem(agenda);
257                 AgendaItemBo node = getAgendaItemById(firstItem, getSelectedAgendaItemId(form));
258                 newAgendaItem.setAlwaysId(node.getAlwaysId());
259                 newAgendaItem.setAlways(node.getAlways());
260                 node.setAlwaysId(newAgendaItem.getId());
261                 node.setAlways(newAgendaItem);
262             }
263         }
264 
265         form.getActionParameters().put(UifParameters.NAVIGATE_TO_PAGE_ID, "AgendaEditorView-Agenda-Page");
266         return super.navigate(form, result, request, response);
267     }
268 
269     /**
270      * This method updates the existing rule in the agenda.
271      */
272     @RequestMapping(params = "methodToCall=" + "editRule")
273     public ModelAndView editRule(@ModelAttribute("KualiForm") UifFormBase form, BindingResult result,
274             HttpServletRequest request, HttpServletResponse response) throws Exception {
275         AgendaBo agenda = getAgenda(form, request);
276         // this is the root of the tree:
277         AgendaItemBo firstItem = getFirstAgendaItem(agenda);
278         AgendaItemBo node = getAgendaItemById(firstItem, getSelectedAgendaItemId(form));
279         AgendaItemBo agendaItemLine = getAgendaItemLine(form);
280         node.setRule(agendaItemLine.getRule());
281 
282         form.getActionParameters().put(UifParameters.NAVIGATE_TO_PAGE_ID, "AgendaEditorView-Agenda-Page");
283         return super.navigate(form, result, request, response);
284     }
285 
286     /**
287      * @return the ALWAYS {@link AgendaItemInstanceChildAccessor} for the last ALWAYS child of the instance accessed by the parameter.
288      * It will by definition refer to null.  If the instanceAccessor parameter refers to null, then it will be returned.  This is useful
289      * for adding a youngest child to a sibling group. 
290      */
291     private AgendaItemInstanceChildAccessor getLastChildsAlwaysAccessor(AgendaItemInstanceChildAccessor instanceAccessor) {
292         AgendaItemBo next = instanceAccessor.getChild();
293         if (next == null) return instanceAccessor;
294         while (next.getAlways() != null) { next = next.getAlways(); };
295         return new AgendaItemInstanceChildAccessor(AgendaItemChildAccessor.always, next);
296     }
297 
298     /**
299      * @return the accessor to the child with the given agendaItemId under the given parent.  This method will search both When TRUE and 
300      * When FALSE sibling groups.  If the instance with the given id is not found, null is returned.
301      */
302     private AgendaItemInstanceChildAccessor getInstanceAccessorToChild(AgendaItemBo parent, String agendaItemId) {
303 
304         // first try When TRUE, then When FALSE via AgendaItemChildAccessor.levelOrderChildren
305         for (AgendaItemChildAccessor levelOrderChildAccessor : AgendaItemChildAccessor.children) {
306 
307             AgendaItemBo next = levelOrderChildAccessor.getChild(parent);
308             
309             // if the first item matches, return the accessor from the parent
310             if (next != null && agendaItemId.equals(next.getId())) return new AgendaItemInstanceChildAccessor(levelOrderChildAccessor, parent);
311 
312             // otherwise walk the children
313             while (next != null && next.getAlwaysId() != null) {
314                 if (next.getAlwaysId().equals(agendaItemId)) return new AgendaItemInstanceChildAccessor(AgendaItemChildAccessor.always, next);
315                 // move down
316                 next = next.getAlways();
317             }
318         }
319         
320         return null;
321     }
322 
323     @RequestMapping(params = "methodToCall=" + "ajaxRefresh")
324     public ModelAndView ajaxRefresh(@ModelAttribute("KualiForm") UifFormBase form, BindingResult result,
325             HttpServletRequest request, HttpServletResponse response)
326             throws Exception {
327         // call the super method to avoid the agenda tree being reloaded from the db
328         return super.updateComponent(form, result, request, response);
329     }
330 
331     @RequestMapping(params = "methodToCall=" + "moveUp")
332     public ModelAndView moveUp(@ModelAttribute("KualiForm") UifFormBase form, BindingResult result,
333             HttpServletRequest request, HttpServletResponse response)
334             throws Exception {
335         moveSelectedSubtreeUp(form, request);
336 
337         return super.refresh(form, result, request, response);
338     }
339 
340     @RequestMapping(params = "methodToCall=" + "ajaxMoveUp")
341     public ModelAndView ajaxMoveUp(@ModelAttribute("KualiForm") UifFormBase form, BindingResult result,
342             HttpServletRequest request, HttpServletResponse response)
343             throws Exception {
344         moveSelectedSubtreeUp(form, request);
345 
346         // call the super method to avoid the agenda tree being reloaded from the db
347         return super.updateComponent(form, result, request, response);
348     }
349 
350     private void moveSelectedSubtreeUp(UifFormBase form, HttpServletRequest request) {
351 
352         /* Rough algorithm for moving a node up.  This is a "level order" move.  Note that in this tree,
353          * level order means something a bit funky.  We are defining a level as it would be displayed in the browser,
354          * so only the traversal of When FALSE or When TRUE links increments the level, since ALWAYS linked nodes are
355          * considered siblings.
356          *
357          * find the following:
358          *   node := the selected node
359          *   parent := the selected node's parent, its containing node (via when true or when false relationship)
360          *   parentsOlderCousin := the parent's level-order predecessor (sibling or cousin)
361          *
362          * if (node is first child in sibling group)
363          *     if (node is in When FALSE group)
364          *         move node to last position in When TRUE group
365          *     else
366          *         find youngest child of parentsOlderCousin and put node after it
367          * else
368          *     move node up within its sibling group
369          */
370 
371         AgendaBo agenda = getAgenda(form, request);
372         // this is the root of the tree:
373         AgendaItemBo firstItem = getFirstAgendaItem(agenda);
374 
375         String selectedItemId = request.getParameter(AGENDA_ITEM_SELECTED);
376         AgendaItemBo node = getAgendaItemById(firstItem, selectedItemId);
377         AgendaItemBo parent = getParent(firstItem, selectedItemId);
378         AgendaItemBo parentsOlderCousin = (parent == null) ? null : getNextOldestOfSameGeneration(firstItem, parent);
379 
380         AgendaItemChildAccessor childAccessor = getOldestChildAccessor(node, parent);
381         if (childAccessor != null) { // node is first child in sibling group
382             if (childAccessor == AgendaItemChildAccessor.whenFalse) {
383                 // move node to last position in When TRUE group
384                 AgendaItemInstanceChildAccessor youngestWhenTrueSiblingInsertionPoint =
385                         getLastChildsAlwaysAccessor(new AgendaItemInstanceChildAccessor(AgendaItemChildAccessor.whenTrue, parent));
386                 youngestWhenTrueSiblingInsertionPoint.setChild(node);
387                 AgendaItemChildAccessor.whenFalse.setChild(parent, node.getAlways());
388                 AgendaItemChildAccessor.always.setChild(node, null);
389 
390             } else if (parentsOlderCousin != null) {
391                 // find youngest child of parentsOlderCousin and put node after it
392                 AgendaItemInstanceChildAccessor youngestWhenFalseSiblingInsertionPoint =
393                         getLastChildsAlwaysAccessor(new AgendaItemInstanceChildAccessor(AgendaItemChildAccessor.whenFalse, parentsOlderCousin));
394                 youngestWhenFalseSiblingInsertionPoint.setChild(node);
395                 AgendaItemChildAccessor.whenTrue.setChild(parent, node.getAlways());
396                 AgendaItemChildAccessor.always.setChild(node, null);
397             }
398         } else if (!selectedItemId.equals(firstItem.getId())) { // conditional to miss special case of first node
399 
400             AgendaItemBo bogusRootNode = null;
401             if (parent == null) {
402                 // special case, this is a top level sibling. rig up special parent node
403                 bogusRootNode = new AgendaItemBo();
404                 AgendaItemChildAccessor.whenTrue.setChild(bogusRootNode, firstItem);
405                 parent = bogusRootNode;
406             }
407 
408             // move node up within its sibling group
409             AgendaItemInstanceChildAccessor accessorToSelectedNode = getInstanceAccessorToChild(parent, node.getId());
410             AgendaItemBo olderSibling = accessorToSelectedNode.getInstance();
411             AgendaItemInstanceChildAccessor accessorToOlderSibling = getInstanceAccessorToChild(parent, olderSibling.getId());
412 
413             accessorToOlderSibling.setChild(node);
414             accessorToSelectedNode.setChild(node.getAlways());
415             AgendaItemChildAccessor.always.setChild(node, olderSibling);
416 
417             if (bogusRootNode != null) {
418                 // clean up special case with bogus root node
419                 agenda.setFirstItemId(bogusRootNode.getWhenTrueId());
420             }
421         }
422     }
423 
424     @RequestMapping(params = "methodToCall=" + "moveDown")
425     public ModelAndView moveDown(@ModelAttribute("KualiForm") UifFormBase form, BindingResult result,
426             HttpServletRequest request, HttpServletResponse response)
427             throws Exception {
428         moveSelectedSubtreeDown(form, request);
429         
430         return super.refresh(form, result, request, response);
431     }
432 
433     @RequestMapping(params = "methodToCall=" + "ajaxMoveDown")
434     public ModelAndView ajaxMoveDown(@ModelAttribute("KualiForm") UifFormBase form, BindingResult result,
435             HttpServletRequest request, HttpServletResponse response)
436             throws Exception {
437         moveSelectedSubtreeDown(form, request);
438 
439         // call the super method to avoid the agenda tree being reloaded from the db
440         return super.updateComponent(form, result, request, response);
441     }
442 
443     private void moveSelectedSubtreeDown(UifFormBase form, HttpServletRequest request) {
444 
445         /* Rough algorithm for moving a node down.  This is a "level order" move.  Note that in this tree,
446          * level order means something a bit funky.  We are defining a level as it would be displayed in the browser,
447          * so only the traversal of When FALSE or When TRUE links increments the level, since ALWAYS linked nodes are
448          * considered siblings.
449          *
450          * find the following:
451          *   node := the selected node
452          *   parent := the selected node's parent, its containing node (via when true or when false relationship)
453          *   parentsYoungerCousin := the parent's level-order successor (sibling or cousin)
454          *
455          * if (node is last child in sibling group)
456          *     if (node is in When TRUE group)
457          *         move node to first position in When FALSE group
458          *     else
459          *         move to first child of parentsYoungerCousin
460          * else
461          *     move node down within its sibling group
462          */
463 
464         AgendaBo agenda = getAgenda(form, request);
465         // this is the root of the tree:
466         AgendaItemBo firstItem = getFirstAgendaItem(agenda);
467 
468         String selectedItemId = request.getParameter(AGENDA_ITEM_SELECTED);
469         AgendaItemBo node = getAgendaItemById(firstItem, selectedItemId);
470         AgendaItemBo parent = getParent(firstItem, selectedItemId);
471         AgendaItemBo parentsYoungerCousin = (parent == null) ? null : getNextYoungestOfSameGeneration(firstItem, parent);
472 
473         if (node.getAlways() == null && parent != null) { // node is last child in sibling group
474             // set link to selected node to null
475             if (parent.getWhenTrue() != null && isSiblings(parent.getWhenTrue(), node)) { // node is in When TRUE group
476                 // move node to first child under When FALSE
477 
478                 AgendaItemInstanceChildAccessor accessorToSelectedNode = getInstanceAccessorToChild(parent, node.getId());
479                 accessorToSelectedNode.setChild(null);
480 
481                 AgendaItemBo parentsFirstChild = parent.getWhenFalse();
482                 AgendaItemChildAccessor.whenFalse.setChild(parent, node);
483                 AgendaItemChildAccessor.always.setChild(node, parentsFirstChild);
484             } else if (parentsYoungerCousin != null) { // node is in the When FALSE group
485                 // move to first child of parentsYoungerCousin under When TRUE
486 
487                 AgendaItemInstanceChildAccessor accessorToSelectedNode = getInstanceAccessorToChild(parent, node.getId());
488                 accessorToSelectedNode.setChild(null);
489 
490                 AgendaItemBo parentsYoungerCousinsFirstChild = parentsYoungerCousin.getWhenTrue();
491                 AgendaItemChildAccessor.whenTrue.setChild(parentsYoungerCousin, node);
492                 AgendaItemChildAccessor.always.setChild(node, parentsYoungerCousinsFirstChild);
493             }
494         } else if (node.getAlways() != null) { // move node down within its sibling group
495 
496             AgendaItemBo bogusRootNode = null;
497             if (parent == null) {
498                 // special case, this is a top level sibling. rig up special parent node
499                 bogusRootNode = new AgendaItemBo();
500                 AgendaItemChildAccessor.whenFalse.setChild(bogusRootNode, firstItem);
501                 parent = bogusRootNode;
502             }
503 
504             // move node down within its sibling group
505             AgendaItemInstanceChildAccessor accessorToSelectedNode = getInstanceAccessorToChild(parent, node.getId());
506             AgendaItemBo youngerSibling = node.getAlways();
507             accessorToSelectedNode.setChild(youngerSibling);
508             AgendaItemChildAccessor.always.setChild(node, youngerSibling.getAlways());
509             AgendaItemChildAccessor.always.setChild(youngerSibling, node);
510 
511             if (bogusRootNode != null) {
512                 // clean up special case with bogus root node
513                 agenda.setFirstItemId(bogusRootNode.getWhenFalseId());
514             }
515         }
516     }
517 
518     @RequestMapping(params = "methodToCall=" + "moveLeft")
519     public ModelAndView moveLeft(@ModelAttribute("KualiForm") UifFormBase form, BindingResult result,
520             HttpServletRequest request, HttpServletResponse response)
521             throws Exception {
522         moveSelectedSubtreeLeft(form, request);
523         
524         return super.refresh(form, result, request, response);
525     }
526 
527     @RequestMapping(params = "methodToCall=" + "ajaxMoveLeft")
528     public ModelAndView ajaxMoveLeft(@ModelAttribute("KualiForm") UifFormBase form, BindingResult result,
529             HttpServletRequest request, HttpServletResponse response)
530             throws Exception {
531 
532         moveSelectedSubtreeLeft(form, request);
533 
534         // call the super method to avoid the agenda tree being reloaded from the db
535         return super.updateComponent(form, result, request, response);
536     }
537 
538     private void moveSelectedSubtreeLeft(UifFormBase form, HttpServletRequest request) {
539 
540         /*
541          * Move left means make it a younger sibling of it's parent.
542          */
543 
544         AgendaBo agenda = getAgenda(form, request);
545         // this is the root of the tree:
546         AgendaItemBo firstItem = getFirstAgendaItem(agenda);
547 
548         String selectedItemId = request.getParameter(AGENDA_ITEM_SELECTED);
549         AgendaItemBo node = getAgendaItemById(firstItem, selectedItemId);
550         AgendaItemBo parent = getParent(firstItem, selectedItemId);
551 
552         if (parent != null) {
553             AgendaItemInstanceChildAccessor accessorToSelectedNode = getInstanceAccessorToChild(parent, node.getId());
554             accessorToSelectedNode.setChild(node.getAlways());
555 
556             AgendaItemChildAccessor.always.setChild(node, parent.getAlways());
557             AgendaItemChildAccessor.always.setChild(parent, node);
558         }
559     }
560 
561 
562     @RequestMapping(params = "methodToCall=" + "moveRight")
563     public ModelAndView moveRight(@ModelAttribute("KualiForm") UifFormBase form, BindingResult result,
564             HttpServletRequest request, HttpServletResponse response)
565             throws Exception {
566 
567         moveSelectedSubtreeRight(form, request);
568 
569         return super.refresh(form, result, request, response);
570     }
571 
572     @RequestMapping(params = "methodToCall=" + "ajaxMoveRight")
573     public ModelAndView ajaxMoveRight(@ModelAttribute("KualiForm") UifFormBase form, BindingResult result,
574             HttpServletRequest request, HttpServletResponse response)
575             throws Exception {
576 
577         moveSelectedSubtreeRight(form, request);
578 
579         // call the super method to avoid the agenda tree being reloaded from the db
580         return super.updateComponent(form, result, request, response);
581     }
582 
583     private void moveSelectedSubtreeRight(UifFormBase form, HttpServletRequest request) {
584 
585         /*
586          * Move right prefers moving to bottom of upper sibling's When FALSE branch
587          * ... otherwise ..
588          * moves to top of lower sibling's When TRUE branch
589          */
590 
591         AgendaBo agenda = getAgenda(form, request);
592         // this is the root of the tree:
593         AgendaItemBo firstItem = getFirstAgendaItem(agenda);
594 
595         String selectedItemId = request.getParameter(AGENDA_ITEM_SELECTED);
596         AgendaItemBo node = getAgendaItemById(firstItem, selectedItemId);
597         AgendaItemBo parent = getParent(firstItem, selectedItemId);
598 
599         AgendaItemBo bogusRootNode = null;
600         if (parent == null) {
601             // special case, this is a top level sibling. rig up special parent node
602             bogusRootNode = new AgendaItemBo();
603             AgendaItemChildAccessor.whenFalse.setChild(bogusRootNode, firstItem);
604             parent = bogusRootNode;
605         }
606 
607         AgendaItemInstanceChildAccessor accessorToSelectedNode = getInstanceAccessorToChild(parent, node.getId());
608         AgendaItemBo olderSibling = (accessorToSelectedNode.getInstance() == parent) ? null : accessorToSelectedNode.getInstance();
609 
610         if (olderSibling != null) {
611             accessorToSelectedNode.setChild(node.getAlways());
612             AgendaItemInstanceChildAccessor yougestWhenFalseSiblingInsertionPoint =
613                     getLastChildsAlwaysAccessor(new AgendaItemInstanceChildAccessor(AgendaItemChildAccessor.whenFalse, olderSibling));
614             yougestWhenFalseSiblingInsertionPoint.setChild(node);
615             AgendaItemChildAccessor.always.setChild(node, null);
616         } else if (node.getAlways() != null) { // has younger sibling
617             accessorToSelectedNode.setChild(node.getAlways());
618             AgendaItemBo childsWhenTrue = node.getAlways().getWhenTrue();
619             AgendaItemChildAccessor.whenTrue.setChild(node.getAlways(), node);
620             AgendaItemChildAccessor.always.setChild(node, childsWhenTrue);
621         }
622 
623         if (bogusRootNode != null) {
624             // clean up special case with bogus root node
625             agenda.setFirstItemId(bogusRootNode.getWhenFalseId());
626         }
627     }
628 
629     private boolean isSiblings(AgendaItemBo cousin1, AgendaItemBo cousin2) {
630         if (cousin1.equals(cousin2)) return true; // this is a bit abusive
631         
632         // can you walk to c1 from ALWAYS links of c2?
633         AgendaItemBo candidate = cousin2;
634         while (null != (candidate = candidate.getAlways())) {
635             if (candidate.equals(cousin1)) return true;
636         }
637         // can you walk to c2 from ALWAYS links of c1?
638         candidate = cousin1;
639         while (null != (candidate = candidate.getAlways())) {
640             if (candidate.equals(cousin2)) return true;
641         }
642         return false;
643     }
644 
645     /**
646      * This method returns the level order accessor (getWhenTrue or getWhenFalse) that relates the parent directly 
647      * to the child.  If the two nodes don't have such a relationship, null is returned. 
648      * Note that this only finds accessors for oldest children, not younger siblings.
649      */
650     private AgendaItemChildAccessor getOldestChildAccessor(
651             AgendaItemBo child, AgendaItemBo parent) {
652         AgendaItemChildAccessor levelOrderChildAccessor = null;
653         
654         if (parent != null) {
655             for (AgendaItemChildAccessor childAccessor : AgendaItemChildAccessor.children) {
656                 if (child.equals(childAccessor.getChild(parent))) {
657                     levelOrderChildAccessor = childAccessor;
658                     break;
659                 }
660             }
661         }
662         return levelOrderChildAccessor;
663     }
664     
665     /**
666      * This method finds and returns the first agenda item in the agenda, or null if there are no items presently
667      * 
668      * @param agenda
669      * @return
670      */
671     private AgendaItemBo getFirstAgendaItem(AgendaBo agenda) {
672         AgendaItemBo firstItem = null;
673         for (AgendaItemBo agendaItem : agenda.getItems()) {
674             if (agenda.getFirstItemId().equals(agendaItem.getId())) {
675                 firstItem = agendaItem;
676                 break;
677             }
678         }
679         return firstItem;
680     }
681     
682     /**
683      * @return the closest younger sibling of the agenda item with the given ID, and if there is no such sibling, the closest younger cousin.
684      * If there is no such cousin either, then null is returned.
685      */
686     private AgendaItemBo getNextYoungestOfSameGeneration(AgendaItemBo root, AgendaItemBo agendaItem) {
687 
688         int genNumber = getAgendaItemGenerationNumber(0, root, agendaItem.getId());
689         List<AgendaItemBo> genList = new ArrayList<AgendaItemBo>();
690         buildAgendaItemGenerationList(genList, root, 0, genNumber);
691 
692         int itemIndex = genList.indexOf(agendaItem);
693         if (genList.size() > itemIndex + 1) return genList.get(itemIndex + 1);
694 
695         return null;
696     }
697 
698     private int getAgendaItemGenerationNumber(int currentLevel, AgendaItemBo node, String agendaItemId) {
699         int result = -1;
700         if (agendaItemId.equals(node.getId())) {
701             result = currentLevel;
702         } else {
703             for (AgendaItemChildAccessor childAccessor : AgendaItemChildAccessor.linkedNodes) {
704                 AgendaItemBo child = childAccessor.getChild(node);
705                 if (child != null) {
706                     int nextLevel = currentLevel;
707                     // we don't change the level order parent when we traverse ALWAYS links
708                     if (childAccessor != AgendaItemChildAccessor.always) {
709                         nextLevel = currentLevel +1;
710                     }
711                     result = getAgendaItemGenerationNumber(nextLevel, child, agendaItemId);
712                     if (result != -1) break;
713                 }
714             }
715         }
716         return result;
717     }
718 
719     private void buildAgendaItemGenerationList(List<AgendaItemBo> genList, AgendaItemBo node, int currentLevel, int generation) {
720         if (currentLevel == generation) {
721             genList.add(node);
722         }
723 
724         if (currentLevel > generation) return;
725 
726         for (AgendaItemChildAccessor childAccessor : AgendaItemChildAccessor.linkedNodes) {
727             AgendaItemBo child = childAccessor.getChild(node);
728             if (child != null) {
729                 int nextLevel = currentLevel;
730                 // we don't change the level order parent when we traverse ALWAYS links
731                 if (childAccessor != AgendaItemChildAccessor.always) {
732                     nextLevel = currentLevel +1;
733                 }
734                 buildAgendaItemGenerationList(genList, child, nextLevel, generation);
735             }
736         }
737     }
738     
739 
740     /**
741      * @return the closest older sibling of the agenda item with the given ID, and if there is no such sibling, the closest older cousin.
742      * If there is no such cousin either, then null is returned.
743      */
744     private AgendaItemBo getNextOldestOfSameGeneration(AgendaItemBo root, AgendaItemBo agendaItem) {
745 
746         int genNumber = getAgendaItemGenerationNumber(0, root, agendaItem.getId());
747         List<AgendaItemBo> genList = new ArrayList<AgendaItemBo>();
748         buildAgendaItemGenerationList(genList, root, 0, genNumber);
749 
750         int itemIndex = genList.indexOf(agendaItem);
751         if (itemIndex >= 1) return genList.get(itemIndex - 1);
752 
753         return null;
754     }
755     
756 
757     /**
758      * returns the parent of the item with the passed in id.  Note that {@link AgendaItemBo}s related by ALWAYS relationships are considered siblings.
759      */ 
760     private AgendaItemBo getParent(AgendaItemBo root, String agendaItemId) {
761         return getParentHelper(root, null, agendaItemId);
762     }
763     
764     private AgendaItemBo getParentHelper(AgendaItemBo node, AgendaItemBo levelOrderParent, String agendaItemId) {
765         AgendaItemBo result = null;
766         if (agendaItemId.equals(node.getId())) {
767             result = levelOrderParent;
768         } else {
769             for (AgendaItemChildAccessor childAccessor : AgendaItemChildAccessor.linkedNodes) {
770                 AgendaItemBo child = childAccessor.getChild(node);
771                 if (child != null) {
772                     // we don't change the level order parent when we traverse ALWAYS links 
773                     AgendaItemBo lop = (childAccessor == AgendaItemChildAccessor.always) ? levelOrderParent : node;
774                     result = getParentHelper(child, lop, agendaItemId);
775                     if (result != null) break;
776                 }
777             }
778         }
779         return result;
780     }
781 
782     /**
783      * Search the tree for the agenda item with the given id.
784      */
785     private AgendaItemBo getAgendaItemById(AgendaItemBo node, String agendaItemId) {
786         if (node == null) throw new IllegalArgumentException("node must be non-null");
787 
788         AgendaItemBo result = null;
789         
790         if (agendaItemId.equals(node.getId())) {
791             result = node;
792         } else {
793             for (AgendaItemChildAccessor childAccessor : AgendaItemChildAccessor.linkedNodes) {
794                 AgendaItemBo child = childAccessor.getChild(node);
795                 if (child != null) {
796                     result = getAgendaItemById(child, agendaItemId);
797                     if (result != null) break;
798                 }
799             }
800         } 
801         return result;
802     }
803 
804     /**
805      * This method gets the agenda from the form
806      * 
807      * @param form
808      * @param request
809      * @return
810      */
811     private AgendaBo getAgenda(UifFormBase form, HttpServletRequest request) {
812         MaintenanceForm maintenanceForm = (MaintenanceForm) form;
813         AgendaEditor editorDocument = ((AgendaEditor)maintenanceForm.getDocument().getDocumentDataObject());
814         AgendaBo agenda = editorDocument.getAgenda();
815         return agenda;
816     }
817 
818     private void treeToInOrderList(AgendaItemBo agendaItem, List<AgendaItemBo> listToBuild) {
819         listToBuild.add(agendaItem);
820         for (AgendaItemChildAccessor childAccessor : AgendaItemChildAccessor.linkedNodes) {
821             AgendaItemBo child = childAccessor.getChild(agendaItem);
822             if (child != null) treeToInOrderList(child, listToBuild);
823         }
824     }
825 
826     
827     @RequestMapping(params = "methodToCall=" + "delete")
828     public ModelAndView delete(@ModelAttribute("KualiForm") UifFormBase form, BindingResult result,
829             HttpServletRequest request, HttpServletResponse response)
830             throws Exception {
831 
832         deleteSelectedSubtree(form, request);
833 
834         return super.refresh(form, result, request, response);
835     }
836 
837     @RequestMapping(params = "methodToCall=" + "ajaxDelete")
838     public ModelAndView ajaxDelete(@ModelAttribute("KualiForm") UifFormBase form, BindingResult result,
839             HttpServletRequest request, HttpServletResponse response)
840             throws Exception {
841 
842         deleteSelectedSubtree(form, request);
843 
844         // call the super method to avoid the agenda tree being reloaded from the db
845         return super.updateComponent(form, result, request, response);
846     }
847 
848     
849     private void deleteSelectedSubtree(UifFormBase form, HttpServletRequest request) {AgendaBo agenda = getAgenda(form, request);
850         AgendaItemBo firstItem = getFirstAgendaItem(agenda);
851 
852         String agendaItemSelected = request.getParameter(AGENDA_ITEM_SELECTED);
853 
854         if (firstItem != null) {
855             // need to handle the first item here, our recursive method won't handle it.
856             if (agendaItemSelected.equals(firstItem.getAgendaId())) {
857                 agenda.setFirstItemId(firstItem.getAlwaysId());
858             } else {
859                 deleteAgendaItem(firstItem, agendaItemSelected);
860             }
861         }
862     }
863 
864     private void deleteAgendaItem(AgendaItemBo root, String agendaItemIdToDelete) {
865         if (deleteAgendaItem(root, AgendaItemChildAccessor.whenTrue, agendaItemIdToDelete) || 
866                 deleteAgendaItem(root, AgendaItemChildAccessor.whenFalse, agendaItemIdToDelete) || 
867                 deleteAgendaItem(root, AgendaItemChildAccessor.always, agendaItemIdToDelete)); // TODO: this is confusing, refactor
868     }
869     
870     private boolean deleteAgendaItem(AgendaItemBo agendaItem, AgendaItemChildAccessor childAccessor, String agendaItemIdToDelete) {
871         if (agendaItem == null || childAccessor.getChild(agendaItem) == null) return false;
872         if (agendaItemIdToDelete.equals(childAccessor.getChild(agendaItem).getId())) {
873             // delete the child in such a way that any ALWAYS children don't get lost from the tree
874             AgendaItemBo grandchildToKeep = childAccessor.getChild(agendaItem).getAlways();
875             childAccessor.setChild(agendaItem, grandchildToKeep);
876             return true;
877         } else {
878             AgendaItemBo child = childAccessor.getChild(agendaItem);
879             // recurse
880             for (AgendaItemChildAccessor nextChildAccessor : AgendaItemChildAccessor.linkedNodes) {
881                 if (deleteAgendaItem(child, nextChildAccessor, agendaItemIdToDelete)) return true;
882             }
883         }
884         return false;
885     }
886 
887     /**
888      * binds a child accessor to an AgendaItemBo instance
889      */
890     private static class AgendaItemInstanceChildAccessor {
891         
892         private final AgendaItemChildAccessor accessor;
893         private final AgendaItemBo instance;
894 
895         public AgendaItemInstanceChildAccessor(AgendaItemChildAccessor accessor, AgendaItemBo instance) {
896             this.accessor = accessor;
897             this.instance = instance;
898         }
899         
900         public void setChild(AgendaItemBo child) {
901             accessor.setChild(instance, child);
902         }
903         
904         public AgendaItemBo getChild() {
905             return accessor.getChild(instance);
906         }
907         
908         public AgendaItemBo getInstance() { return instance; }
909     }
910     
911     /**
912      * This class abstracts getting and setting a child of an AgendaItemBo, making some recursive operations 
913      * require less boiler plate 
914      */
915     private static class AgendaItemChildAccessor {
916         
917         private enum Child { WHEN_TRUE, WHEN_FALSE, ALWAYS };
918         
919         private static final AgendaItemChildAccessor whenTrue = new AgendaItemChildAccessor(Child.WHEN_TRUE); 
920         private static final AgendaItemChildAccessor whenFalse = new AgendaItemChildAccessor(Child.WHEN_FALSE); 
921         private static final AgendaItemChildAccessor always = new AgendaItemChildAccessor(Child.ALWAYS); 
922 
923         /**
924          * Accessors for all linked items
925          */
926         private static final AgendaItemChildAccessor [] linkedNodes = { whenTrue, whenFalse, always };
927         
928         /**
929          * Accessors for children (so ALWAYS is omitted);
930          */
931         private static final AgendaItemChildAccessor [] children = { whenTrue, whenFalse };
932         
933         private final Child whichChild;
934         
935         private AgendaItemChildAccessor(Child whichChild) {
936             if (whichChild == null) throw new IllegalArgumentException("whichChild must be non-null");
937             this.whichChild = whichChild;
938         }
939         
940         /**
941          * @return the referenced child
942          */
943         public AgendaItemBo getChild(AgendaItemBo parent) {
944             switch (whichChild) {
945             case WHEN_TRUE: return parent.getWhenTrue();
946             case WHEN_FALSE: return parent.getWhenFalse();
947             case ALWAYS: return parent.getAlways();
948             default: throw new IllegalStateException();
949             }
950         }
951         
952         /**
953          * Sets the child reference and the child id 
954          */
955         public void setChild(AgendaItemBo parent, AgendaItemBo child) {
956             switch (whichChild) {
957             case WHEN_TRUE: 
958                 parent.setWhenTrue(child);
959                 parent.setWhenTrueId(child == null ? null : child.getId());
960                 break;
961             case WHEN_FALSE:
962                 parent.setWhenFalse(child);
963                 parent.setWhenFalseId(child == null ? null : child.getId());
964                 break;
965             case ALWAYS:
966                 parent.setAlways(child);
967                 parent.setAlwaysId(child == null ? null : child.getId());
968                 break;
969             default: throw new IllegalStateException();
970             }
971         }
972     }
973 
974     protected SequenceAccessorService getSequenceAccessorService() {
975         if ( sequenceAccessorService == null ) {
976             sequenceAccessorService = KRADServiceLocator.getSequenceAccessorService();
977         }
978         return sequenceAccessorService;
979     }
980     
981 }