/**
 * Copyright 2005-2016 The Kuali Foundation
 *
 * Licensed under the Educational Community License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.opensource.org/licenses/ecl2.php
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.kuali.rice.krad.uif.widget;

import com.google.common.base.Function;
import com.google.common.collect.Lists;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.ClassUtils;
import org.apache.commons.lang.StringUtils;
import org.kuali.rice.core.api.CoreApiServiceLocator;
import org.kuali.rice.core.api.config.property.ConfigurationService;
import org.kuali.rice.core.api.util.type.KualiDecimal;
import org.kuali.rice.core.api.util.type.KualiInteger;
import org.kuali.rice.core.api.util.type.KualiPercent;
import org.kuali.rice.krad.datadictionary.parse.BeanTag;
import org.kuali.rice.krad.datadictionary.parse.BeanTagAttribute;
import org.kuali.rice.krad.datadictionary.parse.BeanTags;
import org.kuali.rice.krad.lookup.LookupView;
import org.kuali.rice.krad.uif.UifConstants;
import org.kuali.rice.krad.uif.UifParameters;
import org.kuali.rice.krad.uif.component.Component;
import org.kuali.rice.krad.uif.component.ComponentBase;
import org.kuali.rice.krad.uif.container.CollectionGroup;
import org.kuali.rice.krad.uif.control.CheckboxControl;
import org.kuali.rice.krad.uif.control.CheckboxGroupControl;
import org.kuali.rice.krad.uif.control.Control;
import org.kuali.rice.krad.uif.control.RadioGroupControl;
import org.kuali.rice.krad.uif.control.SelectControl;
import org.kuali.rice.krad.uif.field.DataField;
import org.kuali.rice.krad.uif.field.Field;
import org.kuali.rice.krad.uif.field.FieldGroup;
import org.kuali.rice.krad.uif.field.InputField;
import org.kuali.rice.krad.uif.field.LinkField;
import org.kuali.rice.krad.uif.field.MessageField;
import org.kuali.rice.krad.uif.layout.LayoutManager;
import org.kuali.rice.krad.uif.layout.TableLayoutManager;
import org.kuali.rice.krad.uif.lifecycle.ViewLifecycle;
import org.kuali.rice.krad.uif.util.LifecycleElement;
import org.kuali.rice.krad.uif.util.ObjectPropertyUtils;
import org.kuali.rice.krad.uif.view.View;
import org.kuali.rice.krad.util.KRADConstants;
import org.kuali.rice.krad.util.KRADUtils;
import org.kuali.rice.krad.web.form.UifFormBase;

import javax.annotation.Nullable;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Set;

/**
 * Decorates a HTML Table client side with various tools
 *
 * <p>
 * Decorations implemented depend on widget implementation. Examples are sorting, paging and
 * skinning.
 * </p>
 *
 * @author Kuali Rice Team (rice.collab@kuali.org)
 */
@BeanTags({@BeanTag(name = "richTable", parent = "Uif-RichTable"),
        @BeanTag(name = "pagedRichTable", parent = "Uif-PagedRichTable"),
        @BeanTag(name = "scrollableRichTable", parent = "Uif-ScrollableRichTable")})
public class RichTable extends WidgetBase {
    private static final long serialVersionUID = 4671589690877390070L;

    private String emptyTableMessage;
    private boolean disableTableSort;

    private boolean forceAoColumnDefsOverride;

    private boolean forceLocalJsonData;
    private int nestedLevel;
    private String aaData;

    private Set<String> hiddenColumns;
    private Set<String> sortableColumns;
    private List<String> cellCssClasses;

    private String ajaxSource;

    private boolean showExportOption;

    private String groupingOptionsJSString;

    public RichTable() {
        super();
        groupingOptionsJSString = "null";
        cellCssClasses = new ArrayList<String>();
    }

