001/** 002 * Copyright 2005-2018 The Kuali Foundation 003 * 004 * Licensed under the Educational Community License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.opensource.org/licenses/ecl2.php 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.kuali.rice.krad.uif.container; 017 018import org.apache.commons.collections.ListUtils; 019import org.apache.commons.lang.StringUtils; 020import org.apache.commons.logging.Log; 021import org.apache.commons.logging.LogFactory; 022import org.kuali.rice.core.api.mo.common.active.Inactivatable; 023import org.kuali.rice.krad.uif.UifConstants; 024import org.kuali.rice.krad.uif.UifParameters; 025import org.kuali.rice.krad.uif.UifPropertyPaths; 026import org.kuali.rice.krad.uif.component.Component; 027import org.kuali.rice.krad.uif.container.collections.LineBuilderContext; 028import org.kuali.rice.krad.uif.element.Action; 029import org.kuali.rice.krad.uif.lifecycle.ViewLifecycle; 030import org.kuali.rice.krad.uif.lifecycle.ViewLifecycleUtils; 031import org.kuali.rice.krad.uif.util.ComponentFactory; 032import org.kuali.rice.krad.uif.util.ComponentUtils; 033import org.kuali.rice.krad.uif.util.ContextUtils; 034import org.kuali.rice.krad.uif.util.ObjectPropertyUtils; 035import org.kuali.rice.krad.uif.util.ScriptUtils; 036import org.kuali.rice.krad.uif.view.ExpressionEvaluator; 037import org.kuali.rice.krad.uif.view.FormView; 038import org.kuali.rice.krad.uif.view.View; 039import org.kuali.rice.krad.uif.view.ViewModel; 040import org.kuali.rice.krad.util.KRADUtils; 041import org.kuali.rice.krad.web.form.UifFormBase; 042 043import java.io.Serializable; 044import java.util.ArrayList; 045import java.util.Collection; 046import java.util.HashMap; 047import java.util.List; 048import java.util.Map; 049 050/** 051 * Builds out the {@link org.kuali.rice.krad.uif.field.Field} instances for a collection group with a 052 * series of steps that interact with the configured {@link org.kuali.rice.krad.uif.layout.CollectionLayoutManager} 053 * to assemble the fields as necessary for the layout. 054 * 055 * @author Kuali Rice Team (rice.collab@kuali.org) 056 */ 057public class CollectionGroupBuilder implements Serializable { 058 059 private static final long serialVersionUID = -4762031957079895244L; 060 private static Log LOG = LogFactory.getLog(CollectionGroupBuilder.class); 061 062 /** 063 * Invoked within the lifecycle to carry out the collection build process. 064 * 065 * <p>The corresponding collection is retrieved from the model and iterated 066 * over to create the necessary fields. The binding path for fields that 067 * implement {@code DataBinding} is adjusted to point to the collection 068 * line it is apart of. For example, field 'number' of collection 'accounts' 069 * for line 1 will be set to 'accounts[0].number', and for line 2 070 * 'accounts[1].number'. Finally parameters are set on the line's action 071 * fields to indicate what collection and line they apply to.</p> 072 * 073 * <p>Only the lines that are to be rendered (as specified by the displayStart 074 * and displayLength properties of the CollectionGroup) will be built.</p> 075 * 076 * @param view View instance the collection belongs to 077 * @param model Top level object containing the data 078 * @param collectionGroup CollectionGroup component for the collection 079 */ 080 public void build(View view, Object model, CollectionGroup collectionGroup) { 081 // create add line 082 if (collectionGroup.isRenderAddLine() && !Boolean.TRUE.equals(collectionGroup.getReadOnly()) && 083 !collectionGroup.isRenderAddBlankLineButton()) { 084 buildAddLine(view, model, collectionGroup); 085 } 086 087 // if add line button enabled setup to refresh the collection group 088 if (collectionGroup.isRenderAddBlankLineButton() && (collectionGroup.getAddBlankLineAction() != null)) { 089 collectionGroup.getAddBlankLineAction().setRefreshId(collectionGroup.getId()); 090 } 091 092 // get the collection for this group from the model 093 List<Object> modelCollection = ObjectPropertyUtils.getPropertyValue(model, 094 collectionGroup.getBindingInfo().getBindingPath()); 095 096 if (modelCollection == null) { 097 return; 098 } 099 100 // filter inactive model 101 List<Integer> showIndexes = performCollectionFiltering(view, model, collectionGroup, modelCollection); 102 103 if (collectionGroup.getDisplayCollectionSize() != -1 && showIndexes.size() > collectionGroup 104 .getDisplayCollectionSize()) { 105 // remove all indexes in showIndexes beyond the collection's size limitation 106 List<Integer> newShowIndexes = new ArrayList<Integer>(); 107 Integer counter = 0; 108 109 for (int index = 0; index < showIndexes.size(); index++) { 110 newShowIndexes.add(showIndexes.get(index)); 111 112 counter++; 113 114 if (counter == collectionGroup.getDisplayCollectionSize()) { 115 break; 116 } 117 } 118 119 showIndexes = newShowIndexes; 120 } 121 122 // dataTables needs to know the number of filtered elements for rendering purposes 123 List<IndexedElement> filteredIndexedElements = buildFilteredIndexedCollection(showIndexes, modelCollection); 124 collectionGroup.setFilteredCollectionSize(filteredIndexedElements.size()); 125 126 buildLinesForDisplayedRows(filteredIndexedElements, view, model, collectionGroup); 127 } 128 129 /** 130 * Build a filtered and indexed version of the model collection based on showIndexes. 131 * 132 * <p>The items in the returned collection contain 133 * <ul> 134 * <li>an <b>index</b> property which refers to the original position within the unfiltered model collection</li> 135 * <li>an <b>element</b> property which is a reference to the element in the model collection</li> 136 * </ul> 137 * </p> 138 * 139 * @param showIndexes A List of indexes to model collection elements that were not filtered out 140 * @param modelCollection the model collection 141 * @return a filtered and indexed version of the model collection 142 * 143 * @see IndexedElement 144 */ 145 private List<IndexedElement> buildFilteredIndexedCollection(List<Integer> showIndexes, 146 List<Object> modelCollection) { 147 // apply the filtering in a way that preserves the original indices for binding path use 148 List<IndexedElement> filteredIndexedElements = new ArrayList<IndexedElement>(modelCollection.size()); 149 150 for (Integer showIndex : showIndexes) { 151 filteredIndexedElements.add(new IndexedElement(showIndex, modelCollection.get(showIndex))); 152 } 153 154 return filteredIndexedElements; 155 } 156 157 /** 158 * Build the lines for the collection rows to be rendered. 159 * 160 * @param filteredIndexedElements a filtered and indexed list of the model collection elements 161 * @param view View instance the collection belongs to 162 * @param model Top level object containing the data 163 * @param collectionGroup CollectionGroup component for the collection 164 */ 165 protected void buildLinesForDisplayedRows(List<IndexedElement> filteredIndexedElements, View view, Object model, 166 CollectionGroup collectionGroup) { 167 168 // if we are doing server paging, but the display length wasn't set (which will be the case on the page render) 169 // then only render one line. Needed to force the table to show up in the page. 170 if (collectionGroup.isUseServerPaging() && collectionGroup.getDisplayLength() == -1) { 171 collectionGroup.setDisplayLength(1); 172 } 173 174 int displayStart = (collectionGroup.getDisplayStart() != -1 && collectionGroup.isUseServerPaging()) ? 175 collectionGroup.getDisplayStart() : 0; 176 177 int displayLength = (collectionGroup.getDisplayLength() != -1 && collectionGroup.isUseServerPaging()) ? 178 collectionGroup.getDisplayLength() : filteredIndexedElements.size() - displayStart; 179 180 // make sure we don't exceed the size of our collection 181 int displayEndExclusive = 182 (displayStart + displayLength > filteredIndexedElements.size()) ? filteredIndexedElements.size() : 183 displayStart + displayLength; 184 185 // get a view of the elements that will be displayed on the page (if paging is enabled) 186 List<IndexedElement> renderedIndexedElements = filteredIndexedElements.subList(displayStart, 187 displayEndExclusive); 188 189 // for each unfiltered collection row to be rendered, build the line fields 190 for (IndexedElement indexedElement : renderedIndexedElements) { 191 Object currentLine = indexedElement.element; 192 193 String bindingPathPrefix = 194 collectionGroup.getBindingInfo().getBindingPrefixForNested() + "[" + indexedElement.index + "]"; 195 196 // initialize the line dialogs, like edit line dialog 197 initializeEditLineDialog(collectionGroup, indexedElement.index, currentLine, model); 198 199 List<Component> actionComponents = new ArrayList<>(ComponentUtils.copy(collectionGroup.getLineActions())); 200 201 // initialize the line actions 202 List<? extends Component> lineActions = initializeLineActions(actionComponents, view, collectionGroup, 203 currentLine, indexedElement.index); 204 205 LineBuilderContext lineBuilderContext = new LineBuilderContext(indexedElement.index, currentLine, 206 bindingPathPrefix, false, (ViewModel) model, collectionGroup, lineActions); 207 208 getCollectionGroupLineBuilder(lineBuilderContext).buildLine(); 209 } 210 } 211 212 /** 213 * Helper method to initialize the edit line dialog and add it to the line dialogs for the group. 214 * 215 * @param collectionGroup the collection group to initialize the line dialogs for 216 * @param lineIndex the current line index 217 * @param currentLine the data object bound to the current line 218 * @param model the view's data 219 */ 220 protected void initializeEditLineDialog(CollectionGroup collectionGroup, int lineIndex, Object currentLine, 221 Object model) { 222 if (!collectionGroup.isEditWithDialog()) { 223 return; 224 } 225 226 String lineSuffix = UifConstants.IdSuffixes.LINE + Integer.toString(lineIndex); 227 228 // use the edit line dialog prototype to initialilze the edit line dialog 229 DialogGroup editLineDialog = ComponentUtils.copy(collectionGroup.getEditLineDialogPrototype()); 230 editLineDialog.setId(ComponentFactory.EDIT_LINE_DIALOG + "_" + collectionGroup.getId() + lineSuffix); 231 editLineDialog.setRetrieveViaAjax(true); 232 233 if (refreshEditLineDialogContents(editLineDialog, model, collectionGroup, lineIndex)) { 234 // if this is an edit line, set up the edit line dialog and add it to the list of dialogs 235 currentLine = ((UifFormBase) model).getDialogDataObject(); 236 setupEditLineDialog(editLineDialog, collectionGroup, lineIndex, lineSuffix, currentLine); 237 } 238 239 // add the edit line dialog to the list of line dialogs for the group 240 if (collectionGroup.getLineDialogs() == null || collectionGroup.getLineDialogs().isEmpty()) { 241 collectionGroup.setLineDialogs((new ArrayList<DialogGroup>())); 242 } 243 collectionGroup.getLineDialogs().add(editLineDialog); 244 } 245 246 /** 247 * Helper method to create and setup the edit line dialog for the indexed line. 248 * 249 * @param editLineDialog the dialog to setup for editing the line 250 * @param group the collection group to create line dialogs for 251 * @param lineIndex the current line index 252 * @param lineSuffix the line suffix to use on dialog component id's 253 * @param currentLine the data object bound to the current line 254 */ 255 protected void setupEditLineDialog(DialogGroup editLineDialog, CollectionGroup group, int lineIndex, 256 String lineSuffix, Object currentLine) { 257 // use the edit line dialog's save action prototype to initialilze the edit line dialog's save action 258 Action editLineInDialogSaveAction = ComponentUtils.copy(group.getEditInDialogSaveActionPrototype()); 259 editLineInDialogSaveAction.setId(editLineDialog.getId() + "_" + 260 ComponentFactory.EDIT_LINE_IN_DIALOG_SAVE_ACTION + Integer.toString(lineIndex)); 261 262 // setup the cancel action for the edit line dialog 263 Action cancelEditLineInDialogAction = (Action) ComponentFactory. 264 getNewComponentInstance(ComponentFactory.DIALOG_DISMISS_ACTION); 265 cancelEditLineInDialogAction.setId(editLineDialog.getId() + "_" + 266 ComponentFactory.DIALOG_DISMISS_ACTION + Integer.toString(lineIndex)); 267 cancelEditLineInDialogAction.setRefreshId(group.getId()); 268 cancelEditLineInDialogAction.setMethodToCall(UifConstants.MethodToCallNames.CLOSE_EDIT_LINE_DIALOG); 269 cancelEditLineInDialogAction.setDialogDismissOption("REQUEST"); 270 271 // add the created save action to the dialog's footer items 272 List<Component> actionComponents = new ArrayList<Component>(); 273 if (editLineDialog.getFooter().getItems() != null) { 274 actionComponents.addAll(editLineDialog.getFooter().getItems()); 275 } 276 277 actionComponents.add(editLineInDialogSaveAction); 278 actionComponents.add(cancelEditLineInDialogAction); 279 editLineDialog.getFooter().setItems(actionComponents); 280 281 // initialize the dialog actions 282 List<Action> actions = ViewLifecycleUtils.getElementsOfTypeDeep(actionComponents, Action.class); 283 group.getCollectionGroupBuilder().initializeActions(actions, group, lineIndex); 284 editLineDialog.getFooter().setItems(actionComponents); 285 286 // set the header actions (for example the close button/icon) to refresh the underlying edit line 287 // collection and resetting the edit line dialog 288 if (editLineDialog.getHeader().getUpperGroup().getItems() != null) { 289 List<Action> headerActions = ViewLifecycleUtils.getElementsOfTypeDeep(editLineDialog.getHeader(). 290 getUpperGroup().getItems(), Action.class); 291 initializeActions(headerActions, group, lineIndex); 292 for (Action headerAction : headerActions) { 293 headerAction.setRefreshId(group.getId()); 294 headerAction.setMethodToCall(UifConstants.MethodToCallNames.CLOSE_EDIT_LINE_DIALOG); 295 headerAction.setDialogDismissOption("REQUEST"); 296 headerAction.setActionScript(null); 297 } 298 } 299 300 // update the context of the dialog for the current line 301 ContextUtils.updateContextForLine(editLineDialog, group, currentLine, lineIndex, lineSuffix); 302 } 303 304 /** 305 * Performs any filtering necessary on the collection before building the collection fields. 306 * 307 * <p>If showInactive is set to false and the collection line type implements {@code Inactivatable}, 308 * invokes the active collection filter. Then any {@link CollectionFilter} instances configured for the collection 309 * group are invoked to filter the collection. Collections lines must pass all filters in order to be 310 * displayed</p> 311 * 312 * @param view view instance that contains the collection 313 * @param model object containing the views data 314 * @param collectionGroup collection group component instance that will display the collection 315 * @param collection collection instance that will be filtered 316 */ 317 protected List<Integer> performCollectionFiltering(View view, Object model, CollectionGroup collectionGroup, 318 Collection<?> collection) { 319 List<Integer> filteredIndexes = new ArrayList<Integer>(); 320 for (int i = 0; i < collection.size(); i++) { 321 filteredIndexes.add(Integer.valueOf(i)); 322 } 323 324 if (Inactivatable.class.isAssignableFrom(collectionGroup.getCollectionObjectClass()) && !collectionGroup 325 .isShowInactiveLines()) { 326 List<Integer> activeIndexes = collectionGroup.getActiveCollectionFilter().filter(view, model, 327 collectionGroup); 328 filteredIndexes = ListUtils.intersection(filteredIndexes, activeIndexes); 329 } 330 331 for (CollectionFilter collectionFilter : collectionGroup.getFilters()) { 332 List<Integer> indexes = collectionFilter.filter(view, model, collectionGroup); 333 filteredIndexes = ListUtils.intersection(filteredIndexes, indexes); 334 if (filteredIndexes.isEmpty()) { 335 break; 336 } 337 } 338 339 return filteredIndexes; 340 } 341 342 /** 343 * Builds the fields for holding the collection add line and if necessary makes call to setup 344 * the new line instance. 345 * 346 * @param view view instance the collection belongs to 347 * @param collectionGroup collection group the layout manager applies to 348 * @param model Object containing the view data, should extend UifFormBase 349 * if using framework managed new lines 350 */ 351 protected void buildAddLine(View view, Object model, CollectionGroup collectionGroup) { 352 // initialize new line if one does not already exist 353 initializeNewCollectionLine(view, model, collectionGroup, false); 354 355 String addLineBindingPath = collectionGroup.getAddLineBindingInfo().getBindingPath(); 356 List<? extends Component> actionComponents = getAddLineActionComponents(view, model, collectionGroup); 357 358 Object addLine = ObjectPropertyUtils.getPropertyValue(model, addLineBindingPath); 359 360 boolean bindToForm = false; 361 if (StringUtils.isBlank(collectionGroup.getAddLinePropertyName())) { 362 bindToForm = true; 363 } 364 365 LineBuilderContext lineBuilderContext = new LineBuilderContext(-1, addLine, addLineBindingPath, bindToForm, 366 (ViewModel) model, collectionGroup, actionComponents); 367 368 getCollectionGroupLineBuilder(lineBuilderContext).buildLine(); 369 } 370 371 /** 372 * Creates new {@code Action} instances for the line. 373 * 374 * <p>Adds context to the action fields for the given line so that the line the action was performed on can be 375 * determined when that action is selected</p> 376 * 377 * @param lineActions the actions to copy 378 * @param view view instance the collection belongs to 379 * @param collectionGroup collection group component for the collection 380 * @param collectionLine object instance for the current line 381 * @param lineIndex index of the line the actions should apply to 382 */ 383 protected List<? extends Component> initializeLineActions(List<? extends Component> lineActions, View view, 384 CollectionGroup collectionGroup, Object collectionLine, int lineIndex) { 385 List<Component> actionComponents = new ArrayList<Component>(ComponentUtils.copy(lineActions)); 386 387 // if it is edit with dialog, then add the edit line action to the group's line actions 388 if (collectionGroup.isEditWithDialog()) { 389 Action editLineActionForDialog = setupEditLineActionForDialog(collectionGroup, 390 UifConstants.IdSuffixes.LINE + Integer.toString(lineIndex), lineIndex, 391 actionComponents.size()); 392 actionComponents.add(editLineActionForDialog); 393 } 394 395 for (Component actionComponent : actionComponents) { 396 view.getViewHelperService().setElementContext(actionComponent, collectionGroup); 397 } 398 399 String lineSuffix = UifConstants.IdSuffixes.LINE + Integer.toString(lineIndex); 400 ContextUtils.updateContextsForLine(actionComponents, collectionGroup, collectionLine, lineIndex, lineSuffix); 401 402 ExpressionEvaluator expressionEvaluator = ViewLifecycle.getExpressionEvaluator(); 403 for (Component actionComponent : actionComponents) { 404 expressionEvaluator.evaluatePropertyExpression(view, actionComponent.getContext(), actionComponent, 405 UifPropertyPaths.ID, true); 406 } 407 408 ComponentUtils.updateIdsWithSuffixNested(actionComponents, lineSuffix); 409 410 List<Action> actions = ViewLifecycleUtils.getElementsOfTypeDeep(actionComponents, Action.class); 411 initializeActions(actions, collectionGroup, lineIndex); 412 413 return actionComponents; 414 } 415 416 /** 417 * Helper method to setup the edit line action to show the dialog 418 * 419 * @param collectionGroup the collection group the line belongs to 420 * @param lineSuffix the line index of the current line 421 * @param lineIndex the current line index 422 * @param actionIndex the action index used in the id 423 * @return the line action for edit line in dialog 424 */ 425 protected Action setupEditLineActionForDialog(CollectionGroup collectionGroup, String lineSuffix, int lineIndex, 426 int actionIndex) { 427 428 Action action = ComponentUtils.copy(collectionGroup.getEditWithDialogActionPrototype()); 429 430 action.setId(ComponentFactory.EDIT_LINE_IN_DIALOG_ACTION + "_" + collectionGroup.getId() + 431 lineSuffix + UifConstants.IdSuffixes.ACTION + actionIndex); 432 433 String actionScript = UifConstants.JsFunctions.SHOW_EDIT_LINE_DIALOG + "('" + 434 ComponentFactory.EDIT_LINE_DIALOG + "_" + collectionGroup.getId() + lineSuffix + "', '" + 435 collectionGroup.getBindingInfo().getBindingName() + "', " + lineIndex + ");"; 436 action.setActionScript(actionScript); 437 438 return action; 439 } 440 441 /** 442 * Updates the action parameters, jump to, refresh id, and validation configuration for the list of actions 443 * associated with the given collection group and line index. 444 * 445 * @param actions list of action components to update 446 * @param collectionGroup collection group instance the actions belong to 447 * @param lineIndex index of the line the actions are associate with 448 */ 449 public void initializeActions(List<Action> actions, CollectionGroup collectionGroup, int lineIndex) { 450 for (Action action : actions) { 451 if (ComponentUtils.containsPropertyExpression(action, UifPropertyPaths.ACTION_PARAMETERS, true)) { 452 // need to update the actions expressions so our settings do not get overridden 453 action.getPropertyExpressions().put( 454 UifPropertyPaths.ACTION_PARAMETERS + "['" + UifParameters.SELECTED_COLLECTION_PATH + "']", 455 UifConstants.EL_PLACEHOLDER_PREFIX + "'" + collectionGroup.getBindingInfo().getBindingPath() + 456 "'" + UifConstants.EL_PLACEHOLDER_SUFFIX 457 ); 458 action.getPropertyExpressions().put( 459 UifPropertyPaths.ACTION_PARAMETERS + "['" + UifParameters.SELECTED_COLLECTION_ID + "']", 460 UifConstants.EL_PLACEHOLDER_PREFIX + "'" + collectionGroup.getId() + 461 "'" + UifConstants.EL_PLACEHOLDER_SUFFIX 462 ); 463 action.getPropertyExpressions().put( 464 UifPropertyPaths.ACTION_PARAMETERS + "['" + UifParameters.SELECTED_LINE_INDEX + "']", 465 UifConstants.EL_PLACEHOLDER_PREFIX + "'" + Integer.toString(lineIndex) + 466 "'" + UifConstants.EL_PLACEHOLDER_SUFFIX 467 ); 468 action.getPropertyExpressions().put( 469 UifPropertyPaths.ACTION_PARAMETERS + "['" + UifParameters.LINE_INDEX + "']", 470 UifConstants.EL_PLACEHOLDER_PREFIX + "'" + Integer.toString(lineIndex) + 471 "'" + UifConstants.EL_PLACEHOLDER_SUFFIX 472 ); 473 } else { 474 action.addActionParameter(UifParameters.SELECTED_COLLECTION_PATH, 475 collectionGroup.getBindingInfo().getBindingPath()); 476 action.addActionParameter(UifParameters.SELECTED_COLLECTION_ID, collectionGroup.getId()); 477 action.addActionParameter(UifParameters.SELECTED_LINE_INDEX, Integer.toString(lineIndex)); 478 action.addActionParameter(UifParameters.LINE_INDEX, Integer.toString(lineIndex)); 479 } 480 481 if (StringUtils.isBlank(action.getRefreshId()) && StringUtils.isBlank(action.getRefreshPropertyName())) { 482 action.setRefreshId(collectionGroup.getId()); 483 } 484 485 // if marked for validation, add call to validate the line and set validation flag to false 486 // so the entire form will not be validated 487 if (action.isPerformClientSideValidation()) { 488 String preSubmitScript = "var valid=" + UifConstants.JsFunctions.VALIDATE_LINE + "('" + 489 collectionGroup.getBindingInfo().getBindingPath() + "'," + Integer.toString(lineIndex) + 490 ");"; 491 492 // prepend custom presubmit script which should evaluate to a boolean 493 if (StringUtils.isNotBlank(action.getPreSubmitCall())) { 494 preSubmitScript = ScriptUtils.appendScript(preSubmitScript, 495 "if (valid){valid=function(){" + action.getPreSubmitCall() + "}();}"); 496 } 497 498 preSubmitScript += " return valid;"; 499 500 action.setPreSubmitCall(preSubmitScript); 501 action.setPerformClientSideValidation(false); 502 } 503 } 504 } 505 506 /** 507 * Creates new {@code Component} instances for the add line 508 * 509 * <p> 510 * Adds context to the action fields for the add line so that the collection 511 * the action was performed on can be determined 512 * </p> 513 * 514 * @param view view instance the collection belongs to 515 * @param model top level object containing the data 516 * @param collectionGroup collection group component for the collection 517 */ 518 protected List<? extends Component> getAddLineActionComponents(View view, Object model, 519 CollectionGroup collectionGroup) { 520 String lineSuffix = UifConstants.IdSuffixes.ADD_LINE; 521 522 List<? extends Component> lineActionComponents = ComponentUtils.copyComponentList( 523 collectionGroup.getAddLineActions(), lineSuffix); 524 525 List<Action> actions = ViewLifecycleUtils.getElementsOfTypeDeep(lineActionComponents, Action.class); 526 527 if (collectionGroup.isAddWithDialog() && (collectionGroup.getAddLineDialog().getFooter() != null) && 528 !collectionGroup.getAddLineDialog().getFooter().getItems().isEmpty()) { 529 List<Action> addLineDialogActions = ViewLifecycleUtils.getElementsOfTypeDeep( 530 collectionGroup.getAddLineDialog().getFooter().getItems(), Action.class); 531 532 if (addLineDialogActions != null) { 533 actions.addAll(addLineDialogActions); 534 } 535 } 536 537 for (Action action : actions) { 538 action.addActionParameter(UifParameters.SELECTED_COLLECTION_PATH, 539 collectionGroup.getBindingInfo().getBindingPath()); 540 action.addActionParameter(UifParameters.SELECTED_COLLECTION_ID, collectionGroup.getId()); 541 action.setJumpToIdAfterSubmit(collectionGroup.getId()); 542 action.addActionParameter(UifParameters.ACTION_TYPE, UifParameters.ADD_LINE); 543 544 boolean isPageUpdateAction = StringUtils.isNotBlank(action.getAjaxReturnType()) 545 && action.getAjaxReturnType().equals(UifConstants.AjaxReturnTypes.UPDATEPAGE.getKey()); 546 547 if (StringUtils.isBlank(action.getRefreshId()) && !isPageUpdateAction) { 548 action.setRefreshId(collectionGroup.getId()); 549 } 550 551 if (collectionGroup.isAddWithDialog() && view instanceof FormView && ((FormView) view) 552 .isValidateClientSide()) { 553 action.setPerformClientSideValidation(true); 554 } 555 556 if (action.isPerformClientSideValidation()) { 557 String preSubmitScript = "var valid=" + UifConstants.JsFunctions.VALIDATE_ADD_LINE + "('" + 558 collectionGroup.getId() + "');"; 559 560 // prepend custom presubmit script which should evaluate to a boolean 561 if (StringUtils.isNotBlank(action.getPreSubmitCall())) { 562 preSubmitScript = ScriptUtils.appendScript(preSubmitScript, 563 "if (valid){valid=function(){" + action.getPreSubmitCall() + "}();}"); 564 } 565 566 preSubmitScript += "return valid;"; 567 568 action.setPreSubmitCall(preSubmitScript); 569 action.setPerformClientSideValidation(false); 570 } else if (collectionGroup.isAddWithDialog()) { 571 action.setPreSubmitCall("closeLightbox(); return true;"); 572 } 573 } 574 575 // get add line for context 576 String addLinePath = collectionGroup.getAddLineBindingInfo().getBindingPath(); 577 Object addLine = ObjectPropertyUtils.getPropertyValue(model, addLinePath); 578 579 ContextUtils.updateContextForLine(collectionGroup.getAddLineDialog(), collectionGroup, addLine, -1, lineSuffix); 580 ContextUtils.updateContextsForLine(actions, collectionGroup, addLine, -1, lineSuffix); 581 582 return lineActionComponents; 583 } 584 585 /** 586 * Initializes a new instance of the collection data object class for the add line. 587 * 588 * <p>If the add line property was not specified for the collection group the new lines will be 589 * added to the generic map on the {@code UifFormBase}, else it will be added to the property given by 590 * the addLineBindingInfo</p> 591 * 592 * <p>New line will only be created if the current line property is null or clearExistingLine is true. 593 * In the case of a new line default values are also applied</p> 594 */ 595 public void initializeNewCollectionLine(View view, Object model, CollectionGroup collectionGroup, 596 boolean clearExistingLine) { 597 Object newLine = null; 598 599 // determine if we are binding to generic form map or a custom property 600 if (StringUtils.isBlank(collectionGroup.getAddLinePropertyName())) { 601 // bind to form map 602 if (!(model instanceof UifFormBase)) { 603 throw new RuntimeException( 604 "Cannot create new collection line for group: " + collectionGroup.getPropertyName() 605 + ". Model does not extend " + UifFormBase.class.getName() 606 ); 607 } 608 609 // get new collection line map from form 610 Map<String, Object> newCollectionLines = ObjectPropertyUtils.getPropertyValue(model, 611 UifPropertyPaths.NEW_COLLECTION_LINES); 612 if (newCollectionLines == null) { 613 newCollectionLines = new HashMap<String, Object>(); 614 ObjectPropertyUtils.setPropertyValue(model, UifPropertyPaths.NEW_COLLECTION_LINES, newCollectionLines); 615 } 616 617 // set binding path for add line 618 String newCollectionLineKey = KRADUtils.translateToMapSafeKey( 619 collectionGroup.getBindingInfo().getBindingPath()); 620 String addLineBindingPath = UifPropertyPaths.NEW_COLLECTION_LINES + "['" + newCollectionLineKey + "']"; 621 collectionGroup.getAddLineBindingInfo().setBindingPath(addLineBindingPath); 622 623 // if there is not an instance available or we need to clear create a new instance 624 if (!newCollectionLines.containsKey(newCollectionLineKey) || (newCollectionLines.get(newCollectionLineKey) 625 == null) || clearExistingLine) { 626 // create new instance of the collection type for the add line 627 newLine = KRADUtils.createNewObjectFromClass(collectionGroup.getCollectionObjectClass()); 628 newCollectionLines.put(newCollectionLineKey, newLine); 629 } 630 } else { 631 // bind to custom property 632 Object addLine = ObjectPropertyUtils.getPropertyValue(model, 633 collectionGroup.getAddLineBindingInfo().getBindingPath()); 634 if ((addLine == null) || clearExistingLine) { 635 newLine = KRADUtils.createNewObjectFromClass(collectionGroup.getCollectionObjectClass()); 636 ObjectPropertyUtils.setPropertyValue(model, collectionGroup.getAddLineBindingInfo().getBindingPath(), 637 newLine); 638 } 639 } 640 641 // apply default values if a new line was created 642 if (newLine != null) { 643 ViewLifecycle.getHelper().applyDefaultValuesForCollectionLine(collectionGroup, newLine); 644 } 645 } 646 647 /** 648 * Helper method that checks if this is a refresh lifecycle and if the component to be refreshed is the 649 * dialog group, and if the action parameters bind to the same object as the collection's current line, and 650 * if they are then it returns true. 651 * 652 * @param dialogGroup the dialog group to check for 653 * @param model the form data 654 * @param collectionGroup the collection group the line belongs to 655 * @param lineIndex the current line index 656 * @return 657 */ 658 public boolean refreshEditLineDialogContents(DialogGroup dialogGroup, Object model, CollectionGroup collectionGroup, 659 int lineIndex) { 660 UifFormBase formBase = (UifFormBase) model; 661 String selectedCollectionPath = formBase.getActionParamaterValue(UifParameters.SELECTED_COLLECTION_PATH); 662 String selectedLineIndex = formBase.getActionParamaterValue(UifParameters.SELECTED_LINE_INDEX); 663 664 if (ViewLifecycle.isRefreshLifecycle() 665 && StringUtils.equals(dialogGroup.getId(), ViewLifecycle.getRefreshComponentId()) 666 && (StringUtils.equals(selectedCollectionPath, collectionGroup.getBindingInfo().getBindingPath()) 667 || StringUtils.startsWith(selectedCollectionPath, UifPropertyPaths.DIALOG_DATA_OBJECT)) 668 && StringUtils.equals(selectedLineIndex, Integer.toString(lineIndex))) { 669 return true; 670 } 671 return false; 672 } 673 674 /** 675 * Returns an instance of {@link CollectionGroupLineBuilder} for building the line. 676 * 677 * @param lineBuilderContext context of line for initializing line builder 678 * @return CollectionGroupLineBuilder instance 679 */ 680 public CollectionGroupLineBuilder getCollectionGroupLineBuilder(LineBuilderContext lineBuilderContext) { 681 return new CollectionGroupLineBuilder(lineBuilderContext); 682 } 683 684 /** 685 * Wrapper object to enable filtering of a collection while preserving original indices 686 */ 687 private static class IndexedElement { 688 689 /** 690 * The index associated with the given element 691 */ 692 final int index; 693 694 /** 695 * The element itself 696 */ 697 final Object element; 698 699 /** 700 * Constructs an {@link org.kuali.rice.krad.uif.container.CollectionGroupBuilder.IndexedElement} 701 * 702 * @param index the index to associate with the element 703 * @param element the element itself 704 */ 705 private IndexedElement(int index, Object element) { 706 this.index = index; 707 this.element = element; 708 } 709 } 710}