View Javadoc
1   /**
2    * Copyright 2005-2014 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.krad.uif.widget;
17  
18  import java.sql.Timestamp;
19  import java.util.ArrayList;
20  import java.util.HashMap;
21  import java.util.HashSet;
22  import java.util.List;
23  import java.util.Map;
24  import java.util.Set;
25  
26  import javax.annotation.Nullable;
27  
28  import org.apache.commons.collections.CollectionUtils;
29  import org.apache.commons.lang.ClassUtils;
30  import org.apache.commons.lang.StringUtils;
31  import org.kuali.rice.core.api.CoreApiServiceLocator;
32  import org.kuali.rice.core.api.config.property.ConfigurationService;
33  import org.kuali.rice.core.api.util.type.KualiDecimal;
34  import org.kuali.rice.core.api.util.type.KualiInteger;
35  import org.kuali.rice.core.api.util.type.KualiPercent;
36  import org.kuali.rice.krad.datadictionary.parse.BeanTag;
37  import org.kuali.rice.krad.datadictionary.parse.BeanTagAttribute;
38  import org.kuali.rice.krad.datadictionary.parse.BeanTags;
39  import org.kuali.rice.krad.lookup.LookupView;
40  import org.kuali.rice.krad.uif.UifConstants;
41  import org.kuali.rice.krad.uif.UifParameters;
42  import org.kuali.rice.krad.uif.component.Component;
43  import org.kuali.rice.krad.uif.component.ComponentBase;
44  import org.kuali.rice.krad.uif.container.CollectionGroup;
45  import org.kuali.rice.krad.uif.control.CheckboxControl;
46  import org.kuali.rice.krad.uif.control.CheckboxGroupControl;
47  import org.kuali.rice.krad.uif.control.Control;
48  import org.kuali.rice.krad.uif.control.RadioGroupControl;
49  import org.kuali.rice.krad.uif.control.SelectControl;
50  import org.kuali.rice.krad.uif.field.DataField;
51  import org.kuali.rice.krad.uif.field.Field;
52  import org.kuali.rice.krad.uif.field.FieldGroup;
53  import org.kuali.rice.krad.uif.field.InputField;
54  import org.kuali.rice.krad.uif.field.LinkField;
55  import org.kuali.rice.krad.uif.field.MessageField;
56  import org.kuali.rice.krad.uif.layout.LayoutManager;
57  import org.kuali.rice.krad.uif.layout.TableLayoutManager;
58  import org.kuali.rice.krad.uif.lifecycle.ViewLifecycle;
59  import org.kuali.rice.krad.uif.util.LifecycleElement;
60  import org.kuali.rice.krad.uif.util.ObjectPropertyUtils;
61  import org.kuali.rice.krad.uif.view.View;
62  import org.kuali.rice.krad.util.KRADConstants;
63  import org.kuali.rice.krad.util.KRADUtils;
64  import org.kuali.rice.krad.web.form.UifFormBase;
65  
66  import com.google.common.base.Function;
67  import com.google.common.collect.Lists;
68  
69  /**
70   * Decorates a HTML Table client side with various tools
71   * 
72   * <p>
73   * Decorations implemented depend on widget implementation. Examples are sorting, paging and
74   * skinning.
75   * </p>
76   * 
77   * @author Kuali Rice Team (rice.collab@kuali.org)
78   */
79  @BeanTags({@BeanTag(name = "richTable-bean", parent = "Uif-RichTable"),
80          @BeanTag(name = "pagedRichTable-bean", parent = "Uif-PagedRichTable"),
81          @BeanTag(name = "scrollableRichTable-bean", parent = "Uif-ScrollableRichTable")})
82  public class RichTable extends WidgetBase {
83      private static final long serialVersionUID = 4671589690877390070L;
84  
85      private String emptyTableMessage;
86      private boolean disableTableSort;
87  
88      private boolean forceAoColumnDefsOverride;
89  
90      private boolean forceLocalJsonData;
91      private int nestedLevel;
92      private String aaData;
93  
94      private Set<String> hiddenColumns;
95      private Set<String> sortableColumns;
96      private List<String> cellCssClasses;
97  
98      private String ajaxSource;
99  
100     private boolean showExportOption;
101 
102     private String groupingOptionsJSString;
103 
104     public RichTable() {
105         super();
106         groupingOptionsJSString = "null";
107         cellCssClasses = new ArrayList<String>();
108     }
109 
110     /**
111      * The following initialization is performed:
112      * 
113      * <ul>
114      * <li>Initializes component options for empty table message</li>
115      * </ul>
116      */
117     @Override
118     public void performFinalize(Object model, LifecycleElement parent) {
119         super.performFinalize(model, parent);
120 
121         UifFormBase formBase = (UifFormBase) model;
122 
123         if (!isRender()) {
124             return;
125         }
126 
127         if (templateOptions.isEmpty()) {
128             setTemplateOptions(new HashMap<String, String>());
129         }
130 
131         if (StringUtils.isNotBlank(getEmptyTableMessage()) && !templateOptions.containsKey(
132                 UifConstants.TableToolsKeys.LANGUAGE)) {
133             templateOptions.put(UifConstants.TableToolsKeys.LANGUAGE,
134                     "{\"" + UifConstants.TableToolsKeys.EMPTY_TABLE + "\" : \"" + getEmptyTableMessage() + "\"}");
135         }
136 
137         Object domOption = templateOptions.get(UifConstants.TableToolsKeys.SDOM);
138         if (domOption instanceof String) {
139             String sDomOption = (String) domOption;
140 
141             if (StringUtils.isNotBlank(sDomOption)) {
142                 if (!isShowExportOption()) {
143                     sDomOption = StringUtils.remove(sDomOption, "T"); //Removes Export option
144                 }
145                 templateOptions.put(UifConstants.TableToolsKeys.SDOM, sDomOption);
146             }
147         }
148 
149         // for add events, disable initial sorting
150         if (UifConstants.ActionEvents.ADD_LINE.equals(formBase.getActionEvent()) || UifConstants.ActionEvents
151                 .ADD_BLANK_LINE.equals(formBase.getActionEvent())) {
152             templateOptions.put(UifConstants.TableToolsKeys.AASORTING, "[]");
153         }
154 
155         if ((parent instanceof CollectionGroup)) {
156             CollectionGroup collectionGroup = (CollectionGroup) parent;
157             LayoutManager layoutManager = collectionGroup.getLayoutManager();
158 
159             //if useServerPaging is true, add the css cell styling to the template options so it can still be used
160             //since this will not go through the grid ftl
161             if (layoutManager instanceof TableLayoutManager && collectionGroup.isUseServerPaging()) {
162                 addCellStyling((TableLayoutManager) layoutManager);
163             }
164 
165             buildTableOptions(collectionGroup);
166             setTotalOptions(collectionGroup);
167 
168             View view = ViewLifecycle.getActiveLifecycle().getView();
169             if (view instanceof LookupView) {
170                 buildSortOptions((LookupView) view, collectionGroup);
171             }
172         }
173 
174         if (isDisableTableSort()) {
175             templateOptions.put(UifConstants.TableToolsKeys.TABLE_SORT, "false");
176         }
177 
178         String kradUrl = getConfigurationService().getPropertyValueAsString(UifConstants.ConfigProperties.KRAD_URL);
179         if (StringUtils.isNotBlank(ajaxSource)) {
180             templateOptions.put(UifConstants.TableToolsKeys.SAJAX_SOURCE, ajaxSource);
181         } else if (parent instanceof CollectionGroup && ((CollectionGroup) parent).isUseServerPaging()) {
182             // enable required dataTables options for server side paging
183             templateOptions.put(UifConstants.TableToolsKeys.BPROCESSING, "true");
184             templateOptions.put(UifConstants.TableToolsKeys.BSERVER_SIDE, "true");
185 
186             // build sAjaxSource url to call
187             templateOptions.put(UifConstants.TableToolsKeys.SAJAX_SOURCE,
188                     kradUrl + ((UifFormBase) model).getControllerMapping() + "?" +
189                             UifConstants.CONTROLLER_METHOD_DISPATCH_PARAMETER_NAME + "=" +
190                             UifConstants.MethodToCallNames.TABLE_JSON + "&" + UifParameters.UPDATE_COMPONENT_ID + "=" +
191                             parent.getId() + "&" + UifParameters.FORM_KEY + "=" + ((UifFormBase) model).getFormKey() +
192                             "&" + UifParameters.AJAX_RETURN_TYPE + "=" +
193                             UifConstants.AjaxReturnTypes.UPDATECOMPONENT.getKey() + "&" + UifParameters.AJAX_REQUEST +
194                             "=" + "true");
195 
196 
197             //TODO: Figure out where to move this script file constant?
198             String pushLookupSelect = "function (aoData) { " +
199                 "if(jQuery('table.dataTable').length > 0) {    "  +
200                 "    var table = jQuery('table.dataTable');    " +
201                 "    jQuery( table.find(':input:checked')).each( function (index) {     " +
202                 "        aoData.push({'name': (jQuery(this)).attr('name'),'value': (jQuery(this)).attr('value')});  " +
203                     "    console.log(jQuery(this).attr('name') + ':' + jQuery(this).attr('value')); "   +
204                 "    });  " +
205                 "}  " +
206             "}";
207 
208             templateOptions.put(UifConstants.TableToolsKeys.SERVER_PARAMS,pushLookupSelect);
209 
210             // store col defs so columns can be built on paging request
211             ViewLifecycle.getViewPostMetadata().addComponentPostData(parent.getId(),
212                     UifConstants.TableToolsKeys.AO_COLUMN_DEFS, templateOptions.get(
213                     UifConstants.TableToolsKeys.AO_COLUMN_DEFS));
214         }
215 
216         // build export url to call
217         templateOptions.put(UifConstants.TableToolsKeys.SDOWNLOAD_SOURCE,
218                 kradUrl + "/" + UifConstants.ControllerMappings.EXPORT + "?" + UifParameters.UPDATE_COMPONENT_ID + "=" +
219                         parent.getId() + "&" + UifParameters.FORM_KEY + "=" + ((UifFormBase) model).getFormKey() + "&" +
220                         UifParameters.AJAX_RETURN_TYPE + "=" + UifConstants.AjaxReturnTypes.UPDATECOMPONENT.getKey() +
221                         "&" + UifParameters.AJAX_REQUEST + "=" + "true");
222     }
223 
224     /**
225      * Add the css style to the cellCssClasses by column index, later used by the aoColumnDefs
226      * 
227      * @param manager the tableLayoutManager that contains the original fields
228      */
229     private void addCellStyling(TableLayoutManager manager) {
230         if (!CollectionUtils.isEmpty(manager.getAllRowFields())) {
231             for (int index = 0; index < manager.getNumberOfColumns(); index++) {
232                 String cellStyleClasses = ((ComponentBase) manager.getAllRowFields().get(index))
233                         .getWrapperCssClassesAsString();
234                 if (StringUtils.isNotBlank (cellStyleClasses)) {
235                     cellCssClasses.add(cellStyleClasses);
236                 }
237             }
238         }
239     }
240 
241     /**
242      * Builds the footer callback template option for column totals
243      * 
244      * @param collectionGroup the collection group
245      */
246     private void setTotalOptions(CollectionGroup collectionGroup) {
247         LayoutManager layoutManager = collectionGroup.getLayoutManager();
248 
249         if (layoutManager instanceof TableLayoutManager) {
250             List<String> totalColumns = ((TableLayoutManager) layoutManager).getColumnsToCalculate();
251 
252             if (totalColumns.size() > 0) {
253                 String array = "[";
254 
255                 for (String i : totalColumns) {
256                     array = array + i + ",";
257                 }
258                 array = StringUtils.removeEnd(array, ",");
259                 array = array + "]";
260 
261                 templateOptions.put(UifConstants.TableToolsKeys.FOOTER_CALLBACK,
262                         "function (nRow, aaData, iStart, iEnd, aiDisplay) {initializeTotalsFooter (nRow, aaData, iStart, iEnd, aiDisplay, "
263                                 + array
264                                 + " )}");
265             }
266         }
267     }
268 
269     /**
270      * Builds column options for sorting
271      * 
272      * @param collectionGroup
273      */
274     protected void buildTableOptions(CollectionGroup collectionGroup) {
275         checkMutable(false);
276         
277         LayoutManager layoutManager = collectionGroup.getLayoutManager();
278         final boolean isUseServerPaging = collectionGroup.isUseServerPaging();
279 
280         if (templateOptions.isEmpty()) {
281             setTemplateOptions(new HashMap<String, String>());
282         }
283 
284         // if sub collection exists, don't allow the table sortable
285         if (!collectionGroup.getSubCollections().isEmpty()) {
286             setDisableTableSort(true);
287         }
288 
289         if (!isDisableTableSort()) {
290             // if rendering add line, skip that row from col sorting
291             if (collectionGroup.isRenderAddLine()
292                     && !collectionGroup.isReadOnly()
293                     && !((layoutManager instanceof TableLayoutManager) && ((TableLayoutManager) layoutManager)
294                             .isSeparateAddLine())) {
295 
296                 templateOptions.put(UifConstants.TableToolsKeys.SORT_SKIP_ROWS,
297                         "[" + UifConstants.TableToolsValues.ADD_ROW_DEFAULT_INDEX + "]");
298             }
299 
300             StringBuilder tableToolsColumnOptions = new StringBuilder("[");
301 
302             int columnIndex = 0;
303             int actionIndex = UifConstants.TableLayoutValues.ACTIONS_COLUMN_RIGHT_INDEX;
304             boolean actionFieldVisible = collectionGroup.isRenderLineActions() && !collectionGroup.isReadOnly();
305 
306             if (layoutManager instanceof TableLayoutManager) {
307                 actionIndex = ((TableLayoutManager) layoutManager).getActionColumnIndex();
308             }
309 
310             if (actionIndex == UifConstants.TableLayoutValues.ACTIONS_COLUMN_LEFT_INDEX && actionFieldVisible) {
311                 String actionColOptions = constructTableColumnOptions(columnIndex, false, isUseServerPaging, null,
312                         null);
313                 tableToolsColumnOptions.append(actionColOptions + " , ");
314                 columnIndex++;
315             }
316 
317             if (layoutManager instanceof TableLayoutManager && ((TableLayoutManager) layoutManager)
318                     .isRenderSequenceField()) {
319 
320                 //add mData handling if using a json data source
321                 String mDataOption = "";
322                 if (collectionGroup.isUseServerPaging() || this.forceLocalJsonData) {
323                     mDataOption = "\""
324                             + UifConstants.TableToolsKeys.MDATA
325                             +
326                             "\" : function(row,type,newVal){ return _handleColData(row,type,'c"
327                             + columnIndex
328                             + "',newVal);}, ";
329                 }
330 
331                 tableToolsColumnOptions.append("{\""
332                         + UifConstants.TableToolsKeys.SORTABLE
333                         + "\" : "
334                         + false
335                         // auto sequence column is never sortable
336                         + ", \""
337                         + UifConstants.TableToolsKeys.SORT_TYPE
338                         + "\" : \""
339                         + UifConstants.TableToolsValues.NUMERIC
340                         + "\", "
341                         + mDataOption
342                         + "\""
343                         + UifConstants.TableToolsKeys.TARGETS
344                         + "\": ["
345                         + columnIndex
346                         + "]}, ");
347                 columnIndex++;
348                 if (actionIndex == 2 && actionFieldVisible) {
349                     String actionColOptions = constructTableColumnOptions(columnIndex, false, isUseServerPaging, null,
350                             null);
351                     tableToolsColumnOptions.append(actionColOptions + " , ");
352                     columnIndex++;
353                 }
354             }
355 
356             // skip select field if enabled
357             if (collectionGroup.isIncludeLineSelectionField()) {
358                 String colOptions = constructTableColumnOptions(columnIndex, false, isUseServerPaging, null, null);
359                 tableToolsColumnOptions.append(colOptions + " , ");
360                 columnIndex++;
361             }
362 
363             // if data dictionary defines aoColumns, copy here and skip default sorting/visibility behaviour
364             if (!StringUtils.isEmpty(templateOptions.get(UifConstants.TableToolsKeys.AO_COLUMNS))) {
365                 // get the contents of the JS array string
366                 String jsArray = templateOptions.get(UifConstants.TableToolsKeys.AO_COLUMNS);
367                 int startBrace = StringUtils.indexOf(jsArray, "[");
368                 int endBrace = StringUtils.lastIndexOf(jsArray, "]");
369                 tableToolsColumnOptions.append(StringUtils.substring(jsArray, startBrace + 1, endBrace) + ", ");
370 
371                 if (actionFieldVisible && (actionIndex == -1 || actionIndex >= columnIndex)) {
372                     String actionColOptions = constructTableColumnOptions(columnIndex, false, isUseServerPaging, null,
373                             null);
374                     tableToolsColumnOptions.append(actionColOptions);
375                 } else {
376                     tableToolsColumnOptions = new StringBuilder(StringUtils.removeEnd(
377                             tableToolsColumnOptions.toString(),
378                             ", "));
379                 }
380 
381                 tableToolsColumnOptions.append("]");
382                 templateOptions.put(UifConstants.TableToolsKeys.AO_COLUMNS, tableToolsColumnOptions.toString());
383             } else if (!StringUtils.isEmpty(templateOptions.get(UifConstants.TableToolsKeys.AO_COLUMN_DEFS))
384                     && forceAoColumnDefsOverride) {
385                 String jsArray = templateOptions.get(UifConstants.TableToolsKeys.AO_COLUMN_DEFS);
386                 int startBrace = StringUtils.indexOf(jsArray, "[");
387                 int endBrace = StringUtils.lastIndexOf(jsArray, "]");
388                 tableToolsColumnOptions.append(StringUtils.substring(jsArray, startBrace + 1, endBrace) + ", ");
389 
390                 if (actionFieldVisible && (actionIndex == -1 || actionIndex >= columnIndex)) {
391                     String actionColOptions = constructTableColumnOptions(columnIndex, false, isUseServerPaging, null,
392                             null);
393                     tableToolsColumnOptions.append(actionColOptions);
394                 } else {
395                     tableToolsColumnOptions = new StringBuilder(StringUtils.removeEnd(
396                             tableToolsColumnOptions.toString(),
397                             ", "));
398                 }
399 
400                 tableToolsColumnOptions.append("]");
401                 templateOptions.put(UifConstants.TableToolsKeys.AO_COLUMN_DEFS,
402                         tableToolsColumnOptions.toString());
403             } else if (layoutManager instanceof TableLayoutManager) {
404                 List<Field> rowFields = ((TableLayoutManager) layoutManager).getFirstRowFields();
405 
406                 // build column defs from the the first row of the table
407                 for (Component component : rowFields) {
408                     if (actionFieldVisible && columnIndex + 1 == actionIndex) {
409                         String actionColOptions = constructTableColumnOptions(columnIndex, false, isUseServerPaging,
410                                 null, null);
411                         tableToolsColumnOptions.append(actionColOptions + " , ");
412                         columnIndex++;
413                     }
414 
415                     //add mData handling if using a json data source
416                     String mDataOption = "";
417                     if (collectionGroup.isUseServerPaging() || this.forceLocalJsonData) {
418                         mDataOption = "\""
419                                 + UifConstants.TableToolsKeys.MDATA
420                                 +
421                                 "\" : function(row,type,newVal){ return _handleColData(row,type,'c"
422                                 + columnIndex
423                                 + "',newVal);}, ";
424                     }
425 
426                     // for FieldGroup, get the first field from that group
427                     if (component instanceof FieldGroup) {
428                         component = ((FieldGroup) component).getItems().get(0);
429                     }
430 
431                     Map<String, String> componentDataAttributes;
432                     if (component instanceof DataField) {
433                         DataField field = (DataField) component;
434 
435                         // if a field is marked as invisible in hiddenColumns, append options and skip sorting
436                         if (getHiddenColumns() != null && getHiddenColumns().contains(field.getPropertyName())) {
437                             tableToolsColumnOptions.append("{"
438                                     + UifConstants.TableToolsKeys.VISIBLE
439                                     + ": "
440                                     + UifConstants.TableToolsValues.FALSE
441                                     + ", "
442                                     + mDataOption
443                                     + "\""
444                                     + UifConstants.TableToolsKeys.TARGETS
445                                     + "\": ["
446                                     + columnIndex
447                                     + "]"
448                                     + "}, ");
449                             // if sortableColumns is present and a field is marked as sortable or unspecified             if (!isDisableTableSort()) {
450                         } else if (getSortableColumns() != null && !getSortableColumns().isEmpty()) {
451                             if (getSortableColumns().contains(field.getPropertyName())) {
452                                 tableToolsColumnOptions.append(getDataFieldColumnOptions(columnIndex, collectionGroup,
453                                         field) + ", ");
454                             } else {
455                                 tableToolsColumnOptions.append("{'"
456                                         + UifConstants.TableToolsKeys.SORTABLE
457                                         + "': "
458                                         + UifConstants.TableToolsValues.FALSE
459                                         + ", "
460                                         + mDataOption
461                                         + "\""
462                                         + UifConstants.TableToolsKeys.TARGETS
463                                         + "\": ["
464                                         + columnIndex
465                                         + "]"
466                                         + "}, ");
467                             }
468                         } else {// sortable columns not defined
469                             String colOptions = getDataFieldColumnOptions(columnIndex, collectionGroup, field);
470                             tableToolsColumnOptions.append(colOptions + " , ");
471                         }
472                         columnIndex++;
473                     } else if ((component instanceof MessageField)
474                             && (componentDataAttributes = component.getDataAttributes()) != null
475                             && UifConstants.RoleTypes.ROW_GROUPING.equals(componentDataAttributes.get(
476                                     UifConstants.DataAttributes.ROLE))) {
477                         //Grouping column is never shown, so skip
478                         tableToolsColumnOptions.append("{"
479                                 + UifConstants.TableToolsKeys.VISIBLE
480                                 + ": "
481                                 + UifConstants.TableToolsValues.FALSE
482                                 + ", "
483                                 + mDataOption
484                                 + "\""
485                                 + UifConstants.TableToolsKeys.TARGETS
486                                 + "\": ["
487                                 + columnIndex
488                                 + "]"
489                                 + "}, ");
490                         columnIndex++;
491                     } else if (component instanceof LinkField) {
492                         String colOptions = constructTableColumnOptions(columnIndex, true, isUseServerPaging,
493                                 String.class, UifConstants.TableToolsValues.DOM_TEXT);
494                         tableToolsColumnOptions.append(colOptions + " , ");
495                         columnIndex++;
496                     } else {
497                         String colOptions = constructTableColumnOptions(columnIndex, false, isUseServerPaging, null,
498                                 null);
499                         tableToolsColumnOptions.append(colOptions + " , ");
500                         columnIndex++;
501                     }
502                 }
503 
504                 if (actionFieldVisible && (actionIndex == -1 || actionIndex >= columnIndex)) {
505                     String actionColOptions = constructTableColumnOptions(columnIndex, false, isUseServerPaging, null,
506                             null);
507                     tableToolsColumnOptions.append(actionColOptions);
508                 } else {
509                     tableToolsColumnOptions = new StringBuilder(StringUtils.removeEnd(
510                             tableToolsColumnOptions.toString(),
511                             ", "));
512                 }
513 
514                 //merge the aoColumnDefs passed in
515                 if (!StringUtils.isEmpty(templateOptions.get(UifConstants.TableToolsKeys.AO_COLUMN_DEFS))) {
516                     String origAoOptions = templateOptions.get(UifConstants.TableToolsKeys.AO_COLUMN_DEFS).trim();
517                     origAoOptions = StringUtils.removeStart(origAoOptions, "[");
518                     origAoOptions = StringUtils.removeEnd(origAoOptions, "]");
519                     tableToolsColumnOptions.append("," + origAoOptions);
520                 }
521 
522                 tableToolsColumnOptions.append("]");
523                 templateOptions.put(UifConstants.TableToolsKeys.AO_COLUMN_DEFS,
524                         tableToolsColumnOptions.toString());
525             }
526         }
527     }
528 
529     /**
530      * Builds default sorting options.
531      * 
532      * @param lookupView the view for the lookup
533      * @param collectionGroup the collection group for the table
534      */
535     protected void buildSortOptions(LookupView lookupView, CollectionGroup collectionGroup) {
536         if (!isDisableTableSort() && CollectionUtils.isNotEmpty(lookupView.getDefaultSortAttributeNames())) {
537             LayoutManager layoutManager = collectionGroup.getLayoutManager();
538 
539             if (layoutManager instanceof TableLayoutManager) {
540                 TableLayoutManager tableLayoutManager = (TableLayoutManager) layoutManager;
541 
542                 List<String> firstRowPropertyNames = getFirstRowPropertyNames(tableLayoutManager.getFirstRowFields());
543                 List<String> defaultSortAttributeNames = lookupView.getDefaultSortAttributeNames();
544                 String sortDirection = lookupView.isDefaultSortAscending() ? "'asc'" : "'desc'";
545                 boolean actionFieldVisible = collectionGroup.isRenderLineActions() && !collectionGroup.isReadOnly();
546                 int actionIndex = ((TableLayoutManager) layoutManager).getActionColumnIndex();
547                 int columnIndexPrefix = 0;
548 
549                 if (tableLayoutManager.isRenderSequenceField()) {
550                     columnIndexPrefix++;
551                 }
552 
553                 if (tableLayoutManager.getRowDetailsGroup() != null
554                         && CollectionUtils.isNotEmpty(tableLayoutManager.getRowDetailsGroup().getItems())) {
555                     columnIndexPrefix++;
556                 }
557 
558                 StringBuffer tableToolsSortOptions = new StringBuffer("[");
559 
560                 for (String defaultSortAttributeName : defaultSortAttributeNames) {
561                     int index = firstRowPropertyNames.indexOf(defaultSortAttributeName);
562                     if (index >= 0) {
563                         if (tableToolsSortOptions.length() > 1) {
564                             tableToolsSortOptions.append(", ");
565                         }
566                         int columnIndex = columnIndexPrefix + index;
567                         if (actionFieldVisible && actionIndex != -1 && actionIndex <= columnIndex + 1) {
568                             columnIndex++;
569                         }
570                         tableToolsSortOptions.append("[" + columnIndex + ", " + sortDirection + "]");
571                     }
572                 }
573 
574                 tableToolsSortOptions.append("]");
575 
576                 if (templateOptions.isEmpty()) {
577                     setTemplateOptions(new HashMap<String, String>());
578                 }
579                 templateOptions.put(UifConstants.TableToolsKeys.AASORTING, tableToolsSortOptions.toString());
580             }
581         }
582     }
583 
584     private List<String> getFirstRowPropertyNames(List<Field> firstRowFields) {
585         return Lists.transform(firstRowFields, new Function<Field, String>() {
586             @Override
587             public String apply(@Nullable Field field) {
588                 if (field != null && field instanceof DataField) {
589                     return ((DataField) field).getPropertyName();
590                 } else {
591                     return null;
592                 }
593             }
594         });
595     }
596 
597     /**
598      * Construct the column options for a data field
599      * 
600      * @param target column index
601      * @param collectionGroup the collectionGroup in which the data field is defined
602      * @param field the field to construction options for
603      * @return options as valid for datatable
604      */
605     protected String getDataFieldColumnOptions(int target, CollectionGroup collectionGroup, DataField field) {
606         String sortType = null;
607 
608         if (!collectionGroup.isReadOnly()
609                 && (field instanceof InputField)
610                 && ((InputField) field).getControl() != null) {
611             Control control = ((InputField) field).getControl();
612             if (control instanceof SelectControl) {
613                 sortType = UifConstants.TableToolsValues.DOM_SELECT;
614             } else if (control instanceof CheckboxControl || control instanceof CheckboxGroupControl) {
615                 sortType = UifConstants.TableToolsValues.DOM_CHECK;
616             } else if (control instanceof RadioGroupControl) {
617                 sortType = UifConstants.TableToolsValues.DOM_RADIO;
618             } else {
619                 sortType = UifConstants.TableToolsValues.DOM_TEXT;
620             }
621         } else {
622             sortType = UifConstants.TableToolsValues.DOM_TEXT;
623         }
624 
625         Class<?> dataTypeClass = ObjectPropertyUtils.getPropertyType(collectionGroup.getCollectionObjectClass(),
626                 field.getPropertyName());
627 
628         boolean isSortable = true;
629         if (field.isApplyMask()) {
630             isSortable = false;
631         }
632 
633         return constructTableColumnOptions(target, isSortable, collectionGroup.isUseServerPaging(), dataTypeClass,
634                 sortType);
635     }
636 
637     /**
638      * Constructs the sort data type for each data table columns in a format that will be used to
639      * initialize the data table widget via javascript
640      * 
641      * @param target the column index
642      * @param isSortable whether a column should be marked as sortable
643      * @param isUseServerPaging is server side paging enabled?
644      * @param dataTypeClass the class type of the column value - used determine the sType option -
645      *        which identifies the search plugin to use
646      * @param sortDataType Defines a data source type for the sorting which can be used to read
647      *        realtime information from the table
648      * @return a formatted string with data table options for one column
649      */
650     public String constructTableColumnOptions(int target, boolean isSortable, boolean isUseServerPaging,
651             Class<?> dataTypeClass, String sortDataType) {
652         String colOptions = "null";
653 
654         String sortType = "";
655         if (!isSortable || dataTypeClass == null || sortType == null) {
656             colOptions = "\"" + UifConstants.TableToolsKeys.SORTABLE + "\" : false, \"sType\" : \"string\"";
657         } else {
658             if (ClassUtils.isAssignable(dataTypeClass, KualiPercent.class)) {
659                 sortType = UifConstants.TableToolsValues.PERCENT;
660             } else if (ClassUtils.isAssignable(dataTypeClass, KualiInteger.class) || ClassUtils.isAssignable(
661                     dataTypeClass, KualiDecimal.class)) {
662                 sortType = UifConstants.TableToolsValues.CURRENCY;
663             } else if (ClassUtils.isAssignable(dataTypeClass, Timestamp.class)) {
664                 sortType = "date";
665             } else if (ClassUtils.isAssignable(dataTypeClass, java.sql.Date.class) || ClassUtils.isAssignable(
666                     dataTypeClass, java.util.Date.class)) {
667                 sortType = UifConstants.TableToolsValues.DATE;
668             } else if (ClassUtils.isAssignable(dataTypeClass, Number.class)) {
669                 sortType = UifConstants.TableToolsValues.NUMERIC;
670             } else {
671                 sortType = UifConstants.TableToolsValues.STRING;
672             }
673 
674             colOptions = "\"" + UifConstants.TableToolsKeys.SORT_TYPE + "\" : \"" + sortType + "\"";
675 
676             if (!isUseServerPaging && !this.forceLocalJsonData) {
677                 colOptions += ", \"" + UifConstants.TableToolsKeys.SORT_DATA_TYPE + "\" : \"" + sortDataType + "\"";
678             }
679         }
680 
681         if (target < cellCssClasses.size() && target >= 0) {
682             colOptions += ", \""
683                     + UifConstants.TableToolsKeys.CELL_CLASS
684                     + "\" : \""
685                     + cellCssClasses.get(target)
686                     + "\"";
687         }
688 
689         // only use the mDataProp when using json data (only relevant for this table type)
690         if (isUseServerPaging || this.forceLocalJsonData) {
691             colOptions += ", \"" + UifConstants.TableToolsKeys.MDATA +
692                     "\" : function(row,type,newVal){ return _handleColData(row,type,'c" + target + "',newVal);}";
693         }
694 
695         if (!colOptions.equals("null")) {
696             colOptions = "{" + colOptions + ", \"" + UifConstants.TableToolsKeys.TARGETS + "\": [" + target + "]}";
697         } else {
698             colOptions = "{" + colOptions + "}";
699         }
700 
701         return colOptions;
702     }
703 
704     /**
705      * Returns the text which is used to display text when the table is empty
706      * 
707      * @return empty table message
708      */
709     @BeanTagAttribute(name = "emptyTableMessage")
710     public String getEmptyTableMessage() {
711         return emptyTableMessage;
712     }
713 
714     /**
715      * Setter for a text to be displayed when the table is empty
716      * 
717      * @param emptyTableMessage
718      */
719     public void setEmptyTableMessage(String emptyTableMessage) {
720         this.emptyTableMessage = emptyTableMessage;
721     }
722 
723     /**
724      * Returns true if sorting is disabled
725      * 
726      * @return the disableTableSort
727      */
728     @BeanTagAttribute(name = "disableTableSort")
729     public boolean isDisableTableSort() {
730         return this.disableTableSort;
731     }
732 
733     /**
734      * Enables/disables the table sorting
735      * 
736      * @param disableTableSort the disableTableSort to set
737      */
738     public void setDisableTableSort(boolean disableTableSort) {
739         this.disableTableSort = disableTableSort;
740     }
741 
742     /**
743      * Returns true if export option is enabled
744      * 
745      * @return the showExportOption
746      */
747     @BeanTagAttribute(name = "showExportOption")
748     public boolean isShowExportOption() {
749         return this.showExportOption;
750     }
751 
752     /**
753      * Show/Hide the search and export option in tabletools
754      * 
755      * @param showExportOption the showExportOptions to set
756      */
757     public void setShowExportOption(boolean showExportOption) {
758         this.showExportOption = showExportOption;
759     }
760 
761     /**
762      * Holds propertyNames for the ones meant to be hidden since columns are visible by default
763      * 
764      * <p>
765      * Duplicate entries are ignored and the order of entries is not significant
766      * </p>
767      * 
768      * @return a set with propertyNames of columns to be hidden
769      */
770     @BeanTagAttribute(name = "hiddenColumns", type = BeanTagAttribute.AttributeType.SETVALUE)
771     public Set<String> getHiddenColumns() {
772         return hiddenColumns;
773     }
774 
775     /**
776      * Setter for the hidden columns set
777      * 
778      * @param hiddenColumns a set containing propertyNames
779      */
780     public void setHiddenColumns(Set<String> hiddenColumns) {
781         this.hiddenColumns = hiddenColumns;
782     }
783 
784     /**
785      * Holds the propertyNames for columns that are to be sorted
786      * 
787      * <p>
788      * Duplicate entries are ignored and the order of entries is not significant
789      * </p>
790      * 
791      * @return a set of propertyNames with for columns that will be sorted
792      */
793     @BeanTagAttribute(name = "sortableColumns", type = BeanTagAttribute.AttributeType.SETVALUE)
794     public Set<String> getSortableColumns() {
795         return sortableColumns;
796     }
797 
798     /**
799      * Setter for sortable columns
800      * 
801      * @param sortableColumns a set containing propertyNames of columns to be sorted
802      */
803     public void setSortableColumns(Set<String> sortableColumns) {
804         this.sortableColumns = sortableColumns;
805     }
806 
807     /**
808      * Specifies a URL for acquiring the table data with ajax
809      * 
810      * <p>
811      * When the ajax source URL is specified the rich table plugin will retrieve the data by
812      * invoking the URL and building the table rows from the result. This is different from the
813      * standard use of the rich table plugin with uses progressive enhancement to decorate a table
814      * that has already been rendereed
815      * </p>
816      * 
817      * @return URL for ajax source
818      */
819     @BeanTagAttribute(name = "ajaxSource")
820     public String getAjaxSource() {
821         return ajaxSource;
822     }
823 
824     /**
825      * Setter for the Ajax source URL
826      * 
827      * @param ajaxSource
828      */
829     public void setAjaxSource(String ajaxSource) {
830         this.ajaxSource = ajaxSource;
831     }
832 
833     /**
834      * Get groupingOption
835      * 
836      * @return grouping options as a JS string
837      */
838     public String getGroupingOptionsJSString() {
839         return groupingOptionsJSString;
840     }
841 
842     /**
843      * Set the groupingOptions js data. <b>This should not be set through XML configuration.</b>
844      * 
845      * @param groupingOptionsJSString
846      */
847     public void setGroupingOptionsJSString(String groupingOptionsJSString) {
848         this.groupingOptionsJSString = groupingOptionsJSString;
849     }
850 
851     /**
852      * If set to true and the aoColumnDefs template option is explicitly defined in templateOptions,
853      * those aoColumnDefs will be used for this table. Otherwise, if false, the aoColumnDefs will
854      * attempt to be merged with those that are automatically generated by RichTable
855      * 
856      * @return true if the aoColumnDefs set will completely override those that are generated
857      *         automatically by RichTable
858      */
859     public boolean isForceAoColumnDefsOverride() {
860         return forceAoColumnDefsOverride;
861     }
862 
863     /**
864      * Set forceAoColumnDefsOverride
865      * 
866      * @param forceAoColumnDefsOverride
867      */
868     public void setForceAoColumnDefsOverride(boolean forceAoColumnDefsOverride) {
869         this.forceAoColumnDefsOverride = forceAoColumnDefsOverride;
870     }
871 
872     /**
873      * If true, the table will automatically use row JSON data generated by this widget
874      * 
875      * <p>
876      * This forces the table backed by this RichTable to get its content from a template option
877      * called aaData. This will automatically skip row generation in the template, and cause the
878      * table receive its data from the aaData template option automatically generated and set by
879      * this RichTable. This allows the table to take advantage of the bDeferRender option (also
880      * automatically set to true) when this table is a paged table (performance increase for tables
881      * that are more than one page). Note: the CollectionGroup's isUseServerPaging flag will always
882      * override this functionality if it is also true.
883      * </p>
884      * 
885      * @return true if backed by the aaData option in JSON, that is generated during the ftl
886      *         rendering process by this widget for this table
887      */
888     public boolean isForceLocalJsonData() {
889         return forceLocalJsonData;
890     }
891 
892     /**
893      * Set the forceLocalJsonData flag to force this table to use generated row json data
894      * 
895      * @param forceLocalJsonData
896      */
897     public void setForceLocalJsonData(boolean forceLocalJsonData) {
898         this.forceLocalJsonData = forceLocalJsonData;
899     }
900 
901     /**
902      * The nestedLevel property represents how many collection tables deep this particular table is
903      * 
904      * <p>
905      * This property must be manually set if the flag forceLocalJsonData is being used and the
906      * collection table this RichTable represents is a subcollection of a TABLE collection (not
907      * stacked collection). If this is true, add 1 for each level deep (ex. subCollection would be
908      * 1, sub-subCollection would be 2). If this property is not set javascript errors will occur on
909      * the page, as this determines how deep to escape certain characters.
910      * </p>
911      * 
912      * @return the nestedLevel representing the
913      */
914     public int getNestedLevel() {
915         return nestedLevel;
916     }
917 
918     /**
919      * Set the nestedLevel for this table - must be set if using forceLocalJsonData and this is a
920      * subCollection of a TableCollection (also using forceLocalJsonData)
921      * 
922      * @param nestedLevel
923      */
924     public void setNestedLevel(int nestedLevel) {
925         this.nestedLevel = nestedLevel;
926     }
927 
928     /**
929      * Get the translated aaData array generated by calls to addRowToTableData by the ftl
930      * 
931      * <p>
932      * This data is in JSON format and expected to be consumed by datatables when utilizing the
933      * forceLocalJsonData option. This will be populated automatically if that flag is set to true.
934      * </p>
935      * 
936      * @return the generated aaData
937      */
938     public String getAaData() {
939         return aaData;
940     }
941 
942     /**
943      * Set the translated aaData array
944      * 
945      * <p>
946      * This data is in JSON format and expected to be consumed by datatables when utilizing the
947      * forceLocalJsonData. This setter is required for copyProperties()
948      * </p>
949      * 
950      * @param aaData the generated aaData
951      */
952     protected void setAaData(String aaData) {
953         this.aaData = aaData;
954     }
955 
956     /**
957      * Get the simple value as a string that represents the field's sortable value, to be used as
958      * val in the custom uif json data object (accessed by mDataProp option on datatables -
959      * automated by framework) when using the forceLocalJsonData option or the CollectionGroup's
960      * isUseServerPaging option
961      * 
962      * @param model model the current model
963      * @param field the field to retrieve a sortable value from for use in custom json data
964      * @return the value as a String
965      */
966     public String getCellValue(Object model, Field field) {
967         String value = KRADUtils.getSimpleFieldValue(model, field);
968 
969         if (value == null) {
970             value = "null";
971         } else {
972             value = KRADConstants.QUOTE_PLACEHOLDER + value + KRADConstants.QUOTE_PLACEHOLDER;
973         }
974 
975         return value;
976     }
977 
978     /**
979      * Add row content passed from table ftl to the aaData array by converting and escaping the
980      * content to an object (in an array of objects) in JSON format
981      * 
982      * <p>
983      * The data in aaData is expected to be consumed by a call by the datatables plugin using
984      * sAjaxSource or aaData. The addRowToTableData generation call is additive must be made per a
985      * row in the ftl.
986      * </p>
987      * 
988      * @param row the row of content with each cell content surrounded by the @quot@ token and
989      *        followed by a comma
990      */
991     public void addRowToTableData(String row) {
992         String escape = "";
993         
994         if (templateOptions.isEmpty()) {
995             setTemplateOptions(new HashMap<String, String>());
996         }
997 
998         // if nestedLevel is set add the appropriate amount of escape characters per a level of nesting
999         for (int i = 0; i < nestedLevel && forceLocalJsonData; i++) {
1000             escape = escape + "\\";
1001         }
1002 
1003         // remove newlines and replace quotes and single quotes with unicode characters
1004         row = row.trim().replace("\"", escape + "\\u0022").replace("'", escape + "\\u0027").replace("\n", "").replace(
1005                 "\r", "");
1006 
1007         // remove hanging comma
1008         row = StringUtils.removeEnd(row, ",");
1009 
1010         // replace all quote placeholders with actual quote characters
1011         row = row.replace(KRADConstants.QUOTE_PLACEHOLDER, "\"");
1012         row = "{" + row + "}";
1013 
1014         // if first call create aaData and force defer render option, otherwise append
1015         if (StringUtils.isBlank(aaData)) {
1016             aaData = "[" + row + "]";
1017 
1018             if (templateOptions.get(UifConstants.TableToolsKeys.DEFER_RENDER) == null) {
1019                 //make sure deferred rendering is forced if not explicitly set
1020                 templateOptions.put(UifConstants.TableToolsKeys.DEFER_RENDER,
1021                         UifConstants.TableToolsValues.TRUE);
1022             }
1023 
1024         } else if (StringUtils.isNotBlank(row)) {
1025             aaData = aaData.substring(0, aaData.length() - 1) + "," + row + "]";
1026         }
1027 
1028         //force json data use if forceLocalJsonData flag is set
1029         if (forceLocalJsonData) {
1030             templateOptions.put(UifConstants.TableToolsKeys.AA_DATA, aaData);
1031         }
1032     }
1033 
1034     protected ConfigurationService getConfigurationService() {
1035         return CoreApiServiceLocator.getKualiConfigurationService();
1036     }
1037 }