    /**
     * The following initialization is performed:
     *
     * <ul>
     * <li>Initializes component options for empty table message</li>
     * </ul>
     */
    @Override
    public void performFinalize(Object model, LifecycleElement parent) {
        super.performFinalize(model, parent);

        UifFormBase formBase = (UifFormBase) model;

        if (!isRender()) {
            return;
        }

        if (templateOptions.isEmpty()) {
            setTemplateOptions(new HashMap<String, String>());
        }

        if (StringUtils.isNotBlank(getEmptyTableMessage()) && !templateOptions.containsKey(
                UifConstants.TableToolsKeys.LANGUAGE)) {
            templateOptions.put(UifConstants.TableToolsKeys.LANGUAGE,
                    "{\"" + UifConstants.TableToolsKeys.EMPTY_TABLE + "\" : \"" + getEmptyTableMessage() + "\"}");
        }

        Object domOption = templateOptions.get(UifConstants.TableToolsKeys.SDOM);
        if (domOption instanceof String) {
            String sDomOption = (String) domOption;

            if (StringUtils.isNotBlank(sDomOption)) {
                if (!isShowExportOption()) {
                    sDomOption = StringUtils.remove(sDomOption, "T"); //Removes Export option
                }
                templateOptions.put(UifConstants.TableToolsKeys.SDOM, sDomOption);
            }
        }

        // for add events, disable initial sorting
        if (UifConstants.ActionEvents.ADD_LINE.equals(formBase.getActionEvent()) || UifConstants.ActionEvents
                .ADD_BLANK_LINE.equals(formBase.getActionEvent())) {
            templateOptions.put(UifConstants.TableToolsKeys.AASORTING, "[]");
        }

        if ((parent instanceof CollectionGroup)) {
            CollectionGroup collectionGroup = (CollectionGroup) parent;
            LayoutManager layoutManager = collectionGroup.getLayoutManager();

            //if useServerPaging is true, add the css cell styling to the template options so it can still be used
            //since this will not go through the grid ftl
            if (layoutManager instanceof TableLayoutManager && collectionGroup.isUseServerPaging()) {
                addCellStyling((TableLayoutManager) layoutManager);
            }

            buildTableOptions(collectionGroup);
            setTotalOptions(collectionGroup);

            View view = ViewLifecycle.getActiveLifecycle().getView();
            if (view instanceof LookupView) {
                buildSortOptions((LookupView) view, collectionGroup);
            }
        }

        if (isDisableTableSort()) {
            templateOptions.put(UifConstants.TableToolsKeys.TABLE_SORT, "false");
        }

        String kradUrl = getConfigurationService().getPropertyValueAsString(UifConstants.ConfigProperties.KRAD_URL);
        if (StringUtils.isNotBlank(ajaxSource)) {
            templateOptions.put(UifConstants.TableToolsKeys.SAJAX_SOURCE, ajaxSource);
        } else if (parent instanceof CollectionGroup && ((CollectionGroup) parent).isUseServerPaging()) {
            // enable required dataTables options for server side paging
            templateOptions.put(UifConstants.TableToolsKeys.BPROCESSING, "true");
            templateOptions.put(UifConstants.TableToolsKeys.BSERVER_SIDE, "true");

            // build sAjaxSource url to call
            templateOptions.put(UifConstants.TableToolsKeys.SAJAX_SOURCE,
                    kradUrl + ((UifFormBase) model).getControllerMapping() + "?" +
                            UifConstants.CONTROLLER_METHOD_DISPATCH_PARAMETER_NAME + "=" +
                            UifConstants.MethodToCallNames.TABLE_JSON + "&" + UifParameters.UPDATE_COMPONENT_ID + "=" +
                            parent.getId() + "&" + UifParameters.FORM_KEY + "=" + ((UifFormBase) model).getFormKey() +
                            "&" + UifParameters.AJAX_RETURN_TYPE + "=" +
                            UifConstants.AjaxReturnTypes.UPDATECOMPONENT.getKey() + "&" + UifParameters.AJAX_REQUEST +
                            "=" + "true");

            //TODO: Figure out where to move this script file constant?
            String pushLookupSelect = "function (aoData) { "
                    +
                    "if(jQuery('table.dataTable').length > 0) {    "
                    +
                    "    var table = jQuery('table.dataTable');    "
                    +
                    "    jQuery( table.find(':input:checked')).each( function (index) {     "
                    +
                    "        aoData.push({'name': (jQuery(this)).attr('name'),'value': (jQuery(this)).attr('value')});  "
                    +
                    "    console.log(jQuery(this).attr('name') + ':' + jQuery(this).attr('value')); "
                    +
                    "    });  "
                    +
                    "}  "
                    +
                    "}";

            templateOptions.put(UifConstants.TableToolsKeys.SERVER_PARAMS, pushLookupSelect);

            // store col defs so columns can be built on paging request
            ViewLifecycle.getViewPostMetadata().addComponentPostData(parent.getId(),
                    UifConstants.TableToolsKeys.AO_COLUMN_DEFS, templateOptions.get(
                    UifConstants.TableToolsKeys.AO_COLUMN_DEFS));
        }

        // build export url to call
        templateOptions.put(UifConstants.TableToolsKeys.SDOWNLOAD_SOURCE,
                kradUrl + "/" + UifConstants.ControllerMappings.EXPORT + "?" + UifParameters.UPDATE_COMPONENT_ID + "=" +
                        parent.getId() + "&" + UifParameters.FORM_KEY + "=" + ((UifFormBase) model).getFormKey() + "&" +
                        UifParameters.AJAX_RETURN_TYPE + "=" + UifConstants.AjaxReturnTypes.UPDATECOMPONENT.getKey() +
                        "&" + UifParameters.AJAX_REQUEST + "=" + "true");
    }

    /**
     * Add the css style to the cellCssClasses by column index, later used by the aoColumnDefs
     *
     * @param manager the tableLayoutManager that contains the original fields
     */
    private void addCellStyling(TableLayoutManager manager) {
        if (!CollectionUtils.isEmpty(manager.getAllRowFields())) {
            for (int index = 0; index < manager.getNumberOfColumns(); index++) {
                String cellStyleClasses = ((ComponentBase) manager.getAllRowFields().get(index))
                        .getWrapperCssClassesAsString();
                if (StringUtils.isNotBlank(cellStyleClasses)) {
                    cellCssClasses.add(cellStyleClasses);
                }
            }
        }
    }

