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 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                 colIndex++;
354 
355                 if (actionIndex == 2 && actionFieldVisible) {
356                     String options = constructTableColumnOptions(colIndex, false, useServerPaging, null, null);
357                     tableColumnOptions.append(options + ",");
358                     colIndex++;
359                 }
360             }
361 
362             // skip select field if enabled
363             if (collectionGroup.isIncludeLineSelectionField()) {
364                 String options = constructTableColumnOptions(colIndex, false, useServerPaging, null, null);
365                 tableColumnOptions.append(options + ",");
366                 colIndex++;
367             }
368 
369             // if data dictionary defines aoColumns, copy here and skip default sorting/visibility behaviour
370             if (!StringUtils.isEmpty(templateOptions.get(UifConstants.TableToolsKeys.AO_COLUMNS))) {
371                 // get the contents of the JS array string
372                 String jsArray = templateOptions.get(UifConstants.TableToolsKeys.AO_COLUMNS);
373                 int startBrace = StringUtils.indexOf(jsArray, "[");
374                 int endBrace = StringUtils.lastIndexOf(jsArray, "]");
375                 tableColumnOptions.append(StringUtils.substring(jsArray, startBrace + 1, endBrace) + ",");
376 
377                 if (actionFieldVisible && (actionIndex == -1 || actionIndex >= colIndex)) {
378                     String options = constructTableColumnOptions(colIndex, false, useServerPaging, null, null);
379                     tableColumnOptions.append(options);
380                 } else {
381                     tableColumnOptions = new StringBuilder(StringUtils.removeEnd(tableColumnOptions.toString(), ","));
382                 }
383 
384                 tableColumnOptions.append("]");
385                 templateOptions.put(UifConstants.TableToolsKeys.AO_COLUMNS, tableColumnOptions.toString());
386             } else if (!StringUtils.isEmpty(templateOptions.get(UifConstants.TableToolsKeys.AO_COLUMN_DEFS))
387                     && forceAoColumnDefsOverride) {
388                 String jsArray = templateOptions.get(UifConstants.TableToolsKeys.AO_COLUMN_DEFS);
389                 int startBrace = StringUtils.indexOf(jsArray, "[");
390                 int endBrace = StringUtils.lastIndexOf(jsArray, "]");
391                 tableColumnOptions.append(StringUtils.substring(jsArray, startBrace + 1, endBrace) + ",");
392 
393                 if (actionFieldVisible && (actionIndex == -1 || actionIndex >= colIndex)) {
394                     String options = constructTableColumnOptions(colIndex, false, useServerPaging, null, null);
395                     tableColumnOptions.append(options);
396                 } else {
397                     tableColumnOptions = new StringBuilder(StringUtils.removeEnd(tableColumnOptions.toString(), ","));
398                 }
399 
400                 tableColumnOptions.append("]");
401                 templateOptions.put(UifConstants.TableToolsKeys.AO_COLUMN_DEFS, tableColumnOptions.toString());
402             } else if (layoutManager instanceof TableLayoutManager) {
403                 List<Field> rowFields = ((TableLayoutManager) layoutManager).getFirstRowFields();
404 
405                 // build column defs from the the first row of the table
406                 for (Component component : rowFields) {
407                     if (actionFieldVisible && colIndex + 1 == actionIndex) {
408                         String options = constructTableColumnOptions(colIndex, false, useServerPaging, null, null);
409                         tableColumnOptions.append(options + ",");
410                         colIndex++;
411                     }
412 
413                     // for FieldGroup, get the first field from that group
414                     if (component instanceof FieldGroup) {
415                         component = ((FieldGroup) component).getItems().get(0);
416                     }
417 
418                     if (component instanceof DataField) {
419                         DataField field = (DataField) component;
420 
421                         // if a field is marked as invisible in hiddenColumns, append options and skip sorting
422                         if (getHiddenColumns() != null && getHiddenColumns().contains(field.getPropertyName())) {
423                             tableColumnOptions.append("{"
424                                     + visible(false)
425                                     + ","
426                                     + mData(useServerPaging, colIndex)
427                                     + targets(colIndex)
428                                     + "},");
429                         } else if (getSortableColumns() != null && !getSortableColumns().isEmpty()) {
430                             // if specified as a column as sortable then add it
431                             if (getSortableColumns().contains(field.getPropertyName())) {
432                                 tableColumnOptions.append(getDataFieldColumnOptions(colIndex, collectionGroup, field)
433                                         + ",");
434                             } else { // else designate it as not sortable
435                                 tableColumnOptions.append("{"
436                                         + sortable(false)
437                                         + ","
438                                         + mData(useServerPaging, colIndex)
439                                         + targets(colIndex)
440                                         + "},");
441                             }
442                         } else { // sortable columns not defined
443                             String options = getDataFieldColumnOptions(colIndex, collectionGroup, field);
444                             tableColumnOptions.append(options + ",");
445                         }
446                         colIndex++;
447                     } else if (component instanceof MessageField) {
448                         if (component.getDataAttributes() != null && UifConstants.RoleTypes.ROW_GROUPING.equals(
449                                 component.getDataAttributes().get(UifConstants.DataAttributes.ROLE))) {
450                             // Grouping column is never shown, so skip
451                             tableColumnOptions.append("{"
452                                     + visible(false)
453                                     + ","
454                                     + mData(useServerPaging, colIndex)
455                                     + targets(colIndex)
456                                     + "},");
457                         } else {
458                             String options = constructTableColumnOptions(colIndex, true, useServerPaging, String.class,
459                                     UifConstants.TableToolsValues.DOM_TEXT);
460                             tableColumnOptions.append(options + ",");
461                         }
462                         colIndex++;
463                     } else if (component instanceof LinkField) {
464                         LinkField linkField = (LinkField) component;
465 
466                         Class<?> dataTypeClass = String.class;
467                         // check to see if field has custom sort type
468                         if (linkField.getSortAs() != null && linkField.getSortAs().length() > 0) {
469                             if (linkField.getSortAs().equals(UifConstants.TableToolsValues.DATE)) {
470                                 dataTypeClass = java.sql.Date.class;
471                             } else if (linkField.getSortAs().equals(UifConstants.TableToolsValues.NUMERIC)) {
472                                 dataTypeClass = Number.class;
473                             } else if (linkField.getSortAs().equals(UifConstants.TableToolsValues.STRING)) {
474                                 dataTypeClass = String.class;
475                             }
476                         }
477 
478                         String options = constructTableColumnOptions(colIndex, true, useServerPaging, dataTypeClass,
479                                 UifConstants.TableToolsValues.DOM_TEXT);
480                         tableColumnOptions.append(options + ",");
481                         colIndex++;
482                     } else {
483                         String options = constructTableColumnOptions(colIndex, false, useServerPaging, null, null);
484                         tableColumnOptions.append(options + ",");
485                         colIndex++;
486                     }
487                 }
488 
489                 if (actionFieldVisible && (actionIndex == -1 || actionIndex >= colIndex)) {
490                     String options = constructTableColumnOptions(colIndex, false, useServerPaging, null, null);
491                     tableColumnOptions.append(options);
492                 } else {
493                     tableColumnOptions = new StringBuilder(StringUtils.removeEnd(tableColumnOptions.toString(), ","));
494                 }
495 
496                 // merge the aoColumnDefs passed in
497                 if (!StringUtils.isEmpty(templateOptions.get(UifConstants.TableToolsKeys.AO_COLUMN_DEFS))) {
498                     String origAoOptions = templateOptions.get(UifConstants.TableToolsKeys.AO_COLUMN_DEFS).trim();
499                     origAoOptions = StringUtils.removeStart(origAoOptions, "[");
500                     origAoOptions = StringUtils.removeEnd(origAoOptions, "]");
501                     tableColumnOptions.append("," + origAoOptions);
502                 }
503 
504                 tableColumnOptions.append("]");
505                 templateOptions.put(UifConstants.TableToolsKeys.AO_COLUMN_DEFS, tableColumnOptions.toString());
506             }
507         }
508     }
509 
510     /**
511      * Builds default sorting options.
512      *
513      * @param lookupView the view for the lookup
514      * @param collectionGroup the collection group for the table
515      */
516     protected void buildSortOptions(LookupView lookupView, CollectionGroup collectionGroup) {
517         if (!isDisableTableSort() && CollectionUtils.isNotEmpty(lookupView.getDefaultSortAttributeNames())) {
518             LayoutManager layoutManager = collectionGroup.getLayoutManager();
519 
520             if (layoutManager instanceof TableLayoutManager) {
521                 TableLayoutManager tableLayoutManager = (TableLayoutManager) layoutManager;
522 
523                 List<String> firstRowPropertyNames = getFirstRowPropertyNames(tableLayoutManager.getFirstRowFields());
524                 List<String> defaultSortAttributeNames = lookupView.getDefaultSortAttributeNames();
525                 String sortDirection = lookupView.isDefaultSortAscending() ? "'asc'" : "'desc'";
526                 boolean actionFieldVisible = collectionGroup.isRenderLineActions() && !Boolean.TRUE.equals(
527                         collectionGroup.getReadOnly());
528                 int actionIndex = ((TableLayoutManager) layoutManager).getActionColumnIndex();
529                 int columnIndexPrefix = 0;
530 
531                 if (tableLayoutManager.isRenderSequenceField()) {
532                     columnIndexPrefix++;
533                 }
534 
535                 if (tableLayoutManager.getRowDetailsGroup() != null && CollectionUtils.isNotEmpty(
536                         tableLayoutManager.getRowDetailsGroup().getItems())) {
537                     columnIndexPrefix++;
538                 }
539 
540                 StringBuffer tableToolsSortOptions = new StringBuffer("[");
541 
542                 for (String defaultSortAttributeName : defaultSortAttributeNames) {
543                     int index = firstRowPropertyNames.indexOf(defaultSortAttributeName);
544                     if (index >= 0) {
545                         if (tableToolsSortOptions.length() > 1) {
546                             tableToolsSortOptions.append(",");
547                         }
548                         int columnIndex = columnIndexPrefix + index;
549                         if (actionFieldVisible && actionIndex != -1 && actionIndex <= columnIndex + 1) {
550                             columnIndex++;
551                         }
552                         tableToolsSortOptions.append("[" + columnIndex + "," + sortDirection + "]");
553                     }
554                 }
555 
556                 tableToolsSortOptions.append("]");
557 
558                 if (templateOptions.isEmpty()) {
559                     setTemplateOptions(new HashMap<String, String>());
560                 }
561                 templateOptions.put(UifConstants.TableToolsKeys.AASORTING, tableToolsSortOptions.toString());
562             }
563         }
564     }
565 
566     private List<String> getFirstRowPropertyNames(List<Field> firstRowFields) {
567         return Lists.transform(firstRowFields, new Function<Field, String>() {
568             @Override
569             public String apply(@Nullable Field field) {
570                 if (field != null && field instanceof DataField) {
571                     return ((DataField) field).getPropertyName();
572                 } else {
573                     return null;
574                 }
575             }
576         });
577     }
578 
579     /**
580      * Construct the column options for a data field
581      *
582      * @param target column index
583      * @param collectionGroup the collectionGroup in which the data field is defined
584      * @param field the field to construction options for
585      * @return options as valid for datatable
586      */
587     protected String getDataFieldColumnOptions(int target, CollectionGroup collectionGroup, DataField field) {
588         String sortDataType = null;
589 
590         if (!Boolean.TRUE.equals(collectionGroup.getReadOnly())
591                 && (field instanceof InputField)
592                 && ((InputField) field).getControl() != null) {
593             Control control = ((InputField) field).getControl();
594             if (control instanceof SelectControl) {
595                 sortDataType = UifConstants.TableToolsValues.DOM_SELECT;
596             } else if (control instanceof CheckboxControl || control instanceof CheckboxGroupControl) {
597                 sortDataType = UifConstants.TableToolsValues.DOM_CHECK;
598             } else if (control instanceof RadioGroupControl) {
599                 sortDataType = UifConstants.TableToolsValues.DOM_RADIO;
600             } else {
601                 sortDataType = UifConstants.TableToolsValues.DOM_TEXT;
602             }
603         } else {
604             sortDataType = UifConstants.TableToolsValues.DOM_TEXT;
605         }
606 
607         Class<?> dataTypeClass = ObjectPropertyUtils.getPropertyType(collectionGroup.getCollectionObjectClass(),
608                 field.getPropertyName());
609         // check to see if field has custom sort type
610         if (field.getSortAs() != null && field.getSortAs().length() > 0) {
611             if (field.getSortAs().equals(UifConstants.TableToolsValues.CURRENCY)) {
612                 dataTypeClass = KualiDecimal.class;
613             } else if (field.getSortAs().equals(UifConstants.TableToolsValues.DATE)) {
614                 dataTypeClass = java.sql.Date.class;
615             } else if (field.getSortAs().equals(UifConstants.TableToolsValues.NUMERIC)) {
616                 dataTypeClass = Number.class;
617             } else if (field.getSortAs().equals(UifConstants.TableToolsValues.STRING)) {
618                 dataTypeClass = String.class;
619             }
620         }
621 
622         boolean isSortable = true;
623         if (field.isApplyMask()) {
624             isSortable = false;
625         }
626 
627         return constructTableColumnOptions(target, isSortable, collectionGroup.isUseServerPaging(), dataTypeClass,
628                 sortDataType);
629     }
630 
631     /**
632      * Constructs the sort data type for each data table columns in a format that will be used to
633      * initialize the data table widget via javascript
634      *
635      * @param target the column index
636      * @param isSortable whether a column should be marked as sortable
637      * @param isUseServerPaging is server side paging enabled?
638      * @param dataTypeClass the class type of the column value - used determine the sType option -
639      * which identifies the search plugin to use
640      * @param sortDataType Defines a data source type for the sorting which can be used to read
641      * realtime information from the table
642      * @return a formatted string with data table options for one column
643      */
644     public String constructTableColumnOptions(int target, boolean isSortable, boolean isUseServerPaging,
645             Class<?> dataTypeClass, String sortDataType) {
646         String options = "null";
647 
648         if (!isSortable || dataTypeClass == null) {
649             options = sortable(false) + "," + sortType(UifConstants.TableToolsValues.STRING);
650         } else {
651             options = sortType(getSortType(dataTypeClass));
652 
653             if (!isUseServerPaging && !this.forceLocalJsonData) {
654                 options += "," + sortDataType(sortDataType);
655             }
656         }
657 
658         if (target < cellCssClasses.size() && target >= 0) {
659             options += ", \"" + UifConstants.TableToolsKeys.CELL_CLASS + "\" : \"" + cellCssClasses.get(target) + "\"";
660         }
661 
662         // only use the mDataProp when using json data (only relevant for this table type)
663         options += mData(isUseServerPaging, target);
664 
665         if (!options.equals("null")) {
666             options = "{" + options + "," + targets(target) + "}";
667         } else {
668             options = "{" + options + "}";
669         }
670 
671         return options;
672     }
673 
674     private String sortable(boolean sortable) {
675         return "\"" + UifConstants.TableToolsKeys.SORTABLE + "\": " + (sortable ? UifConstants.TableToolsValues.TRUE :
676                 UifConstants.TableToolsValues.FALSE);
677     }
678 
679     private String sortDataType(String sortDataType) {
680         return "\"" + UifConstants.TableToolsKeys.SORT_DATA_TYPE + "\": \"" + sortDataType + "\"";
681     }
682 
683     private String getSortType(Class<?> dataTypeClass) {
684         String sortType = UifConstants.TableToolsValues.STRING;
685         if (ClassUtils.isAssignable(dataTypeClass, KualiPercent.class)) {
686             sortType = UifConstants.TableToolsValues.PERCENT;
687         } else if (ClassUtils.isAssignable(dataTypeClass, KualiInteger.class) || ClassUtils.isAssignable(dataTypeClass,
688                 KualiDecimal.class)) {
689             sortType = UifConstants.TableToolsValues.CURRENCY;
690         } else if (ClassUtils.isAssignable(dataTypeClass, Timestamp.class)) {
691             sortType = "date";
692         } else if (ClassUtils.isAssignable(dataTypeClass, java.sql.Date.class) || ClassUtils.isAssignable(dataTypeClass,
693                 java.util.Date.class)) {
694             sortType = UifConstants.TableToolsValues.DATE;
695         } else if (ClassUtils.isAssignable(dataTypeClass, Number.class)) {
696             sortType = UifConstants.TableToolsValues.NUMERIC;
697         }
698         return sortType;
699     }
700 
701     private String sortType(String sortType) {
702         return "\"" + UifConstants.TableToolsKeys.SORT_TYPE + "\": \"" + sortType + "\"";
703     }
704 
705     private String targets(int target) {
706         return "\"" + UifConstants.TableToolsKeys.TARGETS + "\": [" + target + "]";
707     }
708 
709     private String visible(boolean visible) {
710         return "\"" + UifConstants.TableToolsKeys.VISIBLE + "\": " + (visible ? UifConstants.TableToolsValues.TRUE :
711                 UifConstants.TableToolsValues.FALSE);
712     }
713 
714     private String mData(boolean isUseServerPaging, int target) {
715         if (isUseServerPaging || this.forceLocalJsonData) {
716             return ", \"" + UifConstants.TableToolsKeys.MDATA +
717                     "\" : function(row,type,newVal){ return _handleColData(row,type,'c" + target + "',newVal);}";
718         }
719         return "";
720     }
721 
722     /**
723      * Returns the text which is used to display text when the table is empty
724      *
725      * @return empty table message
726      */
727     @BeanTagAttribute
728     public String getEmptyTableMessage() {
729         return emptyTableMessage;
730     }
731 
732     /**
733      * Setter for a text to be displayed when the table is empty
734      *
735      * @param emptyTableMessage
736      */
737     public void setEmptyTableMessage(String emptyTableMessage) {
738         this.emptyTableMessage = emptyTableMessage;
739     }
740 
741     /**
742      * Returns true if sorting is disabled
743      *
744      * @return the disableTableSort
745      */
746     @BeanTagAttribute
747     public boolean isDisableTableSort() {
748         return this.disableTableSort;
749     }
750 
751     /**
752      * Enables/disables the table sorting
753      *
754      * @param disableTableSort the disableTableSort to set
755      */
756     public void setDisableTableSort(boolean disableTableSort) {
757         this.disableTableSort = disableTableSort;
758     }
759 
760     /**
761      * Returns true if export option is enabled
762      *
763      * @return the showExportOption
764      */
765     @BeanTagAttribute
766     public boolean isShowExportOption() {
767         return this.showExportOption;
768     }
769 
770     /**
771      * Show/Hide the search and export option in tabletools
772      *
773      * @param showExportOption the showExportOptions to set
774      */
775     public void setShowExportOption(boolean showExportOption) {
776         this.showExportOption = showExportOption;
777     }
778 
779     /**
780      * Holds propertyNames for the ones meant to be hidden since columns are visible by default
781      *
782      * <p>
783      * Duplicate entries are ignored and the order of entries is not significant
784      * </p>
785      *
786      * @return a set with propertyNames of columns to be hidden
787      */
788     @BeanTagAttribute(type = BeanTagAttribute.AttributeType.SETVALUE)
789     public Set<String> getHiddenColumns() {
790         return hiddenColumns;
791     }
792 
793     /**
794      * Setter for the hidden columns set
795      *
796      * @param hiddenColumns a set containing propertyNames
797      */
798     public void setHiddenColumns(Set<String> hiddenColumns) {
799         this.hiddenColumns = hiddenColumns;
800     }
801 
802     /**
803      * Holds the propertyNames for columns that are to be sorted
804      *
805      * <p>
806      * Duplicate entries are ignored and the order of entries is not significant
807      * </p>
808      *
809      * @return a set of propertyNames with for columns that will be sorted
810      */
811     @BeanTagAttribute(type = BeanTagAttribute.AttributeType.SETVALUE)
812     public Set<String> getSortableColumns() {
813         return sortableColumns;
814     }
815 
816     /**
817      * Setter for sortable columns
818      *
819      * @param sortableColumns a set containing propertyNames of columns to be sorted
820      */
821     public void setSortableColumns(Set<String> sortableColumns) {
822         this.sortableColumns = sortableColumns;
823     }
824 
825     /**
826      * Specifies a URL for acquiring the table data with ajax
827      *
828      * <p>
829      * When the ajax source URL is specified the rich table plugin will retrieve the data by
830      * invoking the URL and building the table rows from the result. This is different from the
831      * standard use of the rich table plugin with uses progressive enhancement to decorate a table
832      * that has already been rendereed
833      * </p>
834      *
835      * @return URL for ajax source
836      */
837     @BeanTagAttribute
838     public String getAjaxSource() {
839         return ajaxSource;
840     }
841 
842     /**
843      * Setter for the Ajax source URL
844      *
845      * @param ajaxSource
846      */
847     public void setAjaxSource(String ajaxSource) {
848         this.ajaxSource = ajaxSource;
849     }
850 
851     /**
852      * Get groupingOption
853      *
854      * @return grouping options as a JS string
855      */
856     public String getGroupingOptionsJSString() {
857         return groupingOptionsJSString;
858     }
859 
860     /**
861      * Set the groupingOptions js data. <b>This should not be set through XML configuration.</b>
862      *
863      * @param groupingOptionsJSString
864      */
865     public void setGroupingOptionsJSString(String groupingOptionsJSString) {
866         this.groupingOptionsJSString = groupingOptionsJSString;
867     }
868 
869     /**
870      * If set to true and the aoColumnDefs template option is explicitly defined in templateOptions,
871      * those aoColumnDefs will be used for this table. Otherwise, if false, the aoColumnDefs will
872      * attempt to be merged with those that are automatically generated by RichTable
873      *
874      * @return true if the aoColumnDefs set will completely override those that are generated
875      *         automatically by RichTable
876      */
877     @BeanTagAttribute
878     public boolean isForceAoColumnDefsOverride() {
879         return forceAoColumnDefsOverride;
880     }
881 
882     /**
883      * Set forceAoColumnDefsOverride
884      *
885      * @param forceAoColumnDefsOverride
886      */
887     public void setForceAoColumnDefsOverride(boolean forceAoColumnDefsOverride) {
888         this.forceAoColumnDefsOverride = forceAoColumnDefsOverride;
889     }
890 
891     /**
892      * If true, the table will automatically use row JSON data generated by this widget
893      *
894      * <p>
895      * This forces the table backed by this RichTable to get its content from a template option
896      * called aaData. This will automatically skip row generation in the template, and cause the
897      * table receive its data from the aaData template option automatically generated and set by
898      * this RichTable. This allows the table to take advantage of the bDeferRender option (also
899      * automatically set to true) when this table is a paged table (performance increase for tables
900      * that are more than one page). Note: the CollectionGroup's isUseServerPaging flag will always
901      * override this functionality if it is also true.
902      * </p>
903      *
904      * @return true if backed by the aaData option in JSON, that is generated during the ftl
905      *         rendering process by this widget for this table
906      */
907     @BeanTagAttribute
908     public boolean isForceLocalJsonData() {
909         return forceLocalJsonData;
910     }
911 
912     /**
913      * Set the forceLocalJsonData flag to force this table to use generated row json data
914      *
915      * @param forceLocalJsonData
916      */
917     public void setForceLocalJsonData(boolean forceLocalJsonData) {
918         this.forceLocalJsonData = forceLocalJsonData;
919     }
920 
921     /**
922      * The nestedLevel property represents how many collection tables deep this particular table is
923      *
924      * <p>
925      * This property must be manually set if the flag forceLocalJsonData is being used and the
926      * collection table this RichTable represents is a subcollection of a TABLE collection (not
927      * stacked collection). If this is true, add 1 for each level deep (ex. subCollection would be
928      * 1, sub-subCollection would be 2). If this property is not set javascript errors will occur on
929      * the page, as this determines how deep to escape certain characters.
930      * </p>
931      *
932      * @return the nestedLevel representing the
933      */
934     @BeanTagAttribute
935     public int getNestedLevel() {
936         return nestedLevel;
937     }
938 
939     /**
940      * Set the nestedLevel for this table - must be set if using forceLocalJsonData and this is a
941      * subCollection of a TableCollection (also using forceLocalJsonData)
942      *
943      * @param nestedLevel
944      */
945     public void setNestedLevel(int nestedLevel) {
946         this.nestedLevel = nestedLevel;
947     }
948 
949     /**
950      * Get the translated aaData array generated by calls to addRowToTableData by the ftl
951      *
952      * <p>
953      * This data is in JSON format and expected to be consumed by datatables when utilizing the
954      * forceLocalJsonData option. This will be populated automatically if that flag is set to true.
955      * </p>
956      *
957      * @return the generated aaData
958      */
959     public String getAaData() {
960         return aaData;
961     }
962 
963     /**
964      * Set the translated aaData array
965      *
966      * <p>
967      * This data is in JSON format and expected to be consumed by datatables when utilizing the
968      * forceLocalJsonData. This setter is required for copyProperties()
969      * </p>
970      *
971      * @param aaData the generated aaData
972      */
973     protected void setAaData(String aaData) {
974         this.aaData = aaData;
975     }
976 
977     /**
978      * Get the simple value as a string that represents the field's sortable value, to be used as
979      * val in the custom uif json data object (accessed by mDataProp option on datatables -
980      * automated by framework) when using the forceLocalJsonData option or the CollectionGroup's
981      * isUseServerPaging option
982      *
983      * @param model model the current model
984      * @param field the field to retrieve a sortable value from for use in custom json data
985      * @return the value as a String
986      */
987     public String getCellValue(Object model, Field field) {
988         String value = KRADUtils.getSimpleFieldValue(model, field);
989 
990         if (value == null) {
991             value = "null";
992         } else {
993             value = KRADConstants.QUOTE_PLACEHOLDER + value + KRADConstants.QUOTE_PLACEHOLDER;
994         }
995 
996         return value;
997     }
998 
999     /**
1000      * Add row content passed from table ftl to the aaData array by converting and escaping the
1001      * content to an object (in an array of objects) in JSON format
1002      *
1003      * <p>
1004      * The data in aaData is expected to be consumed by a call by the datatables plugin using
1005      * sAjaxSource or aaData. The addRowToTableData generation call is additive must be made per a
1006      * row in the ftl.
1007      * </p>
1008      *
1009      * @param row the row of content with each cell content surrounded by the @quot@ token and
1010      * followed by a comma
1011      */
1012     public void addRowToTableData(String row) {
1013         String escape = "";
1014 
1015         if (templateOptions.isEmpty()) {
1016             setTemplateOptions(new HashMap<String, String>());
1017         }
1018 
1019         // if nestedLevel is set add the appropriate amount of escape characters per a level of nesting
1020         for (int i = 0; i < nestedLevel && forceLocalJsonData; i++) {
1021             escape = escape + "\\";
1022         }
1023 
1024         // remove newlines and replace quotes and single quotes with unicode characters
1025         row = row.trim().replace("\"", escape + "\\u0022").replace("'", escape + "\\u0027").replace("\n", "").replace(
1026                 "\r", "");
1027 
1028         // remove hanging comma
1029         row = StringUtils.removeEnd(row, ",");
1030 
1031         // replace all quote placeholders with actual quote characters
1032         row = row.replace(KRADConstants.QUOTE_PLACEHOLDER, "\"");
1033         row = "{" + row + "}";
1034 
1035         // if first call create aaData and force defer render option, otherwise append
1036         if (StringUtils.isBlank(aaData)) {
1037             aaData = "[" + row + "]";
1038 
1039             if (templateOptions.get(UifConstants.TableToolsKeys.DEFER_RENDER) == null) {
1040                 //make sure deferred rendering is forced if not explicitly set
1041                 templateOptions.put(UifConstants.TableToolsKeys.DEFER_RENDER, UifConstants.TableToolsValues.TRUE);
1042             }
1043 
1044         } else if (StringUtils.isNotBlank(row)) {
1045             aaData = aaData.substring(0, aaData.length() - 1) + "," + row + "]";
1046         }
1047 
1048         //force json data use if forceLocalJsonData flag is set
1049         if (forceLocalJsonData) {
1050             templateOptions.put(UifConstants.TableToolsKeys.AA_DATA, aaData);
1051         }
1052     }
1053 
1054     protected ConfigurationService getConfigurationService() {
1055         return CoreApiServiceLocator.getKualiConfigurationService();
1056     }
1057 }