001// Licensed under the Apache License, Version 2.0 (the "License"); 002// you may not use this file except in compliance with the License. 003// You may obtain a copy of the License at 004// 005// http://www.apache.org/licenses/LICENSE-2.0 006// 007// Unless required by applicable law or agreed to in writing, software 008// distributed under the License is distributed on an "AS IS" BASIS, 009// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 010// See the License for the specific language governing permissions and 011// limitations under the License. 012 013package org.apache.tapestry5.corelib.components; 014 015import org.apache.tapestry5.*; 016import org.apache.tapestry5.annotations.*; 017import org.apache.tapestry5.beanmodel.BeanModel; 018import org.apache.tapestry5.beanmodel.BeanModelUtils; 019import org.apache.tapestry5.beanmodel.PropertyModel; 020import org.apache.tapestry5.beanmodel.services.BeanModelSource; 021import org.apache.tapestry5.corelib.data.GridPagerPosition; 022import org.apache.tapestry5.grid.*; 023import org.apache.tapestry5.internal.TapestryInternalUtils; 024import org.apache.tapestry5.internal.bindings.AbstractBinding; 025import org.apache.tapestry5.ioc.annotations.Inject; 026import org.apache.tapestry5.ioc.internal.util.InternalUtils; 027import org.apache.tapestry5.services.ComponentDefaultProvider; 028import org.apache.tapestry5.services.ComponentEventResultProcessor; 029import org.apache.tapestry5.services.FormSupport; 030import org.apache.tapestry5.services.javascript.JavaScriptSupport; 031 032import java.io.IOException; 033import java.util.Collections; 034import java.util.List; 035 036/** 037 * A grid presents tabular data. It is a composite component, created in terms of several sub-components. The 038 * sub-components are statically wired to the Grid, as it provides access to the data and other models that they need. 039 * 040 * A Grid may operate inside a {@link org.apache.tapestry5.corelib.components.Form}. By overriding the cell renderers of 041 * properties, the default output-only behavior can be changed to produce a complex form with individual control for 042 * editing properties of each row. There is a big caveat here: if the order of rows provided by 043 * the {@link org.apache.tapestry5.grid.GridDataSource} changes between render and form submission, then there's the 044 * possibility that data will be applied to the wrong server-side objects. 045 * 046 * For this reason, when using Grid and Form together, you should generally 047 * provide the Grid with a {@link org.apache.tapestry5.ValueEncoder} (via the 048 * encoder parameter), or use an entity type for the "row" parameter for which 049 * Tapestry can provide a ValueEncoder automatically. This will allow Tapestry 050 * to use a unique ID for each row that doesn't change when rows are reordered. 051 * 052 * @tapestrydoc 053 * @see org.apache.tapestry5.beanmodel.BeanModel 054 * @see org.apache.tapestry5.beanmodel.services.BeanModelSource 055 * @see org.apache.tapestry5.grid.GridDataSource 056 * @see BeanEditForm 057 * @see BeanDisplay 058 * @see Loop 059 */ 060@SupportsInformalParameters 061public class Grid implements GridModel, ClientElement 062{ 063 /** 064 * The source of data for the Grid to display. This will usually be a List or array but can also be an explicit 065 * {@link GridDataSource}. For Lists and object arrays, a GridDataSource is created automatically as a wrapper 066 * around the underlying List. 067 */ 068 @Parameter(required = true, autoconnect = true) 069 private GridDataSource source; 070 071 /** 072 * A wrapper around the provided GridDataSource that caches access to the availableRows property. This is the source 073 * provided to sub-components. 074 */ 075 private GridDataSource cachingSource; 076 077 /** 078 * The number of rows of data displayed on each page. If there are more rows than will fit, the Grid will divide up 079 * the rows into "pages" and (normally) provide a pager to allow the user to navigate within the overall result 080 * set. 081 */ 082 @Parameter(BindingConstants.SYMBOL + ":" + ComponentParameterConstants.GRID_ROWS_PER_PAGE) 083 private int rowsPerPage; 084 085 /** 086 * Defines where the pager (used to navigate within the "pages" of results) should be displayed: "top", "bottom", 087 * "both" or "none". 088 */ 089 @Parameter(value = BindingConstants.SYMBOL + ":" + ComponentParameterConstants.GRID_PAGER_POSITION, 090 defaultPrefix = BindingConstants.LITERAL) 091 private GridPagerPosition pagerPosition; 092 093 /** 094 * Used to store the current object being rendered (for the current row). This is used when parameter blocks are 095 * provided to override the default cell renderer for a particular column ... the components within the block can 096 * use the property bound to the row parameter to know what they should render. 097 */ 098 @Parameter(principal = true) 099 private Object row; 100 101 /** 102 * Optional output parameter used to identify the index of the column being rendered. 103 */ 104 @Parameter 105 private int columnIndex; 106 107 /** 108 * The model used to identify the properties to be presented and the order of presentation. The model may be 109 * omitted, in which case a default model is generated from the first object in the data source (this implies that 110 * the objects provided by the source are uniform). The model may be explicitly specified to override the default 111 * behavior, say to reorder or rename columns or add additional columns. The add, include, 112 * exclude and reorder 113 * parameters are <em>only</em> applied to a default model, not an explicitly provided one. 114 */ 115 @Parameter 116 private BeanModel model; 117 118 /** 119 * The model parameter after modification due to the add, include, exclude and reorder parameters. 120 */ 121 private BeanModel dataModel; 122 123 /** 124 * The model used to handle sorting of the Grid. This is generally not specified, and the built-in model supports 125 * only single column sorting. The sort constraints (the column that is sorted, and ascending vs. descending) is 126 * stored as persistent fields of the Grid component. 127 */ 128 @Parameter 129 private GridSortModel sortModel; 130 131 /** 132 * A comma-separated list of property names to be added to the {@link org.apache.tapestry5.beanmodel.BeanModel}. 133 * Cells for added columns will be blank unless a cell override is provided. This parameter is only used 134 * when a default model is created automatically. 135 */ 136 @Parameter(defaultPrefix = BindingConstants.LITERAL) 137 private String add; 138 139 /** 140 * A comma-separated list of property names to be retained from the 141 * {@link org.apache.tapestry5.beanmodel.BeanModel}. 142 * Only these properties will be retained, and the properties will also be reordered. The names are 143 * case-insensitive. This parameter is only used 144 * when a default model is created automatically. 145 */ 146 @SuppressWarnings("unused") 147 @Parameter(defaultPrefix = BindingConstants.LITERAL) 148 private String include; 149 150 /** 151 * A comma-separated list of property names to be removed from the {@link org.apache.tapestry5.beanmodel.BeanModel} 152 * . 153 * The names are case-insensitive. This parameter is only used 154 * when a default model is created automatically. 155 */ 156 @Parameter(defaultPrefix = BindingConstants.LITERAL) 157 private String exclude; 158 159 /** 160 * A comma-separated list of property names indicating the order in which the properties should be presented. The 161 * names are case insensitive. Any properties not indicated in the list will be appended to the end of the display 162 * order. This parameter is only used 163 * when a default model is created automatically. 164 */ 165 @Parameter(defaultPrefix = BindingConstants.LITERAL) 166 private String reorder; 167 168 /** 169 * A Block to render instead of the table (and pager, etc.) when the source is empty. The default is simply the text 170 * "There is no data to display". This parameter is used to customize that message, possibly including components to 171 * allow the user to create new objects. 172 */ 173 //@Parameter(value = BindingConstants.SYMBOL + ":" + ComponentParameterConstants.GRID_EMPTY_BLOCK, 174 @Parameter(value = "block:empty", 175 defaultPrefix = BindingConstants.LITERAL) 176 @Property(write = false) 177 private Block empty; 178 179 /** 180 * CSS class for the <table> element. In addition, informal parameters to the Grid are rendered in the table 181 * element. 182 */ 183 @Parameter(name = "class", defaultPrefix = BindingConstants.LITERAL, 184 value = BindingConstants.SYMBOL + ":" + ComponentParameterConstants.GRID_TABLE_CSS_CLASS) 185 @Property(write = false) 186 private String tableClass; 187 188 /** 189 * If true, then the Grid will be wrapped in an element that acts like a 190 * {@link org.apache.tapestry5.corelib.components.Zone}; all the paging and sorting links will refresh the zone, 191 * repainting the entire grid within it, but leaving the rest of the page (outside the zone) unchanged. 192 */ 193 @Parameter 194 private boolean inPlace; 195 196 /** 197 * If true, then the Grid will also render a table element complete with headers if the data source is empty. 198 * If set to true, a model parameter will have to be specified. A default model for a specific class can be 199 * created using {@link BeanModelSource#createDisplayModel(Class, org.apache.tapestry5.commons.Messages)}. 200 */ 201 @Parameter 202 private boolean renderTableIfEmpty = false; 203 204 205 /** 206 * The name of the pseudo-zone that encloses the Grid. Starting in 5.4, this is always either 207 * null or "^" and is not really used the way it was in 5.3; instead it triggers the addition 208 * of a {@code data-inplace-grid-links} attribute in a div surrounding any links related to 209 * sorting or pagination. The rest is sorted out on the client. See module {@code t5/core/zone}. 210 */ 211 @Property(write = false) 212 private String zone; 213 214 private boolean didRenderZoneDiv; 215 216 217 /** 218 * The pagination model for the Grid, which encapsulates current page, sort column id, 219 * and sort ascending/descending. If not bound, a persistent property of the Grid is used. 220 * When rendering the Grid in a loop, this should be bound in some way to keep successive instances 221 * of the Grid configured individually. 222 * 223 * @since 5.4 224 */ 225 @Parameter(value = "defaultPaginationModel") 226 private GridPaginationModel paginationModel; 227 228 @Persist 229 private GridPaginationModel defaultPaginationModel; 230 231 @Inject 232 private ComponentResources resources; 233 234 @Inject 235 private BeanModelSource modelSource; 236 237 @Environmental 238 private JavaScriptSupport javaScriptSupport; 239 240 @Component(parameters = 241 {"index=inherit:columnIndex", "lean=inherit:lean", "overrides=overrides", "zone=zone"}) 242 private GridColumns columns; 243 244 @Component(parameters = 245 {"columnIndex=inherit:columnIndex", "rowsPerPage=rowsPerPage", "currentPage=currentPage", "row=row", 246 "overrides=overrides"}, publishParameters = "rowIndex,rowClass,volatile,encoder,lean") 247 private GridRows rows; 248 249 @Component(parameters = 250 {"source=dataSource", "rowsPerPage=rowsPerPage", "currentPage=currentPage", "zone=zone"}) 251 private GridPager pager; 252 253 @Component(parameters = "to=pagerTop") 254 private Delegate pagerTop; 255 256 @Component(parameters = "to=pagerBottom") 257 private Delegate pagerBottom; 258 259 @Component(parameters = "class=tableClass", inheritInformalParameters = true) 260 private Any table; 261 262 @Environmental(false) 263 private FormSupport formSupport; 264 265 /** 266 * Defines where block and label overrides are obtained from. By default, the Grid component provides block 267 * overrides (from its block parameters). 268 */ 269 @Parameter(value = "this", allowNull = false) 270 @Property(write = false) 271 private PropertyOverrides overrides; 272 273 /** 274 * Set up via the traditional or Ajax component event request handler 275 */ 276 @Environmental 277 private ComponentEventResultProcessor componentEventResultProcessor; 278 279 @Inject 280 private ComponentDefaultProvider defaultsProvider; 281 282 ValueEncoder defaultEncoder() 283 { 284 return defaultsProvider.defaultValueEncoder("row", resources); 285 } 286 287 /** 288 * A version of GridDataSource that caches the availableRows and empty properties. This addresses TAPESTRY-2245. 289 */ 290 static class CachingDataSource implements GridDataSource 291 { 292 private final GridDataSource delegate; 293 294 private boolean availableRowsCached; 295 296 private int availableRows; 297 298 private boolean emptyCached; 299 300 private boolean empty; 301 302 CachingDataSource(GridDataSource delegate) 303 { 304 this.delegate = delegate; 305 } 306 307 @Override 308 public boolean isEmpty() 309 { 310 if (!emptyCached) 311 { 312 empty = delegate.isEmpty(); 313 emptyCached = true; 314 if (empty) 315 { 316 availableRows = 0; 317 availableRowsCached = true; 318 } 319 } 320 321 return empty; 322 } 323 324 @Override 325 public int getAvailableRows(int limit) 326 { 327 if (!availableRowsCached) 328 { 329 int result = delegate.getAvailableRows(limit); 330 if (result == 0) 331 { 332 empty = true; 333 emptyCached = true; 334 } else { 335 empty = false; 336 emptyCached = true; 337 } 338 if (result < limit) { 339 availableRows = result; 340 availableRowsCached = true; 341 } 342 return result; 343 } else { 344 return Math.min(availableRows, limit); 345 } 346 } 347 348 public int getAvailableRows() 349 { 350 if (!availableRowsCached) 351 { 352 availableRows = delegate.getAvailableRows(); 353 availableRowsCached = true; 354 if (availableRows == 0) 355 { 356 empty = true; 357 emptyCached = true; 358 } else { 359 empty = false; 360 emptyCached = true; 361 } 362 } 363 364 return availableRows; 365 } 366 367 public void prepare(int startIndex, int endIndex, List<SortConstraint> sortConstraints) 368 { 369 delegate.prepare(startIndex, endIndex, sortConstraints); 370 } 371 372 public Object getRowValue(int index) 373 { 374 return delegate.getRowValue(index); 375 } 376 377 public Class getRowType() 378 { 379 return delegate.getRowType(); 380 } 381 } 382 383 /** 384 * Default implementation that only allows a single column to be the sort column, and stores the sort information as 385 * persistent fields of the Grid component. 386 */ 387 class DefaultGridSortModel implements GridSortModel 388 { 389 public ColumnSort getColumnSort(String columnId) 390 { 391 if (paginationModel == null || !TapestryInternalUtils.isEqual(columnId, paginationModel.getSortColumnId())) 392 { 393 return ColumnSort.UNSORTED; 394 } 395 396 return getColumnSort(); 397 } 398 399 private ColumnSort getColumnSort() 400 { 401 return getSortAscending() ? ColumnSort.ASCENDING : ColumnSort.DESCENDING; 402 } 403 404 public void updateSort(String columnId) 405 { 406 assert InternalUtils.isNonBlank(columnId); 407 408 if (columnId.equals(paginationModel.getSortColumnId())) 409 { 410 setSortAscending(!getSortAscending()); 411 return; 412 } 413 414 paginationModel.setSortColumnId(columnId); 415 setSortAscending(true); 416 } 417 418 public List<SortConstraint> getSortConstraints() 419 { 420 // In a few limited cases we may not have yet hit the SetupRender phase, and the model may be null. 421 // Furthermore, because the sort column id is cached, an unknown value exception must be prevented in case of dynamically created bean models (e.g. usage of the 'include' parameter) 422 final BeanModel dataModel = getDataModel(); 423 424 if ((paginationModel == null || paginationModel.getSortColumnId() == null) || !(dataModel.getPropertyNames().contains(TapestryInternalUtils.extractIdFromPropertyExpression(paginationModel.getSortColumnId())))) 425 { 426 return Collections.emptyList(); 427 } 428 429 PropertyModel sortModel = dataModel.getById(paginationModel.getSortColumnId()); 430 431 SortConstraint constraint = new SortConstraint(sortModel, getColumnSort()); 432 433 return Collections.singletonList(constraint); 434 } 435 436 public void clear() 437 { 438 paginationModel.setSortColumnId(null); 439 paginationModel.setSortAscending(null); 440 } 441 } 442 443 GridSortModel defaultSortModel() 444 { 445 return new DefaultGridSortModel(); 446 } 447 448 /** 449 * Returns a {@link org.apache.tapestry5.Binding} instance that attempts to identify the model from the source 450 * parameter (via {@link org.apache.tapestry5.grid.GridDataSource#getRowType()}. Subclasses may override to provide 451 * a different mechanism. The returning binding is variant (not invariant). 452 * 453 * @see BeanModelSource#createDisplayModel(Class, org.apache.tapestry5.commons.Messages) 454 */ 455 protected Binding defaultModel() 456 { 457 458 return new AbstractBinding() 459 { 460 public Object get() 461 { 462 // Get the default row type from the data source 463 464 GridDataSource gridDataSource = source; 465 466 Class rowType = gridDataSource.getRowType(); 467 468 if (renderTableIfEmpty || rowType == null) 469 throw new RuntimeException( 470 String.format( 471 "Unable to determine the bean type for rows from %s. You should bind the model parameter explicitly.", 472 gridDataSource)); 473 474 // Properties do not have to be read/write 475 476 return modelSource.createDisplayModel(rowType, overrides.getOverrideMessages()); 477 } 478 479 /** 480 * Returns false. This may be overkill, but it basically exists because the model is 481 * inherently mutable and therefore may contain client-specific state and needs to be 482 * discarded at the end of the request. If the model were immutable, then we could leave 483 * invariant as true. 484 */ 485 @Override 486 public boolean isInvariant() 487 { 488 return false; 489 } 490 }; 491 } 492 493 static final ComponentAction<Grid> SETUP_DATA_SOURCE = new ComponentAction<Grid>() 494 { 495 private static final long serialVersionUID = 8545187927995722789L; 496 497 public void execute(Grid component) 498 { 499 component.setupDataSource(); 500 } 501 502 @Override 503 public String toString() 504 { 505 return "Grid.SetupDataSource"; 506 } 507 }; 508 509 Object setupRender() 510 { 511 if (formSupport != null) 512 { 513 formSupport.store(this, SETUP_DATA_SOURCE); 514 } 515 516 setupDataSource(); 517 518 // If there's no rows, display the empty block placeholder. 519 520 return !renderTableIfEmpty && cachingSource.isEmpty() ? empty : null; 521 } 522 523 void cleanupRender() 524 { 525 // if an inPlace Grid is rendered inside a Loop, be sure to generate a new wrapper 526 // zone for each iteration (TAP5-2256) 527 zone = null; 528 529 // If grid is rendered inside a Loop. be sure to generate a new data model for 530 // each iteration (TAP5-2470) 531 dataModel = null; 532 } 533 534 public GridPaginationModel getDefaultPaginationModel() 535 { 536 if (defaultPaginationModel == null) 537 { 538 defaultPaginationModel = new GridPaginationModelImpl(); 539 } 540 541 return defaultPaginationModel; 542 } 543 544 void setupDataSource() 545 { 546 // TAP5-34: We pass the source into the CachingDataSource now; previously 547 // we were accessing source directly, but during submit the value wasn't 548 // cached, and therefore access was very inefficient, and sorting was 549 // very inconsistent during the processing of the form submission. 550 551 int effectiveCurrentPage = getCurrentPage(); 552 553 int numberOfRowsRequiredToShowCurrentPage = 1 + (effectiveCurrentPage - 1) * rowsPerPage; 554 int numberOfRowsRequiredToFillCurrentPage = effectiveCurrentPage * rowsPerPage; 555 556 cachingSource = new CachingDataSource(source); 557 if (pagerPosition != GridPagerPosition.NONE) 558 { 559 // We're going to render the pager, so we need to determine the total number of rows anyway. 560 // We do that eagerly here so we don't have to perform two count operations; the subsequent 561 // ones will return a cached result 562 cachingSource.getAvailableRows(); 563 } 564 int availableRowsWithLimit = cachingSource.getAvailableRows(numberOfRowsRequiredToFillCurrentPage); 565 566 if (availableRowsWithLimit == 0) 567 return; 568 569 // This captures when the number of rows has decreased, typically due to deletions. 570 571 if (numberOfRowsRequiredToShowCurrentPage > availableRowsWithLimit) 572 { 573 int maxPage = ((availableRowsWithLimit - 1) / rowsPerPage) + 1; 574 effectiveCurrentPage = maxPage; 575 } 576 577 int startIndex = (effectiveCurrentPage - 1) * rowsPerPage; 578 579 int endIndex = Math.min(startIndex + rowsPerPage - 1, availableRowsWithLimit - 1); 580 581 cachingSource.prepare(startIndex, endIndex, sortModel.getSortConstraints()); 582 } 583 584 Object beginRender(MarkupWriter writer) 585 { 586 // Skip rendering of component (template, body, etc.) when there's nothing to display. 587 // The empty placeholder will already have rendered. 588 589 if (cachingSource.isEmpty()) 590 return !renderTableIfEmpty ? false : null; 591 592 if (inPlace && zone == null) 593 { 594 javaScriptSupport.require("t5/core/zone"); 595 596 writer.element("div", "data-container-type", "zone"); 597 598 didRenderZoneDiv = true; 599 600 // Through Tapestry 5.3, we had a specific id for the zone that had to be passed down to the 601 // GridPager and etc. That's no longer necessary, so zone will always be null or "^". We don't 602 // even need any special ids to be allocated! 603 zone = "^"; 604 } 605 606 return null; 607 } 608 609 void afterRender(MarkupWriter writer) 610 { 611 if (didRenderZoneDiv) 612 { 613 writer.end(); // div 614 didRenderZoneDiv = false; 615 } 616 } 617 618 public BeanModel getDataModel() 619 { 620 if (dataModel == null) 621 { 622 dataModel = model; 623 624 BeanModelUtils.modify(dataModel, add, include, exclude, reorder); 625 } 626 627 return dataModel; 628 } 629 630 public int getNumberOfProperties() 631 { 632 return getDataModel().getPropertyNames().size(); 633 } 634 635 public GridDataSource getDataSource() 636 { 637 return cachingSource; 638 } 639 640 public GridSortModel getSortModel() 641 { 642 return sortModel; 643 } 644 645 public Object getPagerTop() 646 { 647 return pagerPosition.isMatchTop() ? pager : null; 648 } 649 650 public Object getPagerBottom() 651 { 652 return pagerPosition.isMatchBottom() ? pager : null; 653 } 654 655 public int getCurrentPage() 656 { 657 Integer currentPage = paginationModel.getCurrentPage(); 658 659 return currentPage == null ? 1 : currentPage; 660 } 661 662 public void setCurrentPage(int currentPage) 663 { 664 paginationModel.setCurrentPage(currentPage); 665 } 666 667 private boolean getSortAscending() 668 { 669 Boolean sortAscending = paginationModel.getSortAscending(); 670 671 return sortAscending != null && sortAscending.booleanValue(); 672 } 673 674 private void setSortAscending(boolean sortAscending) 675 { 676 paginationModel.setSortAscending(sortAscending); 677 } 678 679 public int getRowsPerPage() 680 { 681 return rowsPerPage; 682 } 683 684 public Object getRow() 685 { 686 return row; 687 } 688 689 public void setRow(Object row) 690 { 691 this.row = row; 692 } 693 694 /** 695 * Resets the Grid to inital settings; this sets the current page to one, and 696 * {@linkplain org.apache.tapestry5.grid.GridSortModel#clear() clears the sort model}. 697 */ 698 public void reset() 699 { 700 sortModel.clear(); 701 setCurrentPage(1); 702 } 703 704 /** 705 * Event handler for inplaceupdate event triggered from nested components when an Ajax update occurs. The event 706 * context will carry the zone, which is recorded here, to allow the Grid and its sub-components to properly 707 * re-render themselves. Invokes 708 * {@link org.apache.tapestry5.services.ComponentEventResultProcessor#processResultValue(Object)} passing this (the 709 * Grid component) as the content provider for the update. 710 */ 711 void onInPlaceUpdate() throws IOException 712 { 713 this.zone = "^"; 714 715 componentEventResultProcessor.processResultValue(this); 716 } 717 718 public String getClientId() 719 { 720 return table.getClientId(); 721 } 722}