    /**
     * Builds the footer callback template option for column totals
     *
     * @param collectionGroup the collection group
     */
    private void setTotalOptions(CollectionGroup collectionGroup) {
        LayoutManager layoutManager = collectionGroup.getLayoutManager();

        if (layoutManager instanceof TableLayoutManager) {
            List<String> totalColumns = ((TableLayoutManager) layoutManager).getColumnsToCalculate();

            if (totalColumns.size() > 0) {
                String array = "[";

                for (String i : totalColumns) {
                    array = array + i + ",";
                }
                array = StringUtils.removeEnd(array, ",");
                array = array + "]";

                templateOptions.put(UifConstants.TableToolsKeys.FOOTER_CALLBACK,
                        "function (nRow, aaData, iStart, iEnd, aiDisplay) {initializeTotalsFooter (nRow, aaData, iStart, iEnd, aiDisplay, "
                                + array
                                + " )}");
            }
        }
    }

    /**
     * Builds column options for sorting
     *
     * @param collectionGroup
     */
    protected void buildTableOptions(CollectionGroup collectionGroup) {
        checkMutable(false);

        LayoutManager layoutManager = collectionGroup.getLayoutManager();
        final boolean useServerPaging = collectionGroup.isUseServerPaging();

        if (templateOptions.isEmpty()) {
            setTemplateOptions(new HashMap<String, String>());
        }

        // if sub collection exists, don't allow the table sortable
        if (!collectionGroup.getSubCollections().isEmpty()) {
            setDisableTableSort(true);
        }

        if (!isDisableTableSort()) {
            // if rendering add line, skip that row from col sorting
            if (collectionGroup.isRenderAddLine()
                    && !Boolean.TRUE.equals(collectionGroup.getReadOnly())
                    && !((layoutManager instanceof TableLayoutManager) && ((TableLayoutManager) layoutManager)
                    .isSeparateAddLine())) {

                templateOptions.put(UifConstants.TableToolsKeys.SORT_SKIP_ROWS,
                        "[" + UifConstants.TableToolsValues.ADD_ROW_DEFAULT_INDEX + "]");
            }

            StringBuilder tableColumnOptions = new StringBuilder("[");

            int colIndex = 0;
            int actionIndex = UifConstants.TableLayoutValues.ACTIONS_COLUMN_RIGHT_INDEX;
            boolean actionFieldVisible = collectionGroup.isRenderLineActions() && !Boolean.TRUE.equals(
                    collectionGroup.getReadOnly());

            if (layoutManager instanceof TableLayoutManager) {
                actionIndex = ((TableLayoutManager) layoutManager).getActionColumnIndex();
            }

            if (actionIndex == UifConstants.TableLayoutValues.ACTIONS_COLUMN_LEFT_INDEX && actionFieldVisible) {
                String options = constructTableColumnOptions(colIndex, false, useServerPaging, null, null);
                tableColumnOptions.append(options + ",");
                colIndex++;
            }

            // handle sequence field
            if (layoutManager instanceof TableLayoutManager && ((TableLayoutManager) layoutManager)
                    .isRenderSequenceField()) {
                Class<?> dataTypeClass = Number.class;

                if (((TableLayoutManager) layoutManager).getSequenceFieldPrototype() instanceof DataField) {
                    DataField dataField = (DataField) ((TableLayoutManager) layoutManager).getSequenceFieldPrototype();
                    dataTypeClass = ObjectPropertyUtils.getPropertyType(collectionGroup.getCollectionObjectClass(),
                            dataField.getPropertyName());
                    // check to see if field has custom sort type
                    if (dataField.getSortAs() != null && dataField.getSortAs().length() > 0) {
                        if (dataField.getSortAs().equals(UifConstants.TableToolsValues.DATE)) {
                            dataTypeClass = java.sql.Date.class;
                        } else if (dataField.getSortAs().equals(UifConstants.TableToolsValues.NUMERIC)) {
                            dataTypeClass = Number.class;
                        } else if (dataField.getSortAs().equals(UifConstants.TableToolsValues.STRING)) {
                            dataTypeClass = String.class;
                        }
                    }
                }

                // don't allow sorting of sequence field - why?
                // auto sequence column is never sortable
                tableColumnOptions.append("{"
                        + sortable(false)
                        + ","
                        + sortType(getSortType(dataTypeClass))
                        + ","
                        + sortDataType(UifConstants.TableToolsValues.DOM_TEXT)
                        + mData(useServerPaging, colIndex)
                        + ","
                        + targets(colIndex)
                        + "},");

                // the sequence field needs to still be sorted when initially loaded
                templateOptions.put(UifConstants.TableToolsKeys.AASORTING, "[[" + colIndex + ",'asc']]");

                colIndex++;

                if (actionIndex == 2 && actionFieldVisible) {
                    String options = constructTableColumnOptions(colIndex, false, useServerPaging, null, null);
                    tableColumnOptions.append(options + ",");
                    colIndex++;
                }
            }

            // skip select field if enabled
            if (collectionGroup.isIncludeLineSelectionField()) {
                String options = constructTableColumnOptions(colIndex, false, useServerPaging, null, null);
                tableColumnOptions.append(options + ",");
                colIndex++;
            }

            // if data dictionary defines aoColumns, copy here and skip default sorting/visibility behaviour
            if (!StringUtils.isEmpty(templateOptions.get(UifConstants.TableToolsKeys.AO_COLUMNS))) {
                // get the contents of the JS array string
                String jsArray = templateOptions.get(UifConstants.TableToolsKeys.AO_COLUMNS);
                int startBrace = StringUtils.indexOf(jsArray, "[");
                int endBrace = StringUtils.lastIndexOf(jsArray, "]");
                tableColumnOptions.append(StringUtils.substring(jsArray, startBrace + 1, endBrace) + ",");

                if (actionFieldVisible && (actionIndex == -1 || actionIndex >= colIndex)) {
                    String options = constructTableColumnOptions(colIndex, false, useServerPaging, null, null);
                    tableColumnOptions.append(options);
                } else {
                    tableColumnOptions = new StringBuilder(StringUtils.removeEnd(tableColumnOptions.toString(), ","));
                }

                tableColumnOptions.append("]");
                templateOptions.put(UifConstants.TableToolsKeys.AO_COLUMNS, tableColumnOptions.toString());
            } else if (!StringUtils.isEmpty(templateOptions.get(UifConstants.TableToolsKeys.AO_COLUMN_DEFS))
                    && forceAoColumnDefsOverride) {
                String jsArray = templateOptions.get(UifConstants.TableToolsKeys.AO_COLUMN_DEFS);
                int startBrace = StringUtils.indexOf(jsArray, "[");
                int endBrace = StringUtils.lastIndexOf(jsArray, "]");
                tableColumnOptions.append(StringUtils.substring(jsArray, startBrace + 1, endBrace) + ",");

                if (actionFieldVisible && (actionIndex == -1 || actionIndex >= colIndex)) {
                    String options = constructTableColumnOptions(colIndex, false, useServerPaging, null, null);
                    tableColumnOptions.append(options);
                } else {
                    tableColumnOptions = new StringBuilder(StringUtils.removeEnd(tableColumnOptions.toString(), ","));
                }

                tableColumnOptions.append("]");
                templateOptions.put(UifConstants.TableToolsKeys.AO_COLUMN_DEFS, tableColumnOptions.toString());
            } else if (layoutManager instanceof TableLayoutManager) {
                List<Field> rowFields = ((TableLayoutManager) layoutManager).getFirstRowFields();

                // build column defs from the the first row of the table
                for (Component component : rowFields) {
                    if (actionFieldVisible && colIndex + 1 == actionIndex) {
                        String options = constructTableColumnOptions(colIndex, false, useServerPaging, null, null);
                        tableColumnOptions.append(options + ",");
                        colIndex++;
                    }

                    // for FieldGroup, get the first field from that group
                    if (component instanceof FieldGroup) {
                        component = ((FieldGroup) component).getItems().get(0);
                    }

                    if (component instanceof DataField) {
                        DataField field = (DataField) component;

                        // if a field is marked as invisible in hiddenColumns, append options and skip sorting
                        if (getHiddenColumns() != null && getHiddenColumns().contains(field.getPropertyName())) {
                            tableColumnOptions.append("{"
                                    + visible(false)
                                    + ","
                                    + mData(useServerPaging, colIndex)
                                    + targets(colIndex)
                                    + "},");
                        } else if (getSortableColumns() != null && !getSortableColumns().isEmpty()) {
                            // if specified as a column as sortable then add it
                            if (getSortableColumns().contains(field.getPropertyName())) {
                                tableColumnOptions.append(getDataFieldColumnOptions(colIndex, collectionGroup, field)
                                        + ",");
                            } else { // else designate it as not sortable
                                tableColumnOptions.append("{"
                                        + sortable(false)
                                        + ","
                                        + mData(useServerPaging, colIndex)
                                        + targets(colIndex)
                                        + "},");
                            }
                        } else { // sortable columns not defined
                            String options = getDataFieldColumnOptions(colIndex, collectionGroup, field);
                            tableColumnOptions.append(options + ",");
                        }
                        colIndex++;
                    } else if (component instanceof MessageField) {
                        if (component.getDataAttributes() != null && UifConstants.RoleTypes.ROW_GROUPING.equals(
                                component.getDataAttributes().get(UifConstants.DataAttributes.ROLE))) {
                            // Grouping column is never shown, so skip
                            tableColumnOptions.append("{"
                                    + visible(false)
                                    + ","
                                    + mData(useServerPaging, colIndex)
                                    + targets(colIndex)
                                    + "},");
                        } else {
                            String options = constructTableColumnOptions(colIndex, true, useServerPaging, String.class,
                                    UifConstants.TableToolsValues.DOM_TEXT);
                            tableColumnOptions.append(options + ",");
                        }
                        colIndex++;
                    } else if (component instanceof LinkField) {
                        LinkField linkField = (LinkField) component;

                        Class<?> dataTypeClass = String.class;
                        // check to see if field has custom sort type
                        if (linkField.getSortAs() != null && linkField.getSortAs().length() > 0) {
                            if (linkField.getSortAs().equals(UifConstants.TableToolsValues.DATE)) {
                                dataTypeClass = java.sql.Date.class;
                            } else if (linkField.getSortAs().equals(UifConstants.TableToolsValues.NUMERIC)) {
                                dataTypeClass = Number.class;
                            } else if (linkField.getSortAs().equals(UifConstants.TableToolsValues.STRING)) {
                                dataTypeClass = String.class;
                            }
                        }

                        String options = constructTableColumnOptions(colIndex, true, useServerPaging, dataTypeClass,
                                UifConstants.TableToolsValues.DOM_TEXT);
                        tableColumnOptions.append(options + ",");
                        colIndex++;
                    } else {
                        String options = constructTableColumnOptions(colIndex, false, useServerPaging, null, null);
                        tableColumnOptions.append(options + ",");
                        colIndex++;
                    }
                }

                if (actionFieldVisible && (actionIndex == -1 || actionIndex >= colIndex)) {
                    String options = constructTableColumnOptions(colIndex, false, useServerPaging, null, null);
                    tableColumnOptions.append(options);
                } else {
                    tableColumnOptions = new StringBuilder(StringUtils.removeEnd(tableColumnOptions.toString(), ","));
                }

                // merge the aoColumnDefs passed in
                if (!StringUtils.isEmpty(templateOptions.get(UifConstants.TableToolsKeys.AO_COLUMN_DEFS))) {
                    String origAoOptions = templateOptions.get(UifConstants.TableToolsKeys.AO_COLUMN_DEFS).trim();
                    origAoOptions = StringUtils.removeStart(origAoOptions, "[");
                    origAoOptions = StringUtils.removeEnd(origAoOptions, "]");
                    tableColumnOptions.append("," + origAoOptions);
                }

                tableColumnOptions.append("]");
                templateOptions.put(UifConstants.TableToolsKeys.AO_COLUMN_DEFS, tableColumnOptions.toString());
            }
        }
    }

