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