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 }