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 }