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