    /**
     * Builds default sorting options.
     *
     * @param lookupView the view for the lookup
     * @param collectionGroup the collection group for the table
     */
    protected void buildSortOptions(LookupView lookupView, CollectionGroup collectionGroup) {
        if (!isDisableTableSort() && CollectionUtils.isNotEmpty(lookupView.getDefaultSortAttributeNames())) {
            LayoutManager layoutManager = collectionGroup.getLayoutManager();

            if (layoutManager instanceof TableLayoutManager) {
                TableLayoutManager tableLayoutManager = (TableLayoutManager) layoutManager;

                List<String> firstRowPropertyNames = getFirstRowPropertyNames(tableLayoutManager.getFirstRowFields());
                List<String> defaultSortAttributeNames = lookupView.getDefaultSortAttributeNames();
                String sortDirection = lookupView.isDefaultSortAscending() ? "'asc'" : "'desc'";
                boolean actionFieldVisible = collectionGroup.isRenderLineActions() && !Boolean.TRUE.equals(
                        collectionGroup.getReadOnly());
                int actionIndex = ((TableLayoutManager) layoutManager).getActionColumnIndex();
                int columnIndexPrefix = 0;

                if (tableLayoutManager.isRenderSequenceField()) {
                    columnIndexPrefix++;
                }

                if (tableLayoutManager.getRowDetailsGroup() != null && CollectionUtils.isNotEmpty(
                        tableLayoutManager.getRowDetailsGroup().getItems())) {
                    columnIndexPrefix++;
                }

                StringBuffer tableToolsSortOptions = new StringBuffer("[");

                for (String defaultSortAttributeName : defaultSortAttributeNames) {
                    int index = firstRowPropertyNames.indexOf(defaultSortAttributeName);
                    if (index >= 0) {
                        if (tableToolsSortOptions.length() > 1) {
                            tableToolsSortOptions.append(",");
                        }
                        int columnIndex = columnIndexPrefix + index;
                        if (actionFieldVisible && actionIndex != -1 && actionIndex <= columnIndex + 1) {
                            columnIndex++;
                        }
                        tableToolsSortOptions.append("[" + columnIndex + "," + sortDirection + "]");
                    }
                }

                tableToolsSortOptions.append("]");

                if (templateOptions.isEmpty()) {
                    setTemplateOptions(new HashMap<String, String>());
                }
                templateOptions.put(UifConstants.TableToolsKeys.AASORTING, tableToolsSortOptions.toString());
            }
        }
    }

