1 // Copyright 2007, 2008 The Apache Software Foundation 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package org.apache.tapestry5.corelib.components; 16 17 import org.apache.tapestry5; 18 import org.apache.tapestry5.annotations; 19 import org.apache.tapestry5.beaneditor.BeanModel; 20 import org.apache.tapestry5.beaneditor.PropertyModel; 21 import org.apache.tapestry5.corelib.data.GridPagerPosition; 22 import org.apache.tapestry5.grid; 23 import org.apache.tapestry5.internal.TapestryInternalUtils; 24 import org.apache.tapestry5.internal.beaneditor.BeanModelUtils; 25 import org.apache.tapestry5.internal.bindings.AbstractBinding; 26 import org.apache.tapestry5.internal.services.ClientBehaviorSupport; 27 import org.apache.tapestry5.ioc.annotations.Inject; 28 import org.apache.tapestry5.ioc.internal.util.Defense; 29 import org.apache.tapestry5.services.BeanModelSource; 30 import org.apache.tapestry5.services.ComponentEventResultProcessor; 31 import org.apache.tapestry5.services.FormSupport; 32 import org.apache.tapestry5.services.Request; 33 34 import java.io.IOException; 35 import java.util.Collections; 36 import java.util.List; 37 38 /** 39 * A grid presents tabular data. It is a composite component, created in terms of several sub-components. The 40 * sub-components are statically wired to the Grid, as it provides access to the data and other models that they need. 41 * <p/> 42 * A Grid may operate inside a {@link org.apache.tapestry5.corelib.components.Form}. By overriding the cell renderers of 43 * properties, the default output-only behavior can be changed to produce a complex form with individual control for 44 * editing properties of each row. This is currently workable but less than ideal -- if the order of rows provided by 45 * the {@link org.apache.tapestry5.grid.GridDataSource} changes between render and form submission, then there's the 46 * possibility that data will be applied to the wrong server-side objects. In general, when using Grid and Form 47 * together, you want to provide the Grid with a {@link org.apache.tapestry5.PrimaryKeyEncoder} (via the encoder 48 * parameter). 49 * 50 * @see org.apache.tapestry5.beaneditor.BeanModel 51 * @see org.apache.tapestry5.services.BeanModelSource 52 * @see org.apache.tapestry5.grid.GridDataSource 53 */ 54 @SupportsInformalParameters 55 public class Grid implements GridModel 56 { 57 /** 58 * The source of data for the Grid to display. This will usually be a List or array but can also be an explicit 59 * {@link GridDataSource}. For Lists and object arrays, a GridDataSource is created automatically as a wrapper 60 * around the underlying List. 61 */ 62 @Parameter(required = true, autoconnect = true) 63 private GridDataSource source; 64 65 /** 66 * A wrapper around the provided GridDataSource that caches access to the availableRows property. This is the source 67 * provided to sub-components. 68 */ 69 private GridDataSource cachingSource; 70 71 /** 72 * The number of rows of data displayed on each page. If there are more rows than will fit, the Grid will divide up 73 * the rows into "pages" and (normally) provide a pager to allow the user to navigate within the overall result 74 * set. 75 */ 76 @Parameter("25") 77 private int rowsPerPage; 78 79 /** 80 * Defines where the pager (used to navigate within the "pages" of results) should be displayed: "top", "bottom", 81 * "both" or "none". 82 */ 83 @Parameter(value = "top", defaultPrefix = BindingConstants.LITERAL) 84 private GridPagerPosition pagerPosition; 85 86 /** 87 * Used to store the current object being rendered (for the current row). This is used when parameter blocks are 88 * provided to override the default cell renderer for a particular column ... the components within the block can 89 * use the property bound to the row parameter to know what they should render. 90 */ 91 @Parameter 92 private Object row; 93 94 /** 95 * Optional output parameter used to identify the index (from zero) of the row being rendered. 96 */ 97 @Parameter 98 private int rowIndex; 99 100 /** 101 * Optional output parmater used to identify the index of the column being rendered. 102 */ 103 @Parameter 104 private int columnIndex; 105 106 /** 107 * The model used to identify the properties to be presented and the order of presentation. The model may be 108 * omitted, in which case a default model is generated from the first object in the data source (this implies that 109 * the objects provided by the source are uniform). The model may be explicitly specified to override the default 110 * behavior, say to reorder or rename columns or add additional columns. 111 */ 112 @Parameter 113 private BeanModel model; 114 115 /** 116 * The model parameter after modification due to the add, include, exclude and reorder parameters. 117 */ 118 private BeanModel dataModel; 119 120 /** 121 * The model used to handle sorting of the Grid. This is generally not specified, and the built-in model supports 122 * only single column sorting. The sort constraints (the column that is sorted, and ascending vs. descending) is 123 * stored as persistent fields of the Grid component. 124 */ 125 @Parameter 126 private GridSortModel sortModel; 127 128 /** 129 * A comma-seperated list of property names to be added to the {@link org.apache.tapestry5.beaneditor.BeanModel}. 130 * Cells for added columns will be blank unless a cell override is provided. 131 */ 132 @Parameter(defaultPrefix = BindingConstants.LITERAL) 133 private String add; 134 135 /** 136 * A comma-separated list of property names to be retained from the {@link org.apache.tapestry5.beaneditor.BeanModel}. 137 * Only these properties will be retained, and the properties will also be reordered. The names are 138 * case-insensitive. 139 */ 140 @SuppressWarnings("unused") 141 @Parameter(defaultPrefix = BindingConstants.LITERAL) 142 private String include; 143 144 /** 145 * A comma-separated list of property names to be removed from the {@link org.apache.tapestry5.beaneditor.BeanModel}. 146 * The names are case-insensitive. 147 */ 148 @Parameter(defaultPrefix = BindingConstants.LITERAL) 149 private String exclude; 150 151 /** 152 * A comma-separated list of property names indicating the order in which the properties should be presented. The 153 * names are case insensitive. Any properties not indicated in the list will be appended to the end of the display 154 * order. 155 */ 156 @Parameter(defaultPrefix = BindingConstants.LITERAL) 157 private String reorder; 158 159 /** 160 * A Block to render instead of the table (and pager, etc.) when the source is empty. The default is simply the text 161 * "There is no data to display". This parameter is used to customize that message, possibly including components to 162 * allow the user to create new objects. 163 */ 164 @Parameter(value = "block:empty", defaultPrefix = BindingConstants.LITERAL) 165 private Block empty; 166 167 168 /** 169 * If true, then the CSS class on each <TD> and <TH> cell will be omitted, which can reduce the amount 170 * of output from the component overall by a considerable amount. Leave this as false, the default, when you are 171 * leveraging the CSS to customize the look and feel of particular columns. 172 */ 173 @Parameter 174 private boolean lean; 175 176 /** 177 * If true and the Grid is enclosed by a Form, then the normal state persisting logic is turned off. Defaults to 178 * false, enabling state persisting within Forms. If a Grid is present for some reason within a Form, but does not 179 * contain any form control components (such as {@link TextField}), then binding volatile to false will reduce the 180 * amount of client-side state that must be persisted. 181 */ 182 @Parameter(name = "volatile") 183 private boolean volatileState; 184 185 /** 186 * The CSS class for the tr element for each data row. This can be used to highlight particular rows, or cycle 187 * between CSS values (for the "zebra effect"). If null or not bound, then no particular CSS class value is used. 188 */ 189 @Parameter(cache = false) 190 private String rowClass; 191 192 /** 193 * CSS class for the <table> element. In addition, informal parameters to the Grid are rendered in the table 194 * element. 195 */ 196 @Parameter(name = "class", defaultPrefix = BindingConstants.LITERAL, value = "t-data-grid") 197 @Property(write = false) 198 private String tableClass; 199 200 /** 201 * If true, then the Grid will be wrapped in an element that acts like a {@link 202 * org.apache.tapestry5.corelib.components.Zone}; all the paging and sorting links will refresh the zone, 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 * Changes how state is recorded into the form to store the {@linkplain org.apache.tapestry5.PrimaryKeyEncoder#toKey(Object) 210 * primary key} for each row (rather than the index), and restore the {@linkplain 211 * org.apache.tapestry5.PrimaryKeyEncoder#toValue(java.io.Serializable) row values} from the primary keys. 212 */ 213 @Parameter 214 private PrimaryKeyEncoder encoder; 215 216 /** 217 * The name of the psuedo-zone that encloses the Grid. 218 */ 219 @Property(write = false) 220 private String zone; 221 222 private boolean didRenderZoneDiv; 223 224 @Persist 225 private Integer currentPage; 226 227 @Persist 228 private String sortColumnId; 229 230 @Persist 231 private Boolean sortAscending; 232 233 234 @Inject 235 private ComponentResources resources; 236 237 @Inject 238 private BeanModelSource modelSource; 239 240 @Environmental 241 private ClientBehaviorSupport clientBehaviorSupport; 242 243 @Component( 244 parameters = { 245 "index=inherit:columnIndex", 246 "lean=inherit:lean", 247 "overrides=overrides", 248 "zone=zone"}) 249 private GridColumns columns; 250 251 @Component( 252 parameters = { 253 "rowIndex=inherit:rowIndex", 254 "columnIndex=inherit:columnIndex", 255 "rowClass=inherit:rowClass", 256 "rowsPerPage=rowsPerPage", 257 "currentPage=currentPage", 258 "row=row", 259 "overrides=overrides", 260 "volatile=inherit:volatile", 261 "encoder=inherit:encoder", 262 "lean=inherit:lean"}) 263 private GridRows rows; 264 265 @Component(parameters = { 266 "source=dataSource", 267 "rowsPerPage=rowsPerPage", 268 "currentPage=currentPage", 269 "zone=zone"}) 270 private GridPager pager; 271 272 @Component(parameters = "to=pagerTop") 273 private Delegate pagerTop; 274 275 @Component(parameters = "to=pagerBottom") 276 private Delegate pagerBottom; 277 278 @Component(parameters = "class=tableClass", inheritInformalParameters = true) 279 private Any table; 280 281 @Environmental(false) 282 private FormSupport formSupport; 283 284 @Inject 285 private Request request; 286 287 @Environmental 288 private RenderSupport renderSupport; 289 290 /** 291 * Defines where block and label overrides are obtained from. By default, the Grid component provides block 292 * overrides (from its block parameters). 293 */ 294 @Parameter(value = "this", allowNull = false) 295 @Property(write = false) 296 private PropertyOverrides overrides; 297 298 /** 299 * Set up via the traditional or Ajax component event request handler 300 */ 301 @Environmental 302 private ComponentEventResultProcessor componentEventResultProcessor; 303 304 /** 305 * A version of GridDataSource that caches the availableRows property. This addresses TAPESTRY-2245. 306 */ 307 static class CachingDataSource implements GridDataSource 308 { 309 private final GridDataSource delegate; 310 311 private boolean availableRowsCached; 312 313 private int availableRows; 314 315 CachingDataSource(GridDataSource delegate) 316 { 317 this.delegate = delegate; 318 } 319 320 public int getAvailableRows() 321 { 322 if (!availableRowsCached) 323 { 324 availableRows = delegate.getAvailableRows(); 325 availableRowsCached = true; 326 } 327 328 return availableRows; 329 } 330 331 public void prepare(int startIndex, int endIndex, List<SortConstraint> sortConstraints) 332 { 333 delegate.prepare(startIndex, endIndex, sortConstraints); 334 } 335 336 public Object getRowValue(int index) 337 { 338 return delegate.getRowValue(index); 339 } 340 341 public Class getRowType() 342 { 343 return delegate.getRowType(); 344 } 345 } 346 347 /** 348 * Default implementation that only allows a single column to be the sort column, and stores the sort information as 349 * persistent fields of the Grid component. 350 */ 351 class DefaultGridSortModel implements GridSortModel 352 { 353 public ColumnSort getColumnSort(String columnId) 354 { 355 if (!TapestryInternalUtils.isEqual(columnId, sortColumnId)) 356 return ColumnSort.UNSORTED; 357 358 return getColumnSort(); 359 } 360 361 private ColumnSort getColumnSort() 362 { 363 return getSortAscending() ? ColumnSort.ASCENDING : ColumnSort.DESCENDING; 364 } 365 366 367 public void updateSort(String columnId) 368 { 369 Defense.notBlank(columnId, "columnId"); 370 371 if (columnId.equals(sortColumnId)) 372 { 373 setSortAscending(!getSortAscending()); 374 return; 375 } 376 377 sortColumnId = columnId; 378 setSortAscending(true); 379 } 380 381 public List<SortConstraint> getSortConstraints() 382 { 383 if (sortColumnId == null) 384 return Collections.emptyList(); 385 386 PropertyModel sortModel = getDataModel().getById(sortColumnId); 387 388 SortConstraint constraint = new SortConstraint(sortModel, getColumnSort()); 389 390 return Collections.singletonList(constraint); 391 } 392 393 public void clear() 394 { 395 sortColumnId = null; 396 } 397 } 398 399 GridSortModel defaultSortModel() 400 { 401 return new DefaultGridSortModel(); 402 } 403 404 /** 405 * Returns a {@link org.apache.tapestry5.Binding} instance that attempts to identify the model from the source 406 * parameter (via {@link org.apache.tapestry5.grid.GridDataSource#getRowType()}. Subclasses may override to provide 407 * a different mechanism. The returning binding is variant (not invariant). 408 * 409 * @see BeanModelSource#createDisplayModel(Class, org.apache.tapestry5.ioc.Messages) 410 */ 411 protected Binding defaultModel() 412 { 413 return new AbstractBinding() 414 { 415 public Object get() 416 { 417 // Get the default row type from the data source 418 419 GridDataSource gridDataSource = source; 420 421 Class rowType = gridDataSource.getRowType(); 422 423 if (rowType == null) 424 throw new RuntimeException( 425 String.format( 426 "Unable to determine the bean type for rows from %s. You should bind the model parameter explicitly.", 427 gridDataSource)); 428 429 // Properties do not have to be read/write 430 431 return modelSource.createDisplayModel(rowType, overrides.getOverrideMessages()); 432 } 433 434 /** 435 * Returns false. This may be overkill, but it basically exists because the model is 436 * inherently mutable and therefore may contain client-specific state and needs to be 437 * discarded at the end of the request. If the model were immutable, then we could leave 438 * invariant as true. 439 */ 440 @Override 441 public boolean isInvariant() 442 { 443 return false; 444 } 445 }; 446 } 447 448 static final ComponentAction<Grid> SETUP_DATA_SOURCE = new ComponentAction<Grid>() 449 { 450 private static final long serialVersionUID = 8545187927995722789L; 451 452 public void execute(Grid component) 453 { 454 component.setupDataSource(); 455 } 456 457 @Override 458 public String toString() 459 { 460 return "Grid.SetupDataSource"; 461 } 462 }; 463 464 Object setupRender() 465 { 466 if (formSupport != null) formSupport.store(this, SETUP_DATA_SOURCE); 467 468 setupDataSource(); 469 470 // If there's no rows, display the empty block placeholder. 471 472 return cachingSource.getAvailableRows() == 0 ? empty : null; 473 } 474 475 void setupDataSource() 476 { 477 // TAP5-34: We pass the source into the CachingDataSource now; previously 478 // we were accessing source directly, but during submit the value wasn't 479 // cached, and therefore access was very inefficient, and sorting was 480 // very inconsistent during the processing of the form submission. 481 482 cachingSource = new CachingDataSource(source); 483 484 int availableRows = cachingSource.getAvailableRows(); 485 486 if (availableRows == 0) return; 487 488 int maxPage = ((availableRows - 1) / rowsPerPage) + 1; 489 490 // This captures when the number of rows has decreased, typically due to deletions. 491 492 int effectiveCurrentPage = getCurrentPage(); 493 494 if (effectiveCurrentPage > maxPage) 495 effectiveCurrentPage = maxPage; 496 497 int startIndex = (effectiveCurrentPage - 1) * rowsPerPage; 498 499 int endIndex = Math.min(startIndex + rowsPerPage - 1, availableRows - 1); 500 501 dataModel = null; 502 503 cachingSource.prepare(startIndex, endIndex, sortModel.getSortConstraints()); 504 } 505 506 Object beginRender(MarkupWriter writer) 507 { 508 // Skip rendering of component (template, body, etc.) when there's nothing to display. 509 // The empty placeholder will already have rendered. 510 511 if (cachingSource.getAvailableRows() == 0) return false; 512 513 if (inPlace && zone == null) 514 { 515 zone = renderSupport.allocateClientId(resources); 516 517 writer.element("div", "id", zone); 518 519 clientBehaviorSupport.addZone(zone, null, "show"); 520 521 didRenderZoneDiv = true; 522 } 523 524 return null; 525 } 526 527 void afterRender(MarkupWriter writer) 528 { 529 if (didRenderZoneDiv) 530 { 531 writer.end(); // div 532 didRenderZoneDiv = false; 533 } 534 } 535 536 public BeanModel getDataModel() 537 { 538 if (dataModel == null) 539 { 540 dataModel = model; 541 542 BeanModelUtils.modify(dataModel, add, include, exclude, reorder); 543 } 544 545 return dataModel; 546 } 547 548 public GridDataSource getDataSource() 549 { 550 return cachingSource; 551 } 552 553 public GridSortModel getSortModel() 554 { 555 return sortModel; 556 } 557 558 public Object getPagerTop() 559 { 560 return pagerPosition.isMatchTop() ? pager : null; 561 } 562 563 public Object getPagerBottom() 564 { 565 return pagerPosition.isMatchBottom() ? pager : null; 566 } 567 568 public int getCurrentPage() 569 { 570 return currentPage == null ? 1 : currentPage; 571 } 572 573 public void setCurrentPage(int currentPage) 574 { 575 this.currentPage = currentPage; 576 } 577 578 private boolean getSortAscending() 579 { 580 return sortAscending != null && sortAscending.booleanValue(); 581 } 582 583 private void setSortAscending(boolean sortAscending) 584 { 585 this.sortAscending = sortAscending; 586 } 587 588 public int getRowsPerPage() 589 { 590 return rowsPerPage; 591 } 592 593 public Object getRow() 594 { 595 return row; 596 } 597 598 public void setRow(Object row) 599 { 600 this.row = row; 601 } 602 603 /** 604 * Resets the Grid to inital settings; this sets the current page to one, and {@linkplain 605 * org.apache.tapestry5.grid.GridSortModel#clear() clears the sort model}. 606 */ 607 public void reset() 608 { 609 setCurrentPage(1); 610 sortModel.clear(); 611 } 612 613 /** 614 * Event handler for inplaceupdate event triggered from nested components when an Ajax update occurs. The event 615 * context will carry the zone, which is recorded here, to allow the Grid and its sub-components to properly 616 * re-render themselves. Invokes {@link org.apache.tapestry5.services.ComponentEventResultProcessor#processResultValue(Object)} 617 * passing this (the Grid component) as the content provider for the update. 618 */ 619 void onInPlaceUpdate(String zone) throws IOException 620 { 621 this.zone = zone; 622 623 componentEventResultProcessor.processResultValue(this); 624 } 625 }