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