    private List<String> getFirstRowPropertyNames(List<Field> firstRowFields) {
        return Lists.transform(firstRowFields, new Function<Field, String>() {
            @Override
            public String apply(@Nullable Field field) {
                if (field != null && field instanceof DataField) {
                    return ((DataField) field).getPropertyName();
                } else {
                    return null;
                }
            }
        });
    }

    /**
     * Construct the column options for a data field
     *
     * @param target column index
     * @param collectionGroup the collectionGroup in which the data field is defined
     * @param field the field to construction options for
     * @return options as valid for datatable
     */
    protected String getDataFieldColumnOptions(int target, CollectionGroup collectionGroup, DataField field) {
        String sortDataType = null;

        if (!Boolean.TRUE.equals(collectionGroup.getReadOnly())
                && (field instanceof InputField)
                && ((InputField) field).getControl() != null) {
            Control control = ((InputField) field).getControl();
            if (control instanceof SelectControl) {
                sortDataType = UifConstants.TableToolsValues.DOM_SELECT;
            } else if (control instanceof CheckboxControl || control instanceof CheckboxGroupControl) {
                sortDataType = UifConstants.TableToolsValues.DOM_CHECK;
            } else if (control instanceof RadioGroupControl) {
                sortDataType = UifConstants.TableToolsValues.DOM_RADIO;
            } else {
                sortDataType = UifConstants.TableToolsValues.DOM_TEXT;
            }
        } else {
            sortDataType = UifConstants.TableToolsValues.DOM_TEXT;
        }

        Class<?> dataTypeClass = ObjectPropertyUtils.getPropertyType(collectionGroup.getCollectionObjectClass(),
                field.getPropertyName());
        // check to see if field has custom sort type
        if (field.getSortAs() != null && field.getSortAs().length() > 0) {
            if (field.getSortAs().equals(UifConstants.TableToolsValues.CURRENCY)) {
                dataTypeClass = KualiDecimal.class;
            } else if (field.getSortAs().equals(UifConstants.TableToolsValues.DATE)) {
                dataTypeClass = java.sql.Date.class;
            } else if (field.getSortAs().equals(UifConstants.TableToolsValues.NUMERIC)) {
                dataTypeClass = Number.class;
            } else if (field.getSortAs().equals(UifConstants.TableToolsValues.STRING)) {
                dataTypeClass = String.class;
            }
        }

        boolean isSortable = true;
        if (field.isApplyMask()) {
            isSortable = false;
        }

        return constructTableColumnOptions(target, isSortable, collectionGroup.isUseServerPaging(), dataTypeClass,
                sortDataType);
    }

