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