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