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