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