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.beaneditor.BeanModel; 018import org.apache.tapestry5.beaneditor.PropertyModel; 019import org.apache.tapestry5.corelib.data.GridPagerPosition; 020import org.apache.tapestry5.grid.*; 021import org.apache.tapestry5.internal.TapestryInternalUtils; 022import org.apache.tapestry5.internal.beaneditor.BeanModelUtils; 023import org.apache.tapestry5.internal.bindings.AbstractBinding; 024import org.apache.tapestry5.ioc.annotations.Inject; 025import org.apache.tapestry5.ioc.internal.util.InternalUtils; 026import org.apache.tapestry5.services.BeanModelSource; 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.beaneditor.BeanModel 054 * @see org.apache.tapestry5.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.beaneditor.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.beaneditor.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.beaneditor.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.ioc.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 if (paginationModel == null || paginationModel.getSortColumnId() == null) 422 { 423 return Collections.emptyList(); 424 } 425 426 PropertyModel sortModel = getDataModel().getById(paginationModel.getSortColumnId()); 427 428 SortConstraint constraint = new SortConstraint(sortModel, getColumnSort()); 429 430 return Collections.singletonList(constraint); 431 } 432 433 public void clear() 434 { 435 paginationModel.setSortColumnId(null); 436 paginationModel.setSortAscending(null); 437 } 438 } 439 440 GridSortModel defaultSortModel() 441 { 442 return new DefaultGridSortModel(); 443 } 444 445 /** 446 * Returns a {@link org.apache.tapestry5.Binding} instance that attempts to identify the model from the source 447 * parameter (via {@link org.apache.tapestry5.grid.GridDataSource#getRowType()}. Subclasses may override to provide 448 * a different mechanism. The returning binding is variant (not invariant). 449 * 450 * @see BeanModelSource#createDisplayModel(Class, org.apache.tapestry5.ioc.Messages) 451 */ 452 protected Binding defaultModel() 453 { 454 455 return new AbstractBinding() 456 { 457 public Object get() 458 { 459 // Get the default row type from the data source 460 461 GridDataSource gridDataSource = source; 462 463 Class rowType = gridDataSource.getRowType(); 464 465 if (renderTableIfEmpty || rowType == null) 466 throw new RuntimeException( 467 String.format( 468 "Unable to determine the bean type for rows from %s. You should bind the model parameter explicitly.", 469 gridDataSource)); 470 471 // Properties do not have to be read/write 472 473 return modelSource.createDisplayModel(rowType, overrides.getOverrideMessages()); 474 } 475 476 /** 477 * Returns false. This may be overkill, but it basically exists because the model is 478 * inherently mutable and therefore may contain client-specific state and needs to be 479 * discarded at the end of the request. If the model were immutable, then we could leave 480 * invariant as true. 481 */ 482 @Override 483 public boolean isInvariant() 484 { 485 return false; 486 } 487 }; 488 } 489 490 static final ComponentAction<Grid> SETUP_DATA_SOURCE = new ComponentAction<Grid>() 491 { 492 private static final long serialVersionUID = 8545187927995722789L; 493 494 public void execute(Grid component) 495 { 496 component.setupDataSource(); 497 } 498 499 @Override 500 public String toString() 501 { 502 return "Grid.SetupDataSource"; 503 } 504 }; 505 506 Object setupRender() 507 { 508 if (formSupport != null) 509 { 510 formSupport.store(this, SETUP_DATA_SOURCE); 511 } 512 513 setupDataSource(); 514 515 // If there's no rows, display the empty block placeholder. 516 517 return !renderTableIfEmpty && cachingSource.isEmpty() ? empty : null; 518 } 519 520 void cleanupRender() 521 { 522 // if an inPlace Grid is rendered inside a Loop, be sure to generate a new wrapper 523 // zone for each iteration (TAP5-2256) 524 zone = null; 525 526 // If grid is rendered inside a Loop. be sure to generate a new data model for 527 // each iteration (TAP5-2470) 528 dataModel = null; 529 } 530 531 public GridPaginationModel getDefaultPaginationModel() 532 { 533 if (defaultPaginationModel == null) 534 { 535 defaultPaginationModel = new GridPaginationModelImpl(); 536 } 537 538 return defaultPaginationModel; 539 } 540 541 void setupDataSource() 542 { 543 // TAP5-34: We pass the source into the CachingDataSource now; previously 544 // we were accessing source directly, but during submit the value wasn't 545 // cached, and therefore access was very inefficient, and sorting was 546 // very inconsistent during the processing of the form submission. 547 548 int effectiveCurrentPage = getCurrentPage(); 549 550 int numberOfRowsRequiredToShowCurrentPage = 1 + (effectiveCurrentPage - 1) * rowsPerPage; 551 int numberOfRowsRequiredToFillCurrentPage = effectiveCurrentPage * rowsPerPage; 552 553 cachingSource = new CachingDataSource(source); 554 if (pagerPosition != GridPagerPosition.NONE) 555 { 556 // We're going to render the pager, so we need to determine the total number of rows anyway. 557 // We do that eagerly here so we don't have to perform two count operations; the subsequent 558 // ones will return a cached result 559 cachingSource.getAvailableRows(); 560 } 561 int availableRowsWithLimit = cachingSource.getAvailableRows(numberOfRowsRequiredToFillCurrentPage); 562 563 if (availableRowsWithLimit == 0) 564 return; 565 566 // This captures when the number of rows has decreased, typically due to deletions. 567 568 if (numberOfRowsRequiredToShowCurrentPage > availableRowsWithLimit) 569 { 570 int maxPage = ((availableRowsWithLimit - 1) / rowsPerPage) + 1; 571 effectiveCurrentPage = maxPage; 572 } 573 574 int startIndex = (effectiveCurrentPage - 1) * rowsPerPage; 575 576 int endIndex = Math.min(startIndex + rowsPerPage - 1, availableRowsWithLimit - 1); 577 578 cachingSource.prepare(startIndex, endIndex, sortModel.getSortConstraints()); 579 } 580 581 Object beginRender(MarkupWriter writer) 582 { 583 // Skip rendering of component (template, body, etc.) when there's nothing to display. 584 // The empty placeholder will already have rendered. 585 586 if (cachingSource.isEmpty()) 587 return !renderTableIfEmpty ? false : null; 588 589 if (inPlace && zone == null) 590 { 591 javaScriptSupport.require("t5/core/zone"); 592 593 writer.element("div", "data-container-type", "zone"); 594 595 didRenderZoneDiv = true; 596 597 // Through Tapestry 5.3, we had a specific id for the zone that had to be passed down to the 598 // GridPager and etc. That's no longer necessary, so zone will always be null or "^". We don't 599 // even need any special ids to be allocated! 600 zone = "^"; 601 } 602 603 return null; 604 } 605 606 void afterRender(MarkupWriter writer) 607 { 608 if (didRenderZoneDiv) 609 { 610 writer.end(); // div 611 didRenderZoneDiv = false; 612 } 613 } 614 615 public BeanModel getDataModel() 616 { 617 if (dataModel == null) 618 { 619 dataModel = model; 620 621 BeanModelUtils.modify(dataModel, add, include, exclude, reorder); 622 } 623 624 return dataModel; 625 } 626 627 public int getNumberOfProperties() 628 { 629 return getDataModel().getPropertyNames().size(); 630 } 631 632 public GridDataSource getDataSource() 633 { 634 return cachingSource; 635 } 636 637 public GridSortModel getSortModel() 638 { 639 return sortModel; 640 } 641 642 public Object getPagerTop() 643 { 644 return pagerPosition.isMatchTop() ? pager : null; 645 } 646 647 public Object getPagerBottom() 648 { 649 return pagerPosition.isMatchBottom() ? pager : null; 650 } 651 652 public int getCurrentPage() 653 { 654 Integer currentPage = paginationModel.getCurrentPage(); 655 656 return currentPage == null ? 1 : currentPage; 657 } 658 659 public void setCurrentPage(int currentPage) 660 { 661 paginationModel.setCurrentPage(currentPage); 662 } 663 664 private boolean getSortAscending() 665 { 666 Boolean sortAscending = paginationModel.getSortAscending(); 667 668 return sortAscending != null && sortAscending.booleanValue(); 669 } 670 671 private void setSortAscending(boolean sortAscending) 672 { 673 paginationModel.setSortAscending(sortAscending); 674 } 675 676 public int getRowsPerPage() 677 { 678 return rowsPerPage; 679 } 680 681 public Object getRow() 682 { 683 return row; 684 } 685 686 public void setRow(Object row) 687 { 688 this.row = row; 689 } 690 691 /** 692 * Resets the Grid to inital settings; this sets the current page to one, and 693 * {@linkplain org.apache.tapestry5.grid.GridSortModel#clear() clears the sort model}. 694 */ 695 public void reset() 696 { 697 sortModel.clear(); 698 setCurrentPage(1); 699 } 700 701 /** 702 * Event handler for inplaceupdate event triggered from nested components when an Ajax update occurs. The event 703 * context will carry the zone, which is recorded here, to allow the Grid and its sub-components to properly 704 * re-render themselves. Invokes 705 * {@link org.apache.tapestry5.services.ComponentEventResultProcessor#processResultValue(Object)} passing this (the 706 * Grid component) as the content provider for the update. 707 */ 708 void onInPlaceUpdate() throws IOException 709 { 710 this.zone = "^"; 711 712 componentEventResultProcessor.processResultValue(this); 713 } 714 715 public String getClientId() 716 { 717 return table.getClientId(); 718 } 719}