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}