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.krad.uif.container;
017    
018    import org.apache.commons.collections.ListUtils;
019    import org.apache.commons.lang.StringUtils;
020    import org.apache.commons.logging.Log;
021    import org.apache.commons.logging.LogFactory;
022    import org.kuali.rice.core.api.mo.common.active.Inactivatable;
023    import org.kuali.rice.kim.api.identity.Person;
024    import org.kuali.rice.krad.uif.UifConstants;
025    import org.kuali.rice.krad.uif.UifParameters;
026    import org.kuali.rice.krad.uif.UifPropertyPaths;
027    import org.kuali.rice.krad.uif.component.Component;
028    import org.kuali.rice.krad.uif.component.ComponentSecurity;
029    import org.kuali.rice.krad.uif.component.DataBinding;
030    import org.kuali.rice.krad.uif.control.Control;
031    import org.kuali.rice.krad.uif.control.ControlBase;
032    import org.kuali.rice.krad.uif.element.Action;
033    import org.kuali.rice.krad.uif.field.Field;
034    import org.kuali.rice.krad.uif.field.FieldGroup;
035    import org.kuali.rice.krad.uif.field.InputField;
036    import org.kuali.rice.krad.uif.field.RemoteFieldsHolder;
037    import org.kuali.rice.krad.uif.layout.CollectionLayoutManager;
038    import org.kuali.rice.krad.uif.util.ComponentUtils;
039    import org.kuali.rice.krad.uif.util.ObjectPropertyUtils;
040    import org.kuali.rice.krad.uif.util.ScriptUtils;
041    import org.kuali.rice.krad.uif.view.ExpressionEvaluator;
042    import org.kuali.rice.krad.uif.view.View;
043    import org.kuali.rice.krad.uif.view.ViewAuthorizer;
044    import org.kuali.rice.krad.uif.view.ViewModel;
045    import org.kuali.rice.krad.uif.view.ViewPresentationController;
046    import org.kuali.rice.krad.util.GlobalVariables;
047    import org.kuali.rice.krad.util.KRADUtils;
048    import org.kuali.rice.krad.util.ObjectUtils;
049    import org.kuali.rice.krad.web.form.UifFormBase;
050    
051    import java.io.Serializable;
052    import java.util.ArrayList;
053    import java.util.Collection;
054    import java.util.HashMap;
055    import java.util.List;
056    import java.util.Map;
057    
058    /**
059     * Builds out the {@code Field} instances for a collection group with a
060     * series of steps that interact with the configured
061     * {@code CollectionLayoutManager} to assemble the fields as necessary for
062     * the layout
063     *
064     * @author Kuali Rice Team (rice.collab@kuali.org)
065     */
066    public class CollectionGroupBuilder implements Serializable {
067        private static final long serialVersionUID = -4762031957079895244L;
068        private static Log LOG = LogFactory.getLog(CollectionGroupBuilder.class);
069    
070        /**
071         * Creates the {@code Field} instances that make up the table
072         *
073         * <p>
074         * The corresponding collection is retrieved from the model and iterated
075         * over to create the necessary fields. The binding path for fields that
076         * implement {@code DataBinding} is adjusted to point to the collection
077         * line it is apart of. For example, field 'number' of collection 'accounts'
078         * for line 1 will be set to 'accounts[0].number', and for line 2
079         * 'accounts[1].number'. Finally parameters are set on the line's action
080         * fields to indicate what collection and line they apply to.
081         * </p>
082         *
083         * <p>Only the lines that are to be rendered (as specified by the displayStart
084         * and displayLength properties of the CollectionGroup) will be built.</p>
085         *
086         * @param view View instance the collection belongs to
087         * @param model Top level object containing the data
088         * @param collectionGroup CollectionGroup component for the collection
089         */
090        public void build(View view, Object model, CollectionGroup collectionGroup) {
091            // create add line
092            if (collectionGroup.isRenderAddLine() && !collectionGroup.isReadOnly() &&
093                    !collectionGroup.isRenderAddBlankLineButton()) {
094                buildAddLine(view, model, collectionGroup);
095            }
096    
097            // if add line button enabled setup to refresh the collection group
098            if (collectionGroup.isRenderAddBlankLineButton() && (collectionGroup.getAddBlankLineAction() != null)) {
099                collectionGroup.getAddBlankLineAction().setRefreshId(collectionGroup.getId());
100            }
101    
102            // get the collection for this group from the model
103            List<Object> modelCollection = ObjectPropertyUtils.getPropertyValue(model,
104                    ((DataBinding) collectionGroup).getBindingInfo().getBindingPath());
105    
106            if (modelCollection != null) {
107                // filter inactive model
108                List<Integer> showIndexes = performCollectionFiltering(view, model, collectionGroup, modelCollection);
109    
110                if (collectionGroup.getDisplayCollectionSize() != -1 && showIndexes.size() > collectionGroup
111                        .getDisplayCollectionSize()) {
112                    // remove all indexes in showIndexes beyond the collection's size limitation
113                    List<Integer> newShowIndexes = new ArrayList<Integer>();
114                    Integer counter = 0;
115    
116                    for (int index = 0; index < showIndexes.size(); index++) {
117                        newShowIndexes.add(showIndexes.get(index));
118    
119                        counter++;
120    
121                        if (counter == collectionGroup.getDisplayCollectionSize()) {
122                            break;
123                        }
124                    }
125    
126                    showIndexes = newShowIndexes;
127                }
128    
129                List<IndexedElement> filteredIndexedElements = buildFilteredIndexedCollection(showIndexes, modelCollection);
130                // DataTables needs to know the number of filtered elements for rendering purposes
131                collectionGroup.setFilteredCollectionSize(filteredIndexedElements.size());
132                buildLinesForDisplayedRows(filteredIndexedElements, view, model, collectionGroup);
133            }
134        }
135    
136        /**
137         * Build a filtered and indexed version of the model collection based on showIndexes.
138         *
139         * <p>The items in the returned collection contain
140         * <ul>
141         * <li>an <b>index</b> property which refers to the original position within the unfiltered model collection</li>
142         * <li>an <b>element</b> property which is a reference to the element in the model collection</li>
143         * </ul>
144         * </p>
145         *
146         * @param showIndexes A List of indexes to model collection elements that were not filtered out
147         * @param modelCollection the model collection
148         * @return a filtered and indexed version of the model collection
149         * @see IndexedElement
150         */
151        private List<IndexedElement> buildFilteredIndexedCollection(List<Integer> showIndexes,
152                List<Object> modelCollection) {// apply the filtering in a way that preserves the original indices for binding path use
153            List<IndexedElement> filteredIndexedElements = new ArrayList<IndexedElement>(modelCollection.size());
154            for (Integer showIndex : showIndexes) {
155                filteredIndexedElements.add(new IndexedElement(showIndex, modelCollection.get(showIndex)));
156            }
157            return filteredIndexedElements;
158        }
159    
160        /**
161         * Build the lines for the collection rows to be rendered.
162         *
163         * @param filteredIndexedElements a filtered and indexed list of the model collection elements
164         * @param view View instance the collection belongs to
165         * @param model Top level object containing the data
166         * @param collectionGroup CollectionGroup component for the collection
167         */
168        protected void buildLinesForDisplayedRows(List<IndexedElement> filteredIndexedElements, View view, Object model,
169                CollectionGroup collectionGroup) {
170    
171            // if we are doing server paging, but the display length wasn't set (which will be the case on the page render)
172            // then only render one line.  Needed to force the table to show up in the page.
173            if (collectionGroup.isUseServerPaging() && collectionGroup.getDisplayLength() == -1) {
174                collectionGroup.setDisplayLength(1);
175            }
176    
177            final int displayStart = (collectionGroup.getDisplayStart() != -1 && collectionGroup.isUseServerPaging()) ?
178                    collectionGroup.getDisplayStart() : 0;
179    
180            final int displayLength = (collectionGroup.getDisplayLength() != -1 && collectionGroup.isUseServerPaging()) ?
181                    collectionGroup.getDisplayLength() : filteredIndexedElements.size() - displayStart;
182    
183            // make sure we don't exceed the size of our collection
184            final int displayEndExclusive =
185                    (displayStart + displayLength > filteredIndexedElements.size()) ? filteredIndexedElements.size() :
186                            displayStart + displayLength;
187    
188            // get a view of the elements that will be displayed on the page (if paging is enabled)
189            final List<IndexedElement> renderedIndexedElements = filteredIndexedElements.subList(displayStart,
190                    displayEndExclusive);
191    
192            // for each unfiltered collection row to be rendered, build the line fields
193            for (IndexedElement indexedElement : renderedIndexedElements) {
194                final Object currentLine = indexedElement.element;
195    
196                String bindingPathPrefix =
197                        collectionGroup.getBindingInfo().getBindingName() + "[" + indexedElement.index + "]";
198    
199                if (StringUtils.isNotBlank(collectionGroup.getBindingInfo().getBindByNamePrefix())) {
200                    bindingPathPrefix = collectionGroup.getBindingInfo().getBindByNamePrefix() + "." + bindingPathPrefix;
201                }
202    
203                List<Action> lineActions = initializeLineActions(collectionGroup.getLineActions(), view, model,
204                        collectionGroup, currentLine, indexedElement.index);
205    
206                buildLine(view, model, collectionGroup, bindingPathPrefix, lineActions, false, currentLine,
207                        indexedElement.index);
208            }
209        }
210    
211        /**
212         * Performs any filtering necessary on the collection before building the collection fields
213         *
214         * <p>
215         * If showInactive is set to false and the collection line type implements {@code Inactivatable},
216         * invokes the active collection filter. Then any {@link CollectionFilter} instances configured for the collection
217         * group are invoked to filter the collection. Collections lines must pass all filters in order to be
218         * displayed
219         * </p>
220         *
221         * @param view view instance that contains the collection
222         * @param model object containing the views data
223         * @param collectionGroup collection group component instance that will display the collection
224         * @param collection collection instance that will be filtered
225         */
226        protected List<Integer> performCollectionFiltering(View view, Object model, CollectionGroup collectionGroup,
227                Collection<?> collection) {
228            List<Integer> filteredIndexes = new ArrayList<Integer>();
229            for (int i = 0; i < collection.size(); i++) {
230                filteredIndexes.add(Integer.valueOf(i));
231            }
232    
233            if (Inactivatable.class.isAssignableFrom(collectionGroup.getCollectionObjectClass()) && !collectionGroup
234                    .isShowInactiveLines()) {
235                List<Integer> activeIndexes = collectionGroup.getActiveCollectionFilter().filter(view, model,
236                        collectionGroup);
237                filteredIndexes = ListUtils.intersection(filteredIndexes, activeIndexes);
238            }
239    
240            for (CollectionFilter collectionFilter : collectionGroup.getFilters()) {
241                List<Integer> indexes = collectionFilter.filter(view, model, collectionGroup);
242                filteredIndexes = ListUtils.intersection(filteredIndexes, indexes);
243                if (filteredIndexes.isEmpty()) {
244                    break;
245                }
246            }
247    
248            return filteredIndexes;
249        }
250    
251        /**
252         * Builds the fields for holding the collection add line and if necessary
253         * makes call to setup the new line instance
254         *
255         * @param view view instance the collection belongs to
256         * @param collectionGroup collection group the layout manager applies to
257         * @param model Object containing the view data, should extend UifFormBase
258         * if using framework managed new lines
259         */
260        protected void buildAddLine(View view, Object model, CollectionGroup collectionGroup) {
261            boolean addLineBindsToForm = false;
262    
263            // initialize new line if one does not already exist
264            initializeNewCollectionLine(view, model, collectionGroup, false);
265    
266            // determine whether the add line binds to the generic form map or a
267            // specified property
268            if (StringUtils.isBlank(collectionGroup.getAddLinePropertyName())) {
269                addLineBindsToForm = true;
270            }
271    
272            String addLineBindingPath = collectionGroup.getAddLineBindingInfo().getBindingPath();
273            List<Action> actions = getAddLineActions(view, model, collectionGroup);
274    
275            Object addLine = ObjectPropertyUtils.getPropertyValue(model, addLineBindingPath);
276            buildLine(view, model, collectionGroup, addLineBindingPath, actions, addLineBindsToForm, addLine, -1);
277        }
278    
279        /**
280         * Builds the field instances for the collection line. A copy of the
281         * configured items on the {@code CollectionGroup} is made and adjusted
282         * for the line (id and binding). Then a call is made to the
283         * {@code CollectionLayoutManager} to assemble the line as necessary
284         * for the layout
285         *
286         * @param view view instance the collection belongs to
287         * @param model top level object containing the data
288         * @param collectionGroup collection group component for the collection
289         * @param bindingPath binding path for the line fields (if DataBinding)
290         * @param actions List of actions to set in the lines action column
291         * @param bindToForm whether the bindToForm property on the items bindingInfo
292         * should be set to true (needed for add line)
293         * @param currentLine object instance for the current line, or null if add line
294         * @param lineIndex index of the line in the collection, or -1 if we are
295         * building the add line
296         */
297        @SuppressWarnings("unchecked")
298        protected void buildLine(View view, Object model, CollectionGroup collectionGroup, String bindingPath,
299                List<Action> actions, boolean bindToForm, Object currentLine, int lineIndex) {
300            CollectionLayoutManager layoutManager = (CollectionLayoutManager) collectionGroup.getLayoutManager();
301    
302            // copy group items for new line
303            List<? extends Component> lineItems = null;
304            String lineSuffix = null;
305            if (lineIndex == -1) {
306                lineItems = ComponentUtils.copyComponentList(collectionGroup.getAddLineItems(), null);
307                lineSuffix = UifConstants.IdSuffixes.ADD_LINE;
308            } else {
309                lineItems = ComponentUtils.copyComponentList(collectionGroup.getItems(), null);
310                lineSuffix = UifConstants.IdSuffixes.LINE + Integer.toString(lineIndex);
311            }
312    
313            if (StringUtils.isNotBlank(collectionGroup.getSubCollectionSuffix())) {
314                lineSuffix = collectionGroup.getSubCollectionSuffix() + lineSuffix;
315            }
316    
317            // check for remote fields holder
318            List<Field> lineFields = processAnyRemoteFieldsHolder(view, model, collectionGroup, lineItems);
319    
320            // adjust binding path and id to match collection line path
321            ComponentUtils.bindAndIdFieldList(lineFields, bindingPath, lineSuffix);
322    
323            // If the fields contain any collections themselves (details case) adjust their binding path
324            // TODO: does the copyFieldList method above not take care of this?
325            for (Field field : lineFields) {
326                List<CollectionGroup> components = ComponentUtils.getComponentsOfTypeDeep(field, CollectionGroup.class);
327                for (CollectionGroup fieldCollectionGroup : components) {
328                    ComponentUtils.prefixBindingPath(fieldCollectionGroup, bindingPath);
329                    fieldCollectionGroup.setSubCollectionSuffix(lineSuffix);
330                }
331    
332                // OR LightTables in details...
333                List<LightTable> lightTables = ComponentUtils.getComponentsOfTypeDeep(field, LightTable.class);
334                for (LightTable lightTable : lightTables) {
335                    ComponentUtils.prefixBindingPath(lightTable, bindingPath);
336                }
337            }
338    
339            boolean readOnlyLine = collectionGroup.isReadOnly();
340    
341            // update contexts before add line fields are added to the index below
342            ComponentUtils.updateContextsForLine(lineFields, currentLine, lineIndex, lineSuffix);
343    
344            for (Action action : actions) {
345                if (action != null && StringUtils.isNotBlank(action.getFocusOnIdAfterSubmit()) &&
346                        action.getFocusOnIdAfterSubmit().equalsIgnoreCase(UifConstants.Order.LINE_FIRST.toString()) && (
347                        lineFields.size()
348                                > 0)) {
349                    action.setFocusOnIdAfterSubmit(lineFields.get(0).getId() + UifConstants.IdSuffixes.CONTROL);
350                }
351            }
352    
353            // add special css styles to identify the add line client side
354            if (lineIndex == -1) {
355                // do nothing
356            } else {
357                // for existing lines, check view line auth
358                boolean canViewLine = checkViewLineAuthorizationAndPresentationLogic(view, (ViewModel) model,
359                        collectionGroup, currentLine);
360    
361                // if line is not viewable, just return without calling the layout manager to add the line
362                if (!canViewLine) {
363                    return;
364                }
365    
366                // check edit line authorization if collection is not read only
367                if (!collectionGroup.isReadOnly()) {
368                    readOnlyLine = !checkEditLineAuthorizationAndPresentationLogic(view, (ViewModel) model, collectionGroup,
369                            currentLine);
370    
371                    // Add script to fields to activate save button on any change
372                    if (!readOnlyLine && !((UifFormBase) model).isAddedCollectionItem(currentLine) &&
373                            collectionGroup.isRenderSaveLineActions()) {
374                        for (Field f : lineFields) {
375                            if (f instanceof InputField && f.isRender()) {
376                                ControlBase control = (ControlBase) ((InputField) f).getControl();
377                                control.setOnChangeScript(control.getOnChangeScript() == null ?
378                                        ";collectionLineChanged(this, 'uif-newCollectionItem');" :
379                                        control.getOnChangeScript()
380                                                + ";collectionLineChanged(this, 'uif-newCollectionItem');");
381                            }
382                        }
383                    }
384                }
385    
386                ComponentUtils.pushObjectToContext(lineFields, UifConstants.ContextVariableNames.READONLY_LINE,
387                        readOnlyLine);
388                ComponentUtils.pushObjectToContext(actions, UifConstants.ContextVariableNames.READONLY_LINE, readOnlyLine);
389            }
390    
391            // check authorization for line fields
392            applyLineFieldAuthorizationAndPresentationLogic(view, (ViewModel) model, collectionGroup, currentLine,
393                    readOnlyLine, lineFields, actions);
394    
395            if (bindToForm) {
396                ComponentUtils.setComponentsPropertyDeep(lineFields, UifPropertyPaths.BIND_TO_FORM, Boolean.valueOf(true));
397            }
398    
399            // remove fields from the line that have render false
400            lineFields = removeNonRenderLineFields(view, model, collectionGroup, lineFields, currentLine, lineIndex);
401    
402            // if not add line build sub-collection field groups
403            List<FieldGroup> subCollectionFields = new ArrayList<FieldGroup>();
404            if ((lineIndex != -1) && (collectionGroup.getSubCollections() != null)) {
405                for (int subLineIndex = 0; subLineIndex < collectionGroup.getSubCollections().size(); subLineIndex++) {
406                    CollectionGroup subCollectionPrototype = collectionGroup.getSubCollections().get(subLineIndex);
407                    CollectionGroup subCollectionGroup = ComponentUtils.copy(subCollectionPrototype, lineSuffix);
408    
409                    // verify the sub-collection should be rendered
410                    boolean renderSubCollection = checkSubCollectionRender(view, model, collectionGroup,
411                            subCollectionGroup);
412                    if (!renderSubCollection) {
413                        continue;
414                    }
415    
416                    subCollectionGroup.getBindingInfo().setBindByNamePrefix(bindingPath);
417                    if (subCollectionGroup.isRenderAddLine()) {
418                        subCollectionGroup.getAddLineBindingInfo().setBindByNamePrefix(bindingPath);
419                    }
420    
421                    // set sub-collection suffix on group so it can be used for generated groups
422                    String subCollectionSuffix = lineSuffix;
423                    if (StringUtils.isNotBlank(subCollectionGroup.getSubCollectionSuffix())) {
424                        subCollectionSuffix = subCollectionGroup.getSubCollectionSuffix() + lineSuffix;
425                    }
426                    subCollectionGroup.setSubCollectionSuffix(subCollectionSuffix);
427    
428                    FieldGroup fieldGroupPrototype = layoutManager.getSubCollectionFieldGroupPrototype();
429    
430                    FieldGroup subCollectionFieldGroup = ComponentUtils.copy(fieldGroupPrototype,
431                            lineSuffix + UifConstants.IdSuffixes.SUB + subLineIndex);
432                    subCollectionFieldGroup.setGroup(subCollectionGroup);
433    
434                    ComponentUtils.updateContextForLine(subCollectionFieldGroup, currentLine, lineIndex,
435                            lineSuffix + UifConstants.IdSuffixes.SUB + subLineIndex);
436    
437                    subCollectionFields.add(subCollectionFieldGroup);
438                }
439                ComponentUtils.pushObjectToContext(subCollectionFields, UifConstants.ContextVariableNames.PARENT_LINE,
440                        currentLine);
441            }
442    
443            // invoke layout manager to build the complete line
444            layoutManager.buildLine(view, model, collectionGroup, lineFields, subCollectionFields, bindingPath, actions,
445                    lineSuffix, currentLine, lineIndex);
446    
447            //add additional information to the group and fields to allow for correct add control selection
448            String selector = "";
449            if (lineIndex == -1) {
450                List<String> addIds = new ArrayList<String>();
451                for (Field f : lineFields) {
452                    if (f instanceof InputField) {
453                        // sets up - skipping these fields in add area during standard form validation calls
454                        // custom addLineToCollection js call will validate these fields manually on an add
455                        Control control = ((InputField) f).getControl();
456                        if (control != null) {
457                            control.addStyleClass("ignoreValid");
458                            selector = selector + ",#" + f.getId() + UifConstants.IdSuffixes.CONTROL;
459                        }
460                    } else if (f instanceof FieldGroup) {
461                        List<InputField> fields = ComponentUtils.getComponentsOfTypeDeep(((FieldGroup) f).getGroup(),
462                                InputField.class);
463                        for (InputField nestedField : fields) {
464                            Control control = nestedField.getControl();
465                            if (control != null) {
466                                control.addStyleClass("ignoreValid");
467                                selector = selector + ",#" + nestedField.getId() + UifConstants.IdSuffixes.CONTROL;
468                            }
469                        }
470                    }
471                }
472                collectionGroup.addDataAttribute(UifConstants.DataAttributes.ADD_CONTROLS, selector.replaceFirst(",", ""));
473            }
474        }
475    
476        /**
477         * Iterates through the given items checking for {@code RemotableFieldsHolder}, if found
478         * the holder is invoked to retrieved the remotable fields and translate to attribute fields. The translated list
479         * is then inserted into the returned list at the position of the holder
480         *
481         * @param view view instance containing the container
482         * @param model object instance containing the view data
483         * @param group collection group instance to check for any remotable fields holder
484         * @param items list of items to process
485         */
486        protected List<Field> processAnyRemoteFieldsHolder(View view, Object model, CollectionGroup group,
487                List<? extends Component> items) {
488            List<Field> processedItems = new ArrayList<Field>();
489    
490            // check for holders and invoke to retrieve the remotable fields and translate
491            // translated fields are placed into the processed items list at the position of the holder
492            for (Component item : items) {
493                if (item instanceof RemoteFieldsHolder) {
494                    List<InputField> translatedFields = ((RemoteFieldsHolder) item).fetchAndTranslateRemoteFields(view,
495                            model, group);
496                    processedItems.addAll(translatedFields);
497                } else {
498                    processedItems.add((Field) item);
499                }
500            }
501    
502            return processedItems;
503        }
504    
505        /**
506         * Evaluates the render property for the given list of {@code Field}
507         * instances for the line and removes any fields from the returned list that
508         * have render false. The conditional render string is also taken into
509         * account. This needs to be done here as opposed to during the normal
510         * condition evaluation so the the fields are not used while building the
511         * collection lines
512         *
513         * @param view view instance the collection group belongs to
514         * @param model object containing the view data
515         * @param collectionGroup collection group for the line fields
516         * @param lineFields list of fields configured for the line
517         * @param currentLine object containing the line data
518         * @param lineIndex index of the line in the collection
519         * @return list of field instances that should be rendered
520         */
521        protected List<Field> removeNonRenderLineFields(View view, Object model, CollectionGroup collectionGroup,
522                List<Field> lineFields, Object currentLine, int lineIndex) {
523            List<Field> fields = new ArrayList<Field>();
524    
525            ExpressionEvaluator expressionEvaluator = view.getViewHelperService().getExpressionEvaluator();
526    
527            for (Field lineField : lineFields) {
528                String conditionalRender = lineField.getPropertyExpression("render");
529    
530                // evaluate conditional render string if set
531                if (StringUtils.isNotBlank(conditionalRender)) {
532                    Map<String, Object> context = getContextForField(view, collectionGroup, lineField);
533    
534                    // Adjust the condition as ExpressionUtils.adjustPropertyExpressions will only be
535                    // executed after the collection is built.
536                    conditionalRender = expressionEvaluator.replaceBindingPrefixes(view, lineField, conditionalRender);
537    
538                    Boolean render = (Boolean) expressionEvaluator.evaluateExpression(context, conditionalRender);
539                    lineField.setRender(render);
540                }
541    
542                // only add line field if set to render or if it is hidden by progressive render
543                if (lineField.isRender() || StringUtils.isNotBlank(lineField.getProgressiveRender())) {
544                    fields.add(lineField);
545                }
546            }
547    
548            return fields;
549        }
550    
551        /**
552         * Invokes the view's configured authorizer and presentation controller to determine if the user has permission
553         * to view the line (if a permission has been established)
554         *
555         * @param view view instance the collection belongs to and from which the authorizer/presentation controller will
556         * be pulled
557         * @param model object containing the view's data
558         * @param collectionGroup collection group containing the line
559         * @param line object containing the lines data
560         * @return true if the user can view the line, false if not
561         */
562        protected boolean checkViewLineAuthorizationAndPresentationLogic(View view, ViewModel model,
563                CollectionGroup collectionGroup, Object line) {
564            ViewPresentationController presentationController = view.getPresentationController();
565            ViewAuthorizer authorizer = view.getAuthorizer();
566    
567            Person user = GlobalVariables.getUserSession().getPerson();
568    
569            // check view line auth
570            boolean canViewLine = authorizer.canViewLine(view, model, collectionGroup, collectionGroup.getPropertyName(),
571                    line, user);
572            if (canViewLine) {
573                canViewLine = presentationController.canViewLine(view, model, collectionGroup,
574                        collectionGroup.getPropertyName(), line);
575            }
576    
577            return canViewLine;
578        }
579    
580        /**
581         * Invokes the view's configured authorizer and presentation controller to determine if the user has permission
582         * to edit the line (if a permission has been established)
583         *
584         * @param view view instance the collection belongs to and from which the authorizer/presentation controller will
585         * be pulled
586         * @param model object containing the view's data
587         * @param collectionGroup collection group containing the line
588         * @param line object containing the lines data
589         * @return true if the user can edit the line, false if not
590         */
591        protected boolean checkEditLineAuthorizationAndPresentationLogic(View view, ViewModel model,
592                CollectionGroup collectionGroup, Object line) {
593            ViewPresentationController presentationController = view.getPresentationController();
594            ViewAuthorizer authorizer = view.getAuthorizer();
595    
596            Person user = GlobalVariables.getUserSession().getPerson();
597    
598            // check edit line auth
599            boolean canEditLine = authorizer.canEditLine(view, model, collectionGroup, collectionGroup.getPropertyName(),
600                    line, user);
601            if (canEditLine) {
602                canEditLine = presentationController.canEditLine(view, model, collectionGroup,
603                        collectionGroup.getPropertyName(), line);
604            }
605    
606            return canEditLine;
607        }
608    
609        /**
610         * Iterates through the line fields and checks the view field authorization using the view's configured authorizer
611         * and presentation controller. If the field is viewable, then sets the edit field authorization. Finally iterates
612         * through the line actions invoking the authorizer and presentation controller to authorizer the action
613         *
614         * @param view view instance the collection belongs to and from which the authorizer/presentation controller will
615         * be pulled
616         * @param model object containing the view's data
617         * @param collectionGroup collection group containing the line
618         * @param line object containing the lines data
619         * @param readOnlyLine flag indicating whether the line has been marked as read only (which will force the fields
620         * to be read only)
621         * @param lineFields list of fields instances for the line
622         * @param actions list of action field instances for the line
623         */
624        protected void applyLineFieldAuthorizationAndPresentationLogic(View view, ViewModel model,
625                CollectionGroup collectionGroup, Object line, boolean readOnlyLine, List<Field> lineFields,
626                List<Action> actions) {
627            ViewPresentationController presentationController = view.getPresentationController();
628            ViewAuthorizer authorizer = view.getAuthorizer();
629    
630            Person user = GlobalVariables.getUserSession().getPerson();
631    
632            ExpressionEvaluator expressionEvaluator = view.getViewHelperService().getExpressionEvaluator();
633    
634            for (Field lineField : lineFields) {
635                String propertyName = null;
636                if (lineField instanceof DataBinding) {
637                    propertyName = ((DataBinding) lineField).getPropertyName();
638                }
639    
640                // evaluate expression on fields component security (since apply model phase has not been invoked on
641                // them yet
642                ComponentSecurity componentSecurity = lineField.getComponentSecurity();
643    
644                Map<String, Object> context = getContextForField(view, collectionGroup, lineField);
645                expressionEvaluator.evaluateExpressionsOnConfigurable(view, componentSecurity, context);
646    
647                // check view field auth
648                if (lineField.isRender() && !lineField.isHidden()) {
649                    boolean canViewField = authorizer.canViewLineField(view, model, collectionGroup,
650                            collectionGroup.getPropertyName(), line, lineField, propertyName, user);
651                    if (canViewField) {
652                        canViewField = presentationController.canViewLineField(view, model, collectionGroup,
653                                collectionGroup.getPropertyName(), line, lineField, propertyName);
654                    }
655    
656                    if (!canViewField) {
657                        // since removing can impact layout, set to hidden
658                        // TODO: check into encryption setting
659                        lineField.setHidden(true);
660    
661                        if (lineField.getPropertyExpressions().containsKey("hidden")) {
662                            lineField.getPropertyExpressions().remove("hidden");
663                        }
664    
665                        continue;
666                    }
667    
668                    // check edit field auth
669                    boolean canEditField = !readOnlyLine;
670                    if (!readOnlyLine) {
671                        canEditField = authorizer.canEditLineField(view, model, collectionGroup,
672                                collectionGroup.getPropertyName(), line, lineField, propertyName, user);
673                        if (canEditField) {
674                            canEditField = presentationController.canEditLineField(view, model, collectionGroup,
675                                    collectionGroup.getPropertyName(), line, lineField, propertyName);
676                        }
677                    }
678    
679                    if (readOnlyLine || !canEditField) {
680                        lineField.setReadOnly(true);
681    
682                        if (lineField.getPropertyExpressions().containsKey("readOnly")) {
683                            lineField.getPropertyExpressions().remove("readOnly");
684                        }
685                    }
686                }
687            }
688    
689            // check auth on line actions
690            for (Action action : actions) {
691                if (action.isRender()) {
692                    boolean canPerformAction = authorizer.canPerformLineAction(view, model, collectionGroup,
693                            collectionGroup.getPropertyName(), line, action, action.getActionEvent(), action.getId(), user);
694                    if (canPerformAction) {
695                        canPerformAction = presentationController.canPerformLineAction(view, model, collectionGroup,
696                                collectionGroup.getPropertyName(), line, action, action.getActionEvent(), action.getId());
697                    }
698    
699                    if (!canPerformAction) {
700                        action.setRender(false);
701    
702                        if (action.getPropertyExpressions().containsKey("render")) {
703                            action.getPropertyExpressions().remove("render");
704                        }
705                    }
706                }
707            }
708        }
709    
710        /**
711         * Checks whether the given sub-collection should be rendered, any
712         * conditional render string is evaluated
713         *
714         * @param view view instance the sub collection belongs to
715         * @param model object containing the view data
716         * @param collectionGroup collection group the sub collection belongs to
717         * @param subCollectionGroup sub collection group to check render status for
718         * @return true if sub collection should be rendered, false if it
719         *         should not be rendered
720         */
721        protected boolean checkSubCollectionRender(View view, Object model, CollectionGroup collectionGroup,
722                CollectionGroup subCollectionGroup) {
723            String conditionalRender = subCollectionGroup.getPropertyExpression("render");
724    
725            // TODO: check authorizer
726    
727            // evaluate conditional render string if set
728            if (StringUtils.isNotBlank(conditionalRender)) {
729                Map<String, Object> context = new HashMap<String, Object>();
730                Map<String, Object> viewContext = view.getContext();
731                
732                if (viewContext != null) {
733                    context.putAll(viewContext);
734                }
735                
736                context.put(UifConstants.ContextVariableNames.PARENT, collectionGroup);
737                context.put(UifConstants.ContextVariableNames.COMPONENT, subCollectionGroup);
738    
739                Boolean render = (Boolean) view.getViewHelperService().getExpressionEvaluator().evaluateExpression(context,
740                        conditionalRender);
741                subCollectionGroup.setRender(render);
742            }
743    
744            return subCollectionGroup.isRender();
745        }
746    
747        /**
748         * Creates new {@code Action} instances for the line
749         *
750         * <p>
751         * Adds context to the action fields for the given line so that the line the
752         * action was performed on can be determined when that action is selected
753         * </p>
754         *
755         * @param lineActions the actions to copy
756         * @param view view instance the collection belongs to
757         * @param model top level object containing the data
758         * @param collectionGroup collection group component for the collection
759         * @param collectionLine object instance for the current line
760         * @param lineIndex index of the line the actions should apply to
761         */
762        protected List<Action> initializeLineActions(List<Action> lineActions, View view, Object model,
763                CollectionGroup collectionGroup, Object collectionLine, int lineIndex) {
764            String lineSuffix = UifConstants.IdSuffixes.LINE + Integer.toString(lineIndex);
765            if (StringUtils.isNotBlank(collectionGroup.getSubCollectionSuffix())) {
766                lineSuffix = collectionGroup.getSubCollectionSuffix() + lineSuffix;
767            }
768            List<Action> actions = ComponentUtils.copyComponentList(lineActions, lineSuffix);
769    
770            for (Action action : actions) {
771                if (ComponentUtils.containsPropertyExpression(action, UifPropertyPaths.ACTION_PARAMETERS, true)) {
772                    // need to update the actions expressions so our settings do not get overridden
773                    action.getPropertyExpressions().put(
774                            UifPropertyPaths.ACTION_PARAMETERS + "['" + UifParameters.SELLECTED_COLLECTION_PATH + "']",
775                            UifConstants.EL_PLACEHOLDER_PREFIX + "'" + collectionGroup.getBindingInfo().getBindingPath() +
776                                    "'" + UifConstants.EL_PLACEHOLDER_SUFFIX);
777                    action.getPropertyExpressions().put(
778                            UifPropertyPaths.ACTION_PARAMETERS + "['" + UifParameters.SELECTED_LINE_INDEX + "']",
779                            UifConstants.EL_PLACEHOLDER_PREFIX + "'" + Integer.toString(lineIndex) +
780                                    "'" + UifConstants.EL_PLACEHOLDER_SUFFIX);
781                } else {
782                    action.addActionParameter(UifParameters.SELLECTED_COLLECTION_PATH,
783                            collectionGroup.getBindingInfo().getBindingPath());
784                    action.addActionParameter(UifParameters.SELECTED_LINE_INDEX, Integer.toString(lineIndex));
785                }
786    
787                action.setJumpToIdAfterSubmit(collectionGroup.getId());
788                action.setRefreshId(collectionGroup.getId());
789    
790                // if marked for validation, add call to validate the line and set validation flag to false
791                // so the entire form will not be validated
792                if (action.isPerformClientSideValidation()) {
793                    String preSubmitScript = "var valid=validateLine('" +
794                            collectionGroup.getBindingInfo().getBindingPath() + "'," + Integer.toString(lineIndex) +
795                            ");";
796    
797                    // prepend custom presubmit script which should evaluate to a boolean
798                    if (StringUtils.isNotBlank(action.getPreSubmitCall())) {
799                        preSubmitScript = ScriptUtils.appendScript(preSubmitScript,
800                                "if(valid){valid=function(){" + action.getPreSubmitCall() + "}();}");
801                    }
802    
803                    preSubmitScript += " return valid;";
804    
805                    action.setPreSubmitCall(preSubmitScript);
806                    action.setPerformClientSideValidation(false);
807                }
808            }
809    
810            ComponentUtils.updateContextsForLine(actions, collectionLine, lineIndex, lineSuffix);
811    
812            return actions;
813        }
814    
815        /**
816         * Creates new {@code Action} instances for the add line
817         *
818         * <p>
819         * Adds context to the action fields for the add line so that the collection
820         * the action was performed on can be determined
821         * </p>
822         *
823         * @param view view instance the collection belongs to
824         * @param model top level object containing the data
825         * @param collectionGroup collection group component for the collection
826         */
827        protected List<Action> getAddLineActions(View view, Object model, CollectionGroup collectionGroup) {
828            String lineSuffix = UifConstants.IdSuffixes.ADD_LINE;
829            if (StringUtils.isNotBlank(collectionGroup.getSubCollectionSuffix())) {
830                lineSuffix = collectionGroup.getSubCollectionSuffix() + lineSuffix;
831            }
832            List<Action> lineActions = ComponentUtils.copyComponentList(collectionGroup.getAddLineActions(), lineSuffix);
833    
834            for (Action action : lineActions) {
835                action.addActionParameter(UifParameters.SELLECTED_COLLECTION_PATH,
836                        collectionGroup.getBindingInfo().getBindingPath());
837                action.setJumpToIdAfterSubmit(collectionGroup.getId());
838                action.addActionParameter(UifParameters.ACTION_TYPE, UifParameters.ADD_LINE);
839                action.setRefreshId(collectionGroup.getId());
840    
841                String baseId = collectionGroup.getBaseId();
842                if (StringUtils.isNotBlank(collectionGroup.getSubCollectionSuffix())) {
843                    baseId += collectionGroup.getSubCollectionSuffix();
844                }
845    
846                if (action.isPerformClientSideValidation()) {
847                    String preSubmitScript = "var valid=";
848                    if (collectionGroup.isAddViaLightBox()) {
849                        preSubmitScript += "validateAddLine('" + collectionGroup.getId() + "', true);";
850                    } else {
851                        preSubmitScript += "validateAddLine('" + collectionGroup.getId() + "');";
852                    }
853    
854                    // prepend custom presubmit script which should evaluate to a boolean
855                    if (StringUtils.isNotBlank(action.getPreSubmitCall())) {
856                        preSubmitScript = ScriptUtils.appendScript(preSubmitScript,
857                                "if(valid){valid=function(){" + action.getPreSubmitCall() + "}();}");
858                    }
859    
860                    if (collectionGroup.isAddViaLightBox()) {
861                        preSubmitScript += " if(valid){closeLightbox();}";
862                    }
863                    preSubmitScript += "return valid;";
864    
865                    action.setPreSubmitCall(preSubmitScript);
866                    action.setPerformClientSideValidation(false);
867                }
868            }
869    
870            // get add line for context
871            String addLinePath = collectionGroup.getAddLineBindingInfo().getBindingPath();
872            Object addLine = ObjectPropertyUtils.getPropertyValue(model, addLinePath);
873    
874            ComponentUtils.updateContextsForLine(lineActions, addLine, -1, lineSuffix);
875    
876            return lineActions;
877        }
878    
879        /**
880         * Helper method to build the context for a field (needed because the apply model phase for line fields has
881         * not been applied yet and their full context not set)
882         *
883         * @param view view instance the field belongs to
884         * @param collectionGroup collection group instance the field belongs to
885         * @param field field instance to build context for
886         * @return Map<String, Object> context for field
887         */
888        protected Map<String, Object> getContextForField(View view, CollectionGroup collectionGroup, Field field) {
889            Map<String, Object> context = new HashMap<String, Object>();
890            
891            Map<String, Object> viewContext = view.getContext();
892            if (viewContext != null) {
893                context.putAll(viewContext);
894            }
895            
896            Map<String, Object> fieldContext = field.getContext();
897            if (fieldContext != null) {
898                context.putAll(fieldContext);
899            }
900    
901            context.put(UifConstants.ContextVariableNames.PARENT, collectionGroup);
902            context.put(UifConstants.ContextVariableNames.COMPONENT, field);
903    
904            return context;
905        }
906    
907        /**
908         * Initializes a new instance of the collection class
909         *
910         * <p>
911         * If the add line property was not specified for the collection group the
912         * new lines will be added to the generic map on the
913         * {@code UifFormBase}, else it will be added to the property given by
914         * the addLineBindingInfo
915         * </p>
916         *
917         * <p>
918         * New line will only be created if the current line property is null or
919         * clearExistingLine is true. In the case of a new line default values are
920         * also applied
921         * </p>
922         *
923         * @see CollectionGroup#
924         *      initializeNewCollectionLine(View, Object, CollectionGroup, boolean)
925         */
926        public void initializeNewCollectionLine(View view, Object model, CollectionGroup collectionGroup,
927                boolean clearExistingLine) {
928            Object newLine = null;
929    
930            // determine if we are binding to generic form map or a custom property
931            if (StringUtils.isBlank(collectionGroup.getAddLinePropertyName())) {
932                // bind to form map
933                if (!(model instanceof UifFormBase)) {
934                    throw new RuntimeException("Cannot create new collection line for group: "
935                            + collectionGroup.getPropertyName()
936                            + ". Model does not extend "
937                            + UifFormBase.class.getName());
938                }
939    
940                // get new collection line map from form
941                Map<String, Object> newCollectionLines = ObjectPropertyUtils.getPropertyValue(model,
942                        UifPropertyPaths.NEW_COLLECTION_LINES);
943                if (newCollectionLines == null) {
944                    newCollectionLines = new HashMap<String, Object>();
945                    ObjectPropertyUtils.setPropertyValue(model, UifPropertyPaths.NEW_COLLECTION_LINES, newCollectionLines);
946                }
947    
948                // set binding path for add line
949                String newCollectionLineKey = KRADUtils.translateToMapSafeKey(
950                        collectionGroup.getBindingInfo().getBindingPath());
951                String addLineBindingPath = UifPropertyPaths.NEW_COLLECTION_LINES + "['" + newCollectionLineKey + "']";
952                collectionGroup.getAddLineBindingInfo().setBindingPath(addLineBindingPath);
953    
954                // if there is not an instance available or we need to clear create a new instance
955                if (!newCollectionLines.containsKey(newCollectionLineKey) || (newCollectionLines.get(newCollectionLineKey)
956                        == null) || clearExistingLine) {
957                    // create new instance of the collection type for the add line
958                    newLine = ObjectUtils.newInstance(collectionGroup.getCollectionObjectClass());
959                    newCollectionLines.put(newCollectionLineKey, newLine);
960                }
961            } else {
962                // bind to custom property
963                Object addLine = ObjectPropertyUtils.getPropertyValue(model,
964                        collectionGroup.getAddLineBindingInfo().getBindingPath());
965                if ((addLine == null) || clearExistingLine) {
966                    newLine = ObjectUtils.newInstance(collectionGroup.getCollectionObjectClass());
967                    ObjectPropertyUtils.setPropertyValue(model, collectionGroup.getAddLineBindingInfo().getBindingPath(),
968                            newLine);
969                }
970            }
971    
972            // apply default values if a new line was created
973            if (newLine != null) {
974                view.getViewHelperService().applyDefaultValuesForCollectionLine(view, model, collectionGroup, newLine);
975            }
976        }
977    
978        /**
979         * Wrapper object to enable filtering of a collection while preserving original indices
980         */
981        private static class IndexedElement {
982    
983            /**
984             * The index associated with the given element
985             */
986            final int index;
987    
988            /**
989             * The element itself
990             */
991            final Object element;
992    
993            /**
994             * Constructs an {@link org.kuali.rice.krad.uif.container.CollectionGroupBuilder.IndexedElement}
995             *
996             * @param index the index to associate with the element
997             * @param element the element itself
998             */
999            private IndexedElement(int index, Object element) {
1000                this.index = index;
1001                this.element = element;
1002            }
1003        }
1004    
1005    }