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