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