    /**
     * Constructs the sort data type for each data table columns in a format that will be used to
     * initialize the data table widget via javascript
     *
     * @param target the column index
     * @param isSortable whether a column should be marked as sortable
     * @param isUseServerPaging is server side paging enabled?
     * @param dataTypeClass the class type of the column value - used determine the sType option -
     * which identifies the search plugin to use
     * @param sortDataType Defines a data source type for the sorting which can be used to read
     * realtime information from the table
     * @return a formatted string with data table options for one column
     */
    public String constructTableColumnOptions(int target, boolean isSortable, boolean isUseServerPaging,
            Class<?> dataTypeClass, String sortDataType) {
        String options = "null";

        if (!isSortable || dataTypeClass == null) {
            options = sortable(false) + "," + sortType(UifConstants.TableToolsValues.STRING);
        } else {
            options = sortType(getSortType(dataTypeClass));

            if (!isUseServerPaging && !this.forceLocalJsonData) {
                options += "," + sortDataType(sortDataType);
            }
        }

        if (target < cellCssClasses.size() && target >= 0) {
            options += ", \"" + UifConstants.TableToolsKeys.CELL_CLASS + "\" : \"" + cellCssClasses.get(target) + "\"";
        }

        // only use the mDataProp when using json data (only relevant for this table type)
        options += mData(isUseServerPaging, target);

        if (!options.equals("null")) {
            options = "{" + options + "," + targets(target) + "}";
        } else {
            options = "{" + options + "}";
        }

        return options;
    }

    private String sortable(boolean sortable) {
        return "\"" + UifConstants.TableToolsKeys.SORTABLE + "\": " + (sortable ? UifConstants.TableToolsValues.TRUE :
                UifConstants.TableToolsValues.FALSE);
    }

    private String sortDataType(String sortDataType) {
        return "\"" + UifConstants.TableToolsKeys.SORT_DATA_TYPE + "\": \"" + sortDataType + "\"";
    }

    private String getSortType(Class<?> dataTypeClass) {
        String sortType = UifConstants.TableToolsValues.STRING;
        if (ClassUtils.isAssignable(dataTypeClass, KualiPercent.class)) {
            sortType = UifConstants.TableToolsValues.PERCENT;
        } else if (ClassUtils.isAssignable(dataTypeClass, KualiInteger.class) || ClassUtils.isAssignable(dataTypeClass,
                KualiDecimal.class)) {
            sortType = UifConstants.TableToolsValues.CURRENCY;
        } else if (ClassUtils.isAssignable(dataTypeClass, Timestamp.class)) {
            sortType = "date";
        } else if (ClassUtils.isAssignable(dataTypeClass, java.sql.Date.class) || ClassUtils.isAssignable(dataTypeClass,
                java.util.Date.class)) {
            sortType = UifConstants.TableToolsValues.DATE;
        } else if (ClassUtils.isAssignable(dataTypeClass, Number.class)) {
            sortType = UifConstants.TableToolsValues.NUMERIC;
        }
        return sortType;
    }

    private String sortType(String sortType) {
        return "\"" + UifConstants.TableToolsKeys.SORT_TYPE + "\": \"" + sortType + "\"";
    }

    private String targets(int target) {
        return "\"" + UifConstants.TableToolsKeys.TARGETS + "\": [" + target + "]";
    }

    private String visible(boolean visible) {
        return "\"" + UifConstants.TableToolsKeys.VISIBLE + "\": " + (visible ? UifConstants.TableToolsValues.TRUE :
                UifConstants.TableToolsValues.FALSE);
    }

