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