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