View Javadoc

1   /*
2    * Copyright 2007 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 1.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/ecl1.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.kns.uif.layout;
17  
18  import java.util.ArrayList;
19  import java.util.List;
20  
21  import org.kuali.rice.kns.uif.UifConstants;
22  import org.kuali.rice.kns.uif.container.CollectionGroup;
23  import org.kuali.rice.kns.uif.container.Container;
24  import org.kuali.rice.kns.uif.container.View;
25  import org.kuali.rice.kns.uif.core.Component;
26  import org.kuali.rice.kns.uif.field.ActionField;
27  import org.kuali.rice.kns.uif.field.AttributeField;
28  import org.kuali.rice.kns.uif.field.Field;
29  import org.kuali.rice.kns.uif.field.GroupField;
30  import org.kuali.rice.kns.uif.field.LabelField;
31  import org.kuali.rice.kns.uif.field.MessageField;
32  import org.kuali.rice.kns.uif.util.ComponentFactory;
33  import org.kuali.rice.kns.uif.util.ComponentUtils;
34  import org.kuali.rice.kns.uif.widget.TableTools;
35  
36  /**
37   * Layout manager that works with <code>CollectionGroup</code> components and
38   * renders the collection as a Table
39   * 
40   * <p>
41   * Based on the fields defined, the <code>TableLayoutManager</code> will
42   * dynamically create instances of the fields for each collection row. In
43   * addition, the manager can create standard fields like the action and sequence
44   * fields for each row. The manager supports options inherited from the
45   * <code>GridLayoutManager</code> such as rowSpan, colSpan, and cell width
46   * settings.
47   * </p>
48   * 
49   * @author Kuali Rice Team (rice.collab@kuali.org)
50   */
51  public class TableLayoutManager extends GridLayoutManager implements CollectionLayoutManager {
52  	private static final long serialVersionUID = 3622267585541524208L;
53  
54  	private boolean useShortLabels;
55  	private boolean repeatHeader;
56  	private LabelField headerFieldPrototype;
57  
58  	private boolean renderSequenceField;
59  	private String conditionalRenderSequenceField;
60  	private boolean generateAutoSequence;
61  	private Field sequenceFieldPrototype;
62  
63  	private GroupField actionFieldPrototype;
64  
65  	private GroupField subCollectionGroupFieldPrototype;
66  
67  	// internal counter for the data columns (not including sequence, action)
68  	private int numberOfDataColumns;
69  
70  	private List<LabelField> headerFields;
71  	private List<Field> dataFields;
72  
73  	private TableTools tableTools;
74  	private boolean headerAdded = false;
75  
76  	public TableLayoutManager() {
77  		useShortLabels = true;
78  		repeatHeader = false;
79  		renderSequenceField = true;
80  		generateAutoSequence = false;
81  
82  		headerFields = new ArrayList<LabelField>();
83  		dataFields = new ArrayList<Field>();
84  	}
85  	
86  	/**
87  	 * The following actions are performed:
88  	 * 
89  	 * <ul>
90  	 * <li>Sets sequence field prototype if auto sequence is true</li>
91  	 * <li>Initializes the prototypes</li>
92  	 * </ul>
93  	 * 
94  	 * @see org.kuali.rice.kns.uif.layout.BoxLayoutManager#performInitialization(org.kuali.rice.kns.uif.container.View,
95  	 *      org.kuali.rice.kns.uif.container.Container)
96  	 */
97  	@Override
98  	public void performInitialization(View view, Container container) {
99  		super.performInitialization(view, container);
100 		
101         if (generateAutoSequence && !(sequenceFieldPrototype instanceof MessageField)) {
102             sequenceFieldPrototype = ComponentFactory.getMessageField();
103         }
104 
105 		view.getViewHelperService().performComponentInitialization(view, headerFieldPrototype);
106 		view.getViewHelperService().performComponentInitialization(view, sequenceFieldPrototype);
107 		view.getViewHelperService().performComponentInitialization(view, actionFieldPrototype);
108 		view.getViewHelperService().performComponentInitialization(view, subCollectionGroupFieldPrototype);
109 	}
110 
111 	/**
112 	 * Sets up the final column count for rendering based on whether the
113 	 * sequence and action fields have been generated
114 	 * 
115 	 * @see org.kuali.rice.kns.uif.layout.LayoutManagerBase#performFinalize(org.kuali.rice.kns.uif.container.View,
116 	 *      java.lang.Object, org.kuali.rice.kns.uif.container.Container)
117 	 */
118 	@Override
119 	public void performFinalize(View view, Object model, Container container) {
120 		super.performFinalize(view, model, container);
121 
122 		CollectionGroup collectionGroup = (CollectionGroup) container;
123 
124 		int totalColumns = getNumberOfDataColumns();
125 		if (renderSequenceField) {
126 			totalColumns++;
127 		}
128 
129 		if (collectionGroup.isRenderLineActions() && !collectionGroup.isReadOnly()) {
130 			totalColumns++;
131 		}
132 
133 		setNumberOfColumns(totalColumns);
134 
135 	}
136 
137 	/**
138 	 * Assembles the field instances for the collection line. The given sequence
139 	 * field prototype is copied for the line sequence field. Likewise a copy of
140 	 * the actionFieldPrototype is made and the given actions are set as the
141 	 * items for the action field. Finally the generated items are assembled
142 	 * together into the dataFields list with the given lineFields.
143 	 * 
144 	 * @see org.kuali.rice.kns.uif.layout.CollectionLayoutManager#buildLine(org.kuali.rice.kns.uif.container.View,
145 	 *      java.lang.Object, org.kuali.rice.kns.uif.container.CollectionGroup,
146 	 *      java.util.List, java.util.List, java.lang.String, java.util.List,
147 	 *      java.lang.String, java.lang.Object, int)
148 	 */
149 	public void buildLine(View view, Object model, CollectionGroup collectionGroup, List<Field> lineFields,
150 			List<GroupField> subCollectionFields, String bindingPath, List<ActionField> actions, String idSuffix,
151 			Object currentLine, int lineIndex) {
152 		boolean isAddLine = lineIndex == -1;
153 		
154         // if add line or first line set number of data columns
155         if (isAddLine || ((!collectionGroup.isRenderAddLine() || collectionGroup.isReadOnly()) && (lineIndex == 0))) {
156             if (isSuppressLineWrapping()) {
157                 setNumberOfDataColumns(lineFields.size());
158             } else {
159                 setNumberOfDataColumns(getNumberOfColumns());
160             }
161         }
162 
163 		// if add line build table header first
164 		// TODO: implement repeat header
165 		if (!headerAdded) {
166 			headerFields = new ArrayList<LabelField>();
167 			dataFields = new ArrayList<Field>();
168 
169 			buildTableHeaderRows(collectionGroup, lineFields);
170 			ComponentUtils.pushObjectToContext(headerFields, UifConstants.ContextVariableNames.LINE, currentLine);
171 			ComponentUtils.pushObjectToContext(headerFields, UifConstants.ContextVariableNames.INDEX, new Integer(
172 					lineIndex));
173 			headerAdded = true;
174 		}
175 
176 		// set label field rendered to true on line fields
177 		for (Field field : lineFields) {
178 			field.setLabelFieldRendered(true);
179 
180 			// don't display summary message
181 			// TODO: remove once we have modifier
182 			ComponentUtils.setComponentPropertyDeep(field, "summaryMessageField.render", new Boolean(false));
183 		}
184 
185 		int rowCount = calculateNumberOfRows(collectionGroup.getItems());
186 		int rowSpan = rowCount + subCollectionFields.size();
187 
188 		// sequence field is always first and should span all rows for the line
189 		if (renderSequenceField) {
190 			Field sequenceField = null;
191             if (!isAddLine) {
192                 sequenceField = ComponentUtils.copy(sequenceFieldPrototype, idSuffix);
193 
194                 if (generateAutoSequence && (sequenceField instanceof MessageField)) {
195                     ((MessageField) sequenceField).setMessageText(Integer.toString(lineIndex + 1));
196                 }
197             }
198 			else {
199 				sequenceField = ComponentUtils.copy(collectionGroup.getAddLineLabelField(), idSuffix);
200 			}
201 			sequenceField.setRowSpan(rowSpan);
202 
203 			if (sequenceField instanceof AttributeField) {
204 				((AttributeField) sequenceField).getBindingInfo().setBindByNamePrefix(bindingPath);
205 			}
206 
207 			ComponentUtils.updateContextForLine(sequenceField, currentLine, lineIndex);
208 
209 			dataFields.add(sequenceField);
210 		}
211 
212 		// now add the fields in the correct position
213 		int cellPosition = 0;
214 		for (Field lineField : lineFields) {
215 			dataFields.add(lineField);
216 
217 			cellPosition += lineField.getColSpan();
218 
219 			// action field should be in last column
220 			if ((cellPosition == getNumberOfDataColumns()) && collectionGroup.isRenderLineActions()
221 					&& !collectionGroup.isReadOnly()) {
222 				GroupField lineActionsField = ComponentUtils.copy(actionFieldPrototype, idSuffix);
223 
224 				ComponentUtils.updateContextForLine(lineActionsField, currentLine, lineIndex);
225 				lineActionsField.setRowSpan(rowSpan);
226 				lineActionsField.setItems(actions);
227 
228 				dataFields.add(lineActionsField);
229 			}
230 		}
231 
232 		// update colspan on sub-collection fields
233 		for (GroupField subCollectionField : subCollectionFields) {
234 			subCollectionField.setColSpan(numberOfDataColumns);
235 		}
236 
237 		// add sub-collection fields to end of data fields
238 		dataFields.addAll(subCollectionFields);
239 	}
240 
241 	/**
242 	 * Create the <code>LabelField</code> instances that will be used to render
243 	 * the table header
244 	 * 
245 	 * <p>
246 	 * For each column, a copy of headerFieldPrototype is made that determines
247 	 * the label configuration. The actual label text comes from the field for
248 	 * which the header applies to. The first column is always the sequence (if
249 	 * enabled) and the last column contains the actions. Both the sequence and
250 	 * action header fields will span all rows for the header.
251 	 * </p>
252 	 * 
253 	 * <p>
254 	 * The headerFields list will contain the final list of header fields built
255 	 * </p>
256 	 * 
257 	 * @param collectionGroup
258 	 *            - CollectionGroup container the table applies to
259 	 * @param lineFields - fields for the data columns from which the headers are pulled
260 	 */
261 	protected void buildTableHeaderRows(CollectionGroup collectionGroup, List<Field> lineFields) {
262 		// row count needed to determine the row span for the sequence and
263 		// action fields, since they should span all rows for the line
264 		int rowCount = calculateNumberOfRows(collectionGroup.getItems());
265 
266 		// first column is sequence label
267 		if (renderSequenceField) {
268 			sequenceFieldPrototype.setLabelFieldRendered(true);
269 			sequenceFieldPrototype.setRowSpan(rowCount);
270 			addHeaderField(sequenceFieldPrototype, 1);
271 		}
272 
273 		// pull out label fields from the container's items
274 		int cellPosition = 0;
275 		for (Field field : lineFields) {
276 		    if (!field.isRender()) {
277 		        continue;
278 		    }
279 		    
280 			cellPosition += field.getColSpan();
281 			addHeaderField(field, cellPosition);
282 
283 			// add action header as last column in row
284 			if ((cellPosition == getNumberOfDataColumns()) && collectionGroup.isRenderLineActions()
285 					&& !collectionGroup.isReadOnly()) {
286 				actionFieldPrototype.setLabelFieldRendered(true);
287 				actionFieldPrototype.setRowSpan(rowCount);
288 				addHeaderField(actionFieldPrototype, cellPosition);
289 			}
290 		}
291 	}
292 
293 	/**
294 	 * Creates a new instance of the header field prototype and then sets the
295 	 * label to the short (if useShortLabels is set to true) or long label of
296 	 * the given component. After created the header field is added to the list
297 	 * making up the table header
298 	 * 
299 	 * @param field
300 	 *            - field instance the header field is being created for
301 	 * @param column
302 	 *            - column number for the header, used for setting the id
303 	 */
304 	protected void addHeaderField(Field field, int column) {
305 		LabelField headerField = ComponentUtils.copy(headerFieldPrototype, "_c" + column);
306 		if (useShortLabels) {
307 			headerField.setLabelText(field.getLabel());
308 		}
309 		else {
310 			headerField.setLabelText(field.getLabel());
311 		}
312 
313 		headerField.setRowSpan(field.getRowSpan());
314 		headerField.setColSpan(field.getColSpan());
315 
316 		if ((field.getRequired() != null) && field.getRequired().booleanValue()) {
317 			headerField.getRequiredMessageField().setRender(true);
318 		}
319 		else {
320 			headerField.getRequiredMessageField().setRender(false);
321 		}
322 
323 		headerFields.add(headerField);
324 	}
325 
326 	/**
327 	 * Calculates how many rows will be needed per collection line to display
328 	 * the list of fields. Assumption is made that the total number of cells the
329 	 * fields take up is evenly divisible by the configured number of columns
330 	 * 
331 	 * @param items
332 	 *            - list of items that make up one collection line
333 	 * @return int number of rows
334 	 */
335 	protected int calculateNumberOfRows(List<? extends Field> items) {
336 		int rowCount = 0;
337 		
338 		// check flag that indicates only one row should be created
339 		if (isSuppressLineWrapping()) {
340 		    return 1;
341 		}
342 
343 		int cellCount = 0;
344 		for (Field field : items) {
345 			cellCount += field.getColSpan() + field.getRowSpan() - 1;
346 		}
347 
348 		if (cellCount != 0) {
349 			rowCount = cellCount / getNumberOfDataColumns();
350 		}
351 
352 		return rowCount;
353 	}
354 
355 	/**
356 	 * @see org.kuali.rice.kns.uif.layout.ContainerAware#getSupportedContainer()
357 	 */
358 	@Override
359 	public Class<? extends Container> getSupportedContainer() {
360 		return CollectionGroup.class;
361 	}
362 
363 	/**
364 	 * @see org.kuali.rice.kns.uif.layout.LayoutManagerBase#getNestedComponents()
365 	 */
366 	@Override
367 	public List<Component> getNestedComponents() {
368 		List<Component> components = super.getNestedComponents();
369 
370 		components.add(tableTools);
371 		components.addAll(headerFields);
372 		components.addAll(dataFields);
373 
374 		return components;
375 	}
376 
377 	/**
378 	 * Indicates whether the short label for the collection field should be used
379 	 * as the table header or the regular label
380 	 * 
381 	 * @return boolean true if short label should be used, false if long label
382 	 *         should be used
383 	 */
384 	public boolean isUseShortLabels() {
385 		return this.useShortLabels;
386 	}
387 
388 	/**
389 	 * Setter for the use short label indicator
390 	 * 
391 	 * @param useShortLabels
392 	 */
393 	public void setUseShortLabels(boolean useShortLabels) {
394 		this.useShortLabels = useShortLabels;
395 	}
396 
397 	/**
398 	 * Indicates whether the header should be repeated before each collection
399 	 * row. If false the header is only rendered at the beginning of the table
400 	 * 
401 	 * @return boolean true if header should be repeated, false if it should
402 	 *         only be rendered once
403 	 */
404 	public boolean isRepeatHeader() {
405 		return this.repeatHeader;
406 	}
407 
408 	/**
409 	 * Setter for the repeat header indicator
410 	 * 
411 	 * @param repeatHeader
412 	 */
413 	public void setRepeatHeader(boolean repeatHeader) {
414 		this.repeatHeader = repeatHeader;
415 	}
416 
417 	/**
418 	 * <code>LabelField</code> instance to use as a prototype for creating the
419 	 * tables header fields. For each header field the prototype will be copied
420 	 * and adjusted as necessary
421 	 * 
422 	 * @return LabelField instance to serve as prototype
423 	 */
424 	public LabelField getHeaderFieldPrototype() {
425 		return this.headerFieldPrototype;
426 	}
427 
428 	/**
429 	 * Setter for the header field prototype
430 	 * 
431 	 * @param headerFieldPrototype
432 	 */
433 	public void setHeaderFieldPrototype(LabelField headerFieldPrototype) {
434 		this.headerFieldPrototype = headerFieldPrototype;
435 	}
436 
437 	/**
438 	 * List of <code>LabelField</code> instances that should be rendered to make
439 	 * up the tables header
440 	 * 
441 	 * @return List of label field instances
442 	 */
443 	public List<LabelField> getHeaderFields() {
444 		return this.headerFields;
445 	}
446 
447 	/**
448 	 * Indicates whether the sequence field should be rendered for the
449 	 * collection
450 	 * 
451 	 * @return boolean true if sequence field should be rendered, false if not
452 	 */
453 	public boolean isRenderSequenceField() {
454 		return this.renderSequenceField;
455 	}
456 
457 	/**
458 	 * Setter for the render sequence field indicator
459 	 * 
460 	 * @param renderSequenceField
461 	 */
462 	public void setRenderSequenceField(boolean renderSequenceField) {
463 		this.renderSequenceField = renderSequenceField;
464 	}
465 
466 	/**
467 	 * Expression language string for conditionally setting the render sequence
468 	 * field property
469 	 * 
470 	 * @return String el that should evaluate to boolean
471 	 */
472 	public String getConditionalRenderSequenceField() {
473 		return this.conditionalRenderSequenceField;
474 	}
475 
476 	/**
477 	 * Setter for the conditional render sequence field string
478 	 * 
479 	 * @param conditionalRenderSequenceField
480 	 */
481 	public void setConditionalRenderSequenceField(String conditionalRenderSequenceField) {
482 		this.conditionalRenderSequenceField = conditionalRenderSequenceField;
483 	}
484 
485 	/**
486 	 * Attribute name to use as sequence value. For each collection line the
487 	 * value of this field on the line will be retrieved and used as the
488 	 * sequence value
489 	 * 
490 	 * @return String sequence property name
491 	 */
492     public String getSequencePropertyName() {
493         if ((sequenceFieldPrototype != null) && (sequenceFieldPrototype instanceof AttributeField)) {
494             return ((AttributeField) sequenceFieldPrototype).getPropertyName();
495         }
496 
497         return null;
498     }
499 
500     /**
501      * Setter for the sequence property name
502      * 
503      * @param sequencePropertyName
504      */
505     public void setSequencePropertyName(String sequencePropertyName) {
506         if ((sequenceFieldPrototype != null) && (sequenceFieldPrototype instanceof AttributeField)) {
507             ((AttributeField) sequenceFieldPrototype).setPropertyName(sequencePropertyName);
508         }
509     }
510 	
511     /**
512      * Indicates whether the sequence field should be generated with the current
513      * line number
514      * 
515      * <p>
516      * If set to true the sequence field prototype will be changed to a message
517      * field (if not already a message field) and the text will be set to the
518      * current line number
519      * </p>
520      * 
521      * @return boolean true if the sequence field should be generated from the
522      *         line number, false if not
523      */
524     public boolean isGenerateAutoSequence() {
525         return this.generateAutoSequence;
526     }
527 
528     /**
529      * Setter for the generate auto sequence field
530      * 
531      * @param generateAutoSequence
532      */
533     public void setGenerateAutoSequence(boolean generateAutoSequence) {
534         this.generateAutoSequence = generateAutoSequence;
535     }
536 
537     /**
538 	 * <code>Field</code> instance to serve as a prototype for the
539 	 * sequence field. For each collection line this instance is copied and
540 	 * adjusted as necessary
541 	 * 
542 	 * @return Attribute field instance
543 	 */
544 	public Field getSequenceFieldPrototype() {
545 		return this.sequenceFieldPrototype;
546 	}
547 
548 	/**
549 	 * Setter for the sequence field prototype
550 	 * 
551 	 * @param sequenceFieldPrototype
552 	 */
553 	public void setSequenceFieldPrototype(Field sequenceFieldPrototype) {
554 		this.sequenceFieldPrototype = sequenceFieldPrototype;
555 	}
556 
557 	/**
558 	 * <code>GroupField</code> instance to serve as a prototype for the actions
559 	 * column. For each collection line this instance is copied and adjusted as
560 	 * necessary. Note the actual actions for the group come from the collection
561 	 * groups actions List
562 	 * (org.kuali.rice.kns.uif.container.CollectionGroup.getActionFields()). The
563 	 * GroupField prototype is useful for setting styling of the actions column
564 	 * and for the layout of the action fields. Note also the label associated
565 	 * with the prototype is used for the action column header
566 	 * 
567 	 * @return GroupField instance
568 	 */
569 	public GroupField getActionFieldPrototype() {
570 		return this.actionFieldPrototype;
571 	}
572 
573 	/**
574 	 * Setter for the action field prototype
575 	 * 
576 	 * @param actionFieldPrototype
577 	 */
578 	public void setActionFieldPrototype(GroupField actionFieldPrototype) {
579 		this.actionFieldPrototype = actionFieldPrototype;
580 	}
581 
582 	/**
583 	 * @see org.kuali.rice.kns.uif.layout.CollectionLayoutManager#getSubCollectionGroupFieldPrototype()
584 	 */
585 	public GroupField getSubCollectionGroupFieldPrototype() {
586 		return this.subCollectionGroupFieldPrototype;
587 	}
588 
589 	/**
590 	 * Setter for the sub-collection field group prototype
591 	 * 
592 	 * @param subCollectionGroupFieldPrototype
593 	 */
594 	public void setSubCollectionGroupFieldPrototype(GroupField subCollectionGroupFieldPrototype) {
595 		this.subCollectionGroupFieldPrototype = subCollectionGroupFieldPrototype;
596 	}
597 
598 	/**
599 	 * List of <code>Field</code> instances that make up the tables body. Pulled
600 	 * by the layout manager template to send through the Grid layout
601 	 * 
602 	 * @return List<Field> table body fields
603 	 */
604 	public List<Field> getDataFields() {
605 		return this.dataFields;
606 	}
607 
608 	/**
609 	 * Widget associated with the table to add functionality such as sorting,
610 	 * paging, and export
611 	 * 
612 	 * @return TableTools instance
613 	 */
614 	public TableTools getTableTools() {
615 		return this.tableTools;
616 	}
617 
618 	/**
619 	 * Setter for the table tools widget
620 	 * 
621 	 * @param tableTools
622 	 */
623 	public void setTableTools(TableTools tableTools) {
624 		this.tableTools = tableTools;
625 	}
626 
627 	/**
628      * @return the numberOfDataColumns
629      */
630     public int getNumberOfDataColumns() {
631     	return this.numberOfDataColumns;
632     }
633 
634 	/**
635      * @param numberOfDataColumns the numberOfDataColumns to set
636      */
637     public void setNumberOfDataColumns(int numberOfDataColumns) {
638     	this.numberOfDataColumns = numberOfDataColumns;
639     }
640 
641 }