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