View Javadoc

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.datadictionary.parse.BeanTag;
20  import org.kuali.rice.krad.datadictionary.parse.BeanTagAttribute;
21  import org.kuali.rice.krad.datadictionary.parse.BeanTags;
22  import org.kuali.rice.krad.datadictionary.validator.ValidationTrace;
23  import org.kuali.rice.krad.uif.UifConstants;
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.element.Label;
32  import org.kuali.rice.krad.uif.field.DataField;
33  import org.kuali.rice.krad.uif.field.Field;
34  import org.kuali.rice.krad.uif.field.FieldGroup;
35  import org.kuali.rice.krad.uif.field.InputField;
36  import org.kuali.rice.krad.uif.field.MessageField;
37  import org.kuali.rice.krad.uif.util.ColumnCalculationInfo;
38  import org.kuali.rice.krad.uif.util.ComponentFactory;
39  import org.kuali.rice.krad.uif.util.ComponentUtils;
40  import org.kuali.rice.krad.uif.util.ExpressionUtils;
41  import org.kuali.rice.krad.uif.view.View;
42  import org.kuali.rice.krad.uif.widget.RichTable;
43  import org.kuali.rice.krad.web.form.UifFormBase;
44  
45  import java.util.ArrayList;
46  import java.util.List;
47  import java.util.Set;
48  import java.util.TreeMap;
49  
50  /**
51   * Layout manager that works with {@code CollectionGroup} components and
52   * renders the collection as a Table
53   *
54   * <p>
55   * Based on the fields defined, the {@code TableLayoutManager} will
56   * dynamically create instances of the fields for each collection row. In
57   * addition, the manager can create standard fields like the action and sequence
58   * fields for each row. The manager supports options inherited from the
59   * {@code GridLayoutManager} such as rowSpan, colSpan, and cell width
60   * settings.
61   * </p>
62   *
63   * @author Kuali Rice Team (rice.collab@kuali.org)
64   */
65  @BeanTag(name = "tableCollectionLayout", parent = "Uif-TableCollectionLayout")
66  public class TableLayoutManager extends GridLayoutManager implements CollectionLayoutManager {
67      private static final long serialVersionUID = 3622267585541524208L;
68  
69      private boolean useShortLabels;
70      private boolean repeatHeader;
71      private Label headerLabelPrototype;
72  
73      private boolean renderSequenceField;
74      private boolean generateAutoSequence;
75      private Field sequenceFieldPrototype;
76  
77      private FieldGroup actionFieldPrototype;
78      private FieldGroup subCollectionFieldGroupPrototype;
79      private Field selectFieldPrototype;
80  
81      private boolean separateAddLine;
82      private Group addLineGroup;
83  
84      // internal counter for the data columns (not including sequence, action)
85      private int numberOfDataColumns;
86  
87      private List<Label> headerLabels;
88      private List<Component> dataFields;
89  
90      private RichTable richTable;
91      private boolean headerAdded;
92  
93      private int actionColumnIndex = -1;
94      private String actionColumnPlacement;
95  
96      //row details properties
97      private Group rowDetailsGroup;
98      private String rowDetailsLinkName = "Details";
99      private boolean rowDetailsSwapActionImage;
100     private boolean rowDetailsOpen;
101     private boolean showToggleAllDetails;
102     private Action toggleAllDetailsAction;
103     private boolean ajaxDetailsRetrieval;
104     private Action expandDetailsActionPrototype;
105 
106     //grouping properties
107     @KeepExpression
108     private String groupingTitle;
109     private String groupingPrefix;
110     private int groupingColumnIndex;
111     private List<String> groupingPropertyNames;
112 
113     //total properties
114     private boolean renderOnlyLeftTotalLabels;
115     private boolean showTotal;
116     private boolean showPageTotal;
117     private boolean showGroupTotal;
118     private boolean generateGroupTotalRows;
119     private Label totalLabel;
120     private Label pageTotalLabel;
121     private Label groupTotalLabelPrototype;
122 
123     private List<String> columnsToCalculate;
124     private List<ColumnCalculationInfo> columnCalculations;
125     private List<Component> footerCalculationComponents;
126 
127     public TableLayoutManager() {
128         useShortLabels = false;
129         repeatHeader = false;
130         renderSequenceField = true;
131         generateAutoSequence = false;
132         separateAddLine = false;
133         rowDetailsOpen = false;
134 
135         headerLabels = new ArrayList<Label>();
136         dataFields = new ArrayList<Component>();
137         columnsToCalculate = new ArrayList<String>();
138         columnCalculations = new ArrayList<ColumnCalculationInfo>();
139     }
140 
141     /**
142      * The following actions are performed:
143      *
144      * <ul>
145      * <li>Sets sequence field prototype if auto sequence is true</li>
146      * <li>Initializes the prototypes</li>
147      * </ul>
148      *
149      * @see org.kuali.rice.krad.uif.layout.BoxLayoutManager#performInitialization(org.kuali.rice.krad.uif.view.View,
150      *      java.lang.Object, org.kuali.rice.krad.uif.container.Container)
151      */
152     @Override
153     public void performInitialization(View view, Object model, Container container) {
154         CollectionGroup collectionGroup = (CollectionGroup) container;
155 
156         this.setupDetails(collectionGroup, view);
157         this.setupGrouping(collectionGroup, view);
158 
159         if (collectionGroup.isAddViaLightBox()) {
160             setSeparateAddLine(true);
161         }
162 
163         super.performInitialization(view, model, container);
164 
165         getRowCssClasses().clear();
166 
167         if (generateAutoSequence && !(getSequenceFieldPrototype() instanceof MessageField)) {
168             sequenceFieldPrototype = ComponentFactory.getMessageField();
169             view.assignComponentIds(getSequenceFieldPrototype());
170         }
171     }
172 
173     /**
174      * performApplyModel override.  Takes expressions that may be set in the columnCalculation objects
175      * and populates them correctly into those component's propertyExpressions.
176      *
177      * @param view view instance to which the layout manager belongs
178      * @param model Top level object containing the data (could be the form or a
179      * top level business object, dto)
180      * @param container
181      */
182     @Override
183     public void performApplyModel(View view, Object model, Container container) {
184         super.performApplyModel(view, model, container);
185 
186         for (ColumnCalculationInfo cInfo : columnCalculations) {
187             ExpressionUtils.populatePropertyExpressionsFromGraph(cInfo, false);
188         }
189     }
190 
191     /**
192      * Sets up the final column count for rendering based on whether the
193      * sequence and action fields have been generated, sets up column calculations, and richTable rowGrouping
194      * options
195      *
196      * @see org.kuali.rice.krad.uif.layout.LayoutManagerBase#performFinalize(org.kuali.rice.krad.uif.view.View,
197      *      java.lang.Object, org.kuali.rice.krad.uif.container.Container)
198      */
199     @Override
200     public void performFinalize(View view, Object model, Container container) {
201         super.performFinalize(view, model, container);
202 
203         UifFormBase formBase = (UifFormBase) model;
204 
205         CollectionGroup collectionGroup = (CollectionGroup) container;
206 
207         int totalColumns = getNumberOfDataColumns();
208         if (renderSequenceField) {
209             totalColumns++;
210         }
211 
212         if (collectionGroup.isIncludeLineSelectionField()) {
213             totalColumns++;
214         }
215 
216         if (collectionGroup.isRenderLineActions() && !collectionGroup.isReadOnly()) {
217             totalColumns++;
218         }
219 
220         setNumberOfColumns(totalColumns);
221 
222         // if add line event, add highlighting for added row
223         if (UifConstants.ActionEvents.ADD_LINE.equals(formBase.getActionEvent())) {
224             String highlightScript = "jQuery(\"#" + container.getId() + " tr:first\").effect(\"highlight\",{}, 6000);";
225             String onReadyScript = collectionGroup.getOnDocumentReadyScript();
226             if (StringUtils.isNotBlank(onReadyScript)) {
227                 highlightScript = onReadyScript + highlightScript;
228             }
229             collectionGroup.setOnDocumentReadyScript(highlightScript);
230         }
231 
232         //setup the column calculations functionality and components
233         if (columnCalculations != null && richTable != null &&
234                 this.getDataFields() != null && !this.getDataFields().isEmpty()) {
235             setupColumnCalculations(view, model, container, totalColumns);
236         }
237 
238         //set the js properties for rowGrouping on richTables
239         if ((groupingPropertyNames != null || StringUtils.isNotBlank(this.getGroupingTitle())) && richTable != null) {
240             richTable.setGroupingOptionsJSString("{iGroupingColumnIndex: "
241                     + groupingColumnIndex
242                     + ", bGenerateGroupTotalRows:"
243                     + this.generateGroupTotalRows
244                     + ", bSetGroupingClassOnTR: true"
245                     + ", sGroupingClass: 'uif-groupRow'"
246                     + (this.getGroupingPrefix() != null ? ", sGroupLabelPrefix: '" + this.getGroupingPrefix() + "'" :
247                     "")
248                     + "}");
249         }
250     }
251 
252     /**
253      * Sets up the grouping MessageField to be used in the first column of the table layout for grouping
254      * collection content into groups based on values of the line's fields
255      *
256      * @param collectionGroup collection group for this layout
257      * @param view the view
258      */
259     private void setupGrouping(CollectionGroup collectionGroup, View view) {
260         //Grouping setup
261         String groupingTitleExpression = "";
262         if (StringUtils.isNotBlank(this.getPropertyExpression("groupingTitle"))) {
263             groupingTitleExpression = this.getPropertyExpression("groupingTitle");
264             this.setGroupingTitle(this.getPropertyExpression("groupingTitle"));
265         } else if (this.getGroupingPropertyNames() != null) {
266 
267             for (String propertyName : this.getGroupingPropertyNames()) {
268                 groupingTitleExpression = groupingTitleExpression + ", " + propertyName;
269             }
270 
271             groupingTitleExpression = groupingTitleExpression.replaceFirst(", ", "@{#lp.");
272             groupingTitleExpression = groupingTitleExpression.replace(", ", "}, @{#lp.");
273             groupingTitleExpression = groupingTitleExpression.trim() + "}";
274         }
275 
276         if (StringUtils.isNotBlank(groupingTitleExpression)) {
277             MessageField groupingMessageField = ComponentFactory.getColGroupingField();
278             groupingMessageField.getMessage().getPropertyExpressions().put("messageText", groupingTitleExpression);
279             groupingMessageField.setLabel("Group");
280 
281             view.assignComponentIds(groupingMessageField);
282 
283             List<Component> theItems = new ArrayList<Component>();
284             theItems.add(groupingMessageField);
285             theItems.addAll(collectionGroup.getItems());
286             collectionGroup.setItems(theItems);
287         }
288     }
289 
290     /**
291      * Setup the column calculations functionality and components
292      *
293      * @param view the view
294      * @param model the model
295      * @param container the parent container
296      * @param totalColumns total number of columns in the table
297      */
298     private void setupColumnCalculations(View view, Object model, Container container, int totalColumns) {
299         footerCalculationComponents = new ArrayList<Component>(totalColumns);
300 
301         //add nulls for each column to start - nulls will be processed by the ftl as a blank cell
302         for (int i = 0; i < totalColumns; i++) {
303             footerCalculationComponents.add(null);
304         }
305 
306         int leftLabelColumnIndex = 0;
307         if (groupingPropertyNames != null || StringUtils.isNotBlank(this.getGroupingTitle())) {
308             leftLabelColumnIndex = 1;
309         }
310 
311         //process each column calculation
312         for (ColumnCalculationInfo cInfo : columnCalculations) {
313             //propertyName is REQUIRED throws exception if not set
314             if (StringUtils.isNotBlank(cInfo.getPropertyName())) {
315                 for (int i = 0; i < this.getNumberOfColumns(); i++) {
316                     Component component = this.getDataFields().get(i);
317                     if (component != null && component instanceof DataField &&
318                             ((DataField) component).getPropertyName().equals(cInfo.getPropertyName())) {
319                         cInfo.setColumnNumber(i);
320                     }
321                 }
322                 this.getColumnsToCalculate().add(cInfo.getColumnNumber().toString());
323             } else {
324                 throw new RuntimeException("TableLayoutManager(" + container.getId() + "->" + this.getId() +
325                         ") ColumnCalculationInfo MUST have a propertyName set");
326             }
327 
328             FieldGroup calculationFieldGroup;
329             List<Component> groupItems;
330             Component columnComponent = footerCalculationComponents.get(cInfo.getColumnNumber());
331 
332             //Create a new field group to hold the totals fields
333             calculationFieldGroup = ComponentFactory.getFieldGroup();
334             calculationFieldGroup.addDataAttribute("role", "totalsBlock");
335             groupItems = new ArrayList<Component>();
336 
337             //setup page total field and add it to footer's group for this column
338             if (cInfo.isShowPageTotal()) {
339                 Field pageTotalDataField = setupTotalField(cInfo.getPageTotalField(), cInfo, this.isShowPageTotal(),
340                         this.getPageTotalLabel(), "pageTotal", leftLabelColumnIndex);
341                 groupItems.add(pageTotalDataField);
342             }
343 
344             //setup total field and add it to footer's group for this column
345             if (cInfo.isShowTotal()) {
346                 Field totalDataField = setupTotalField(cInfo.getTotalField(), cInfo, this.isShowTotal(),
347                         this.getTotalLabel(), "total", leftLabelColumnIndex);
348                 /*                if(((MessageField)totalDataField).getMessage().getMessageText().contains("@{")){
349                     ((MessageField)totalDataField).getMessage().getPropertyExpressions().put("messageText",
350                             ((MessageField)totalDataField).getMessage().getMessageText());
351                 }*/
352                 if (!cInfo.isRecalculateTotalClientside()) {
353                     totalDataField.addDataAttribute("skipTotal", "true");
354                 }
355 
356                 groupItems.add(totalDataField);
357             }
358 
359             //setup total field and add it to footer's group for this column
360             //do not generate group total rows if group totals are not being shown
361             if (cInfo.isShowGroupTotal()) {
362                 Field groupTotalDataField = setupTotalField(cInfo.getGroupTotalFieldPrototype(), cInfo,
363                         this.isShowGroupTotal(), this.getGroupTotalLabelPrototype(), "groupTotal",
364                         leftLabelColumnIndex);
365                 groupTotalDataField.setId(container.getId() + "_gTotal" + cInfo.getColumnNumber());
366                 groupTotalDataField.setStyle("display: none;");
367                 groupItems.add(groupTotalDataField);
368 
369                 if (this.isRenderOnlyLeftTotalLabels() && !this.isShowGroupTotal()) {
370                     generateGroupTotalRows = false;
371                 } else {
372                     generateGroupTotalRows = true;
373                 }
374             }
375 
376             calculationFieldGroup.setItems(groupItems);
377             view.assignComponentIds(calculationFieldGroup);
378 
379             //Determine if there is already a fieldGroup present for this column's footer
380             //if so create a new group and add the new calculation fields to the already existing ones
381             //otherwise just add it
382             Component component = footerCalculationComponents.get(cInfo.getColumnNumber());
383             if (component != null && component instanceof FieldGroup) {
384                 Group verticalComboCalcGroup = ComponentFactory.getVerticalBoxGroup();
385                 view.assignComponentIds(verticalComboCalcGroup);
386                 List<Component> comboGroupItems = new ArrayList<Component>();
387                 comboGroupItems.add(component);
388                 comboGroupItems.add(calculationFieldGroup);
389                 verticalComboCalcGroup.setItems(comboGroupItems);
390                 footerCalculationComponents.set(cInfo.getColumnNumber(), verticalComboCalcGroup);
391             } else if (component != null && component instanceof Group) {
392                 List<Component> comboGroupItems = new ArrayList<Component>();
393                 comboGroupItems.addAll(((Group) component).getItems());
394                 comboGroupItems.add(calculationFieldGroup);
395                 ((Group) component).setItems(comboGroupItems);
396                 footerCalculationComponents.set(cInfo.getColumnNumber(), component);
397             } else {
398                 footerCalculationComponents.set(cInfo.getColumnNumber(), calculationFieldGroup);
399             }
400         }
401 
402         //special processing for the left labels - when there are no total fields in this column
403         //add the label to the column footer directly
404         if (this.renderOnlyLeftTotalLabels && footerCalculationComponents.get(leftLabelColumnIndex) == null) {
405             Group labelGroup = ComponentFactory.getVerticalBoxGroup();
406             view.assignComponentIds(labelGroup);
407             List<Component> groupItems = new ArrayList<Component>();
408 
409             if (this.isShowGroupTotal()) {
410                 //display none - this label is copied by the javascript
411                 groupTotalLabelPrototype.setStyle("display: none;");
412                 groupTotalLabelPrototype.addDataAttribute("role", "groupTotalLabel");
413                 view.assignComponentIds(groupTotalLabelPrototype);
414                 groupItems.add(groupTotalLabelPrototype);
415             }
416 
417             if (this.isShowPageTotal()) {
418                 view.assignComponentIds(pageTotalLabel);
419                 groupItems.add(pageTotalLabel);
420             }
421 
422             if (this.isShowTotal()) {
423                 view.assignComponentIds(totalLabel);
424                 groupItems.add(totalLabel);
425             }
426 
427             labelGroup.setItems(groupItems);
428 
429             footerCalculationComponents.set(leftLabelColumnIndex, labelGroup);
430         }
431 
432         //perform the lifecycle for all the newly generated components as a result of processing the
433         //column calculations
434         for (Component component : footerCalculationComponents) {
435             if (component != null) {
436                 component.performInitialization(view, model);
437                 component.performApplyModel(view, model, container);
438                 component.performFinalize(view, model, container);
439             }
440         }
441 
442     }
443 
444     /**
445      * Setup the totalField with the columnCalculationInfo(cInfo) passed in.  Param show represents the
446      * tableLayoutManager's setting for the type of total being processed.
447      *
448      * @param totalField the field to setup
449      * @param cInfo ColumnCalculation info to use to setup the field
450      * @param show show the field (if renderOnlyLeftTotalLabels is true, otherwise uses value in cInfo)
451      * @param leftLabel the leftLabel, not used if renderOnlyLeftTotalLabels is false
452      * @param type type used to set the dataAttribute role - used by the js for selection
453      * @param leftLabelColumnIndex index of the leftLabelColumn (0 or 1 if grouping enabled - hidden column)
454      * @return the field with cInfo and tableLayoutManager settings applied as appropriate
455      */
456     private Field setupTotalField(Field totalField, ColumnCalculationInfo cInfo, boolean show, Label leftLabel,
457             String type, int leftLabelColumnIndex) {
458         //setup the totals field
459         Field totalDataField = totalField;
460         totalDataField.addDataAttribute("role", type);
461         totalDataField.addDataAttribute("function", cInfo.getCalculationFunctionName());
462         totalDataField.addDataAttribute("params", cInfo.getCalculationFunctionExtraData());
463 
464         if (cInfo.getColumnNumber() != leftLabelColumnIndex) {
465             //do not render labels for columns which have totals and the renderOnlyLeftTotalLabels
466             //flag is set
467             totalDataField.getFieldLabel().setRender(!this.isRenderOnlyLeftTotalLabels());
468         } else if (cInfo.getColumnNumber() == leftLabelColumnIndex && this.isRenderOnlyLeftTotalLabels()) {
469             //renderOnlyLeftTotalLabel is set to true, but the column has a total itself - set the layout
470             //manager settings directly into the field
471             totalDataField.setFieldLabel(leftLabel);
472         }
473 
474         if (this.isRenderOnlyLeftTotalLabels()) {
475             totalDataField.setRender(show);
476         }
477 
478         return totalDataField;
479     }
480 
481     /**
482      * Assembles the field instances for the collection line. The given sequence
483      * field prototype is copied for the line sequence field. Likewise a copy of
484      * the actionFieldPrototype is made and the given actions are set as the
485      * items for the action field. Finally the generated items are assembled
486      * together into the dataFields list with the given lineFields.
487      *
488      * @see org.kuali.rice.krad.uif.layout.CollectionLayoutManager#buildLine(org.kuali.rice.krad.uif.view.View,
489      *      java.lang.Object, org.kuali.rice.krad.uif.container.CollectionGroup,
490      *      java.util.List, java.util.List, java.lang.String, java.util.List,
491      *      java.lang.String, java.lang.Object, int)
492      */
493     public void buildLine(View view, Object model, CollectionGroup collectionGroup, List<Field> lineFields,
494             List<FieldGroup> subCollectionFields, String bindingPath, List<Action> actions, String idSuffix,
495             Object currentLine, int lineIndex) {
496 
497         // if first line for table set number of data columns
498         if (dataFields.isEmpty()) {
499             if (isSuppressLineWrapping()) {
500                 setNumberOfDataColumns(lineFields.size());
501             } else {
502                 setNumberOfDataColumns(getNumberOfColumns());
503             }
504         }
505 
506         boolean isAddLine = false;
507 
508         // If first row or row wrap is happening
509         if (lineIndex == -1 || (lineFields.size() != numberOfDataColumns
510                 && ((lineIndex + 1) * numberOfDataColumns) < lineFields.size())) {
511             isAddLine = true;
512         }
513 
514         boolean renderActions = collectionGroup.isRenderLineActions() && !collectionGroup.isReadOnly();
515         int extraColumns = 0;
516 
517         if (collectionGroup.isHighlightNewItems() && ((UifFormBase) model).isAddedCollectionItem(currentLine)) {
518             getRowCssClasses().add(collectionGroup.getNewItemsCssClass());
519         } else if (isAddLine
520                 && collectionGroup.isRenderAddLine()
521                 && !collectionGroup.isReadOnly()
522                 && !isSeparateAddLine()) {
523             getRowCssClasses().add(collectionGroup.getAddItemCssClass());
524             this.addStyleClass("uif-hasAddLine");
525         } else if (lineIndex != -1) {
526             getRowCssClasses().add("");
527         }
528 
529         // if separate add line prepare the add line group
530         if (isAddLine && separateAddLine) {
531             if (StringUtils.isBlank(addLineGroup.getTitle()) && StringUtils.isBlank(
532                     addLineGroup.getHeader().getHeaderText())) {
533                 addLineGroup.getHeader().setHeaderText(collectionGroup.getAddLabel());
534             }
535             addLineGroup.setItems(lineFields);
536 
537             List<Component> footerItems = new ArrayList<Component>(actions);
538             footerItems.addAll(addLineGroup.getFooter().getItems());
539             addLineGroup.getFooter().setItems(footerItems);
540 
541             if (collectionGroup.isAddViaLightBox()) {
542                 String actionScript = "showLightboxComponent('" + addLineGroup.getId() + "');";
543                 if (StringUtils.isNotBlank(collectionGroup.getAddViaLightBoxAction().getActionScript())) {
544                     actionScript = collectionGroup.getAddViaLightBoxAction().getActionScript() + actionScript;
545                 }
546                 collectionGroup.getAddViaLightBoxAction().setActionScript(actionScript);
547                 addLineGroup.setStyle("display: none");
548             }
549 
550             return;
551         }
552 
553         // TODO: implement repeat header
554         if (!headerAdded) {
555             headerLabels = new ArrayList<Label>();
556             dataFields = new ArrayList<Component>();
557 
558             buildTableHeaderRows(collectionGroup, lineFields);
559             ComponentUtils.pushObjectToContext(headerLabels, UifConstants.ContextVariableNames.LINE, currentLine);
560             ComponentUtils.pushObjectToContext(headerLabels, UifConstants.ContextVariableNames.INDEX, new Integer(
561                     lineIndex));
562             headerAdded = true;
563         }
564 
565         // set label field rendered to true on line fields
566         for (Field field : lineFields) {
567             field.setLabelRendered(true);
568             field.setFieldLabel(null);
569         }
570 
571         int rowCount = calculateNumberOfRows(lineFields);
572         int rowSpan = rowCount + subCollectionFields.size();
573 
574         if (actionColumnIndex == 1 && renderActions) {
575             addActionColumn(idSuffix, currentLine, lineIndex, rowSpan, actions);
576         }
577 
578         // sequence field is always first and should span all rows for the line
579         if (renderSequenceField) {
580             Component sequenceField = null;
581             if (!isAddLine) {
582                 sequenceField = ComponentUtils.copy(getSequenceFieldPrototype(), idSuffix);
583                 //Ignore in validation processing
584                 sequenceField.addDataAttribute(UifConstants.DataAttributes.VIGNORE, "yes");
585                 if (generateAutoSequence && (sequenceField instanceof MessageField)) {
586                     ((MessageField) sequenceField).setMessageText(Integer.toString(lineIndex + 1));
587                 }
588             } else {
589                 sequenceField = ComponentUtils.copy(collectionGroup.getAddLineLabel(), idSuffix);
590             }
591             sequenceField.setRowSpan(rowSpan);
592 
593             if (sequenceField instanceof DataBinding) {
594                 ((DataBinding) sequenceField).getBindingInfo().setBindByNamePrefix(bindingPath);
595             }
596 
597             ComponentUtils.updateContextForLine(sequenceField, currentLine, lineIndex, idSuffix);
598             dataFields.add(sequenceField);
599             extraColumns++;
600 
601             if (actionColumnIndex == 2 && renderActions) {
602                 addActionColumn(idSuffix, currentLine, lineIndex, rowSpan, actions);
603             }
604         }
605 
606         // select field will come after sequence field (if enabled) or be first column
607         if (collectionGroup.isIncludeLineSelectionField()) {
608             Field selectField = ComponentUtils.copy(getSelectFieldPrototype(), idSuffix);
609             CollectionLayoutUtils.prepareSelectFieldForLine(selectField, collectionGroup, bindingPath, currentLine);
610 
611             ComponentUtils.updateContextForLine(selectField, currentLine, lineIndex, idSuffix);
612             dataFields.add(selectField);
613             extraColumns++;
614 
615             if (renderActions) {
616                 if ((actionColumnIndex == 3 && renderSequenceField) || (actionColumnIndex == 2
617                         && !renderSequenceField)) {
618                     addActionColumn(idSuffix, currentLine, lineIndex, rowSpan, actions);
619                 }
620             }
621         }
622 
623         // now add the fields in the correct position
624         int cellPosition = 0;
625         int columnNumber = 0;
626 
627         boolean renderActionsLast = actionColumnIndex == -1 || actionColumnIndex > lineFields.size() + extraColumns;
628         boolean hasGrouping = (groupingPropertyNames != null || StringUtils.isNotBlank(this.getGroupingTitle()));
629         boolean insertActionField = false;
630 
631         for (Field lineField : lineFields) {
632             //Check to see if ActionField needs to be inserted before this lineField because of wrapping.
633             // Since actionField has a colSpan of 1 add that to the previous cellPosition instead of the
634             // current lineField's colSpan.
635             // Only insert if ActionField has to be placed at the end. Else the specification of actionColumnIndex should
636             // take care of putting it in the right location
637             insertActionField = (cellPosition != 0 && lineFields.size() != numberOfDataColumns) && renderActions && renderActionsLast && ((cellPosition % numberOfDataColumns) == 0);
638 
639             cellPosition += lineField.getColSpan();
640             columnNumber++;
641 
642             //special handling for grouping field - this field MUST be first
643             if (hasGrouping && lineField instanceof MessageField &&
644                     lineField.getDataAttributes().get("role") != null && lineField.getDataAttributes().get("role")
645                     .equals("grouping")) {
646                 int groupFieldIndex = dataFields.size() - extraColumns;
647                 dataFields.add(groupFieldIndex, lineField);
648                 groupingColumnIndex = 0;
649                 if (isAddLine) {
650                     ((MessageField) lineField).getMessage().getPropertyExpressions().remove("messageText");
651                     ((MessageField) lineField).getMessage().setMessageText("addLine");
652                 }
653             } else {
654                 // If the row wraps before the last element
655                 if (insertActionField) {
656                     addActionColumn(idSuffix, currentLine, lineIndex, rowSpan, actions);
657                 }
658 
659                 dataFields.add(lineField);
660             }
661 
662             // action field
663             if (!renderActionsLast && cellPosition == (actionColumnIndex - extraColumns - 1)) {
664                 addActionColumn(idSuffix, currentLine, lineIndex, rowSpan, actions);
665             }
666 
667             //details action
668             if (lineField instanceof FieldGroup && ((FieldGroup) lineField).getItems() != null) {
669                 for (Component component : ((FieldGroup) lineField).getItems()) {
670                     if (component != null && component instanceof Action && component.getDataAttributes().get("role")
671                             != null && component.getDataAttributes().get("role").equals("detailsLink") &&
672                             StringUtils.isBlank(((Action) component).getActionScript())) {
673                         ((Action) component).setActionScript("rowDetailsActionHandler(this,'" + this.getId() + "');");
674                     }
675                 }
676             }
677 
678             //special column calculation handling to identify what type of handler will be attached
679             //and add special styling
680             if (lineField instanceof InputField && columnCalculations != null) {
681                 for (ColumnCalculationInfo cInfo : columnCalculations) {
682                     if (cInfo.getPropertyName().equals(((InputField) lineField).getPropertyName())) {
683                         if (cInfo.isCalculateOnKeyUp()) {
684                             lineField.addDataAttribute("total", "keyup");
685                         } else {
686                             lineField.addDataAttribute("total", "change");
687                         }
688                         lineField.addStyleClass("uif-calculationField");
689                     }
690                 }
691             }
692 
693         }
694 
695         if (lineFields.size() == numberOfDataColumns && renderActions && renderActionsLast) {
696             addActionColumn(idSuffix, currentLine, lineIndex, rowSpan, actions);
697         }
698 
699         // update colspan on sub-collection fields
700         for (FieldGroup subCollectionField : subCollectionFields) {
701             subCollectionField.setColSpan(numberOfDataColumns);
702         }
703 
704         // add sub-collection fields to end of data fields
705         dataFields.addAll(subCollectionFields);
706     }
707 
708     /**
709      * Adds the action field in a row
710      *
711      * @param idSuffix
712      * @param currentLine
713      * @param lineIndex
714      * @param rowSpan
715      * @param actions
716      */
717     private void addActionColumn(String idSuffix, Object currentLine, int lineIndex, int rowSpan,
718             List<Action> actions) {
719         FieldGroup lineActionsField = ComponentUtils.copy(getActionFieldPrototype(), idSuffix);
720 
721         ComponentUtils.updateContextForLine(lineActionsField, currentLine, lineIndex, idSuffix);
722         lineActionsField.setRowSpan(rowSpan);
723         lineActionsField.setItems(actions);
724 
725         dataFields.add(lineActionsField);
726     }
727 
728     /**
729      * Create the {@code Label} instances that will be used to render
730      * the table header
731      *
732      * <p>
733      * For each column, a copy of headerLabelPrototype is made that determines
734      * the label configuration. The actual label text comes from the field for
735      * which the header applies to. The first column is always the sequence (if
736      * enabled) and the last column contains the actions. Both the sequence and
737      * action header fields will span all rows for the header.
738      * </p>
739      *
740      * <p>
741      * The headerLabels list will contain the final list of header fields built
742      * </p>
743      *
744      * @param collectionGroup - CollectionGroup container the table applies to
745      * @param lineFields - fields for the data columns from which the headers are pulled
746      */
747     protected void buildTableHeaderRows(CollectionGroup collectionGroup, List<Field> lineFields) {
748         // row count needed to determine the row span for the sequence and
749         // action fields, since they should span all rows for the line
750         int rowCount = calculateNumberOfRows(lineFields);
751         boolean renderActions = collectionGroup.isRenderLineActions() && !collectionGroup.isReadOnly();
752         int extraColumns = 0;
753 
754         if (actionColumnIndex == 1 && renderActions) {
755             addActionHeader(rowCount, 1);
756         }
757 
758         // first column is sequence label (if action column not 1)
759         if (renderSequenceField) {
760             getSequenceFieldPrototype().setLabelRendered(true);
761             getSequenceFieldPrototype().setRowSpan(rowCount);
762             addHeaderField(getSequenceFieldPrototype(), 1);
763             extraColumns++;
764 
765             if (actionColumnIndex == 2 && renderActions) {
766                 addActionHeader(rowCount, 2);
767             }
768         }
769 
770         // next is select field
771         if (collectionGroup.isIncludeLineSelectionField()) {
772             getSelectFieldPrototype().setLabelRendered(true);
773             getSelectFieldPrototype().setRowSpan(rowCount);
774             addHeaderField(getSelectFieldPrototype(), 1);
775             extraColumns++;
776 
777             if (actionColumnIndex == 3 && renderActions && renderSequenceField) {
778                 addActionHeader(rowCount, 3);
779             } else if (actionColumnIndex == 2 && renderActions) {
780                 addActionHeader(rowCount, 2);
781             }
782         }
783 
784         // pull out label fields from the container's items
785         int cellPosition = 0;
786         boolean renderActionsLast = actionColumnIndex == -1 || actionColumnIndex > lineFields.size() + extraColumns;
787         boolean insertActionHeader = false;
788         for (Field field : lineFields) {
789             if (!field.isRender() && StringUtils.isEmpty(field.getProgressiveRender())) {
790                 continue;
791             }
792 
793             //Check to see if ActionField needs to be inserted before this lineField because of wrapping.
794             // Since actionField has a colSpan of 1 add that to the previous cellPosition instead of the
795             // current lineField's colSpan.
796             // Only Insert if ActionField has to be placed at the end. Else the specification of actionColumnIndex
797             // should take care of putting it in the right location
798             insertActionHeader = (cellPosition != 0 && lineFields.size() != numberOfDataColumns && renderActions && renderActionsLast && ((cellPosition % numberOfDataColumns) == 0));
799 
800             if ( insertActionHeader) {
801                 addActionHeader(rowCount, cellPosition);
802             }
803 
804             cellPosition += field.getColSpan();
805             addHeaderField(field, cellPosition);
806 
807             // add action header
808             if (renderActions && !renderActionsLast && cellPosition == actionColumnIndex - extraColumns - 1) {
809                 cellPosition += 1;
810                 addActionHeader(rowCount, cellPosition);
811             }
812         }
813 
814         if (lineFields.size() == numberOfDataColumns && renderActions && renderActionsLast) {
815             cellPosition += 1;
816             addActionHeader(rowCount, cellPosition);
817         }
818     }
819 
820     /**
821      * Adds the action header
822      *
823      * @param rowCount
824      * @param cellPosition
825      */
826     private void addActionHeader(int rowCount, int cellPosition) {
827         getActionFieldPrototype().setLabelRendered(true);
828         getActionFieldPrototype().setRowSpan(rowCount);
829         addHeaderField(getActionFieldPrototype(), cellPosition);
830     }
831 
832     /**
833      * Creates a new instance of the header field prototype and then sets the
834      * label to the short (if useShortLabels is set to true) or long label of
835      * the given component. After created the header field is added to the list
836      * making up the table header
837      *
838      * @param field - field instance the header field is being created for
839      * @param column - column number for the header, used for setting the id
840      */
841     protected void addHeaderField(Field field, int column) {
842         Label headerLabel = ComponentUtils.copy(getHeaderLabelPrototype(), "_c" + column);
843         if (useShortLabels) {
844             headerLabel.setLabelText(field.getShortLabel());
845         } else {
846             headerLabel.setLabelText(field.getLabel());
847         }
848 
849         headerLabel.setRowSpan(field.getRowSpan());
850         headerLabel.setColSpan(field.getColSpan());
851 
852         //copy cell css classes for this label
853         headerLabel.setCellCssClasses(field.getCellCssClasses());
854 
855         if ((field.getRequired() != null) && field.getRequired().booleanValue()) {
856             headerLabel.getRequiredMessage().setRender(!field.isReadOnly());
857         } else {
858             headerLabel.getRequiredMessage().setRender(false);
859         }
860 
861         headerLabels.add(headerLabel);
862     }
863 
864     /**
865      * Calculates how many rows will be needed per collection line to display
866      * the list of fields. Assumption is made that the total number of cells the
867      * fields take up is evenly divisible by the configured number of columns
868      *
869      * @param items - list of items that make up one collection line
870      * @return int number of rows
871      */
872     protected int calculateNumberOfRows(List<? extends Field> items) {
873         int rowCount = 0;
874 
875         // check flag that indicates only one row should be created
876         if (isSuppressLineWrapping()) {
877             return 1;
878         }
879 
880         // If Overflow is greater than 0 then calculate the col span for the last item in the overflowed row
881         if(items.size() % getNumberOfDataColumns() > 0){
882             //get the last line item
883             Field field = items.get(items.size()-1);
884             field.setColSpan(1+(numberOfDataColumns-(items.size()%numberOfDataColumns)));
885             rowCount = ((items.size()/getNumberOfDataColumns())+1);
886         } else{
887             rowCount = items.size()/getNumberOfDataColumns();
888         }
889         return rowCount;
890     }
891 
892     /**
893      * @see CollectionLayoutManager#getSupportedContainer()
894      */
895     @Override
896     public Class<? extends Container> getSupportedContainer() {
897         return CollectionGroup.class;
898     }
899 
900     /**
901      * @see org.kuali.rice.krad.uif.layout.LayoutManagerBase#getComponentsForLifecycle()
902      */
903     @Override
904     public List<Component> getComponentsForLifecycle() {
905         List<Component> components = super.getComponentsForLifecycle();
906 
907         components.add(richTable);
908         components.add(addLineGroup);
909         components.addAll(headerLabels);
910         components.addAll(dataFields);
911 
912         for (ColumnCalculationInfo cInfo : columnCalculations) {
913             components.add(cInfo.getTotalField());
914             components.add(cInfo.getPageTotalField());
915             components.add(cInfo.getGroupTotalFieldPrototype());
916         }
917 
918         if (isShowToggleAllDetails()) {
919             components.add(toggleAllDetailsAction);
920         }
921 
922         return components;
923     }
924 
925     /**
926      * @see org.kuali.rice.krad.uif.layout.LayoutManager#getComponentPrototypes()
927      */
928     @Override
929     public List<Component> getComponentPrototypes() {
930         List<Component> components = super.getComponentPrototypes();
931 
932         components.add(getHeaderLabelPrototype());
933         components.add(getSequenceFieldPrototype());
934         components.add(getActionFieldPrototype());
935         components.add(getSubCollectionFieldGroupPrototype());
936         components.add(getSelectFieldPrototype());
937 
938         return components;
939     }
940 
941     /**
942      * Indicates whether the short label for the collection field should be used
943      * as the table header or the regular label
944      *
945      * @return boolean true if short label should be used, false if long label
946      *         should be used
947      */
948     @BeanTagAttribute(name = "useShortLabels")
949     public boolean isUseShortLabels() {
950         return this.useShortLabels;
951     }
952 
953     /**
954      * Setter for the use short label indicator
955      *
956      * @param useShortLabels
957      */
958     public void setUseShortLabels(boolean useShortLabels) {
959         this.useShortLabels = useShortLabels;
960     }
961 
962     /**
963      * Indicates whether the header should be repeated before each collection
964      * row. If false the header is only rendered at the beginning of the table
965      *
966      * @return boolean true if header should be repeated, false if it should
967      *         only be rendered once
968      */
969     @BeanTagAttribute(name = "repeatHeader")
970     public boolean isRepeatHeader() {
971         return this.repeatHeader;
972     }
973 
974     /**
975      * Setter for the repeat header indicator
976      *
977      * @param repeatHeader
978      */
979     public void setRepeatHeader(boolean repeatHeader) {
980         this.repeatHeader = repeatHeader;
981     }
982 
983     /**
984      * {@code Label} instance to use as a prototype for creating the
985      * tables header fields. For each header field the prototype will be copied
986      * and adjusted as necessary
987      *
988      * @return Label instance to serve as prototype
989      */
990     @BeanTagAttribute(name = "headerLabelPrototype", type = BeanTagAttribute.AttributeType.SINGLEBEAN)
991     public Label getHeaderLabelPrototype() {
992         return this.headerLabelPrototype;
993     }
994 
995     /**
996      * Setter for the header field prototype
997      *
998      * @param headerLabelPrototype
999      */
1000     public void setHeaderLabelPrototype(Label headerLabelPrototype) {
1001         this.headerLabelPrototype = headerLabelPrototype;
1002     }
1003 
1004     /**
1005      * List of {@code Label} instances that should be rendered to make
1006      * up the tables header
1007      *
1008      * @return List of label field instances
1009      */
1010     public List<Label> getHeaderLabels() {
1011         return this.headerLabels;
1012     }
1013 
1014     /**
1015      * Indicates whether the sequence field should be rendered for the
1016      * collection
1017      *
1018      * @return boolean true if sequence field should be rendered, false if not
1019      */
1020     @BeanTagAttribute(name = "renderSequenceField")
1021     public boolean isRenderSequenceField() {
1022         return this.renderSequenceField;
1023     }
1024 
1025     /**
1026      * Setter for the render sequence field indicator
1027      *
1028      * @param renderSequenceField
1029      */
1030     public void setRenderSequenceField(boolean renderSequenceField) {
1031         this.renderSequenceField = renderSequenceField;
1032     }
1033 
1034     /**
1035      * Attribute name to use as sequence value. For each collection line the
1036      * value of this field on the line will be retrieved and used as the
1037      * sequence value
1038      *
1039      * @return String sequence property name
1040      */
1041     @BeanTagAttribute(name = "sequencePropertyName")
1042     public String getSequencePropertyName() {
1043         if ((getSequenceFieldPrototype() != null) && (getSequenceFieldPrototype() instanceof DataField)) {
1044             return ((DataField) getSequenceFieldPrototype()).getPropertyName();
1045         }
1046 
1047         return null;
1048     }
1049 
1050     /**
1051      * Setter for the sequence property name
1052      *
1053      * @param sequencePropertyName
1054      */
1055     public void setSequencePropertyName(String sequencePropertyName) {
1056         if ((getSequenceFieldPrototype() != null) && (getSequenceFieldPrototype() instanceof DataField)) {
1057             ((DataField) getSequenceFieldPrototype()).setPropertyName(sequencePropertyName);
1058         }
1059     }
1060 
1061     /**
1062      * Indicates whether the sequence field should be generated with the current
1063      * line number
1064      *
1065      * <p>
1066      * If set to true the sequence field prototype will be changed to a message
1067      * field (if not already a message field) and the text will be set to the
1068      * current line number
1069      * </p>
1070      *
1071      * @return boolean true if the sequence field should be generated from the
1072      *         line number, false if not
1073      */
1074     @BeanTagAttribute(name = "generateAutoSequence")
1075     public boolean isGenerateAutoSequence() {
1076         return this.generateAutoSequence;
1077     }
1078 
1079     /**
1080      * Setter for the generate auto sequence field
1081      *
1082      * @param generateAutoSequence
1083      */
1084     public void setGenerateAutoSequence(boolean generateAutoSequence) {
1085         this.generateAutoSequence = generateAutoSequence;
1086     }
1087 
1088     /**
1089      * {@code Field} instance to serve as a prototype for the
1090      * sequence field. For each collection line this instance is copied and
1091      * adjusted as necessary
1092      *
1093      * @return Attribute field instance
1094      */
1095     @BeanTagAttribute(name = "sequenceFieldPrototype", type = BeanTagAttribute.AttributeType.SINGLEBEAN)
1096     public Field getSequenceFieldPrototype() {
1097         return this.sequenceFieldPrototype;
1098     }
1099 
1100     /**
1101      * Setter for the sequence field prototype
1102      *
1103      * @param sequenceFieldPrototype
1104      */
1105     public void setSequenceFieldPrototype(Field sequenceFieldPrototype) {
1106         this.sequenceFieldPrototype = sequenceFieldPrototype;
1107     }
1108 
1109     /**
1110      * {@code FieldGroup} instance to serve as a prototype for the actions
1111      * column. For each collection line this instance is copied and adjusted as
1112      * necessary. Note the actual actions for the group come from the collection
1113      * groups actions List
1114      * (org.kuali.rice.krad.uif.container.CollectionGroup.getActions()). The
1115      * FieldGroup prototype is useful for setting styling of the actions column
1116      * and for the layout of the action fields. Note also the label associated
1117      * with the prototype is used for the action column header
1118      *
1119      * @return GroupField instance
1120      */
1121     @BeanTagAttribute(name = "actionFieldPrototype", type = BeanTagAttribute.AttributeType.SINGLEBEAN)
1122     public FieldGroup getActionFieldPrototype() {
1123         return this.actionFieldPrototype;
1124     }
1125 
1126     /**
1127      * Setter for the action field prototype
1128      *
1129      * @param actionFieldPrototype
1130      */
1131     public void setActionFieldPrototype(FieldGroup actionFieldPrototype) {
1132         this.actionFieldPrototype = actionFieldPrototype;
1133     }
1134 
1135     /**
1136      * @see org.kuali.rice.krad.uif.layout.CollectionLayoutManager#getSubCollectionFieldGroupPrototype()
1137      */
1138     @BeanTagAttribute(name = "subCollectionFieldGroupPrototype", type = BeanTagAttribute.AttributeType.SINGLEBEAN)
1139     public FieldGroup getSubCollectionFieldGroupPrototype() {
1140         return this.subCollectionFieldGroupPrototype;
1141     }
1142 
1143     /**
1144      * Setter for the sub-collection field group prototype
1145      *
1146      * @param subCollectionFieldGroupPrototype
1147      */
1148     public void setSubCollectionFieldGroupPrototype(FieldGroup subCollectionFieldGroupPrototype) {
1149         this.subCollectionFieldGroupPrototype = subCollectionFieldGroupPrototype;
1150     }
1151 
1152     /**
1153      * Field instance that serves as a prototype for creating the select field on each line when
1154      * {@link org.kuali.rice.krad.uif.container.CollectionGroup#isIncludeLineSelectionField()} is true
1155      *
1156      * <p>
1157      * This prototype can be used to set the control used for the select field (generally will be a checkbox control)
1158      * in addition to styling and other setting. The binding path will be formed with using the
1159      * {@link org.kuali.rice.krad.uif.container.CollectionGroup#getLineSelectPropertyName()} or if not set the
1160      * framework
1161      * will use {@link org.kuali.rice.krad.web.form.UifFormBase#getSelectedCollectionLines()}
1162      * </p>
1163      *
1164      * @return Field select field prototype instance
1165      */
1166     @BeanTagAttribute(name = "selectFieldPrototype", type = BeanTagAttribute.AttributeType.SINGLEBEAN)
1167     public Field getSelectFieldPrototype() {
1168         return selectFieldPrototype;
1169     }
1170 
1171     /**
1172      * Setter for the prototype instance for select fields
1173      *
1174      * @param selectFieldPrototype
1175      */
1176     public void setSelectFieldPrototype(Field selectFieldPrototype) {
1177         this.selectFieldPrototype = selectFieldPrototype;
1178     }
1179 
1180     /**
1181      * Indicates whether the add line should be rendered in a separate group, or as part of the table (first line)
1182      *
1183      * <p>
1184      * When separate add line is enabled, the fields for the add line will be placed in the {@link #getAddLineGroup()}.
1185      * This group can be used to configure the add line presentation. In addition to the fields, the header on the
1186      * group (unless already set) will be set to
1187      * {@link org.kuali.rice.krad.uif.container.CollectionGroup#getAddLabel()} and the add line actions will
1188      * be placed into the group's footer.
1189      * </p>
1190      *
1191      * @return boolean true if add line should be separated, false if it should be placed into the table
1192      */
1193     @BeanTagAttribute(name = "separateAddLine")
1194     public boolean isSeparateAddLine() {
1195         return separateAddLine;
1196     }
1197 
1198     /**
1199      * Setter for the separate add line indicator
1200      *
1201      * @param separateAddLine
1202      */
1203     public void setSeparateAddLine(boolean separateAddLine) {
1204         this.separateAddLine = separateAddLine;
1205     }
1206 
1207     /**
1208      * When {@link #isSeparateAddLine()} is true, this group will be used to render the add line
1209      *
1210      * <p>
1211      * This group can be used to configure how the add line will be rendered. For example the layout manager configured
1212      * on the group will be used to rendered the add line fields. If the header (title) is not set on the group, it
1213      * will be set from
1214      * {@link org.kuali.rice.krad.uif.container.CollectionGroup#getAddLabel()}. In addition,
1215      * {@link org.kuali.rice.krad.uif.container.CollectionGroup#getAddLineActions()} will be added to the group
1216      * footer items.
1217      * </p>
1218      *
1219      * @return Group instance for the collection add line
1220      */
1221     @BeanTagAttribute(name = "addLineGroup", type = BeanTagAttribute.AttributeType.SINGLEBEAN)
1222     public Group getAddLineGroup() {
1223         return addLineGroup;
1224     }
1225 
1226     /**
1227      * Setter for the add line Group
1228      *
1229      * @param addLineGroup
1230      */
1231     public void setAddLineGroup(Group addLineGroup) {
1232         this.addLineGroup = addLineGroup;
1233     }
1234 
1235     /**
1236      * List of {@code Component} instances that make up the tables body. Pulled
1237      * by the layout manager template to send through the Grid layout
1238      *
1239      * @return List<Component> table body fields
1240      */
1241     public List<Component> getDataFields() {
1242         return this.dataFields;
1243     }
1244 
1245     /**
1246      * Widget associated with the table to add functionality such as sorting,
1247      * paging, and export
1248      *
1249      * @return RichTable instance
1250      */
1251     @BeanTagAttribute(name = "richTable", type = BeanTagAttribute.AttributeType.SINGLEBEAN)
1252     public RichTable getRichTable() {
1253         return this.richTable;
1254     }
1255 
1256     /**
1257      * Setter for the rich table widget
1258      *
1259      * @param richTable
1260      */
1261     public void setRichTable(RichTable richTable) {
1262         this.richTable = richTable;
1263     }
1264 
1265     /**
1266      * @return the numberOfDataColumns
1267      */
1268     @BeanTagAttribute(name = "numberOfDataColumns")
1269     public int getNumberOfDataColumns() {
1270         return this.numberOfDataColumns;
1271     }
1272 
1273     /**
1274      * @param numberOfDataColumns the numberOfDataColumns to set
1275      */
1276     public void setNumberOfDataColumns(int numberOfDataColumns) {
1277         this.numberOfDataColumns = numberOfDataColumns;
1278     }
1279 
1280     /**
1281      * @see org.kuali.rice.krad.uif.widget.RichTable#getHiddenColumns()
1282      */
1283     @BeanTagAttribute(name = "hiddenColumns", type = BeanTagAttribute.AttributeType.SETVALUE)
1284     public Set<String> getHiddenColumns() {
1285         if (richTable != null) {
1286             return richTable.getHiddenColumns();
1287         }
1288 
1289         return null;
1290     }
1291 
1292     /**
1293      * @see org.kuali.rice.krad.uif.widget.RichTable#setHiddenColumns(java.util.Set<java.lang.String>)
1294      */
1295     public void setHiddenColumns(Set<String> hiddenColumns) {
1296         if (richTable != null) {
1297             richTable.setHiddenColumns(hiddenColumns);
1298         }
1299     }
1300 
1301     /**
1302      * @see org.kuali.rice.krad.uif.widget.RichTable#getSortableColumns()
1303      */
1304     @BeanTagAttribute(name = "sortableColumns", type = BeanTagAttribute.AttributeType.SETVALUE)
1305     public Set<String> getSortableColumns() {
1306         if (richTable != null) {
1307             return richTable.getSortableColumns();
1308         }
1309 
1310         return null;
1311     }
1312 
1313     /**
1314      * @see org.kuali.rice.krad.uif.widget.RichTable#setSortableColumns(java.util.Set<java.lang.String>)
1315      */
1316     public void setSortableColumns(Set<String> sortableColumns) {
1317         if (richTable != null) {
1318             richTable.setSortableColumns(sortableColumns);
1319         }
1320     }
1321 
1322     /**
1323      * Indicates the index of the action column
1324      *
1325      * @return int = the action column index
1326      */
1327     @BeanTagAttribute(name = "actionColumnIndex")
1328     public int getActionColumnIndex() {
1329         return actionColumnIndex;
1330     }
1331 
1332     /**
1333      * Indicates the actions column placement
1334      *
1335      * <p>
1336      * Valid values are 'LEFT', 'RIGHT' or any valid number. The default is 'RIGHT' or '-1'. The column placement index
1337      * takes all displayed columns, including sequence and selection columns, into account.
1338      * </p>
1339      *
1340      * @return String - the action column placement
1341      */
1342     @BeanTagAttribute(name = "actionColumnPlacement")
1343     public String getActionColumnPlacement() {
1344         return actionColumnPlacement;
1345     }
1346 
1347     /**
1348      * Setter for the action column placement
1349      *
1350      * @param actionColumnPlacement - action column placement string
1351      */
1352     public void setActionColumnPlacement(String actionColumnPlacement) {
1353         this.actionColumnPlacement = actionColumnPlacement;
1354 
1355         if ("LEFT".equals(actionColumnPlacement)) {
1356             actionColumnIndex = 1;
1357         } else if ("RIGHT".equals(actionColumnPlacement)) {
1358             actionColumnIndex = -1;
1359         } else if (StringUtils.isNumeric(actionColumnPlacement)) {
1360             actionColumnIndex = Integer.parseInt(actionColumnPlacement);
1361         }
1362     }
1363 
1364     /**
1365      * The row details info group to use when using a TableLayoutManager with the a richTable.
1366      *
1367      * <p>This group will be displayed when the user clicks the "Details" link/image on a row.
1368      * This allows extra/long data to be hidden in table rows and then revealed during interaction
1369      * with the table without the need to leave the page.  Allows for any group content.</p>
1370      *
1371      * <p>Does not currently work with javascript required content.</p>
1372      *
1373      * @return rowDetailsGroup component
1374      */
1375     @BeanTagAttribute(name = "rowDetailsGroup", type = BeanTagAttribute.AttributeType.SINGLEBEAN)
1376     public Group getRowDetailsGroup() {
1377         return rowDetailsGroup;
1378     }
1379 
1380     /**
1381      * Set the row details info group
1382      *
1383      * @param rowDetailsGroup row details group
1384      */
1385     public void setRowDetailsGroup(Group rowDetailsGroup) {
1386         this.rowDetailsGroup = rowDetailsGroup;
1387     }
1388 
1389     /**
1390      * Name of the link for displaying row details in a TableLayoutManager CollectionGroup
1391      *
1392      * @return name of the link
1393      */
1394     @BeanTagAttribute(name = "rowDetailsLinkName")
1395     public String getRowDetailsLinkName() {
1396         return rowDetailsLinkName;
1397     }
1398 
1399     /**
1400      * Row details link name
1401      *
1402      * @param rowDetailsLinkName name of the details link
1403      */
1404     public void setRowDetailsLinkName(String rowDetailsLinkName) {
1405         this.rowDetailsLinkName = rowDetailsLinkName;
1406     }
1407 
1408     /**
1409      * If true, the row details link will use an image instead of a link to display row details in
1410      * a TableLayoutManager CollectionGroup
1411      *
1412      * @return true if displaying an image instead of a link for row details
1413      */
1414     @BeanTagAttribute(name = "rowDetailsSwapActionImage")
1415     public boolean isRowDetailsSwapActionImage() {
1416         return rowDetailsSwapActionImage;
1417     }
1418 
1419     /**
1420      * Sets row details link use image flag
1421      *
1422      * @param rowDetailsSwapActionImage true to use image for details, false otherwise
1423      */
1424     public void setRowDetailsSwapActionImage(boolean rowDetailsSwapActionImage) {
1425         this.rowDetailsSwapActionImage = rowDetailsSwapActionImage;
1426     }
1427 
1428     /**
1429      * Creates the details group for the line using the information setup through the setter methods of this
1430      * interface.  Line details are currently only supported in TableLayoutManagers which use richTable.
1431      *
1432      * @param collectionGroup the CollectionGroup for this TableLayoutManager
1433      * @param view the current view
1434      */
1435     public void setupDetails(CollectionGroup collectionGroup, View view) {
1436         if (getRowDetailsGroup() == null || this.getRichTable() == null || !this.getRichTable().isRender()) {
1437             return;
1438         }
1439 
1440         //data attribute to mark this group to open itself when rendered
1441         collectionGroup.addDataAttribute("detailsDefaultOpen", Boolean.toString(this.rowDetailsOpen));
1442 
1443         toggleAllDetailsAction.addDataAttribute("open", Boolean.toString(this.rowDetailsOpen));
1444         toggleAllDetailsAction.addDataAttribute("tableid", this.getId());
1445 
1446         this.getRowDetailsGroup().setHidden(true);
1447 
1448         FieldGroup detailsFieldGroup = ComponentFactory.getFieldGroup();
1449 
1450         TreeMap<String, String> dataAttributes = new TreeMap<String, String>();
1451         dataAttributes.put("role", "detailsFieldGroup");
1452         detailsFieldGroup.setDataAttributes(dataAttributes);
1453 
1454         Action rowDetailsAction = this.getExpandDetailsActionPrototype();
1455         rowDetailsAction.addDataAttribute("role", "detailsLink");
1456         rowDetailsAction.addDataAttribute("swap", Boolean.toString(this.isRowDetailsSwapActionImage()));
1457         rowDetailsAction.setId(collectionGroup.getId() + "_detLink");
1458 
1459         List<Component> detailsItems = new ArrayList<Component>();
1460         detailsItems.add(rowDetailsAction);
1461 
1462         dataAttributes = new TreeMap<String, String>();
1463         dataAttributes.put("role", "details");
1464         this.getRowDetailsGroup().setDataAttributes(dataAttributes);
1465 
1466         if (ajaxDetailsRetrieval) {
1467             this.getRowDetailsGroup().setRender(false);
1468             this.getRowDetailsGroup().setDisclosedByAction(true);
1469         }
1470 
1471         detailsItems.add(getRowDetailsGroup());
1472         detailsFieldGroup.setItems(detailsItems);
1473         detailsFieldGroup.setId(collectionGroup.getId() + "_detGroup");
1474         view.assignComponentIds(detailsFieldGroup);
1475 
1476 
1477         List<Component> theItems = new ArrayList<Component>();
1478         theItems.add(detailsFieldGroup);
1479         theItems.addAll(collectionGroup.getItems());
1480         collectionGroup.setItems(theItems);
1481     }
1482 
1483     /**
1484      * A list of all the columns to be calculated
1485      *
1486      * <p>
1487      * The list must contain valid column indexes. The indexes takes all displayed columns into account.
1488      * </p>
1489      *
1490      * @return List<String> the total columns list
1491      */
1492     public List<String> getColumnsToCalculate() {
1493         return columnsToCalculate;
1494     }
1495 
1496     /**
1497      * Validates different requirements of component compiling a series of reports detailing information on errors
1498      * found in the component.  Used by the RiceDictionaryValidator.
1499      *
1500      * @param tracer Record of component's location
1501      * @return A list of ErrorReports detailing errors found within the component and referenced within it
1502      */
1503     public void completeValidation(ValidationTrace tracer) {
1504         tracer.addBean("TableLayoutManager", getId());
1505 
1506         if (getRowDetailsGroup() != null) {
1507             boolean validTable = false;
1508             if (getRichTable() != null) {
1509                 if (getRichTable().isRender()) {
1510                     validTable = true;
1511                 }
1512             }
1513             if (!validTable) {
1514                 String currentValues[] = {"rowDetailsGroup =" + getRowDetailsGroup(), "richTable =" + getRichTable()};
1515                 tracer.createError("If rowDetailsGroup is set richTable must be set and its render true",
1516                         currentValues);
1517             }
1518 
1519         }
1520     }
1521 
1522     /**
1523      * Gets showTotal. showTotal shows/calculates the total field when true, otherwise it is not rendered.
1524      * <br/>
1525      * <b>Only used when renderOnlyLeftTotalLabels is TRUE, this overrides the ColumnConfigurationInfo setting.
1526      * Otherwise, the ColumnConfigurationInfo setting takes precedence.</b>
1527      *
1528      * @return true if showing the total, false otherwise.
1529      */
1530     @BeanTagAttribute(name = "showTotal")
1531     public boolean isShowTotal() {
1532         return showTotal;
1533     }
1534 
1535     /**
1536      * Sets showTotal. showTotal shows/calculates the total field when true, otherwise it is not rendered.
1537      * <br/>
1538      * <b>Only used when renderOnlyLeftTotalLabels is TRUE, this overrides the ColumnConfigurationInfo setting.
1539      * Otherwise, the ColumnConfigurationInfo setting takes precedence.</b>
1540      *
1541      * @param showTotal
1542      */
1543     public void setShowTotal(boolean showTotal) {
1544         this.showTotal = showTotal;
1545     }
1546 
1547     /**
1548      * Gets showTotal. showTotal shows/calculates the total field when true, otherwise it is not rendered.
1549      * <br/>
1550      * <b>Only used when renderOnlyLeftTotalLabels is TRUE, this overrides the ColumnConfigurationInfo setting.
1551      * Otherwise, the ColumnConfigurationInfo setting takes precedence.</b>
1552      *
1553      * @return true if showing the page total, false otherwise.
1554      */
1555     @BeanTagAttribute(name = "showPageTotal")
1556     public boolean isShowPageTotal() {
1557         return showPageTotal;
1558     }
1559 
1560     /**
1561      * Sets showPageTotal. showPageTotal shows/calculates the total field for the page when true (and only
1562      * when the table actually has pages), otherwise it is not rendered.
1563      * <br/>
1564      * <b>Only used when renderOnlyLeftTotalLabels is TRUE, this overrides the ColumnConfigurationInfo setting.
1565      * Otherwise, the ColumnConfigurationInfo setting takes precedence.</b>
1566      *
1567      * @param showPageTotal
1568      */
1569     public void setShowPageTotal(boolean showPageTotal) {
1570         this.showPageTotal = showPageTotal;
1571     }
1572 
1573     /**
1574      * Gets showGroupTotal. showGroupTotal shows/calculates the total field for each grouping when true (and only
1575      * when the table actually has grouping turned on), otherwise it is not rendered.
1576      * <br/>
1577      * <b>Only used when renderOnlyLeftTotalLabels is TRUE, this overrides the ColumnConfigurationInfo setting.
1578      * Otherwise, the ColumnConfigurationInfo setting takes precedence.</b>
1579      *
1580      * @return true if showing the group total, false otherwise.
1581      */
1582     @BeanTagAttribute(name = "showGroupTotal")
1583     public boolean isShowGroupTotal() {
1584         return showGroupTotal;
1585     }
1586 
1587     /**
1588      * Sets showGroupTotal. showGroupTotal shows/calculates the total field for each grouping when true (and only
1589      * when the table actually has grouping turned on), otherwise it is not rendered.
1590      * <br/>
1591      * <b>Only used when renderOnlyLeftTotalLabels is TRUE, this overrides the ColumnConfigurationInfo setting.
1592      * Otherwise, the ColumnConfigurationInfo setting takes precedence.</b>
1593      *
1594      * @param showGroupTotal
1595      */
1596     public void setShowGroupTotal(boolean showGroupTotal) {
1597         this.showGroupTotal = showGroupTotal;
1598     }
1599 
1600     /**
1601      * The total label to use when renderOnlyLeftTotalLabels is TRUE for total.
1602      * This label will appear in the left most column.
1603      *
1604      * @return the totalLabel
1605      */
1606     @BeanTagAttribute(name = "totalLabel", type = BeanTagAttribute.AttributeType.SINGLEBEAN)
1607     public Label getTotalLabel() {
1608         return totalLabel;
1609     }
1610 
1611     /**
1612      * Sets the total label to use when renderOnlyLeftTotalLabels is TRUE for total.
1613      *
1614      * @param totalLabel
1615      */
1616     public void setTotalLabel(Label totalLabel) {
1617         this.totalLabel = totalLabel;
1618     }
1619 
1620     /**
1621      * The pageTotal label to use when renderOnlyLeftTotalLabels is TRUE for total.  This label will appear in the
1622      * left most column.
1623      *
1624      * @return the totalLabel
1625      */
1626     @BeanTagAttribute(name = "pageTotalLabel", type = BeanTagAttribute.AttributeType.SINGLEBEAN)
1627     public Label getPageTotalLabel() {
1628         return pageTotalLabel;
1629     }
1630 
1631     /**
1632      * Sets the pageTotal label to use when renderOnlyLeftTotalLabels is TRUE for total.
1633      *
1634      * @param pageTotalLabel
1635      */
1636     public void setPageTotalLabel(Label pageTotalLabel) {
1637         this.pageTotalLabel = pageTotalLabel;
1638     }
1639 
1640     /**
1641      * The groupTotal label to use when renderOnlyLeftTotalLabels is TRUE.  This label will appear in the left most
1642      * column.
1643      *
1644      * @return the totalLabel
1645      */
1646     @BeanTagAttribute(name = "groupTotalLabelPrototype", type = BeanTagAttribute.AttributeType.SINGLEBEAN)
1647     public Label getGroupTotalLabelPrototype() {
1648         return groupTotalLabelPrototype;
1649     }
1650 
1651     /**
1652      * Sets the groupTotal label to use when renderOnlyLeftTotalLabels is TRUE.
1653      *
1654      * @param groupTotalLabelPrototype
1655      */
1656     public void setGroupTotalLabelPrototype(Label groupTotalLabelPrototype) {
1657         this.groupTotalLabelPrototype = groupTotalLabelPrototype;
1658     }
1659 
1660     /**
1661      * Gets the column calculations.  This is a list of ColumnCalcuationInfo that when set provides calculations
1662      * to be performed on the columns they specify.  These calculations appear in the table's footer.  This feature is
1663      * only available when using richTable functionality.
1664      *
1665      * @return the columnCalculations to use
1666      */
1667     @BeanTagAttribute(name = "columnCalculations", type = BeanTagAttribute.AttributeType.LISTBEAN)
1668     public List<ColumnCalculationInfo> getColumnCalculations() {
1669         return columnCalculations;
1670     }
1671 
1672     /**
1673      * Sets the columnCalculations.
1674      *
1675      * @param columnCalculations
1676      */
1677     public void setColumnCalculations(List<ColumnCalculationInfo> columnCalculations) {
1678         this.columnCalculations = columnCalculations;
1679     }
1680 
1681     /**
1682      * When true, labels for the totals fields will only appear in the left most column.  Showing of the totals
1683      * is controlled by the settings on the TableLayoutManager itself when this property is true.
1684      *
1685      * @return true when rendering totals footer labels in the left-most column, false otherwise
1686      */
1687     @BeanTagAttribute(name = "renderOnlyLeftTotalLabels")
1688     public boolean isRenderOnlyLeftTotalLabels() {
1689         return renderOnlyLeftTotalLabels;
1690     }
1691 
1692     /**
1693      * Set the renderOnlyLeftTotalLabels flag for rendring total labels in the left-most column
1694      *
1695      * @param renderOnlyLeftTotalLabels
1696      */
1697     public void setRenderOnlyLeftTotalLabels(boolean renderOnlyLeftTotalLabels) {
1698         this.renderOnlyLeftTotalLabels = renderOnlyLeftTotalLabels;
1699     }
1700 
1701     /**
1702      * Gets the footer calculation components to be used by the layout.  These are set by the framework and cannot
1703      * be set directly.
1704      *
1705      * @return the list of components for the footer
1706      */
1707     public List<Component> getFooterCalculationComponents() {
1708         return footerCalculationComponents;
1709     }
1710 
1711     /**
1712      * Gets the list of property names to use for grouping.
1713      *
1714      * <p>
1715      * When this property is set, grouping for this
1716      * collection will be enabled and the lines of the collection will be grouped by the propertyName(s) supplied.
1717      * Supplying multiple property names will cause the grouping to be on multiple fields and ordered
1718      * alphabetically on "propetyValue1, propertyValue2" (this is also how the group title will display for each
1719      * group).
1720      * The property names supplied must be relative to the line, so #lp
1721      * SHOULD NOT be used (it is assumed automatically).
1722      * </p>
1723      *
1724      * @return propertyNames to group on
1725      */
1726     @BeanTagAttribute(name = "groupingPropertyNames", type = BeanTagAttribute.AttributeType.LISTVALUE)
1727     public List<String> getGroupingPropertyNames() {
1728         return groupingPropertyNames;
1729     }
1730 
1731     /**
1732      * Sets the list of property names to use for grouping.
1733      *
1734      * @param groupingPropertyNames
1735      */
1736     public void setGroupingPropertyNames(List<String> groupingPropertyNames) {
1737         this.groupingPropertyNames = groupingPropertyNames;
1738     }
1739 
1740     /**
1741      * Get the groupingTitle.  The groupingTitle MUST contain a SpringEL expression to uniquely identify a
1742      * group's line (ie it cannot be a static string because each group must be identified by some value).
1743      * <b>This overrides groupingPropertyNames(if set) because it provides full control of grouping value used by
1744      * the collection.  SpringEL defined here must use #lp if referencing values of the line.</b>
1745      *
1746      * @return groupingTitle to be used
1747      */
1748     @BeanTagAttribute(name = "groupingTitle")
1749     public String getGroupingTitle() {
1750         return groupingTitle;
1751     }
1752 
1753     /**
1754      * Set the groupingTitle.  This will throw an exception if the title does not contain a SpringEL expression.
1755      *
1756      * @param groupingTitle
1757      */
1758     public void setGroupingTitle(String groupingTitle) {
1759         if (groupingTitle != null && !groupingTitle.contains("@{")) {
1760             throw new RuntimeException("groupingTitle MUST contain a springEL expression to uniquely"
1761                     + " identify a collection group (often related to some value of the line). "
1762                     + "Value provided: "
1763                     + this.getGroupingTitle());
1764         }
1765         this.groupingTitle = groupingTitle;
1766     }
1767 
1768     /**
1769      * Get the groupingPrefix.  The groupingPrefix is used to prefix the generated title (not used when
1770      * groupingTitle is set directly) when using groupingPropertyNames.
1771      *
1772      * @return
1773      */
1774     @BeanTagAttribute(name = "groupingPrefix")
1775     public String getGroupingPrefix() {
1776         return groupingPrefix;
1777     }
1778 
1779     /**
1780      * Set the groupingPrefix.  This is not used when groupingTitle is set directly.
1781      *
1782      * @param groupingPrefix
1783      */
1784     public void setGroupingPrefix(String groupingPrefix) {
1785         this.groupingPrefix = groupingPrefix;
1786     }
1787 
1788     /**
1789      * If true, all details will be opened by default when the table loads.  Can only be used on tables that have
1790      * row details setup.
1791      *
1792      * @return true if row details
1793      */
1794     public boolean isRowDetailsOpen() {
1795         return rowDetailsOpen;
1796     }
1797 
1798     /**
1799      * Set if row details should be open on table load
1800      *
1801      * @param rowDetailsOpen
1802      */
1803     public void setRowDetailsOpen(boolean rowDetailsOpen) {
1804         this.rowDetailsOpen = rowDetailsOpen;
1805     }
1806 
1807     /**
1808      * If true, the toggleAllDetailsAction will be shown.  This button allows all details to
1809      * be open/closed simultaneously.
1810      *
1811      * @return true if the action button to toggle all row details opened/closed
1812      */
1813     public boolean isShowToggleAllDetails() {
1814         return showToggleAllDetails;
1815     }
1816 
1817     /**
1818      * Set if the toggleAllDetailsAction should be shown
1819      *
1820      * @param showToggleAllDetails
1821      */
1822     public void setShowToggleAllDetails(boolean showToggleAllDetails) {
1823         this.showToggleAllDetails = showToggleAllDetails;
1824     }
1825 
1826     /**
1827      * The toggleAllDetailsAction action component used to toggle all row details open/closed.  This property is set
1828      * by the default configuration and should not be reset in most cases.
1829      *
1830      * @return Action component to use for the toggle action button
1831      */
1832     public Action getToggleAllDetailsAction() {
1833         return toggleAllDetailsAction;
1834     }
1835 
1836     /**
1837      * Set the toggleAllDetailsAction action component used to toggle all row details open/closed.  This property is
1838      * set
1839      * by the default configuration and should not be reset in most cases.
1840      *
1841      * @param toggleAllDetailsAction
1842      */
1843     public void setToggleAllDetailsAction(Action toggleAllDetailsAction) {
1844         this.toggleAllDetailsAction = toggleAllDetailsAction;
1845     }
1846 
1847     /**
1848      * If true, when a row details open action is performed, it will get the details content from the server the first
1849      * time it is opened.  The methodToCall will be a component "refresh" call by default (this can be set on
1850      * expandDetailsActionPrototype) and the additional action parameters sent to the server will be those set
1851      * on the expandDetailsActionPrototype (lineIndex will be sent by default).
1852      *
1853      * @return true if ajax row details retrieval will be used
1854      */
1855     public boolean isAjaxDetailsRetrieval() {
1856         return ajaxDetailsRetrieval;
1857     }
1858 
1859     /**
1860      * Set if row details content should be retrieved fromt he server
1861      *
1862      * @param ajaxDetailsRetrieval
1863      */
1864     public void setAjaxDetailsRetrieval(boolean ajaxDetailsRetrieval) {
1865         this.ajaxDetailsRetrieval = ajaxDetailsRetrieval;
1866     }
1867 
1868     /**
1869      * The Action prototype used for the row details expand link.  Should be set to "Uif-ExpandDetailsAction" or
1870      * "Uif-ExpandDetailsImageAction".  Properties can be configured to allow for different methodToCall and
1871      * actionParameters to be set for ajax row details retrieval.
1872      *
1873      * @return the Action details link prototype
1874      */
1875     public Action getExpandDetailsActionPrototype() {
1876         return expandDetailsActionPrototype;
1877     }
1878 
1879     /**
1880      * Set the expand details Action prototype link
1881      *
1882      * @param expandDetailsActionPrototype
1883      */
1884     public void setExpandDetailsActionPrototype(Action expandDetailsActionPrototype) {
1885         this.expandDetailsActionPrototype = expandDetailsActionPrototype;
1886     }
1887 }