001    /**
002     * Copyright 2005-2012 The Kuali Foundation
003     *
004     * Licensed under the Educational Community License, Version 2.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/ecl2.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.layout;
017    
018    import org.apache.commons.lang.StringUtils;
019    import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
020    import org.kuali.rice.krad.uif.component.KeepExpression;
021    import org.kuali.rice.krad.uif.container.CollectionGroup;
022    import org.kuali.rice.krad.uif.container.Container;
023    import org.kuali.rice.krad.uif.container.Group;
024    import org.kuali.rice.krad.uif.element.Action;
025    import org.kuali.rice.krad.uif.field.FieldGroup;
026    import org.kuali.rice.krad.uif.view.View;
027    import org.kuali.rice.krad.uif.component.Component;
028    import org.kuali.rice.krad.uif.component.DataBinding;
029    import org.kuali.rice.krad.uif.field.Field;
030    import org.kuali.rice.krad.uif.util.ComponentUtils;
031    import org.kuali.rice.krad.uif.util.ObjectPropertyUtils;
032    import org.kuali.rice.krad.web.form.UifFormBase;
033    
034    import java.util.ArrayList;
035    import java.util.List;
036    
037    /**
038     * Layout manager that works with {@code CollectionGroup} containers and
039     * renders the collection lines in a vertical row
040     *
041     * <p>
042     * For each line of the collection, a {@code Group} instance is created.
043     * The group header contains a label for the line (summary information), the
044     * group fields are the collection line fields, and the group footer contains
045     * the line actions. All the groups are rendered using the
046     * {@code BoxLayoutManager} with vertical orientation.
047     * </p>
048     *
049     * <p>
050     * Modify the lineGroupPrototype to change header/footer styles or any other
051     * customization for the line groups
052     * </p>
053     *
054     * @author Kuali Rice Team (rice.collab@kuali.org)
055     */
056    public class StackedLayoutManager extends LayoutManagerBase implements CollectionLayoutManager {
057        private static final long serialVersionUID = 4602368505430238846L;
058    
059        @KeepExpression
060        private String summaryTitle;
061        private List<String> summaryFields;
062    
063        private Group addLineGroup;
064        private Group lineGroupPrototype;
065        private FieldGroup subCollectionFieldGroupPrototype;
066        private Field selectFieldPrototype;
067        private Group wrapperGroup;
068    
069        private List<Group> stackedGroups;
070    
071        public StackedLayoutManager() {
072            super();
073    
074            summaryFields = new ArrayList<String>();
075            stackedGroups = new ArrayList<Group>();
076        }
077    
078        /**
079         * The following actions are performed:
080         *
081         * <ul>
082         * <li>Initializes the prototypes</li>
083         * </ul>
084         *
085         * @see org.kuali.rice.krad.uif.layout.BoxLayoutManager#performInitialization(org.kuali.rice.krad.uif.view.View,
086         *      java.lang.Object, org.kuali.rice.krad.uif.container.Container)
087         */
088        @Override
089        public void performInitialization(View view, Object model, Container container) {
090            super.performInitialization(view, model, container);
091    
092            stackedGroups = new ArrayList<Group>();
093    
094            if (addLineGroup != null) {
095                view.getViewHelperService().performComponentInitialization(view, model, addLineGroup);
096            }
097            view.getViewHelperService().performComponentInitialization(view, model, lineGroupPrototype);
098            view.getViewHelperService().performComponentInitialization(view, model, subCollectionFieldGroupPrototype);
099            view.getViewHelperService().performComponentInitialization(view, model, selectFieldPrototype);
100        }
101    
102        /**
103         * The following actions are performed:
104         *
105         * <ul>
106         * <li>If wrapper group is specified, places the stacked groups into the wrapper</li>
107         * </ul>
108         *
109         * @see org.kuali.rice.krad.uif.layout.BoxLayoutManager#performApplyModel(org.kuali.rice.krad.uif.view.View,
110         *      java.lang.Object, org.kuali.rice.krad.uif.container.Container)
111         */
112        @Override
113        public void performApplyModel(View view, Object model, Container container) {
114            super.performApplyModel(view, model, container);
115    
116            if (wrapperGroup != null) {
117                wrapperGroup.setItems(stackedGroups);
118            }
119        }
120    
121        /**
122         * Builds a {@code Group} instance for a collection line. The group is
123         * built by first creating a copy of the configured prototype. Then the
124         * header for the group is created using the configured summary fields on
125         * the {@code CollectionGroup}. The line fields passed in are set as
126         * the items for the group, and finally the actions are placed into the
127         * group footer
128         *
129         * @see CollectionLayoutManager#buildLine(org.kuali.rice.krad.uif.view.View,
130         *      Object, org.kuali.rice.krad.uif.container.CollectionGroup,
131         *      java.util.List, java.util.List, String, java.util.List,
132         *      String, Object, int)
133         */
134        public void buildLine(View view, Object model, CollectionGroup collectionGroup, List<Field> lineFields,
135                List<FieldGroup> subCollectionFields, String bindingPath, List<Action> actions, String idSuffix,
136                Object currentLine, int lineIndex) {
137            boolean isAddLine = lineIndex == -1;
138    
139            if (isAddLine && collectionGroup.isRenderAddBlankLineButton()) {
140                return;
141            }
142    
143            // construct new group
144            Group lineGroup = null;
145            if (isAddLine) {
146                stackedGroups = new ArrayList<Group>();
147    
148                if (addLineGroup == null) {
149                    lineGroup = ComponentUtils.copy(lineGroupPrototype, idSuffix);
150                } else {
151                    lineGroup = ComponentUtils.copy(getAddLineGroup(), idSuffix);
152                    lineGroup.addStyleClass(collectionGroup.getAddItemCssClass());
153                }
154    
155                if (collectionGroup.isAddViaLightBox()) {
156                    String actionScript = "showLightboxComponent('" + lineGroup.getId() +  "');";
157                    if (StringUtils.isNotBlank(collectionGroup.getAddViaLightBoxAction().getActionScript())) {
158                        actionScript = collectionGroup.getAddViaLightBoxAction().getActionScript() + actionScript;
159                    }
160                    collectionGroup.getAddViaLightBoxAction().setActionScript(actionScript);
161                    lineGroup.setStyle("display: none");
162                }
163            } else {
164                lineGroup = ComponentUtils.copy(lineGroupPrototype, idSuffix);
165            }
166    
167            if (((UifFormBase)model).isAddedCollectionItem(currentLine)) {
168                lineGroup.addStyleClass(collectionGroup.getNewItemsCssClass());
169            }
170            
171            ComponentUtils.updateContextForLine(lineGroup, currentLine, lineIndex);
172    
173            // build header text for group
174            String headerText = "";
175            if (isAddLine) {
176                headerText = collectionGroup.getAddLabel();
177            } else {
178                // get the collection for this group from the model
179                List<Object> modelCollection = ObjectPropertyUtils.getPropertyValue(model,
180                        ((DataBinding) collectionGroup).getBindingInfo().getBindingPath());
181    
182                headerText = buildLineHeaderText(modelCollection.get(lineIndex), lineGroup);
183            }
184    
185            // don't set header if text is blank (could already be set by other means)
186            if (StringUtils.isNotBlank(headerText) && lineGroup.getHeader() != null) {
187                lineGroup.getHeader().setHeaderText(headerText);
188            }
189    
190            // stack all fields (including sub-collections) for the group
191            List<Field> groupFields = new ArrayList<Field>();
192            groupFields.addAll(lineFields);
193            groupFields.addAll(subCollectionFields);
194    
195            lineGroup.setItems(groupFields);
196    
197            // set line actions on group footer
198            if (collectionGroup.isRenderLineActions() && !collectionGroup.isReadOnly() && (lineGroup.getFooter() != null)) {
199                lineGroup.getFooter().setItems(actions);
200            }
201    
202            stackedGroups.add(lineGroup);
203        }
204    
205        /**
206         * Builds the header text for the collection line
207         *
208         * <p>
209         * Header text is built up by first the collection label, either specified
210         * on the collection definition or retrieved from the dictionary. Then for
211         * each summary field defined, the value from the model is retrieved and
212         * added to the header.
213         * </p>
214         *
215         * <p>
216         * Note the {@link #getSummaryTitle()} field may have expressions defined, in which cause it will be copied to the
217         * property expressions map to set the title for the line group (which will have the item context variable set)
218         * </p>
219         *
220         * @param line - Collection line containing data
221         * @param lineGroup - Group instance for rendering the line and whose title should be built
222         * @return String header text for line
223         */
224        protected String buildLineHeaderText(Object line, Group lineGroup) {
225            // check for expression on summary title
226            if (KRADServiceLocatorWeb.getExpressionEvaluatorService().containsElPlaceholder(summaryTitle)) {
227                lineGroup.getPropertyExpressions().put("title", summaryTitle);
228                return null;
229            }
230    
231            // build up line summary from declared field values and fixed title
232            String summaryFieldString = "";
233            for (String summaryField : summaryFields) {
234                Object summaryFieldValue = ObjectPropertyUtils.getPropertyValue(line, summaryField);
235                if (StringUtils.isNotBlank(summaryFieldString)) {
236                    summaryFieldString += " - ";
237                }
238    
239                if (summaryFieldValue != null) {
240                    summaryFieldString += summaryFieldValue;
241                } else {
242                    summaryFieldString += "Null";
243                }
244            }
245    
246            String headerText = summaryTitle;
247            if (StringUtils.isNotBlank(summaryFieldString)) {
248                headerText += " ( " + summaryFieldString + " )";
249            }
250    
251            return headerText;
252        }
253    
254        /**
255         * @see org.kuali.rice.krad.uif.layout.LayoutManager#getSupportedContainer()
256         */
257        @Override
258        public Class<? extends Container> getSupportedContainer() {
259            return CollectionGroup.class;
260        }
261    
262        /**
263         * @see org.kuali.rice.krad.uif.layout.LayoutManagerBase#getComponentsForLifecycle()
264         */
265        @Override
266        public List<Component> getComponentsForLifecycle() {
267            List<Component> components = super.getComponentsForLifecycle();
268    
269            if (wrapperGroup != null) {
270                components.add(wrapperGroup);
271            } else {
272                components.addAll(stackedGroups);
273            }
274    
275            return components;
276        }
277    
278        /**
279         * @see org.kuali.rice.krad.uif.layout.LayoutManager#getComponentPrototypes()
280         */
281        @Override
282        public List<Component> getComponentPrototypes() {
283            List<Component> components = super.getComponentPrototypes();
284    
285            components.add(addLineGroup);
286            components.add(lineGroupPrototype);
287            components.add(subCollectionFieldGroupPrototype);
288            components.add(selectFieldPrototype);
289    
290            return components;
291        }
292    
293        /**
294         * Text to appears in the header for each collection lines Group. Used in
295         * conjunction with {@link #getSummaryFields()} to build up the final header
296         * text
297         *
298         * @return String summary title text
299         */
300        public String getSummaryTitle() {
301            return this.summaryTitle;
302        }
303    
304        /**
305         * Setter for the summary title text
306         *
307         * @param summaryTitle
308         */
309        public void setSummaryTitle(String summaryTitle) {
310            this.summaryTitle = summaryTitle;
311        }
312    
313        /**
314         * List of attribute names from the collection line class that should be
315         * used to build the line summary. To build the summary the value for each
316         * attribute is retrieved from the line instance. All the values are then
317         * placed together with a separator.
318         *
319         * @return List<String> summary field names
320         * @see #buildLineHeaderText(Object, org.kuali.rice.krad.uif.container.Group)
321         */
322        public List<String> getSummaryFields() {
323            return this.summaryFields;
324        }
325    
326        /**
327         * Setter for the summary field name list
328         *
329         * @param summaryFields
330         */
331        public void setSummaryFields(List<String> summaryFields) {
332            this.summaryFields = summaryFields;
333        }
334    
335        /**
336         * Group instance that will be used for the add line
337         *
338         * <p>
339         * Add line fields and actions configured on the
340         * {@code CollectionGroup} will be set onto the add line group (if add
341         * line is enabled). If the add line group is not configured, a new instance
342         * of the line group prototype will be used for the add line.
343         * </p>
344         *
345         * @return Group add line group instance
346         * @see #getAddLineGroup()
347         */
348        public Group getAddLineGroup() {
349            return this.addLineGroup;
350        }
351    
352        /**
353         * Setter for the add line group
354         *
355         * @param addLineGroup
356         */
357        public void setAddLineGroup(Group addLineGroup) {
358            this.addLineGroup = addLineGroup;
359        }
360    
361        /**
362         * Group instance that is used as a prototype for creating the collection
363         * line groups. For each line a copy of the prototype is made and then
364         * adjusted as necessary
365         *
366         * @return Group instance to use as prototype
367         */
368        public Group getLineGroupPrototype() {
369            return this.lineGroupPrototype;
370        }
371    
372        /**
373         * Setter for the line group prototype
374         *
375         * @param lineGroupPrototype
376         */
377        public void setLineGroupPrototype(Group lineGroupPrototype) {
378            this.lineGroupPrototype = lineGroupPrototype;
379        }
380    
381        /**
382         * @see org.kuali.rice.krad.uif.layout.CollectionLayoutManager#getSubCollectionFieldGroupPrototype()
383         */
384        public FieldGroup getSubCollectionFieldGroupPrototype() {
385            return this.subCollectionFieldGroupPrototype;
386        }
387    
388        /**
389         * Setter for the sub-collection field group prototype
390         *
391         * @param subCollectionFieldGroupPrototype
392         */
393        public void setSubCollectionFieldGroupPrototype(FieldGroup subCollectionFieldGroupPrototype) {
394            this.subCollectionFieldGroupPrototype = subCollectionFieldGroupPrototype;
395        }
396    
397        /**
398         * Field instance that serves as a prototype for creating the select field on each line when
399         * {@link org.kuali.rice.krad.uif.container.CollectionGroup#isIncludeLineSelectionField()} is true
400         *
401         * <p>
402         * This prototype can be used to set the control used for the select field (generally will be a checkbox control)
403         * in addition to styling and other setting. The binding path will be formed with using the
404         * {@link org.kuali.rice.krad.uif.container.CollectionGroup#getLineSelectPropertyName()} or if not set the framework
405         * will use {@link org.kuali.rice.krad.web.form.UifFormBase#getSelectedCollectionLines()}
406         * </p>
407         *
408         * @return Field select field prototype instance
409         */
410        public Field getSelectFieldPrototype() {
411            return selectFieldPrototype;
412        }
413    
414        /**
415         * Setter for the prototype instance for select fields
416         *
417         * @param selectFieldPrototype
418         */
419        public void setSelectFieldPrototype(Field selectFieldPrototype) {
420            this.selectFieldPrototype = selectFieldPrototype;
421        }
422    
423        /**
424         * Group that will 'wrap' the generated collection lines so that they have a different layout from the general
425         * stacked layout
426         *
427         * <p>
428         * By default (when the wrapper group is null), each collection line will become a group and the groups are
429         * rendered one after another. If the wrapper group is configured, the generated groups will be inserted as the
430         * items for the wrapper group, and the layout manager configured for the wrapper group will determine how they
431         * are rendered. For example, the layout manager could be a grid layout configured for three columns, which would
432         * layout the first three lines horizontally then break to a new row.
433         * </p>
434         *
435         * @return Group instance whose items list should be populated with the generated groups, or null to use the
436         *         default layout
437         */
438        public Group getWrapperGroup() {
439            return wrapperGroup;
440        }
441    
442        /**
443         * Setter for the wrapper group that will receive the generated line groups
444         *
445         * @param wrapperGroup
446         */
447        public void setWrapperGroup(Group wrapperGroup) {
448            this.wrapperGroup = wrapperGroup;
449        }
450    
451        /**
452         * Final {@code List} of Groups to render for the collection
453         *
454         * @return List<Group> collection groups
455         */
456        public List<Group> getStackedGroups() {
457            return this.stackedGroups;
458        }
459    
460        /**
461         * Setter for the collection groups
462         *
463         * @param stackedGroups
464         */
465        public void setStackedGroups(List<Group> stackedGroups) {
466            this.stackedGroups = stackedGroups;
467        }
468    
469    }