    private String mData(boolean isUseServerPaging, int target) {
        if (isUseServerPaging || this.forceLocalJsonData) {
            return ", \"" + UifConstants.TableToolsKeys.MDATA +
                    "\" : function(row,type,newVal){ return _handleColData(row,type,'c" + target + "',newVal);}";
        }
        return "";
    }

    /**
     * Returns the text which is used to display text when the table is empty
     *
     * @return empty table message
     */
    @BeanTagAttribute
    public String getEmptyTableMessage() {
        return emptyTableMessage;
    }

    /**
     * Setter for a text to be displayed when the table is empty
     *
     * @param emptyTableMessage
     */
    public void setEmptyTableMessage(String emptyTableMessage) {
        this.emptyTableMessage = emptyTableMessage;
    }

    /**
     * Returns true if sorting is disabled
     *
     * @return the disableTableSort
     */
    @BeanTagAttribute
    public boolean isDisableTableSort() {
        return this.disableTableSort;
    }

    /**
     * Enables/disables the table sorting
     *
     * @param disableTableSort the disableTableSort to set
     */
    public void setDisableTableSort(boolean disableTableSort) {
        this.disableTableSort = disableTableSort;
    }

    /**
     * Returns true if export option is enabled
     *
     * @return the showExportOption
     */
    @BeanTagAttribute
    public boolean isShowExportOption() {
        return this.showExportOption;
    }

    /**
     * Show/Hide the search and export option in tabletools
     *
     * @param showExportOption the showExportOptions to set
     */
    public void setShowExportOption(boolean showExportOption) {
        this.showExportOption = showExportOption;
    }

    /**
     * Holds propertyNames for the ones meant to be hidden since columns are visible by default
     *
     * <p>
     * Duplicate entries are ignored and the order of entries is not significant
     * </p>
     *
     * @return a set with propertyNames of columns to be hidden
     */
    @BeanTagAttribute(type = BeanTagAttribute.AttributeType.SETVALUE)
    public Set<String> getHiddenColumns() {
        return hiddenColumns;
    }

    /**
     * Setter for the hidden columns set
     *
     * @param hiddenColumns a set containing propertyNames
     */
    public void setHiddenColumns(Set<String> hiddenColumns) {
        this.hiddenColumns = hiddenColumns;
    }

    /**
     * Holds the propertyNames for columns that are to be sorted
     *
     * <p>
     * Duplicate entries are ignored and the order of entries is not significant
     * </p>
     *
     * @return a set of propertyNames with for columns that will be sorted
     */
    @BeanTagAttribute(type = BeanTagAttribute.AttributeType.SETVALUE)
    public Set<String> getSortableColumns() {
        return sortableColumns;
    }

    /**
     * Setter for sortable columns
     *
     * @param sortableColumns a set containing propertyNames of columns to be sorted
     */
    public void setSortableColumns(Set<String> sortableColumns) {
        this.sortableColumns = sortableColumns;
    }

    /**
     * Specifies a URL for acquiring the table data with ajax
     *
     * <p>
     * When the ajax source URL is specified the rich table plugin will retrieve the data by
     * invoking the URL and building the table rows from the result. This is different from the
     * standard use of the rich table plugin with uses progressive enhancement to decorate a table
     * that has already been rendereed
     * </p>
     *
     * @return URL for ajax source
     */
    @BeanTagAttribute
    public String getAjaxSource() {
        return ajaxSource;
    }

    /**
     * Setter for the Ajax source URL
     *
     * @param ajaxSource
     */
    public void setAjaxSource(String ajaxSource) {
        this.ajaxSource = ajaxSource;
    }

    /**
     * Get groupingOption
     *
     * @return grouping options as a JS string
     */
    public String getGroupingOptionsJSString() {
        return groupingOptionsJSString;
    }

    /**
     * Set the groupingOptions js data. <b>This should not be set through XML configuration.</b>
     *
     * @param groupingOptionsJSString
     */
    public void setGroupingOptionsJSString(String groupingOptionsJSString) {
        this.groupingOptionsJSString = groupingOptionsJSString;
    }

    /**
     * If set to true and the aoColumnDefs template option is explicitly defined in templateOptions,
     * those aoColumnDefs will be used for this table. Otherwise, if false, the aoColumnDefs will
     * attempt to be merged with those that are automatically generated by RichTable
     *
     * @return true if the aoColumnDefs set will completely override those that are generated
     *         automatically by RichTable
     */
    @BeanTagAttribute
    public boolean isForceAoColumnDefsOverride() {
        return forceAoColumnDefsOverride;
    }

    /**
     * Set forceAoColumnDefsOverride
     *
     * @param forceAoColumnDefsOverride
     */
    public void setForceAoColumnDefsOverride(boolean forceAoColumnDefsOverride) {
        this.forceAoColumnDefsOverride = forceAoColumnDefsOverride;
    }

    /**
     * If true, the table will automatically use row JSON data generated by this widget
     *
     * <p>
     * This forces the table backed by this RichTable to get its content from a template option
     * called aaData. This will automatically skip row generation in the template, and cause the
     * table receive its data from the aaData template option automatically generated and set by
     * this RichTable. This allows the table to take advantage of the bDeferRender option (also
     * automatically set to true) when this table is a paged table (performance increase for tables
     * that are more than one page). Note: the CollectionGroup's isUseServerPaging flag will always
     * override this functionality if it is also true.
     * </p>
     *
     * @return true if backed by the aaData option in JSON, that is generated during the ftl
     *         rendering process by this widget for this table
     */
    @BeanTagAttribute
    public boolean isForceLocalJsonData() {
        return forceLocalJsonData;
    }

