View Javadoc

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