| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package org.apache.tapestry5.corelib.components; |
| |
| import org.apache.tapestry5.*; |
| import org.apache.tapestry5.annotations.*; |
| import org.apache.tapestry5.beaneditor.BeanModel; |
| import org.apache.tapestry5.beaneditor.PropertyModel; |
| import org.apache.tapestry5.corelib.data.GridPagerPosition; |
| import org.apache.tapestry5.grid.*; |
| import org.apache.tapestry5.internal.TapestryInternalUtils; |
| import org.apache.tapestry5.internal.beaneditor.BeanModelUtils; |
| import org.apache.tapestry5.internal.bindings.AbstractBinding; |
| import org.apache.tapestry5.ioc.annotations.Inject; |
| import org.apache.tapestry5.ioc.internal.util.InternalUtils; |
| import org.apache.tapestry5.services.BeanModelSource; |
| import org.apache.tapestry5.services.ComponentDefaultProvider; |
| import org.apache.tapestry5.services.ComponentEventResultProcessor; |
| import org.apache.tapestry5.services.FormSupport; |
| import org.apache.tapestry5.services.javascript.JavaScriptSupport; |
| |
| import java.io.IOException; |
| import java.util.Collections; |
| import java.util.List; |
| |
| /** |
| * A grid presents tabular data. It is a composite component, created in terms of several sub-components. The |
| * sub-components are statically wired to the Grid, as it provides access to the data and other models that they need. |
| * <p/> |
| * A Grid may operate inside a {@link org.apache.tapestry5.corelib.components.Form}. By overriding the cell renderers of |
| * properties, the default output-only behavior can be changed to produce a complex form with individual control for |
| * editing properties of each row. There is a big caveat here: if the order of rows provided by |
| * the {@link org.apache.tapestry5.grid.GridDataSource} changes between render and form submission, then there's the |
| * possibility that data will be applied to the wrong server-side objects. |
| * <p/> |
| * For this reason, when using Grid and Form together, you should generally |
| * provide the Grid with a {@link org.apache.tapestry5.ValueEncoder} (via the |
| * encoder parameter), or use an entity type for the "row" parameter for which |
| * Tapestry can provide a ValueEncoder automatically. This will allow Tapestry |
| * to use a unique ID for each row that doesn't change when rows are reordered. |
| * |
| * @tapestrydoc |
| * @see org.apache.tapestry5.beaneditor.BeanModel |
| * @see org.apache.tapestry5.services.BeanModelSource |
| * @see org.apache.tapestry5.grid.GridDataSource |
| * @see BeanEditForm |
| * @see BeanDisplay |
| * @see Loop |
| */ |
| @SupportsInformalParameters |
| public class Grid implements GridModel, ClientElement |
| { |
| /** |
| * The source of data for the Grid to display. This will usually be a List or array but can also be an explicit |
| * {@link GridDataSource}. For Lists and object arrays, a GridDataSource is created automatically as a wrapper |
| * around the underlying List. |
| */ |
| @Parameter(required = true, autoconnect = true) |
| private GridDataSource source; |
| |
| /** |
| * A wrapper around the provided GridDataSource that caches access to the availableRows property. This is the source |
| * provided to sub-components. |
| */ |
| private GridDataSource cachingSource; |
| |
| /** |
| * The number of rows of data displayed on each page. If there are more rows than will fit, the Grid will divide up |
| * the rows into "pages" and (normally) provide a pager to allow the user to navigate within the overall result |
| * set. |
| */ |
| @Parameter(BindingConstants.SYMBOL + ":" + ComponentParameterConstants.GRID_ROWS_PER_PAGE) |
| private int rowsPerPage; |
| |
| /** |
| * Defines where the pager (used to navigate within the "pages" of results) should be displayed: "top", "bottom", |
| * "both" or "none". |
| */ |
| @Parameter(value = BindingConstants.SYMBOL + ":" + ComponentParameterConstants.GRID_PAGER_POSITION, |
| defaultPrefix = BindingConstants.LITERAL) |
| private GridPagerPosition pagerPosition; |
| |
| /** |
| * Used to store the current object being rendered (for the current row). This is used when parameter blocks are |
| * provided to override the default cell renderer for a particular column ... the components within the block can |
| * use the property bound to the row parameter to know what they should render. |
| */ |
| @Parameter(principal = true) |
| private Object row; |
| |
| /** |
| * Optional output parameter used to identify the index of the column being rendered. |
| */ |
| @Parameter |
| private int columnIndex; |
| |
| /** |
| * The model used to identify the properties to be presented and the order of presentation. The model may be |
| * omitted, in which case a default model is generated from the first object in the data source (this implies that |
| * the objects provided by the source are uniform). The model may be explicitly specified to override the default |
| * behavior, say to reorder or rename columns or add additional columns. The add, include, |
| * exclude and reorder |
| * parameters are <em>only</em> applied to a default model, not an explicitly provided one. |
| */ |
| @Parameter |
| private BeanModel model; |
| |
| /** |
| * The model parameter after modification due to the add, include, exclude and reorder parameters. |
| */ |
| private BeanModel dataModel; |
| |
| /** |
| * The model used to handle sorting of the Grid. This is generally not specified, and the built-in model supports |
| * only single column sorting. The sort constraints (the column that is sorted, and ascending vs. descending) is |
| * stored as persistent fields of the Grid component. |
| */ |
| @Parameter |
| private GridSortModel sortModel; |
| |
| /** |
| * A comma-separated list of property names to be added to the {@link org.apache.tapestry5.beaneditor.BeanModel}. |
| * Cells for added columns will be blank unless a cell override is provided. This parameter is only used |
| * when a default model is created automatically. |
| */ |
| @Parameter(defaultPrefix = BindingConstants.LITERAL) |
| private String add; |
| |
| /** |
| * A comma-separated list of property names to be retained from the |
| * {@link org.apache.tapestry5.beaneditor.BeanModel}. |
| * Only these properties will be retained, and the properties will also be reordered. The names are |
| * case-insensitive. This parameter is only used |
| * when a default model is created automatically. |
| */ |
| @SuppressWarnings("unused") |
| @Parameter(defaultPrefix = BindingConstants.LITERAL) |
| private String include; |
| |
| /** |
| * A comma-separated list of property names to be removed from the {@link org.apache.tapestry5.beaneditor.BeanModel} |
| * . |
| * The names are case-insensitive. This parameter is only used |
| * when a default model is created automatically. |
| */ |
| @Parameter(defaultPrefix = BindingConstants.LITERAL) |
| private String exclude; |
| |
| /** |
| * A comma-separated list of property names indicating the order in which the properties should be presented. The |
| * names are case insensitive. Any properties not indicated in the list will be appended to the end of the display |
| * order. This parameter is only used |
| * when a default model is created automatically. |
| */ |
| @Parameter(defaultPrefix = BindingConstants.LITERAL) |
| private String reorder; |
| |
| /** |
| * A Block to render instead of the table (and pager, etc.) when the source is empty. The default is simply the text |
| * "There is no data to display". This parameter is used to customize that message, possibly including components to |
| * allow the user to create new objects. |
| */ |
| //@Parameter(value = BindingConstants.SYMBOL + ":" + ComponentParameterConstants.GRID_EMPTY_BLOCK, |
| @Parameter(value = "block:empty", |
| defaultPrefix = BindingConstants.LITERAL) |
| private Block empty; |
| |
| /** |
| * CSS class for the <table> element. In addition, informal parameters to the Grid are rendered in the table |
| * element. |
| */ |
| @Parameter(name = "class", defaultPrefix = BindingConstants.LITERAL, |
| value = BindingConstants.SYMBOL + ":" + ComponentParameterConstants.GRID_TABLE_CSS_CLASS) |
| @Property(write = false) |
| private String tableClass; |
| |
| /** |
| * If true, then the Grid will be wrapped in an element that acts like a |
| * {@link org.apache.tapestry5.corelib.components.Zone}; all the paging and sorting links will refresh the zone, |
| * repainting the entire grid within it, but leaving the rest of the page (outside the zone) unchanged. |
| */ |
| @Parameter |
| private boolean inPlace; |
| |
| /** |
| * If true, then the Grid will also render a table element complete with headers if the data source is empty. |
| * If set to true, a model parameter will have to be specified. A default model for a specific class can be |
| * created using {@link BeanModelSource#createDisplayModel(Class, org.apache.tapestry5.ioc.Messages)}. |
| */ |
| @Parameter |
| private boolean renderTableIfEmpty = false; |
| |
| |
| /** |
| * The name of the pseudo-zone that encloses the Grid. Starting in 5.4, this is always either |
| * null or "^" and is not really used the way it was in 5.3; instead it triggers the addition |
| * of a {@code data-inplace-grid-links} attribute in a div surrounding any links related to |
| * sorting or pagination. The rest is sorted out on the client. See module {@code t5/core/zone}. |
| */ |
| @Property(write = false) |
| private String zone; |
| |
| private boolean didRenderZoneDiv; |
| |
| |
| /** |
| * The pagination model for the Grid, which encapsulates current page, sort column id, |
| * and sort ascending/descending. If not bound, a persistent property of the Grid is used. |
| * When rendering the Grid in a loop, this should be bound in some way to keep successive instances |
| * of the Grid configured individually. |
| * |
| * @since 5.4 |
| */ |
| @Parameter(value = "defaultPaginationModel") |
| private GridPaginationModel paginationModel; |
| |
| @Property |
| @Persist |
| private GridPaginationModel defaultPaginationModel; |
| |
| @Inject |
| private ComponentResources resources; |
| |
| @Inject |
| private BeanModelSource modelSource; |
| |
| @Environmental |
| private JavaScriptSupport javaScriptSupport; |
| |
| @Component(parameters = |
| {"index=inherit:columnIndex", "lean=inherit:lean", "overrides=overrides", "zone=zone"}) |
| private GridColumns columns; |
| |
| @Component(parameters = |
| {"columnIndex=inherit:columnIndex", "rowsPerPage=rowsPerPage", "currentPage=currentPage", "row=row", |
| "overrides=overrides"}, publishParameters = "rowIndex,rowClass,volatile,encoder,lean") |
| private GridRows rows; |
| |
| @Component(parameters = |
| {"source=dataSource", "rowsPerPage=rowsPerPage", "currentPage=currentPage", "zone=zone"}) |
| private GridPager pager; |
| |
| @Component(parameters = "to=pagerTop") |
| private Delegate pagerTop; |
| |
| @Component(parameters = "to=pagerBottom") |
| private Delegate pagerBottom; |
| |
| @Component(parameters = "class=tableClass", inheritInformalParameters = true) |
| private Any table; |
| |
| @Environmental(false) |
| private FormSupport formSupport; |
| |
| /** |
| * Defines where block and label overrides are obtained from. By default, the Grid component provides block |
| * overrides (from its block parameters). |
| */ |
| @Parameter(value = "this", allowNull = false) |
| @Property(write = false) |
| private PropertyOverrides overrides; |
| |
| /** |
| * Set up via the traditional or Ajax component event request handler |
| */ |
| @Environmental |
| private ComponentEventResultProcessor componentEventResultProcessor; |
| |
| @Inject |
| private ComponentDefaultProvider defaultsProvider; |
| |
| ValueEncoder defaultEncoder() |
| { |
| return defaultsProvider.defaultValueEncoder("row", resources); |
| } |
| |
| /** |
| * A version of GridDataSource that caches the availableRows property. This addresses TAPESTRY-2245. |
| */ |
| static class CachingDataSource implements GridDataSource |
| { |
| private final GridDataSource delegate; |
| |
| private boolean availableRowsCached; |
| |
| private int availableRows; |
| |
| CachingDataSource(GridDataSource delegate) |
| { |
| this.delegate = delegate; |
| } |
| |
| public int getAvailableRows() |
| { |
| if (!availableRowsCached) |
| { |
| availableRows = delegate.getAvailableRows(); |
| availableRowsCached = true; |
| } |
| |
| return availableRows; |
| } |
| |
| public void prepare(int startIndex, int endIndex, List<SortConstraint> sortConstraints) |
| { |
| delegate.prepare(startIndex, endIndex, sortConstraints); |
| } |
| |
| public Object getRowValue(int index) |
| { |
| return delegate.getRowValue(index); |
| } |
| |
| public Class getRowType() |
| { |
| return delegate.getRowType(); |
| } |
| } |
| |
| /** |
| * Default implementation that only allows a single column to be the sort column, and stores the sort information as |
| * persistent fields of the Grid component. |
| */ |
| class DefaultGridSortModel implements GridSortModel |
| { |
| public ColumnSort getColumnSort(String columnId) |
| { |
| if (paginationModel == null || !TapestryInternalUtils.isEqual(columnId, paginationModel.getSortColumnId())) |
| { |
| return ColumnSort.UNSORTED; |
| } |
| |
| return getColumnSort(); |
| } |
| |
| private ColumnSort getColumnSort() |
| { |
| return getSortAscending() ? ColumnSort.ASCENDING : ColumnSort.DESCENDING; |
| } |
| |
| public void updateSort(String columnId) |
| { |
| assert InternalUtils.isNonBlank(columnId); |
| |
| setupPaginationModel(); |
| |
| if (columnId.equals(paginationModel.getSortColumnId())) |
| { |
| setSortAscending(!getSortAscending()); |
| return; |
| } |
| |
| paginationModel.setSortColumnId(columnId); |
| setSortAscending(true); |
| } |
| |
| public List<SortConstraint> getSortConstraints() |
| { |
| // In a few limited cases we may not have yet hit the SetupRender phase, and the model may be null. |
| if (paginationModel == null || paginationModel.getSortColumnId() == null) |
| { |
| return Collections.emptyList(); |
| } |
| |
| PropertyModel sortModel = getDataModel().getById(paginationModel.getSortColumnId()); |
| |
| SortConstraint constraint = new SortConstraint(sortModel, getColumnSort()); |
| |
| return Collections.singletonList(constraint); |
| } |
| |
| public void clear() |
| { |
| setupPaginationModel(); |
| paginationModel.setSortColumnId(null); |
| paginationModel.setSortAscending(null); |
| } |
| } |
| |
| GridSortModel defaultSortModel() |
| { |
| return new DefaultGridSortModel(); |
| } |
| |
| /** |
| * Returns a {@link org.apache.tapestry5.Binding} instance that attempts to identify the model from the source |
| * parameter (via {@link org.apache.tapestry5.grid.GridDataSource#getRowType()}. Subclasses may override to provide |
| * a different mechanism. The returning binding is variant (not invariant). |
| * |
| * @see BeanModelSource#createDisplayModel(Class, org.apache.tapestry5.ioc.Messages) |
| */ |
| protected Binding defaultModel() |
| { |
| |
| return new AbstractBinding() |
| { |
| public Object get() |
| { |
| // Get the default row type from the data source |
| |
| GridDataSource gridDataSource = source; |
| |
| Class rowType = gridDataSource.getRowType(); |
| |
| if (renderTableIfEmpty || rowType == null) |
| throw new RuntimeException( |
| String.format( |
| "Unable to determine the bean type for rows from %s. You should bind the model parameter explicitly.", |
| gridDataSource)); |
| |
| // Properties do not have to be read/write |
| |
| return modelSource.createDisplayModel(rowType, overrides.getOverrideMessages()); |
| } |
| |
| /** |
| * Returns false. This may be overkill, but it basically exists because the model is |
| * inherently mutable and therefore may contain client-specific state and needs to be |
| * discarded at the end of the request. If the model were immutable, then we could leave |
| * invariant as true. |
| */ |
| @Override |
| public boolean isInvariant() |
| { |
| return false; |
| } |
| }; |
| } |
| |
| static final ComponentAction<Grid> SETUP_DATA_SOURCE = new ComponentAction<Grid>() |
| { |
| private static final long serialVersionUID = 8545187927995722789L; |
| |
| public void execute(Grid component) |
| { |
| component.setupDataSource(); |
| } |
| |
| @Override |
| public String toString() |
| { |
| return "Grid.SetupDataSource"; |
| } |
| }; |
| |
| Object setupRender() |
| { |
| zone = null; |
| |
| setupPaginationModel(); |
| |
| if (formSupport != null) |
| { |
| formSupport.store(this, SETUP_DATA_SOURCE); |
| } |
| |
| setupDataSource(); |
| |
| // If there's no rows, display the empty block placeholder. |
| |
| return !renderTableIfEmpty && cachingSource.getAvailableRows() == 0 ? empty : null; |
| } |
| |
| private void setupPaginationModel() |
| { |
| if (paginationModel == null) |
| { |
| paginationModel = new GridPaginationModelImpl(); |
| } |
| } |
| |
| void setupDataSource() |
| { |
| // TAP5-34: We pass the source into the CachingDataSource now; previously |
| // we were accessing source directly, but during submit the value wasn't |
| // cached, and therefore access was very inefficient, and sorting was |
| // very inconsistent during the processing of the form submission. |
| |
| cachingSource = new CachingDataSource(source); |
| |
| int availableRows = cachingSource.getAvailableRows(); |
| |
| if (availableRows == 0) |
| return; |
| |
| int maxPage = ((availableRows - 1) / rowsPerPage) + 1; |
| |
| // This captures when the number of rows has decreased, typically due to deletions. |
| |
| int effectiveCurrentPage = getCurrentPage(); |
| |
| if (effectiveCurrentPage > maxPage) |
| effectiveCurrentPage = maxPage; |
| |
| int startIndex = (effectiveCurrentPage - 1) * rowsPerPage; |
| |
| int endIndex = Math.min(startIndex + rowsPerPage - 1, availableRows - 1); |
| |
| dataModel = null; |
| |
| cachingSource.prepare(startIndex, endIndex, sortModel.getSortConstraints()); |
| } |
| |
| Object beginRender(MarkupWriter writer) |
| { |
| // Skip rendering of component (template, body, etc.) when there's nothing to display. |
| // The empty placeholder will already have rendered. |
| |
| if (cachingSource.getAvailableRows() == 0) |
| return !renderTableIfEmpty ? false : null; |
| |
| if (inPlace && zone == null) |
| { |
| javaScriptSupport.require("t5/core/zone"); |
| |
| writer.element("div", "data-container-type", "zone"); |
| |
| didRenderZoneDiv = true; |
| |
| // Through Tapestry 5.3, we had a specific id for the zone that had to be passed down to the |
| // GridPager and etc. That's no longer necessary, so zone will always be null or "^". We don't |
| // even need any special ids to be allocated! |
| zone = "^"; |
| } |
| |
| return null; |
| } |
| |
| void afterRender(MarkupWriter writer) |
| { |
| if (didRenderZoneDiv) |
| { |
| writer.end(); // div |
| didRenderZoneDiv = false; |
| } |
| } |
| |
| public BeanModel getDataModel() |
| { |
| if (dataModel == null) |
| { |
| dataModel = model; |
| |
| BeanModelUtils.modify(dataModel, add, include, exclude, reorder); |
| } |
| |
| return dataModel; |
| } |
| |
| public int getNumberOfProperties() |
| { |
| return getDataModel().getPropertyNames().size(); |
| } |
| |
| public GridDataSource getDataSource() |
| { |
| return cachingSource; |
| } |
| |
| public GridSortModel getSortModel() |
| { |
| return sortModel; |
| } |
| |
| public Object getPagerTop() |
| { |
| return pagerPosition.isMatchTop() ? pager : null; |
| } |
| |
| public Object getPagerBottom() |
| { |
| return pagerPosition.isMatchBottom() ? pager : null; |
| } |
| |
| public int getCurrentPage() |
| { |
| Integer currentPage = paginationModel.getCurrentPage(); |
| |
| return currentPage == null ? 1 : currentPage; |
| } |
| |
| public void setCurrentPage(int currentPage) |
| { |
| paginationModel.setCurrentPage(currentPage); |
| } |
| |
| private boolean getSortAscending() |
| { |
| Boolean sortAscending = paginationModel.getSortAscending(); |
| |
| return sortAscending != null && sortAscending.booleanValue(); |
| } |
| |
| private void setSortAscending(boolean sortAscending) |
| { |
| paginationModel.setSortAscending(sortAscending); |
| } |
| |
| public int getRowsPerPage() |
| { |
| return rowsPerPage; |
| } |
| |
| public Object getRow() |
| { |
| return row; |
| } |
| |
| public void setRow(Object row) |
| { |
| this.row = row; |
| } |
| |
| /** |
| * Resets the Grid to inital settings; this sets the current page to one, and |
| * {@linkplain org.apache.tapestry5.grid.GridSortModel#clear() clears the sort model}. |
| */ |
| public void reset() |
| { |
| setCurrentPage(1); |
| sortModel.clear(); |
| } |
| |
| /** |
| * Event handler for inplaceupdate event triggered from nested components when an Ajax update occurs. The event |
| * context will carry the zone, which is recorded here, to allow the Grid and its sub-components to properly |
| * re-render themselves. Invokes |
| * {@link org.apache.tapestry5.services.ComponentEventResultProcessor#processResultValue(Object)} passing this (the |
| * Grid component) as the content provider for the update. |
| */ |
| void onInPlaceUpdate() throws IOException |
| { |
| this.zone = "^"; |
| |
| componentEventResultProcessor.processResultValue(this); |
| } |
| |
| public String getClientId() |
| { |
| return table.getClientId(); |
| } |
| } |