    /**
     * Set the forceLocalJsonData flag to force this table to use generated row json data
     *
     * @param forceLocalJsonData
     */
    public void setForceLocalJsonData(boolean forceLocalJsonData) {
        this.forceLocalJsonData = forceLocalJsonData;
    }

    /**
     * The nestedLevel property represents how many collection tables deep this particular table is
     *
     * <p>
     * This property must be manually set if the flag forceLocalJsonData is being used and the
     * collection table this RichTable represents is a subcollection of a TABLE collection (not
     * stacked collection). If this is true, add 1 for each level deep (ex. subCollection would be
     * 1, sub-subCollection would be 2). If this property is not set javascript errors will occur on
     * the page, as this determines how deep to escape certain characters.
     * </p>
     *
     * @return the nestedLevel representing the
     */
    @BeanTagAttribute
    public int getNestedLevel() {
        return nestedLevel;
    }

    /**
     * Set the nestedLevel for this table - must be set if using forceLocalJsonData and this is a
     * subCollection of a TableCollection (also using forceLocalJsonData)
     *
     * @param nestedLevel
     */
    public void setNestedLevel(int nestedLevel) {
        this.nestedLevel = nestedLevel;
    }

    /**
     * Get the translated aaData array generated by calls to addRowToTableData by the ftl
     *
     * <p>
     * This data is in JSON format and expected to be consumed by datatables when utilizing the
     * forceLocalJsonData option. This will be populated automatically if that flag is set to true.
     * </p>
     *
     * @return the generated aaData
     */
    public String getAaData() {
        return aaData;
    }

    /**
     * Set the translated aaData array
     *
     * <p>
     * This data is in JSON format and expected to be consumed by datatables when utilizing the
     * forceLocalJsonData. This setter is required for copyProperties()
     * </p>
     *
     * @param aaData the generated aaData
     */
    protected void setAaData(String aaData) {
        this.aaData = aaData;
    }

    /**
     * Get the simple value as a string that represents the field's sortable value, to be used as
     * val in the custom uif json data object (accessed by mDataProp option on datatables -
     * automated by framework) when using the forceLocalJsonData option or the CollectionGroup's
     * isUseServerPaging option
     *
     * @param model model the current model
     * @param field the field to retrieve a sortable value from for use in custom json data
     * @return the value as a String
     */
    public String getCellValue(Object model, Field field) {
        String value = KRADUtils.getSimpleFieldValue(model, field);

        if (value == null) {
            value = "null";
        } else {
            value = KRADConstants.QUOTE_PLACEHOLDER + value + KRADConstants.QUOTE_PLACEHOLDER;
        }

        return value;
    }

    /**
     * Add row content passed from table ftl to the aaData array by converting and escaping the
     * content to an object (in an array of objects) in JSON format
     *
     * <p>
     * The data in aaData is expected to be consumed by a call by the datatables plugin using
     * sAjaxSource or aaData. The addRowToTableData generation call is additive must be made per a
     * row in the ftl.
     * </p>
     *
     * @param row the row of content with each cell content surrounded by the @quot@ token and
     * followed by a comma
     */
    public void addRowToTableData(String row) {
        String escape = "";

        if (templateOptions.isEmpty()) {
            setTemplateOptions(new HashMap<String, String>());
        }

        // if nestedLevel is set add the appropriate amount of escape characters per a level of nesting
        for (int i = 0; i < nestedLevel && forceLocalJsonData; i++) {
            escape = escape + "\\";
        }

        // remove newlines and replace quotes and single quotes with unicode characters
        row = row.trim().replace("\"", escape + "\\u0022").replace("'", escape + "\\u0027").replace("\n", "").replace(
                "\r", "");

        // remove hanging comma
        row = StringUtils.removeEnd(row, ",");

        // replace all quote placeholders with actual quote characters
        row = row.replace(KRADConstants.QUOTE_PLACEHOLDER, "\"");
        row = "{" + row + "}";

        // if first call create aaData and force defer render option, otherwise append
        if (StringUtils.isBlank(aaData)) {
            aaData = "[" + row + "]";

            if (templateOptions.get(UifConstants.TableToolsKeys.DEFER_RENDER) == null) {
                //make sure deferred rendering is forced if not explicitly set
                templateOptions.put(UifConstants.TableToolsKeys.DEFER_RENDER, UifConstants.TableToolsValues.TRUE);
            }

        } else if (StringUtils.isNotBlank(row)) {
            aaData = aaData.substring(0, aaData.length() - 1) + "," + row + "]";
        }

        //force json data use if forceLocalJsonData flag is set
        if (forceLocalJsonData) {
            templateOptions.put(UifConstants.TableToolsKeys.AA_DATA, aaData);
        }
    }

    protected ConfigurationService getConfigurationService() {
        return CoreApiServiceLocator.getKualiConfigurationService();
    }
}
