View Javadoc

1   /**
2    * Copyright 2005-2011 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.krad.uif.container;
17  
18  import org.apache.commons.collections.ListUtils;
19  import org.apache.commons.lang.StringUtils;
20  import org.kuali.rice.core.api.mo.common.active.Inactivatable;
21  import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
22  import org.kuali.rice.krad.uif.UifConstants;
23  import org.kuali.rice.krad.uif.UifParameters;
24  import org.kuali.rice.krad.uif.UifPropertyPaths;
25  import org.kuali.rice.krad.uif.control.Control;
26  import org.kuali.rice.krad.uif.component.DataBinding;
27  import org.kuali.rice.krad.uif.field.ActionField;
28  import org.kuali.rice.krad.uif.field.InputField;
29  import org.kuali.rice.krad.uif.field.Field;
30  import org.kuali.rice.krad.uif.field.FieldGroup;
31  import org.kuali.rice.krad.uif.layout.CollectionLayoutManager;
32  import org.kuali.rice.krad.uif.service.ExpressionEvaluatorService;
33  import org.kuali.rice.krad.uif.util.ComponentUtils;
34  import org.kuali.rice.krad.uif.util.ObjectPropertyUtils;
35  import org.kuali.rice.krad.uif.view.View;
36  import org.kuali.rice.krad.util.KRADUtils;
37  import org.kuali.rice.krad.util.ObjectUtils;
38  import org.kuali.rice.krad.web.form.UifFormBase;
39  
40  import java.io.Serializable;
41  import java.util.ArrayList;
42  import java.util.Collection;
43  import java.util.HashMap;
44  import java.util.List;
45  import java.util.Map;
46  
47  /**
48   * Builds out the <code>Field</code> instances for a collection group with a
49   * series of steps that interact with the configured
50   * <code>CollectionLayoutManager</code> to assemble the fields as necessary for
51   * the layout
52   * 
53   * @author Kuali Rice Team (rice.collab@kuali.org)
54   */
55  public class CollectionGroupBuilder implements Serializable {
56  	private static final long serialVersionUID = -4762031957079895244L;
57  
58  	/**
59  	 * Creates the <code>Field</code> instances that make up the table
60  	 * 
61  	 * <p>
62  	 * The corresponding collection is retrieved from the model and iterated
63  	 * over to create the necessary fields. The binding path for fields that
64  	 * implement <code>DataBinding</code> is adjusted to point to the collection
65  	 * line it is apart of. For example, field 'number' of collection 'accounts'
66  	 * for line 1 will be set to 'accounts[0].number', and for line 2
67  	 * 'accounts[1].number'. Finally parameters are set on the line's action
68  	 * fields to indicate what collection and line they apply to.
69  	 * </p>
70  	 * 
71  	 * @param view
72  	 *            - View instance the collection belongs to
73  	 * @param model
74  	 *            - Top level object containing the data
75  	 * @param collectionGroup
76  	 *            - CollectionGroup component for the collection
77  	 */
78      public void build(View view, Object model, CollectionGroup collectionGroup) {
79          // create add line
80          if (collectionGroup.isRenderAddLine() && !collectionGroup.isReadOnly()) {
81              buildAddLine(view, model, collectionGroup);
82          }
83  
84          // get the collection for this group from the model
85          List<Object> modelCollection = ObjectPropertyUtils.getPropertyValue(model, ((DataBinding) collectionGroup)
86                  .getBindingInfo().getBindingPath());
87  
88          if (modelCollection != null) {
89              // filter inactive model
90              List<Integer> showIndexes = performCollectionFiltering(view, model, collectionGroup, modelCollection);
91  
92              // for each collection row build the line fields
93              for (int index = 0; index < modelCollection.size(); index++) {
94                  // display only records that passed filtering
95                  if (showIndexes.contains(index)) {
96                      String bindingPathPrefix = collectionGroup.getBindingInfo().getBindingName() + "[" + index + "]";
97                      if (StringUtils.isNotBlank(collectionGroup.getBindingInfo().getBindByNamePrefix())) {
98                          bindingPathPrefix =
99                                  collectionGroup.getBindingInfo().getBindByNamePrefix() + "." + bindingPathPrefix;
100                     }
101 
102                     Object currentLine = modelCollection.get(index);
103 
104                     List<ActionField> actions = getLineActions(view, model, collectionGroup, currentLine, index);
105                     buildLine(view, model, collectionGroup, bindingPathPrefix, actions, false, currentLine, index);
106                 }
107             }
108         }
109     }
110 
111     /**
112      * Performs any filtering necessary on the collection before building the collection fields
113      *
114      * <p>
115      * If showInactive is set to false and the collection line type implements <code>Inactivatable</code>,
116      * invokes the active collection filter. Then any {@link CollectionFilter} instances configured for the collection
117      * group are invoked to filter the collection. Collections lines must pass all filters in order to be
118      * displayed
119      * </p>
120      *
121      * @param view - view instance that contains the collection
122      * @param model - object containing the views data
123      * @param collectionGroup - collection group component instance that will display the collection
124      * @param collection - collection instance that will be filtered
125      */
126     protected List<Integer> performCollectionFiltering(View view, Object model, CollectionGroup collectionGroup,
127             Collection<?> collection) {
128         List<Integer> filteredIndexes = new ArrayList<Integer>();
129         for (int i = 0; i < collection.size(); i++) {
130             filteredIndexes.add(new Integer(i));
131         }
132 
133         if (Inactivatable.class.isAssignableFrom(collectionGroup.getCollectionObjectClass()) && !collectionGroup
134                 .isShowInactive()) {
135             List<Integer> activeIndexes = collectionGroup.getActiveCollectionFilter().filter(view, model,
136                     collectionGroup);
137             filteredIndexes = ListUtils.intersection(filteredIndexes, activeIndexes);
138         }
139 
140         for (CollectionFilter collectionFilter : collectionGroup.getFilters()) {
141             List<Integer> indexes = collectionFilter.filter(view, model, collectionGroup);
142             filteredIndexes = ListUtils.intersection(filteredIndexes, indexes);
143             if (filteredIndexes.isEmpty()) {
144                 break;
145             }
146         }
147 
148         return filteredIndexes;
149     }
150 
151 	/**
152 	 * Builds the fields for holding the collection add line and if necessary
153 	 * makes call to setup the new line instance
154 	 * 
155 	 * @param view
156 	 *            - view instance the collection belongs to
157 	 * @param collectionGroup
158 	 *            - collection group the layout manager applies to
159 	 * @param model
160 	 *            - Object containing the view data, should extend UifFormBase
161 	 *            if using framework managed new lines
162 	 */
163     protected void buildAddLine(View view, Object model, CollectionGroup collectionGroup) {
164         boolean addLineBindsToForm = false;
165 
166         // initialize new line if one does not already exist
167         initializeNewCollectionLine(view, model, collectionGroup, false);
168 
169         // determine whether the add line binds to the generic form map or a
170         // specified property
171         if (StringUtils.isBlank(collectionGroup.getAddLinePropertyName())) {
172             addLineBindsToForm = true;
173         }
174 
175         String addLineBindingPath = collectionGroup.getAddLineBindingInfo().getBindingPath();
176         List<ActionField> actions = getAddLineActions(view, model, collectionGroup);
177 
178         Object addLine = ObjectPropertyUtils.getPropertyValue(model, addLineBindingPath);
179         buildLine(view, model, collectionGroup, addLineBindingPath, actions, addLineBindsToForm, addLine, -1);
180     }
181 
182 	/**
183 	 * Builds the field instances for the collection line. A copy of the
184 	 * configured items on the <code>CollectionGroup</code> is made and adjusted
185 	 * for the line (id and binding). Then a call is made to the
186 	 * <code>CollectionLayoutManager</code> to assemble the line as necessary
187 	 * for the layout
188 	 * 
189 	 * @param view
190 	 *            - view instance the collection belongs to
191 	 * @param model
192 	 *            - top level object containing the data
193 	 * @param collectionGroup
194 	 *            - collection group component for the collection
195 	 * @param bindingPath
196 	 *            - binding path for the line fields (if DataBinding)
197 	 * @param actions
198 	 *            - List of actions to set in the lines action column
199 	 * @param bindToForm
200 	 *            - whether the bindToForm property on the items bindingInfo
201 	 *            should be set to true (needed for add line)
202 	 * @param currentLine
203 	 *            - object instance for the current line, or null if add line
204 	 * @param lineIndex
205 	 *            - index of the line in the collection, or -1 if we are
206 	 *            building the add line
207 	 */
208 	@SuppressWarnings("unchecked")
209 	protected void buildLine(View view, Object model, CollectionGroup collectionGroup, String bindingPath,
210 			List<ActionField> actions, boolean bindToForm, Object currentLine, int lineIndex) {
211 		CollectionLayoutManager layoutManager = (CollectionLayoutManager) collectionGroup.getLayoutManager();
212 
213 		// copy group items for new line
214         List<Field> lineFields = (List<Field>) collectionGroup.getItems();
215         String lineSuffix = UifConstants.IdSuffixes.LINE + Integer.toString(lineIndex);
216         if (lineIndex == -1) {
217             lineFields = (List<Field>) collectionGroup.getAddLineFields();
218             lineSuffix = UifConstants.IdSuffixes.ADD_LINE;
219         }
220         if (StringUtils.isNotBlank(collectionGroup.getSubCollectionSuffix())) {
221             lineSuffix = collectionGroup.getSubCollectionSuffix() + lineSuffix;
222         }
223 
224         lineFields = (List<Field>) ComponentUtils.copyFieldList(lineFields, bindingPath, lineSuffix);
225 
226 		if(lineIndex == -1 && !lineFields.isEmpty()){
227     		for(Field f: lineFields){
228     		    if(f instanceof InputField){
229     		        //sets up - skipping these fields in add area during standard form validation calls
230     		        //custom addLineToCollection js call will validate these fields manually on an add
231     		    	Control control = ((InputField) f).getControl();
232     		    	if (control != null) {
233     		    	    control.addStyleClass(collectionGroup.getFactoryId() + "-addField");
234     		    		control.addStyleClass("ignoreValid");
235     		    	}
236     		    }
237     		}
238     		for(ActionField action: actions){
239     		    if(action.getActionParameter(UifParameters.ACTION_TYPE).equals(UifParameters.ADD_LINE)){
240     		        action.setFocusOnAfterSubmit(lineFields.get(0).getId());
241     		    }
242     		}
243 		}
244 		
245 		ComponentUtils.updateContextsForLine(lineFields, currentLine, lineIndex);
246 
247 		if (bindToForm) {
248 			ComponentUtils.setComponentsPropertyDeep(lineFields, UifPropertyPaths.BIND_TO_FORM, new Boolean(true));
249 		}		
250 		
251         // remove fields from the line that have render false
252         lineFields = removeNonRenderLineFields(view, model, collectionGroup, lineFields, currentLine, lineIndex);
253 
254 		// if not add line build sub-collection field groups
255 		List<FieldGroup> subCollectionFields = new ArrayList<FieldGroup>();
256         if ((lineIndex != -1) && (collectionGroup.getSubCollections() != null)) {
257             for (int subLineIndex = 0; subLineIndex < collectionGroup.getSubCollections().size(); subLineIndex++) {
258                 CollectionGroup subCollectionPrototype = collectionGroup.getSubCollections().get(subLineIndex);
259                 CollectionGroup subCollectionGroup = ComponentUtils.copy(subCollectionPrototype, lineSuffix);
260 
261                 // verify the sub-collection should be rendered
262                 boolean renderSubCollection = checkSubCollectionRender(view, model, collectionGroup,
263                         subCollectionGroup);
264                 if (!renderSubCollection) {
265                     continue;
266                 }
267 
268                 subCollectionGroup.getBindingInfo().setBindByNamePrefix(bindingPath);
269                 if (subCollectionGroup.isRenderAddLine()) {
270                     subCollectionGroup.getAddLineBindingInfo().setBindByNamePrefix(bindingPath);
271                 }
272 
273                 // set sub-collection suffix on group so it can be used for generated groups
274                 String subCollectionSuffix = lineSuffix;
275                 if (StringUtils.isNotBlank(subCollectionGroup.getSubCollectionSuffix())) {
276                     subCollectionSuffix = subCollectionGroup.getSubCollectionSuffix() + lineSuffix;
277                 }
278                 subCollectionGroup.setSubCollectionSuffix(subCollectionSuffix);
279 
280                 FieldGroup fieldGroupPrototype = layoutManager.getSubCollectionFieldGroupPrototype();
281                 FieldGroup subCollectionFieldGroup = ComponentUtils.copy(fieldGroupPrototype,
282                         lineSuffix + UifConstants.IdSuffixes.SUB + subLineIndex);
283                 subCollectionFieldGroup.setGroup(subCollectionGroup);
284 
285                 subCollectionFields.add(subCollectionFieldGroup);
286             }
287         }
288 
289 		// invoke layout manager to build the complete line
290 		layoutManager.buildLine(view, model, collectionGroup, lineFields, subCollectionFields, bindingPath, actions,
291 				lineSuffix, currentLine, lineIndex);
292 	}
293 
294 	
295     /**
296      * Evaluates the render property for the given list of <code>Field</code>
297      * instances for the line and removes any fields from the returned list that
298      * have render false. The conditional render string is also taken into
299      * account. This needs to be done here as opposed to during the normal
300      * condition evaluation so the the fields are not used while building the
301      * collection lines
302      * 
303      * @param view
304      *            - view instance the collection group belongs to
305      * @param model
306      *            - object containing the view data
307      * @param collectionGroup
308      *            - collection group for the line fields
309      * @param lineFields
310      *            - list of fields configured for the line
311      * @param currentLine
312      *            - object containing the line data
313      * @param lineIndex
314      *            - index of the line in the collection
315      * @return List<Field> list of field instances that should be rendered
316      */
317     protected List<Field> removeNonRenderLineFields(View view, Object model, CollectionGroup collectionGroup,
318             List<Field> lineFields, Object currentLine, int lineIndex) {
319         List<Field> fields = new ArrayList<Field>();
320 
321         for (Field lineField : lineFields) {
322             String conditionalRender = lineField.getPropertyExpression("render");
323 
324             // evaluate conditional render string if set
325             if (StringUtils.isNotBlank(conditionalRender)) {
326                 Map<String, Object> context = new HashMap<String, Object>();
327                 context.putAll(view.getContext());
328                 context.put(UifConstants.ContextVariableNames.PARENT, collectionGroup);
329                 context.put(UifConstants.ContextVariableNames.COMPONENT, lineField);
330                 context.put(UifConstants.ContextVariableNames.LINE, currentLine);
331                 context.put(UifConstants.ContextVariableNames.INDEX, new Integer(lineIndex));
332                 context.put(UifConstants.ContextVariableNames.IS_ADD_LINE, new Boolean(lineIndex == -1));
333 
334                 Boolean render = (Boolean) getExpressionEvaluatorService().evaluateExpression(model, context,
335                         conditionalRender);
336                 lineField.setRender(render);
337             }
338 
339             // only add line field if set to render or if it is hidden by progressive render
340             if (lineField.isRender() || StringUtils.isNotBlank(lineField.getProgressiveRender())) {
341                 fields.add(lineField);
342             }
343         }
344 
345         return fields;
346     }
347     
348     /**
349      * Checks whether the given sub-collection should be rendered, any
350      * conditional render string is evaluated
351      * 
352      * @param view
353      *            - view instance the sub collection belongs to
354      * @param model
355      *            - object containing the view data
356      * @param collectionGroup
357      *            - collection group the sub collection belongs to
358      * @param subCollectionGroup
359      *            - sub collection group to check render status for
360      * @return boolean true if sub collection should be rendered, false if it
361      *         should not be rendered
362      */
363     protected boolean checkSubCollectionRender(View view, Object model, CollectionGroup collectionGroup,
364             CollectionGroup subCollectionGroup) {
365         String conditionalRender = subCollectionGroup.getPropertyExpression("render");
366 
367         // evaluate conditional render string if set
368         if (StringUtils.isNotBlank(conditionalRender)) {
369             Map<String, Object> context = new HashMap<String, Object>();
370             context.putAll(view.getContext());
371             context.put(UifConstants.ContextVariableNames.PARENT, collectionGroup);
372             context.put(UifConstants.ContextVariableNames.COMPONENT, subCollectionGroup);
373 
374             Boolean render = (Boolean) getExpressionEvaluatorService().evaluateExpression(model, context,
375                     conditionalRender);
376             subCollectionGroup.setRender(render);
377         }
378 
379         return subCollectionGroup.isRender();
380     }
381 
382 	/**
383 	 * Creates new <code>ActionField</code> instances for the line
384 	 * 
385 	 * <p>
386 	 * Adds context to the action fields for the given line so that the line the
387 	 * action was performed on can be determined when that action is selected
388 	 * </p>
389 	 * 
390 	 * @param view
391 	 *            - view instance the collection belongs to
392 	 * @param model
393 	 *            - top level object containing the data
394 	 * @param collectionGroup
395 	 *            - collection group component for the collection
396 	 * @param collectionLine
397 	 *            - object instance for the current line
398 	 * @param lineIndex
399 	 *            - index of the line the actions should apply to
400 	 */
401 	protected List<ActionField> getLineActions(View view, Object model, CollectionGroup collectionGroup,
402 			Object collectionLine, int lineIndex) {
403 		List<ActionField> lineActions = ComponentUtils.copyFieldList(collectionGroup.getActionFields(), Integer.toString(lineIndex));
404 		for (ActionField actionField : lineActions) {
405 			actionField.addActionParameter(UifParameters.SELLECTED_COLLECTION_PATH, collectionGroup.getBindingInfo()
406 					.getBindingPath());
407 			actionField.addActionParameter(UifParameters.SELECTED_LINE_INDEX, Integer.toString(lineIndex));
408 			actionField.setJumpToIdAfterSubmit(collectionGroup.getId() + "_div");
409 
410             actionField.setClientSideJs("performCollectionAction('"+collectionGroup.getId()+"');");
411 		}
412 
413 		ComponentUtils.updateContextsForLine(lineActions, collectionLine, lineIndex);
414 
415 		return lineActions;
416 	}
417 
418 	/**
419 	 * Creates new <code>ActionField</code> instances for the add line
420 	 * 
421 	 * <p>
422 	 * Adds context to the action fields for the add line so that the collection
423 	 * the action was performed on can be determined
424 	 * </p>
425 	 * 
426 	 * @param view
427 	 *            - view instance the collection belongs to
428 	 * @param model
429 	 *            - top level object containing the data
430 	 * @param collectionGroup
431 	 *            - collection group component for the collection
432 	 */
433 	protected List<ActionField> getAddLineActions(View view, Object model, CollectionGroup collectionGroup) {
434 		List<ActionField> lineActions = ComponentUtils.copyFieldList(collectionGroup.getAddLineActionFields(), "_add");
435 		for (ActionField actionField : lineActions) {
436 			actionField.addActionParameter(UifParameters.SELLECTED_COLLECTION_PATH, collectionGroup.getBindingInfo()
437 					.getBindingPath());
438 			//actionField.addActionParameter(UifParameters.COLLECTION_ID, collectionGroup.getId());
439 			actionField.setJumpToIdAfterSubmit(collectionGroup.getId() + "_div");
440 			actionField.addActionParameter(UifParameters.ACTION_TYPE, UifParameters.ADD_LINE);
441 
442             String baseId = collectionGroup.getFactoryId();
443             if (StringUtils.isNotBlank(collectionGroup.getSubCollectionSuffix())) {
444                 baseId += collectionGroup.getSubCollectionSuffix();
445             }
446 
447             actionField.setClientSideJs("addLineToCollection('"+collectionGroup.getId()+"', '"+ baseId +"');");
448 		}
449 
450 		// get add line for context
451 		String addLinePath = collectionGroup.getAddLineBindingInfo().getBindingPath();
452 		Object addLine = ObjectPropertyUtils.getPropertyValue(model, addLinePath);
453 
454 		ComponentUtils.updateContextsForLine(lineActions, addLine, -1);
455 
456 		return lineActions;
457 	}
458 
459     /**
460      * Initializes a new instance of the collection class
461      * 
462      * <p>
463      * If the add line property was not specified for the collection group the
464      * new lines will be added to the generic map on the
465      * <code>UifFormBase</code>, else it will be added to the property given by
466      * the addLineBindingInfo
467      * </p>
468      * 
469      * <p>
470      * New line will only be created if the current line property is null or
471      * clearExistingLine is true. In the case of a new line default values are
472      * also applied
473      * </p>
474      * 
475      * @see org.kuali.rice.krad.uif.container.CollectionGroup#
476      *      initializeNewCollectionLine(View, Object, CollectionGroup, boolean)
477      */
478     public void initializeNewCollectionLine(View view, Object model, CollectionGroup collectionGroup,
479             boolean clearExistingLine) {
480         Object newLine = null;
481 
482         // determine if we are binding to generic form map or a custom property
483         if (StringUtils.isBlank(collectionGroup.getAddLinePropertyName())) {
484             // bind to form map
485             if (!(model instanceof UifFormBase)) {
486                 throw new RuntimeException("Cannot create new collection line for group: "
487                         + collectionGroup.getPropertyName() + ". Model does not extend " + UifFormBase.class.getName());
488             }
489 
490             // get new collection line map from form
491             Map<String, Object> newCollectionLines = ObjectPropertyUtils.getPropertyValue(model,
492                     UifPropertyPaths.NEW_COLLECTION_LINES);
493             if (newCollectionLines == null) {
494                 newCollectionLines = new HashMap<String, Object>();
495                 ObjectPropertyUtils.setPropertyValue(model, UifPropertyPaths.NEW_COLLECTION_LINES, newCollectionLines);
496             }
497             
498             // set binding path for add line
499             String newCollectionLineKey = KRADUtils
500                     .translateToMapSafeKey(collectionGroup.getBindingInfo().getBindingPath());
501             String addLineBindingPath = UifPropertyPaths.NEW_COLLECTION_LINES + "['" + newCollectionLineKey + "']";
502             collectionGroup.getAddLineBindingInfo().setBindingPath(addLineBindingPath);
503 
504             // if there is not an instance available or we need to clear create
505             // a new instance
506             if (!newCollectionLines.containsKey(newCollectionLineKey)
507                     || (newCollectionLines.get(newCollectionLineKey) == null) || clearExistingLine) {
508                 // create new instance of the collection type for the add line
509                 newLine = ObjectUtils.newInstance(collectionGroup.getCollectionObjectClass());
510                 newCollectionLines.put(newCollectionLineKey, newLine);
511             }
512         } else {
513             // bind to custom property
514             Object addLine = ObjectPropertyUtils.getPropertyValue(model, collectionGroup.getAddLineBindingInfo()
515                     .getBindingPath());
516             if ((addLine == null) || clearExistingLine) {
517                 newLine = ObjectUtils.newInstance(collectionGroup.getCollectionObjectClass());
518                 ObjectPropertyUtils.setPropertyValue(model, collectionGroup.getAddLineBindingInfo().getBindingPath(),
519                         newLine);
520             }
521         }
522 
523         // apply default values if a new line was created
524         if (newLine != null) {
525             view.getViewHelperService().applyDefaultValuesForCollectionLine(view, model, collectionGroup, newLine);
526         }
527     }
528     
529     protected ExpressionEvaluatorService getExpressionEvaluatorService() {
530         return KRADServiceLocatorWeb.getExpressionEvaluatorService();
531     }
532 
533 }