View Javadoc

1   /**
2    * Copyright 2005-2014 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.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/ecl2.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.layout;
17  
18  import com.google.common.collect.Lists;
19  import org.apache.commons.lang.StringUtils;
20  import org.kuali.rice.krad.datadictionary.parse.BeanTag;
21  import org.kuali.rice.krad.datadictionary.parse.BeanTagAttribute;
22  import org.kuali.rice.krad.datadictionary.parse.BeanTags;
23  import org.kuali.rice.krad.uif.UifPropertyPaths;
24  import org.kuali.rice.krad.uif.component.Component;
25  import org.kuali.rice.krad.uif.component.DataBinding;
26  import org.kuali.rice.krad.uif.component.KeepExpression;
27  import org.kuali.rice.krad.uif.container.CollectionGroup;
28  import org.kuali.rice.krad.uif.container.Container;
29  import org.kuali.rice.krad.uif.container.Group;
30  import org.kuali.rice.krad.uif.element.Action;
31  import org.kuali.rice.krad.uif.field.Field;
32  import org.kuali.rice.krad.uif.field.FieldGroup;
33  import org.kuali.rice.krad.uif.util.ComponentUtils;
34  import org.kuali.rice.krad.uif.util.ObjectPropertyUtils;
35  import org.kuali.rice.krad.uif.view.View;
36  import org.kuali.rice.krad.uif.widget.Pager;
37  import org.kuali.rice.krad.web.form.UifFormBase;
38  
39  import java.util.ArrayList;
40  import java.util.List;
41  
42  /**
43   * Layout manager that works with {@code CollectionGroup} containers and
44   * renders the collection lines in a vertical row
45   *
46   * <p>
47   * For each line of the collection, a {@code Group} instance is created.
48   * The group header contains a label for the line (summary information), the
49   * group fields are the collection line fields, and the group footer contains
50   * the line actions. All the groups are rendered using the
51   * {@code BoxLayoutManager} with vertical orientation.
52   * </p>
53   *
54   * <p>
55   * Modify the lineGroupPrototype to change header/footer styles or any other
56   * customization for the line groups
57   * </p>
58   *
59   * @author Kuali Rice Team (rice.collab@kuali.org)
60   */
61  @BeanTags({@BeanTag(name = "stackedCollectionLayout-bean", parent = "Uif-StackedCollectionLayoutBase"),
62          @BeanTag(name = "stackedCollectionLayout-withGridItems-bean",
63                  parent = "Uif-StackedCollectionLayout-WithGridItems"),
64          @BeanTag(name = "stackedCollectionLayout-withBoxItems-bean",
65                  parent = "Uif-StackedCollectionLayout-WithBoxItems"),
66          @BeanTag(name = "stackedCollectionLayout-list-bean", parent = "Uif-StackedCollectionLayout-List")})
67  public class StackedLayoutManager extends LayoutManagerBase implements CollectionLayoutManager {
68      private static final long serialVersionUID = 4602368505430238846L;
69  
70      @KeepExpression
71      private String summaryTitle;
72      private List<String> summaryFields;
73  
74      private Group addLineGroup;
75      private Group lineGroupPrototype;
76      private FieldGroup subCollectionFieldGroupPrototype;
77      private Field selectFieldPrototype;
78      private Group wrapperGroup;
79      private Pager pagerWidget;
80  
81      private List<Group> stackedGroups;
82  
83      private boolean actionsInLineGroup;
84  
85      public StackedLayoutManager() {
86          super();
87  
88          summaryFields = new ArrayList<String>();
89          stackedGroups = new ArrayList<Group>();
90      }
91  
92      /**
93       * The following actions are performed:
94       *
95       * <ul>
96       * <li>Initializes the prototypes</li>
97       * </ul>
98       *
99       * @see org.kuali.rice.krad.uif.layout.BoxLayoutManager#performInitialization(org.kuali.rice.krad.uif.view.View,
100      *      java.lang.Object, org.kuali.rice.krad.uif.container.Container)
101      */
102     @Override
103     public void performInitialization(View view, Object model, Container container) {
104         super.performInitialization(view, model, container);
105 
106         stackedGroups = new ArrayList<Group>();
107     }
108 
109     /**
110      * The following actions are performed:
111      *
112      * <ul>
113      * <li>If wrapper group is specified, places the stacked groups into the wrapper</li>
114      * </ul>
115      *
116      * @see org.kuali.rice.krad.uif.layout.BoxLayoutManager#performApplyModel(org.kuali.rice.krad.uif.view.View,
117      *      java.lang.Object, org.kuali.rice.krad.uif.container.Container)
118      */
119     @Override
120     public void performApplyModel(View view, Object model, Container container) {
121         super.performApplyModel(view, model, container);
122 
123         if (wrapperGroup != null) {
124             wrapperGroup.setItems(stackedGroups);
125         }
126     }
127 
128     /**
129      * Calculates the values that must be pased into the pagerWidget if using paging
130      *
131      * @see BoxLayoutManager#performFinalize(org.kuali.rice.krad.uif.view.View, Object,
132      *      org.kuali.rice.krad.uif.container.Container)
133      */
134     @Override
135     public void performFinalize(View view, Object model, Container container) {
136         super.performFinalize(view, model, container);
137 
138         // Calculate the number of pages for the pager widget if we are using server paging
139         if (container instanceof CollectionGroup
140                 && ((CollectionGroup) container).isUseServerPaging()
141                 && this.getPagerWidget() != null) {
142             CollectionGroup collectionGroup = (CollectionGroup) container;
143 
144             // Set the appropriate page, total pages, and link script into the Pager
145             CollectionLayoutUtils.setupPagerWidget(pagerWidget, collectionGroup, model);
146         }
147     }
148 
149     /**
150      * Builds a {@code Group} instance for a collection line. The group is
151      * built by first creating a copy of the configured prototype. Then the
152      * header for the group is created using the configured summary fields on
153      * the {@code CollectionGroup}. The line fields passed in are set as
154      * the items for the group, and finally the actions are placed into the
155      * group footer
156      *
157      * @see CollectionLayoutManager#buildLine(org.kuali.rice.krad.uif.view.View,
158      *      Object, org.kuali.rice.krad.uif.container.CollectionGroup,
159      *      java.util.List, java.util.List, String, java.util.List,
160      *      String, Object, int)
161      */
162     public void buildLine(View view, Object model, CollectionGroup collectionGroup, List<Field> lineFields,
163             List<FieldGroup> subCollectionFields, String bindingPath, List<Action> actions, String idSuffix,
164             Object currentLine, int lineIndex) {
165         boolean isAddLine = lineIndex == -1;
166 
167         // construct new group
168         Group lineGroup = null;
169         if (isAddLine) {
170             stackedGroups = new ArrayList<Group>();
171 
172             if (addLineGroup == null) {
173                 lineGroup = ComponentUtils.copy(lineGroupPrototype, idSuffix);
174             } else {
175                 lineGroup = ComponentUtils.copy(getAddLineGroup(), idSuffix);
176                 lineGroup.addStyleClass(collectionGroup.getAddItemCssClass());
177             }
178 
179             if (collectionGroup.isAddViaLightBox()) {
180                 String actionScript = "showLightboxComponent('" + lineGroup.getId() + "');";
181                 if (StringUtils.isNotBlank(collectionGroup.getAddViaLightBoxAction().getActionScript())) {
182                     actionScript = collectionGroup.getAddViaLightBoxAction().getActionScript() + actionScript;
183                 }
184                 collectionGroup.getAddViaLightBoxAction().setActionScript(actionScript);
185                 lineGroup.setStyle("display: none");
186             }
187         } else {
188             lineGroup = ComponentUtils.copy(lineGroupPrototype, idSuffix);
189         }
190 
191         if (((UifFormBase) model).isAddedCollectionItem(currentLine)) {
192             lineGroup.addStyleClass(collectionGroup.getNewItemsCssClass());
193         }
194 
195         ComponentUtils.updateContextForLine(lineGroup, currentLine, lineIndex, idSuffix);
196 
197         // build header for the group
198         if (isAddLine) {
199             if (lineGroup.getHeader() != null) {
200                 lineGroup.getHeader().setRichHeaderMessage(collectionGroup.getAddLineLabel());
201             }
202         } else {
203             // get the collection for this group from the model
204             List<Object> modelCollection = ObjectPropertyUtils.getPropertyValue(model,
205                     ((DataBinding) collectionGroup).getBindingInfo().getBindingPath());
206 
207             String headerText = buildLineHeaderText(view, modelCollection.get(lineIndex), lineGroup);
208 
209             // don't set header if text is blank (could already be set by other means)
210             if (StringUtils.isNotBlank(headerText) && lineGroup.getHeader() != null) {
211                 lineGroup.getHeader().setHeaderText(headerText);
212             }
213         }
214 
215         // stack all fields (including sub-collections) for the group
216         List<Component> groupFields = new ArrayList<Component>();
217         groupFields.addAll(lineFields);
218         groupFields.addAll(subCollectionFields);
219 
220         // set line actions on group footer
221         if (collectionGroup.isRenderLineActions() && !collectionGroup.isReadOnly() && (lineGroup.getFooter() != null)) {
222             // add the actions to the line group if isActionsInLineGroup flag is true
223             if (isActionsInLineGroup()) {
224                 groupFields.addAll(actions);
225                 lineGroup.setRenderFooter(false);
226             } else {
227                 lineGroup.getFooter().setItems(actions);
228             }
229         }
230 
231         lineGroup.setItems(groupFields);
232 
233         stackedGroups.add(lineGroup);
234     }
235 
236     /**
237      * Builds the header text for the collection line
238      *
239      * <p>
240      * Header text is built up by first the collection label, either specified
241      * on the collection definition or retrieved from the dictionary. Then for
242      * each summary field defined, the value from the model is retrieved and
243      * added to the header.
244      * </p>
245      *
246      * <p>
247      * Note the {@link #getSummaryTitle()} field may have expressions defined, in which cause it will be copied to the
248      * property expressions map to set the title for the line group (which will have the item context variable set)
249      * </p>
250      *
251      * @param view view instance the collection belongs to, used to get the expression evaluator
252      * @param line Collection line containing data
253      * @param lineGroup Group instance for rendering the line and whose title should be built
254      * @return header text for line
255      */
256     protected String buildLineHeaderText(View view, Object line, Group lineGroup) {
257         // check for expression on summary title
258         if (view.getViewHelperService().getExpressionEvaluator().containsElPlaceholder(summaryTitle)) {
259             lineGroup.getPropertyExpressions().put(UifPropertyPaths.HEADER_TEXT, summaryTitle);
260             return null;
261         }
262 
263         // build up line summary from declared field values and fixed title
264         String summaryFieldString = "";
265         for (String summaryField : summaryFields) {
266             Object summaryFieldValue = ObjectPropertyUtils.getPropertyValue(line, summaryField);
267             if (StringUtils.isNotBlank(summaryFieldString)) {
268                 summaryFieldString += " - ";
269             }
270 
271             if (summaryFieldValue != null) {
272                 summaryFieldString += summaryFieldValue;
273             } else {
274                 summaryFieldString += "Null";
275             }
276         }
277 
278         String headerText = summaryTitle;
279         if (StringUtils.isNotBlank(summaryFieldString)) {
280             headerText += " ( " + summaryFieldString + " )";
281         }
282 
283         return headerText;
284     }
285 
286     /**
287      * @see org.kuali.rice.krad.uif.layout.LayoutManager#getSupportedContainer()
288      */
289     @Override
290     public Class<? extends Container> getSupportedContainer() {
291         return CollectionGroup.class;
292     }
293 
294     /**
295      * @see org.kuali.rice.krad.uif.layout.LayoutManagerBase#getComponentsForLifecycle()
296      */
297     @Override
298     public List<Component> getComponentsForLifecycle() {
299         List<Component> components = super.getComponentsForLifecycle();
300 
301         if (wrapperGroup != null) {
302             components.add(wrapperGroup);
303         } else {
304             components.addAll(stackedGroups);
305         }
306 
307         if (pagerWidget != null) {
308             components.add(pagerWidget);
309         }
310 
311         return components;
312     }
313 
314     /**
315      * @see org.kuali.rice.krad.uif.layout.LayoutManager#getComponentPrototypes()
316      */
317     @Override
318     public List<Component> getComponentPrototypes() {
319         List<Component> components = super.getComponentPrototypes();
320 
321         components.add(addLineGroup);
322         components.add(lineGroupPrototype);
323         components.add(subCollectionFieldGroupPrototype);
324         components.add(selectFieldPrototype);
325 
326         return components;
327     }
328 
329     /**
330      * Text to appears in the header for each collection lines Group. Used in
331      * conjunction with {@link #getSummaryFields()} to build up the final header
332      * text
333      *
334      * @return summary title text
335      */
336     @BeanTagAttribute(name = "summaryTitle")
337     public String getSummaryTitle() {
338         return this.summaryTitle;
339     }
340 
341     /**
342      * Setter for the summary title text
343      *
344      * @param summaryTitle
345      */
346     public void setSummaryTitle(String summaryTitle) {
347         this.summaryTitle = summaryTitle;
348     }
349 
350     /**
351      * List of attribute names from the collection line class that should be
352      * used to build the line summary. To build the summary the value for each
353      * attribute is retrieved from the line instance. All the values are then
354      * placed together with a separator.
355      *
356      * @return summary field names
357      * @see #buildLineHeaderText(Object, org.kuali.rice.krad.uif.container.Group)
358      */
359     @BeanTagAttribute(name = "summaryFields", type = BeanTagAttribute.AttributeType.LISTVALUE)
360     public List<String> getSummaryFields() {
361         return this.summaryFields;
362     }
363 
364     /**
365      * Setter for the summary field name list
366      *
367      * @param summaryFields
368      */
369     public void setSummaryFields(List<String> summaryFields) {
370         this.summaryFields = summaryFields;
371     }
372 
373     /**
374      * Group instance that will be used for the add line
375      *
376      * <p>
377      * Add line fields and actions configured on the
378      * {@code CollectionGroup} will be set onto the add line group (if add
379      * line is enabled). If the add line group is not configured, a new instance
380      * of the line group prototype will be used for the add line.
381      * </p>
382      *
383      * @return add line group instance
384      * @see #getAddLineGroup()
385      */
386     @BeanTagAttribute(name = "addLineGroup", type = BeanTagAttribute.AttributeType.SINGLEBEAN)
387     public Group getAddLineGroup() {
388         return this.addLineGroup;
389     }
390 
391     /**
392      * Setter for the add line group
393      *
394      * @param addLineGroup
395      */
396     public void setAddLineGroup(Group addLineGroup) {
397         this.addLineGroup = addLineGroup;
398     }
399 
400     /**
401      * Group instance that is used as a prototype for creating the collection
402      * line groups. For each line a copy of the prototype is made and then
403      * adjusted as necessary
404      *
405      * @return Group instance to use as prototype
406      */
407     @BeanTagAttribute(name = "lineGroupPrototype", type = BeanTagAttribute.AttributeType.SINGLEBEAN)
408     public Group getLineGroupPrototype() {
409         return this.lineGroupPrototype;
410     }
411 
412     /**
413      * Setter for the line group prototype
414      *
415      * @param lineGroupPrototype
416      */
417     public void setLineGroupPrototype(Group lineGroupPrototype) {
418         this.lineGroupPrototype = lineGroupPrototype;
419     }
420 
421     /**
422      * @see org.kuali.rice.krad.uif.layout.CollectionLayoutManager#getSubCollectionFieldGroupPrototype()
423      */
424     @BeanTagAttribute(name = "subCollectionFieldGroupPrototype", type = BeanTagAttribute.AttributeType.SINGLEBEAN)
425     public FieldGroup getSubCollectionFieldGroupPrototype() {
426         return this.subCollectionFieldGroupPrototype;
427     }
428 
429     /**
430      * Setter for the sub-collection field group prototype
431      *
432      * @param subCollectionFieldGroupPrototype
433      */
434     public void setSubCollectionFieldGroupPrototype(FieldGroup subCollectionFieldGroupPrototype) {
435         this.subCollectionFieldGroupPrototype = subCollectionFieldGroupPrototype;
436     }
437 
438     /**
439      * Field instance that serves as a prototype for creating the select field on each line when
440      * {@link org.kuali.rice.krad.uif.container.CollectionGroup#isIncludeLineSelectionField()} is true
441      *
442      * <p>
443      * This prototype can be used to set the control used for the select field (generally will be a checkbox control)
444      * in addition to styling and other setting. The binding path will be formed with using the
445      * {@link org.kuali.rice.krad.uif.container.CollectionGroup#getLineSelectPropertyName()} or if not set the
446      * framework
447      * will use {@link org.kuali.rice.krad.web.form.UifFormBase#getSelectedCollectionLines()}
448      * </p>
449      *
450      * @return select field prototype instance
451      */
452     @BeanTagAttribute(name = "selectFieldPrototype", type = BeanTagAttribute.AttributeType.SINGLEBEAN)
453     public Field getSelectFieldPrototype() {
454         return selectFieldPrototype;
455     }
456 
457     /**
458      * Setter for the prototype instance for select fields
459      *
460      * @param selectFieldPrototype
461      */
462     public void setSelectFieldPrototype(Field selectFieldPrototype) {
463         this.selectFieldPrototype = selectFieldPrototype;
464     }
465 
466     /**
467      * Group that will 'wrap' the generated collection lines so that they have a different layout from the general
468      * stacked layout
469      *
470      * <p>
471      * By default (when the wrapper group is null), each collection line will become a group and the groups are
472      * rendered one after another. If the wrapper group is configured, the generated groups will be inserted as the
473      * items for the wrapper group, and the layout manager configured for the wrapper group will determine how they
474      * are rendered. For example, the layout manager could be a grid layout configured for three columns, which would
475      * layout the first three lines horizontally then break to a new row.
476      * </p>
477      *
478      * @return Group instance whose items list should be populated with the generated groups, or null to use the
479      *         default layout
480      */
481     @BeanTagAttribute(name = "wrapperGroup", type = BeanTagAttribute.AttributeType.SINGLEBEAN)
482     public Group getWrapperGroup() {
483         return wrapperGroup;
484     }
485 
486     /**
487      * Setter for the wrapper group that will receive the generated line groups
488      *
489      * @param wrapperGroup
490      */
491     public void setWrapperGroup(Group wrapperGroup) {
492         this.wrapperGroup = wrapperGroup;
493     }
494 
495     /**
496      * The pagerWidget used for paging when the StackedLayout is using paging
497      *
498      * @return the pagerWidget
499      */
500     public Pager getPagerWidget() {
501         return pagerWidget;
502     }
503 
504     /**
505      * Set the pagerWidget used for paging StackedLayouts
506      *
507      * @param pagerWidget
508      */
509     public void setPagerWidget(Pager pagerWidget) {
510         this.pagerWidget = pagerWidget;
511     }
512 
513     /**
514      * Final {@code List} of Groups to render for the collection
515      *
516      * @return collection groups
517      */
518     @BeanTagAttribute(name = "stackedGroups", type = BeanTagAttribute.AttributeType.LISTBEAN)
519     public List<Group> getStackedGroups() {
520         return this.stackedGroups;
521     }
522 
523     /**
524      * Setter for the collection groups
525      *
526      * @param stackedGroups
527      */
528     public void setStackedGroups(List<Group> stackedGroups) {
529         this.stackedGroups = stackedGroups;
530     }
531 
532     /**
533      * Flag that indicates whether actions will be added in the same group as the line items instead of in the
534      * footer of the line group
535      *
536      * @return boolean
537      */
538     public boolean isActionsInLineGroup() {
539         return actionsInLineGroup;
540     }
541 
542     /**
543      * Set flag to add actions in the same group as the line items
544      *
545      * @param actionsInLineGroup
546      */
547     public void setActionsInLineGroup(boolean actionsInLineGroup) {
548         this.actionsInLineGroup = actionsInLineGroup;
549     }
550 
551     /**
552      * @see org.kuali.rice.krad.uif.component.ComponentBase#copy()
553      */
554     @Override
555     protected <T> void copyProperties(T layoutManager) {
556         super.copyProperties(layoutManager);
557 
558         StackedLayoutManager stackedLayoutManagerCopy = (StackedLayoutManager) layoutManager;
559 
560         stackedLayoutManagerCopy.setSummaryTitle(this.getSummaryTitle());
561 
562         if (summaryFields != null) {
563             stackedLayoutManagerCopy.setSummaryFields(new ArrayList<String>(summaryFields));
564         }
565 
566         if (this.addLineGroup != null) {
567             stackedLayoutManagerCopy.setAddLineGroup((Group) this.getAddLineGroup().copy());
568         }
569 
570         if (this.lineGroupPrototype != null) {
571             stackedLayoutManagerCopy.setLineGroupPrototype((Group) this.getLineGroupPrototype().copy());
572         }
573 
574         if (this.wrapperGroup != null) {
575             stackedLayoutManagerCopy.setWrapperGroup((Group) this.getWrapperGroup().copy());
576         }
577 
578         if (this.subCollectionFieldGroupPrototype != null) {
579             stackedLayoutManagerCopy.setSubCollectionFieldGroupPrototype(
580                     (FieldGroup) this.getSubCollectionFieldGroupPrototype().copy());
581         }
582 
583         if (this.selectFieldPrototype != null) {
584             stackedLayoutManagerCopy.setSelectFieldPrototype((Field) this.getSelectFieldPrototype().copy());
585         }
586 
587         if (this.stackedGroups != null) {
588             List<Group> stackedGroupsCopy = Lists.newArrayListWithExpectedSize(stackedGroups.size());
589             for (Group stackedGroup : stackedGroups) {
590                 stackedGroupsCopy.add((Group) stackedGroup.copy());
591             }
592             stackedLayoutManagerCopy.setStackedGroups(stackedGroupsCopy);
593         }
594 
595         stackedLayoutManagerCopy.setPagerWidget((Pager) this.getPagerWidget().copy());
596 
597         stackedLayoutManagerCopy.setActionsInLineGroup(this.isActionsInLineGroup());
598     }
599 }