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    }