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