    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// Copyright 2007, 2008 The Apache Software Foundation
    013//
    014// Licensed under the Apache License, Version 2.0 (the "License");
    015// you may not use this file except in compliance with the License.
    016// You may obtain a copy of the License at
    017//
    018//     http://www.apache.org/licenses/LICENSE-2.0
    019//
    020// Unless required by applicable law or agreed to in writing, software
    021// distributed under the License is distributed on an "AS IS" BASIS,
    022// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    023// See the License for the specific language governing permissions and
    024// limitations under the License.
    025
    026package org.apache.tapestry5.corelib.components;
    027
    028import org.apache.tapestry5.ComponentAction;
    029import org.apache.tapestry5.MarkupWriter;
    030import org.apache.tapestry5.PropertyOverrides;
    031import org.apache.tapestry5.ValueEncoder;
    032import org.apache.tapestry5.annotations.Environmental;
    033import org.apache.tapestry5.annotations.Parameter;
    034import org.apache.tapestry5.annotations.Property;
    035import org.apache.tapestry5.beanmodel.PropertyModel;
    036import org.apache.tapestry5.grid.GridDataSource;
    037import org.apache.tapestry5.grid.GridModel;
    038import org.apache.tapestry5.services.FormSupport;
    039
    040import java.util.List;
    041
    042/**
    043 * Renders out a series of rows within the table.
    044 *
    045 * Inside a {@link Form}, a series of row index numbers are stored into the form
    046 * ( {@linkplain FormSupport#store(Object, ComponentAction) as
    047 * ComponentActions}). This can be a problem in situations where the data set
    048 * can shift between the form render and the form submission, with a risk of
    049 * applying changes to the wrong objects.
    050 *
    051 * For this reason, when using GridRows inside a Form, you should generally
    052 * provide a {@link org.apache.tapestry5.ValueEncoder} (via the encoder
    053 * parameter), or use an entity type for the "row" parameter for which
    054 * Tapestry can provide a ValueEncoder automatically. This will allow Tapestry
    055 * to use a unique ID for each row that doesn't change when rows are reordered.
    056 *
    057 * @tapestrydoc
    058 */
    059@SuppressWarnings({"unchecked"})
    060public class GridRows
    061{
    062    private int startRow;
    063
    064    private boolean recordStateByIndex;
    065
    066    private boolean recordStateByEncoder;
    067
    068    /**
    069     * This action is used when a {@link org.apache.tapestry5.ValueEncoder} is not provided.
    070     */
    071    static class SetupForRowByIndex implements ComponentAction<GridRows>
    072    {
    073        private static final long serialVersionUID = -3216282071752371975L;
    074
    075        private final int rowIndex;
    076
    077        public SetupForRowByIndex(int rowIndex)
    078        {
    079            this.rowIndex = rowIndex;
    080        }
    081
    082        public void execute(GridRows component)
    083        {
    084            component.setupForRow(rowIndex);
    085        }
    086
    087        @Override
    088        public String toString()
    089        {
    090            return String.format("GridRows.SetupForRowByIndex[%d]", rowIndex);
    091        }
    092    }
    093
    094    /**
    095     * This action is used when a {@link org.apache.tapestry5.ValueEncoder} is provided.
    096     */
    097    static class SetupForRowWithClientValue implements ComponentAction<GridRows>
    098    {
    099        private final String clientValue;
    100
    101        SetupForRowWithClientValue(String clientValue)
    102        {
    103            this.clientValue = clientValue;
    104        }
    105
    106        public void execute(GridRows component)
    107        {
    108            component.setupForRowWithClientValue(clientValue);
    109        }
    110
    111        @Override
    112        public String toString()
    113        {
    114            return String.format("GridRows.SetupForRowWithClientValue[%s]", clientValue);
    115        }
    116    }
    117
    118    /**
    119     * Parameter used to set the CSS class for each row (each &lt;tr&gt; element) within the &lt;tbody&gt;). This is not
    120     * cached, so it will be recomputed for each row.
    121     */
    122    @Parameter(cache = false)
    123    private String rowClass;
    124
    125    /**
    126     * Object that provides access to the bean and data models used to render the Grid.
    127     */
    128    @Parameter(value = "componentResources.container")
    129    private GridModel gridModel;
    130
    131    /**
    132     * Where to search for property override blocks.
    133     */
    134    @Parameter(required = true, allowNull = false)
    135    @Property
    136    private PropertyOverrides overrides;
    137
    138    /**
    139     * Number of rows displayed on each page. Long result sets are split across multiple pages.
    140     */
    141    @Parameter(required = true)
    142    private int rowsPerPage;
    143
    144    /**
    145     * The current page number within the available pages (indexed from 1).
    146     */
    147    @Parameter(required = true)
    148    private int currentPage;
    149
    150    /**
    151     * The current row being rendered, this is primarily an output parameter used to allow the Grid, and the Grid's
    152     * container, to know what object is being rendered.
    153     */
    154    @Parameter(required = true)
    155    @Property(write = false)
    156    private Object row;
    157
    158    /**
    159     * If true, then the CSS class on each &lt;TD&gt; cell will be omitted, which can reduce the amount of output from
    160     * the component overall by a considerable amount. Leave this as false, the default, when you are leveraging the CSS
    161     * to customize the look and feel of particular columns.
    162     */
    163    @Parameter
    164    private boolean lean;
    165
    166    /**
    167     * If true and the component is enclosed by a Form, then the normal state saving logic is turned off. Defaults to
    168     * false, enabling state saving logic within Forms. This can be set to false when form elements within the Grid are
    169     * not related to the current row of the grid, or where another component (such as {@link
    170     * org.apache.tapestry5.corelib.components.Hidden}) is used to maintain row state.
    171     */
    172    @Parameter(name = "volatile")
    173    private boolean volatileState;
    174
    175    /**
    176     * A ValueEncoder used to convert server-side objects (provided by the
    177     * "row" parameter) into unique client-side strings (typically IDs) and
    178     * back. In general, when using Grid and Form together, you should either
    179     * provide the encoder parameter or use a "row" type for which Tapestry is
    180     * configured to provide a ValueEncoder automatically. Otherwise Tapestry
    181     * must fall back to using the plain index of each row, rather
    182     * than the ValueEncoder-provided unique ID, for recording state into the
    183     * form.
    184     */
    185    @Parameter
    186    private ValueEncoder encoder;
    187
    188
    189    /**
    190     * Optional output parameter (only set during rendering) that identifies the current row index. This is the index on
    191     * the page (i.e., always numbered from zero) as opposed to the row index inside the {@link
    192     * org.apache.tapestry5.grid.GridDataSource}.
    193     */
    194    @Parameter
    195    private int rowIndex;
    196
    197    /**
    198     * Optional output parameter that stores the current column index.
    199     */
    200    @Parameter
    201    @Property
    202    private int columnIndex;
    203
    204    @Environmental(false)
    205    private FormSupport formSupport;
    206
    207
    208    private int endRow;
    209
    210    /**
    211     * Index into the {@link org.apache.tapestry5.grid.GridDataSource}.
    212     */
    213    private int dataRowIndex;
    214
    215    private String propertyName;
    216
    217    @Property(write = false)
    218    private PropertyModel columnModel;
    219
    220    void onBeginRenderFromRow(MarkupWriter writer)
    221    {
    222
    223        if (dataRowIndex == startRow)
    224        {
    225            writer.attributes("data-grid-row", "first");
    226        }
    227
    228        if (dataRowIndex == endRow)
    229        {
    230            writer.attributes("data-grid-row", "last");
    231        }
    232
    233        // Not a cached parameter, so careful to only access it once.
    234
    235        String rc = rowClass;
    236
    237        if (rc != null)
    238        {
    239            writer.attributes("class", rc);
    240        }
    241    }
    242
    243    void onBeginRenderFromColumn(MarkupWriter writer)
    244    {
    245
    246        String id = gridModel.getDataModel().get(propertyName).getId();
    247
    248        if (!lean)
    249        {
    250            writer.attributes("data-grid-property", id);
    251        }
    252
    253        switch (gridModel.getSortModel().getColumnSort(id))
    254        {
    255            case ASCENDING:
    256                writer.attributes("data-grid-column-sort", "ascending");
    257                break;
    258
    259            case DESCENDING:
    260                writer.attributes("data-grid-column-sort", "descending");
    261                break;
    262
    263            default:
    264        }
    265    }
    266
    267    void setupRender()
    268    {
    269        GridDataSource dataSource = gridModel.getDataSource();
    270
    271        int numberOfRowsRequiredToShowCurrentPage = 1 + (currentPage - 1) * rowsPerPage;
    272        int numberOfRowsRequiredToFillCurrentPage = currentPage * rowsPerPage;
    273
    274        int availableRowsWithLimit = dataSource.getAvailableRows(numberOfRowsRequiredToFillCurrentPage);
    275
    276        // This can sometimes happen when the number of items shifts between requests.
    277
    278        if (numberOfRowsRequiredToShowCurrentPage > availableRowsWithLimit)
    279        {
    280            int maxPages = ((availableRowsWithLimit - 1) / rowsPerPage) + 1;
    281            currentPage = maxPages;
    282        }
    283        startRow = (currentPage - 1) * rowsPerPage;
    284        endRow = Math.min(availableRowsWithLimit - 1, startRow + rowsPerPage - 1);
    285
    286        dataRowIndex = startRow;
    287
    288        boolean recordingStateInsideForm = !volatileState && formSupport != null;
    289
    290        recordStateByIndex = recordingStateInsideForm && (encoder == null);
    291        recordStateByEncoder = recordingStateInsideForm && (encoder != null);
    292    }
    293
    294    /**
    295     * Callback method, used when recording state to a form, or called directly when not recording state.
    296     */
    297    void setupForRow(int rowIndex)
    298    {
    299        row = gridModel.getDataSource().getRowValue(rowIndex);
    300    }
    301
    302    /**
    303     * Callback method that bypasses the data source and converts a primary key back into a row value (via {@link
    304     * org.apache.tapestry5.ValueEncoder#toValue(String)}).
    305     */
    306    void setupForRowWithClientValue(String clientValue)
    307    {
    308        row = encoder.toValue(clientValue);
    309
    310        if (row == null)
    311            throw new IllegalArgumentException(
    312                    String.format("%s returned null for client value '%s'.", encoder, clientValue));
    313    }
    314
    315
    316    boolean beginRender()
    317    {
    318        // Setup for this row.
    319
    320        setupForRow(dataRowIndex);
    321
    322        // Update the index parameter (which starts from zero).
    323        rowIndex = dataRowIndex - startRow;
    324
    325
    326        if (row != null)
    327        {
    328            // When needed, store a callback used when the form is submitted.
    329
    330            if (recordStateByIndex)
    331                formSupport.store(this, new SetupForRowByIndex(dataRowIndex));
    332
    333            if (recordStateByEncoder)
    334            {
    335                String key = encoder.toClient(row);
    336                formSupport.store(this, new SetupForRowWithClientValue(key));
    337            }
    338        }
    339
    340        // If the row is null, it's because the rowIndex is too large (see the notes
    341        // on GridDataSource).  When row is null, return false to not render anything for this iteration
    342        // of the loop.
    343
    344        return row != null;
    345    }
    346
    347    boolean afterRender()
    348    {
    349        dataRowIndex++;
    350
    351        // Abort the loop when we hit a null row, or when we've exhausted the range we need to
    352        // display.
    353
    354        return row == null || dataRowIndex > endRow;
    355    }
    356
    357    public List<String> getPropertyNames()
    358    {
    359        return gridModel.getDataModel().getPropertyNames();
    360    }
    361
    362    public String getPropertyName()
    363    {
    364        return propertyName;
    365    }
    366
    367    public void setPropertyName(String propertyName)
    368    {
    369        this.propertyName = propertyName;
    370
    371        columnModel = gridModel.getDataModel().get(propertyName);
    372    }
    373}