1 /**
2 * Copyright 2005-2012 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 org.apache.commons.lang.StringUtils;
19 import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
20 import org.kuali.rice.krad.uif.component.KeepExpression;
21 import org.kuali.rice.krad.uif.container.CollectionGroup;
22 import org.kuali.rice.krad.uif.container.Container;
23 import org.kuali.rice.krad.uif.container.Group;
24 import org.kuali.rice.krad.uif.field.FieldGroup;
25 import org.kuali.rice.krad.uif.view.View;
26 import org.kuali.rice.krad.uif.component.Component;
27 import org.kuali.rice.krad.uif.component.DataBinding;
28 import org.kuali.rice.krad.uif.field.ActionField;
29 import org.kuali.rice.krad.uif.field.Field;
30 import org.kuali.rice.krad.uif.util.ComponentUtils;
31 import org.kuali.rice.krad.uif.util.ObjectPropertyUtils;
32
33 import java.util.ArrayList;
34 import java.util.List;
35
36 /**
37 * Layout manager that works with <code>CollectionGroup</code> containers and
38 * renders the collection lines in a vertical row
39 *
40 * <p>
41 * For each line of the collection, a <code>Group</code> instance is created.
42 * The group header contains a label for the line (summary information), the
43 * group fields are the collection line fields, and the group footer contains
44 * the line actions. All the groups are rendered using the
45 * <code>BoxLayoutManager</code> with vertical orientation.
46 * </p>
47 *
48 * <p>
49 * Modify the lineGroupPrototype to change header/footer styles or any other
50 * customization for the line groups
51 * </p>
52 *
53 * @author Kuali Rice Team (rice.collab@kuali.org)
54 */
55 public class StackedLayoutManager extends LayoutManagerBase implements CollectionLayoutManager {
56 private static final long serialVersionUID = 4602368505430238846L;
57
58 @KeepExpression
59 private String summaryTitle;
60 private List<String> summaryFields;
61
62 private Group addLineGroup;
63 private Group lineGroupPrototype;
64 private FieldGroup subCollectionFieldGroupPrototype;
65 private Field selectFieldPrototype;
66 private Group wrapperGroup;
67
68 private List<Group> stackedGroups;
69
70 public StackedLayoutManager() {
71 super();
72
73 summaryFields = new ArrayList<String>();
74 stackedGroups = new ArrayList<Group>();
75 }
76
77 /**
78 * The following actions are performed:
79 *
80 * <ul>
81 * <li>Initializes the prototypes</li>
82 * </ul>
83 *
84 * @see org.kuali.rice.krad.uif.layout.BoxLayoutManager#performInitialization(org.kuali.rice.krad.uif.view.View,
85 * java.lang.Object, org.kuali.rice.krad.uif.container.Container)
86 */
87 @Override
88 public void performInitialization(View view, Object model, Container container) {
89 super.performInitialization(view, model, container);
90
91 stackedGroups = new ArrayList<Group>();
92
93 if (addLineGroup != null) {
94 view.getViewHelperService().performComponentInitialization(view, model, addLineGroup);
95 }
96 view.getViewHelperService().performComponentInitialization(view, model, lineGroupPrototype);
97 view.getViewHelperService().performComponentInitialization(view, model, subCollectionFieldGroupPrototype);
98 view.getViewHelperService().performComponentInitialization(view, model, selectFieldPrototype);
99 }
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 }