View Javadoc

1   /**
2    * Copyright 2005-2014 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 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 }