    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.beaneditor.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 availableRows = dataSource.getAvailableRows();
    272
    273        int maxPages = ((availableRows - 1) / rowsPerPage) + 1;
    274
    275        // This can sometimes happen when the number of items shifts between requests.
    276
    277        if (currentPage > maxPages) currentPage = maxPages;
    278
    279        startRow = (currentPage - 1) * rowsPerPage;
    280        endRow = Math.min(availableRows - 1, startRow + rowsPerPage - 1);
    281
    282        dataRowIndex = startRow;
    283
    284        boolean recordingStateInsideForm = !volatileState && formSupport != null;
    285
    286        recordStateByIndex = recordingStateInsideForm && (encoder == null);
    287        recordStateByEncoder = recordingStateInsideForm && (encoder != null);
    288    }
    289
    290    /**
    291     * Callback method, used when recording state to a form, or called directly when not recording state.
    292     */
    293    void setupForRow(int rowIndex)
    294    {
    295        row = gridModel.getDataSource().getRowValue(rowIndex);
    296    }
    297
    298    /**
    299     * Callback method that bypasses the data source and converts a primary key back into a row value (via {@link
    300     * org.apache.tapestry5.ValueEncoder#toValue(String)}).
    301     */
    302    void setupForRowWithClientValue(String clientValue)
    303    {
    304        row = encoder.toValue(clientValue);
    305
    306        if (row == null)
    307            throw new IllegalArgumentException(
    308                    String.format("%s returned null for client value '%s'.", encoder, clientValue));
    309    }
    310
    311
    312    boolean beginRender()
    313    {
    314        // Setup for this row.
    315
    316        setupForRow(dataRowIndex);
    317
    318        // Update the index parameter (which starts from zero).
    319        rowIndex = dataRowIndex - startRow;
    320
    321
    322        if (row != null)
    323        {
    324            // When needed, store a callback used when the form is submitted.
    325
    326            if (recordStateByIndex)
    327                formSupport.store(this, new SetupForRowByIndex(dataRowIndex));
    328
    329            if (recordStateByEncoder)
    330            {
    331                String key = encoder.toClient(row);
    332                formSupport.store(this, new SetupForRowWithClientValue(key));
    333            }
    334        }
    335
    336        // If the row is null, it's because the rowIndex is too large (see the notes
    337        // on GridDataSource).  When row is null, return false to not render anything for this iteration
    338        // of the loop.
    339
    340        return row != null;
    341    }
    342
    343    boolean afterRender()
    344    {
    345        dataRowIndex++;
    346
    347        // Abort the loop when we hit a null row, or when we've exhausted the range we need to
    348        // display.
    349
    350        return row == null || dataRowIndex > endRow;
    351    }
    352
    353    public List<String> getPropertyNames()
    354    {
    355        return gridModel.getDataModel().getPropertyNames();
    356    }
    357
    358    public String getPropertyName()
    359    {
    360        return propertyName;
    361    }
    362
    363    public void setPropertyName(String propertyName)
    364    {
    365        this.propertyName = propertyName;
    366
    367        columnModel = gridModel.getDataModel().get(propertyName);
    368    }
    369}