001 /* 002 * Copyright 2011 The Kuali Foundation 003 * 004 * Licensed under the Educational Community License, Version 1.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/ecl1.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.lang.StringUtils; 019 import org.kuali.rice.krad.service.KRADServiceLocatorWeb; 020 import org.kuali.rice.krad.uif.UifConstants; 021 import org.kuali.rice.krad.uif.UifParameters; 022 import org.kuali.rice.krad.uif.UifPropertyPaths; 023 import org.kuali.rice.krad.uif.control.Control; 024 import org.kuali.rice.krad.uif.core.DataBinding; 025 import org.kuali.rice.krad.uif.field.ActionField; 026 import org.kuali.rice.krad.uif.field.AttributeField; 027 import org.kuali.rice.krad.uif.field.Field; 028 import org.kuali.rice.krad.uif.field.GroupField; 029 import org.kuali.rice.krad.uif.layout.CollectionLayoutManager; 030 import org.kuali.rice.krad.uif.service.ExpressionEvaluatorService; 031 import org.kuali.rice.krad.uif.util.ComponentUtils; 032 import org.kuali.rice.krad.uif.util.ObjectPropertyUtils; 033 import org.kuali.rice.krad.util.KRADUtils; 034 import org.kuali.rice.krad.util.ObjectUtils; 035 import org.kuali.rice.krad.web.form.UifFormBase; 036 037 import java.io.Serializable; 038 import java.util.ArrayList; 039 import java.util.HashMap; 040 import java.util.List; 041 import java.util.Map; 042 043 /** 044 * Builds out the <code>Field</code> instances for a collection group with a 045 * series of steps that interact with the configured 046 * <code>CollectionLayoutManager</code> to assemble the fields as necessary for 047 * the layout 048 * 049 * @author Kuali Rice Team (rice.collab@kuali.org) 050 */ 051 public class CollectionGroupBuilder implements Serializable { 052 private static final long serialVersionUID = -4762031957079895244L; 053 054 /** 055 * Creates the <code>Field</code> instances that make up the table 056 * 057 * <p> 058 * The corresponding collection is retrieved from the model and iterated 059 * over to create the necessary fields. The binding path for fields that 060 * implement <code>DataBinding</code> is adjusted to point to the collection 061 * line it is apart of. For example, field 'number' of collection 'accounts' 062 * for line 1 will be set to 'accounts[0].number', and for line 2 063 * 'accounts[1].number'. Finally parameters are set on the line's action 064 * fields to indicate what collection and line they apply to. 065 * </p> 066 * 067 * @param view 068 * - View instance the collection belongs to 069 * @param model 070 * - Top level object containing the data 071 * @param collectionGroup 072 * - CollectionGroup component for the collection 073 */ 074 public void build(View view, Object model, CollectionGroup collectionGroup) { 075 // create add line 076 if (collectionGroup.isRenderAddLine() && !collectionGroup.isReadOnly()) { 077 buildAddLine(view, model, collectionGroup); 078 } 079 080 // get the collection for this group from the model 081 List<Object> modelCollection = ObjectPropertyUtils.getPropertyValue(model, ((DataBinding) collectionGroup) 082 .getBindingInfo().getBindingPath()); 083 084 // filter inactive model 085 List<Integer> showIndexes = collectionGroup.performCollectionFiltering(view, model); 086 087 // for each collection row build the line fields 088 if (modelCollection != null) { 089 for (int index = 0; index < modelCollection.size(); index++) { 090 091 // Display only records that passed filtering 092 if (showIndexes == null || showIndexes.contains(index)) { 093 String bindingPathPrefix = collectionGroup.getBindingInfo().getBindingName() + "[" + index + "]"; 094 if (StringUtils.isNotBlank(collectionGroup.getBindingInfo().getBindByNamePrefix())) { 095 bindingPathPrefix = collectionGroup.getBindingInfo().getBindByNamePrefix() + "." 096 + bindingPathPrefix; 097 } 098 099 Object currentLine = modelCollection.get(index); 100 101 List<ActionField> actions = getLineActions(view, model, collectionGroup, currentLine, index); 102 buildLine(view, model, collectionGroup, bindingPathPrefix, actions, false, currentLine, index); 103 } 104 } 105 } 106 } 107 108 /** 109 * Builds the fields for holding the collection add line and if necessary 110 * makes call to setup the new line instance 111 * 112 * @param view 113 * - view instance the collection belongs to 114 * @param collectionGroup 115 * - collection group the layout manager applies to 116 * @param model 117 * - Object containing the view data, should extend UifFormBase 118 * if using framework managed new lines 119 */ 120 protected void buildAddLine(View view, Object model, CollectionGroup collectionGroup) { 121 boolean addLineBindsToForm = false; 122 123 // initialize new line if one does not already exist 124 initializeNewCollectionLine(view, model, collectionGroup, false); 125 126 // determine whether the add line binds to the generic form map or a 127 // specified property 128 if (StringUtils.isBlank(collectionGroup.getAddLinePropertyName())) { 129 addLineBindsToForm = true; 130 } 131 132 String addLineBindingPath = collectionGroup.getAddLineBindingInfo().getBindingPath(); 133 List<ActionField> actions = getAddLineActions(view, model, collectionGroup); 134 135 Object addLine = ObjectPropertyUtils.getPropertyValue(model, addLineBindingPath); 136 buildLine(view, model, collectionGroup, addLineBindingPath, actions, addLineBindsToForm, addLine, -1); 137 } 138 139 /** 140 * Builds the field instances for the collection line. A copy of the 141 * configured items on the <code>CollectionGroup</code> is made and adjusted 142 * for the line (id and binding). Then a call is made to the 143 * <code>CollectionLayoutManager</code> to assemble the line as necessary 144 * for the layout 145 * 146 * @param view 147 * - view instance the collection belongs to 148 * @param model 149 * - top level object containing the data 150 * @param collectionGroup 151 * - collection group component for the collection 152 * @param bindingPath 153 * - binding path for the line fields (if DataBinding) 154 * @param actions 155 * - List of actions to set in the lines action column 156 * @param bindLineToForm 157 * - whether the bindToForm property on the items bindingInfo 158 * should be set to true (needed for add line) 159 * @param currentLine 160 * - object instance for the current line, or null if add line 161 * @param lineIndex 162 * - index of the line in the collection, or -1 if we are 163 * building the add line 164 */ 165 @SuppressWarnings("unchecked") 166 protected void buildLine(View view, Object model, CollectionGroup collectionGroup, String bindingPath, 167 List<ActionField> actions, boolean bindToForm, Object currentLine, int lineIndex) { 168 CollectionLayoutManager layoutManager = (CollectionLayoutManager) collectionGroup.getLayoutManager(); 169 170 // copy group items for new line 171 List<Field> lineFields = null; 172 if (lineIndex == -1) { 173 lineFields = (List<Field>) ComponentUtils.copyFieldList(collectionGroup.getAddLineFields(), bindingPath, 174 "_add"); 175 } else { 176 lineFields = (List<Field>) ComponentUtils.copyFieldList(collectionGroup.getItems(), bindingPath, "_" 177 + Integer.toString(lineIndex)); 178 } 179 180 if(lineIndex == -1 && !lineFields.isEmpty()){ 181 for(Field f: lineFields){ 182 if(f instanceof AttributeField){ 183 //sets up - skipping these fields in add area during standard form validation calls 184 //custom addLineToCollection js call will validate these fields manually on an add 185 Control control = ((AttributeField) f).getControl(); 186 if (control != null) { 187 control.addStyleClass(collectionGroup.getBaseId() + "-addField"); 188 control.addStyleClass("ignoreValid"); 189 } 190 } 191 } 192 for(ActionField action: actions){ 193 if(action.getActionParameter(UifParameters.ACTION_TYPE).equals(UifParameters.ADD_LINE)){ 194 action.setFocusOnAfterSubmit(lineFields.get(0).getId()); 195 } 196 } 197 } 198 199 ComponentUtils.updateContextsForLine(lineFields, currentLine, lineIndex); 200 201 if (bindToForm) { 202 ComponentUtils.setComponentsPropertyDeep(lineFields, UifPropertyPaths.BIND_TO_FORM, new Boolean(true)); 203 } 204 205 // remove fields from the line that have render false 206 lineFields = removeNonRenderLineFields(view, model, collectionGroup, lineFields, currentLine, lineIndex); 207 208 // if not add line build sub-collection field groups 209 List<GroupField> subCollectionFields = new ArrayList<GroupField>(); 210 if ((lineIndex != -1) && (collectionGroup.getSubCollections() != null)) { 211 for (int subLineIndex = 0; subLineIndex < collectionGroup.getSubCollections().size(); subLineIndex++) { 212 CollectionGroup subCollectionPrototype = collectionGroup.getSubCollections().get(subLineIndex); 213 CollectionGroup subCollectionGroup = ComponentUtils.copy(subCollectionPrototype, collectionGroup.getId() + "s" + subLineIndex); 214 215 // verify the sub-collection should be rendered 216 boolean renderSubCollection = checkSubCollectionRender(view, model, collectionGroup, subCollectionGroup); 217 if (!renderSubCollection) { 218 continue; 219 } 220 221 subCollectionGroup.getBindingInfo().setBindByNamePrefix(bindingPath); 222 subCollectionGroup.getAddLineBindingInfo().setBindByNamePrefix(bindingPath); 223 224 GroupField groupFieldPrototype = layoutManager.getSubCollectionGroupFieldPrototype(); 225 GroupField subCollectionGroupField = ComponentUtils.copy(groupFieldPrototype, collectionGroup.getId() + "s" + subLineIndex); 226 subCollectionGroupField.setGroup(subCollectionGroup); 227 228 subCollectionFields.add(subCollectionGroupField); 229 } 230 } 231 232 233 // invoke layout manager to build the complete line 234 layoutManager.buildLine(view, model, collectionGroup, lineFields, subCollectionFields, bindingPath, actions, 235 "_l" + lineIndex, currentLine, lineIndex); 236 } 237 238 239 /** 240 * Evaluates the render property for the given list of <code>Field</code> 241 * instances for the line and removes any fields from the returned list that 242 * have render false. The conditional render string is also taken into 243 * account. This needs to be done here as opposed to during the normal 244 * condition evaluation so the the fields are not used while building the 245 * collection lines 246 * 247 * @param view 248 * - view instance the collection group belongs to 249 * @param model 250 * - object containing the view data 251 * @param collectionGroup 252 * - collection group for the line fields 253 * @param lineFields 254 * - list of fields configured for the line 255 * @param currentLine 256 * - object containing the line data 257 * @param lineIndex 258 * - index of the line in the collection 259 * @return List<Field> list of field instances that should be rendered 260 */ 261 protected List<Field> removeNonRenderLineFields(View view, Object model, CollectionGroup collectionGroup, 262 List<Field> lineFields, Object currentLine, int lineIndex) { 263 List<Field> fields = new ArrayList<Field>(); 264 265 for (Field lineField : lineFields) { 266 String conditionalRender = lineField.getPropertyExpression("render"); 267 268 // evaluate conditional render string if set 269 if (StringUtils.isNotBlank(conditionalRender)) { 270 Map<String, Object> context = new HashMap<String, Object>(); 271 context.putAll(view.getContext()); 272 context.put(UifConstants.ContextVariableNames.PARENT, collectionGroup); 273 context.put(UifConstants.ContextVariableNames.COMPONENT, lineField); 274 context.put(UifConstants.ContextVariableNames.LINE, currentLine); 275 context.put(UifConstants.ContextVariableNames.INDEX, new Integer(lineIndex)); 276 context.put(UifConstants.ContextVariableNames.IS_ADD_LINE, new Boolean(lineIndex == -1)); 277 278 Boolean render = (Boolean) getExpressionEvaluatorService().evaluateExpression(model, context, 279 conditionalRender); 280 lineField.setRender(render); 281 } 282 283 // only add line field if set to render or if it is hidden by progressive render 284 if (lineField.isRender() || StringUtils.isNotBlank(lineField.getProgressiveRender())) { 285 fields.add(lineField); 286 } 287 } 288 289 return fields; 290 } 291 292 /** 293 * Checks whether the given sub-collection should be rendered, any 294 * conditional render string is evaluated 295 * 296 * @param view 297 * - view instance the sub collection belongs to 298 * @param model 299 * - object containing the view data 300 * @param collectionGroup 301 * - collection group the sub collection belongs to 302 * @param subCollectionGroup 303 * - sub collection group to check render status for 304 * @return boolean true if sub collection should be rendered, false if it 305 * should not be rendered 306 */ 307 protected boolean checkSubCollectionRender(View view, Object model, CollectionGroup collectionGroup, 308 CollectionGroup subCollectionGroup) { 309 String conditionalRender = subCollectionGroup.getPropertyExpression("render"); 310 311 // evaluate conditional render string if set 312 if (StringUtils.isNotBlank(conditionalRender)) { 313 Map<String, Object> context = new HashMap<String, Object>(); 314 context.putAll(view.getContext()); 315 context.put(UifConstants.ContextVariableNames.PARENT, collectionGroup); 316 context.put(UifConstants.ContextVariableNames.COMPONENT, subCollectionGroup); 317 318 Boolean render = (Boolean) getExpressionEvaluatorService().evaluateExpression(model, context, 319 conditionalRender); 320 subCollectionGroup.setRender(render); 321 } 322 323 return subCollectionGroup.isRender(); 324 } 325 326 /** 327 * Creates new <code>ActionField</code> instances for the line 328 * 329 * <p> 330 * Adds context to the action fields for the given line so that the line the 331 * action was performed on can be determined when that action is selected 332 * </p> 333 * 334 * @param view 335 * - view instance the collection belongs to 336 * @param model 337 * - top level object containing the data 338 * @param collectionGroup 339 * - collection group component for the collection 340 * @param collectionLine 341 * - object instance for the current line 342 * @param lineIndex 343 * - index of the line the actions should apply to 344 */ 345 protected List<ActionField> getLineActions(View view, Object model, CollectionGroup collectionGroup, 346 Object collectionLine, int lineIndex) { 347 List<ActionField> lineActions = ComponentUtils.copyFieldList(collectionGroup.getActionFields(), Integer.toString(lineIndex)); 348 for (ActionField actionField : lineActions) { 349 actionField.addActionParameter(UifParameters.SELLECTED_COLLECTION_PATH, collectionGroup.getBindingInfo() 350 .getBindingPath()); 351 actionField.addActionParameter(UifParameters.SELECTED_LINE_INDEX, Integer.toString(lineIndex)); 352 actionField.setJumpToIdAfterSubmit(collectionGroup.getId() + "_div"); 353 actionField.setClientSideJs("performCollectionAction('"+collectionGroup.getId()+"');"); 354 } 355 356 ComponentUtils.updateContextsForLine(lineActions, collectionLine, lineIndex); 357 358 return lineActions; 359 } 360 361 /** 362 * Creates new <code>ActionField</code> instances for the add line 363 * 364 * <p> 365 * Adds context to the action fields for the add line so that the collection 366 * the action was performed on can be determined 367 * </p> 368 * 369 * @param view 370 * - view instance the collection belongs to 371 * @param model 372 * - top level object containing the data 373 * @param collectionGroup 374 * - collection group component for the collection 375 */ 376 protected List<ActionField> getAddLineActions(View view, Object model, CollectionGroup collectionGroup) { 377 List<ActionField> lineActions = ComponentUtils.copyFieldList(collectionGroup.getAddLineActionFields(), "_add"); 378 for (ActionField actionField : lineActions) { 379 actionField.addActionParameter(UifParameters.SELLECTED_COLLECTION_PATH, collectionGroup.getBindingInfo() 380 .getBindingPath()); 381 //actionField.addActionParameter(UifParameters.COLLECTION_ID, collectionGroup.getId()); 382 actionField.setJumpToIdAfterSubmit(collectionGroup.getId() + "_div"); 383 actionField.addActionParameter(UifParameters.ACTION_TYPE, UifParameters.ADD_LINE); 384 actionField.setClientSideJs("addLineToCollection('"+collectionGroup.getId()+"', '"+ collectionGroup.getBaseId() +"');"); 385 } 386 387 // get add line for context 388 String addLinePath = collectionGroup.getAddLineBindingInfo().getBindingPath(); 389 Object addLine = ObjectPropertyUtils.getPropertyValue(model, addLinePath); 390 391 ComponentUtils.updateContextsForLine(lineActions, addLine, -1); 392 393 return lineActions; 394 } 395 396 /** 397 * Initializes a new instance of the collection class 398 * 399 * <p> 400 * If the add line property was not specified for the collection group the 401 * new lines will be added to the generic map on the 402 * <code>UifFormBase</code>, else it will be added to the property given by 403 * the addLineBindingInfo 404 * </p> 405 * 406 * <p> 407 * New line will only be created if the current line property is null or 408 * clearExistingLine is true. In the case of a new line default values are 409 * also applied 410 * </p> 411 * 412 * @see org.kuali.rice.krad.uif.container.CollectionGroup. 413 * initializeNewCollectionLine(View, Object, CollectionGroup, boolean) 414 */ 415 public void initializeNewCollectionLine(View view, Object model, CollectionGroup collectionGroup, 416 boolean clearExistingLine) { 417 Object newLine = null; 418 419 // determine if we are binding to generic form map or a custom property 420 if (StringUtils.isBlank(collectionGroup.getAddLinePropertyName())) { 421 // bind to form map 422 if (!(model instanceof UifFormBase)) { 423 throw new RuntimeException("Cannot create new collection line for group: " 424 + collectionGroup.getPropertyName() + ". Model does not extend " + UifFormBase.class.getName()); 425 } 426 427 // get new collection line map from form 428 Map<String, Object> newCollectionLines = ObjectPropertyUtils.getPropertyValue(model, 429 UifPropertyPaths.NEW_COLLECTION_LINES); 430 if (newCollectionLines == null) { 431 newCollectionLines = new HashMap<String, Object>(); 432 ObjectPropertyUtils.setPropertyValue(model, UifPropertyPaths.NEW_COLLECTION_LINES, newCollectionLines); 433 } 434 435 // set binding path for add line 436 String newCollectionLineKey = KRADUtils 437 .translateToMapSafeKey(collectionGroup.getBindingInfo().getBindingPath()); 438 String addLineBindingPath = UifPropertyPaths.NEW_COLLECTION_LINES + "['" + newCollectionLineKey + "']"; 439 collectionGroup.getAddLineBindingInfo().setBindingPath(addLineBindingPath); 440 441 // if there is not an instance available or we need to clear create 442 // a new instance 443 if (!newCollectionLines.containsKey(newCollectionLineKey) 444 || (newCollectionLines.get(newCollectionLineKey) == null) || clearExistingLine) { 445 // create new instance of the collection type for the add line 446 newLine = ObjectUtils.newInstance(collectionGroup.getCollectionObjectClass()); 447 newCollectionLines.put(newCollectionLineKey, newLine); 448 } 449 } else { 450 // bind to custom property 451 Object addLine = ObjectPropertyUtils.getPropertyValue(model, collectionGroup.getAddLineBindingInfo() 452 .getBindingPath()); 453 if ((addLine == null) || clearExistingLine) { 454 newLine = ObjectUtils.newInstance(collectionGroup.getCollectionObjectClass()); 455 ObjectPropertyUtils.setPropertyValue(model, collectionGroup.getAddLineBindingInfo().getBindingPath(), 456 newLine); 457 } 458 } 459 460 // apply default values if a new line was created 461 if (newLine != null) { 462 view.getViewHelperService().applyDefaultValuesForCollectionLine(view, model, collectionGroup, newLine); 463 } 464 } 465 466 protected ExpressionEvaluatorService getExpressionEvaluatorService() { 467 return KRADServiceLocatorWeb.getExpressionEvaluatorService(); 468 } 469 470 }