View Javadoc

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