    001    // Copyright 2007, 2008, 2009, 2011 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
    028    package org.apache.tapestry5.corelib.components;
    029
    030    import org.apache.tapestry5.ComponentAction;
    031    import org.apache.tapestry5.PropertyOverrides;
    032    import org.apache.tapestry5.ValueEncoder;
    033    import org.apache.tapestry5.annotations.Environmental;
    034    import org.apache.tapestry5.annotations.Parameter;
    035    import org.apache.tapestry5.annotations.Property;
    036    import org.apache.tapestry5.beaneditor.PropertyModel;
    037    import org.apache.tapestry5.grid.GridConstants;
    038    import org.apache.tapestry5.grid.GridDataSource;
    039    import org.apache.tapestry5.grid.GridModel;
    040    import org.apache.tapestry5.internal.TapestryInternalUtils;
    041    import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
    042    import org.apache.tapestry5.services.FormSupport;
    043
    044    import java.util.List;
    045
    046    /**
    047     * Renders out a series of rows within the table.
    048     * <p/>
    049     * Inside a {@link Form}, a series of row index numbers are stored into the form
    050     * ( {@linkplain FormSupport#store(Object, ComponentAction) as
    051     * ComponentActions}). This can be a problem in situations where the data set
    052     * can shift between the form render and the form submission, with a risk of
    053     * applying changes to the wrong objects.
    054     * <p/>
    055     * For this reason, when using GridRows inside a Form, you should generally
    056     * provide a {@link org.apache.tapestry5.ValueEncoder} (via the encoder
    057     * parameter), or use an entity type for the "row" parameter for which
    058     * Tapestry can provide a ValueEncoder automatically. This will allow Tapestry
    059     * to use a unique ID for each row that doesn't change when rows are reordered.
    060     *
    061     * @tapestrydoc
    062     */
    063    @SuppressWarnings({ "unchecked" })
    064    public class GridRows
    065    {
    066        private int startRow;
    067
    068        private boolean recordStateByIndex;
    069
    070        private boolean recordStateByEncoder;
    071
    072        /**
    073         * This action is used when a {@link org.apache.tapestry5.ValueEncoder} is not provided.
    074         */
    075        static class SetupForRowByIndex implements ComponentAction<GridRows>
    076        {
    077            private static final long serialVersionUID = -3216282071752371975L;
    078
    079            private final int rowIndex;
    080
    081            public SetupForRowByIndex(int rowIndex)
    082            {
    083                this.rowIndex = rowIndex;
    084            }
    085
    086            public void execute(GridRows component)
    087            {
    088                component.setupForRow(rowIndex);
    089            }
    090
    091            @Override
    092            public String toString()
    093            {
    094                return String.format("GridRows.SetupForRowByIndex[%d]", rowIndex);
    095            }
    096        }
    097
    098        /**
    099         * This action is used when a {@link org.apache.tapestry5.ValueEncoder} is provided.
    100         */
    101        static class SetupForRowWithClientValue implements ComponentAction<GridRows>
    102        {
    103            private final String clientValue;
    104
    105            SetupForRowWithClientValue(String clientValue)
    106            {
    107                this.clientValue = clientValue;
    108            }
    109
    110            public void execute(GridRows component)
    111            {
    112                component.setupForRowWithClientValue(clientValue);
    113            }
    114
    115            @Override
    116            public String toString()
    117            {
    118                return String.format("GridRows.SetupForRowWithClientValue[%s]", clientValue);
    119            }
    120        }
    121
    122        /**
    123         * Parameter used to set the CSS class for each row (each &lt;tr&gt; element) within the &lt;tbody&gt;). This is not
    124         * cached, so it will be recomputed for each row.
    125         */
    126        @Parameter(cache = false)
    127        private String rowClass;
    128
    129        /**
    130         * Object that provides access to the bean and data models used to render the Grid.
    131         */
    132        @Parameter(value = "componentResources.container")
    133        private GridModel gridModel;
    134
    135        /**
    136         * Where to search for property override blocks.
    137         */
    138        @Parameter(required = true, allowNull = false)
    139        @Property
    140        private PropertyOverrides overrides;
    141
    142        /**
    143         * Number of rows displayed on each page. Long result sets are split across multiple pages.
    144         */
    145        @Parameter(required = true)
    146        private int rowsPerPage;
    147
    148        /**
    149         * The current page number within the available pages (indexed from 1).
    150         */
    151        @Parameter(required = true)
    152        private int currentPage;
    153
    154        /**
    155         * The current row being rendered, this is primarily an output parameter used to allow the Grid, and the Grid's
    156         * container, to know what object is being rendered.
    157         */
    158        @Parameter(required = true)
    159        @Property(write = false)
    160        private Object row;
    161
    162        /**
    163         * If true, then the CSS class on each &lt;TD&gt; cell will be omitted, which can reduce the amount of output from
    164         * the component overall by a considerable amount. Leave this as false, the default, when you are leveraging the CSS
    165         * to customize the look and feel of particular columns.
    166         */
    167        @Parameter
    168        private boolean lean;
    169
    170        /**
    171         * If true and the component is enclosed by a Form, then the normal state saving logic is turned off. Defaults to
    172         * false, enabling state saving logic within Forms. This can be set to false when form elements within the Grid are
    173         * not related to the current row of the grid, or where another component (such as {@link
    174         * org.apache.tapestry5.corelib.components.Hidden}) is used to maintain row state.
    175         */
    176        @Parameter(name = "volatile")
    177        private boolean volatileState;
    178
    179        /**
    180         * A ValueEncoder used to convert server-side objects (provided by the
    181         * "row" parameter) into unique client-side strings (typically IDs) and
    182         * back. In general, when using Grid and Form together, you should either
    183         * provide the encoder parameter or use a "row" type for which Tapestry is
    184         * configured to provide a ValueEncoder automatically. Otherwise Tapestry
    185         * must fall back to using the plain index of each row, rather
    186         * than the ValueEncoder-provided unique ID, for recording state into the
    187         * form.
    188         */
    189        @Parameter
    190        private ValueEncoder encoder;
    191
    192
    193        /**
    194         * Optional output parameter (only set during rendering) that identifies the current row index. This is the index on
    195         * the page (i.e., always numbered from zero) as opposed to the row index inside the {@link
    196         * org.apache.tapestry5.grid.GridDataSource}.
    197         */
    198        @Parameter
    199        private int rowIndex;
    200
    201        /**
    202         * Optional output parameter that stores the current column index.
    203         */
    204        @Parameter
    205        @Property
    206        private int columnIndex;
    207
    208        @Environmental(false)
    209        private FormSupport formSupport;
    210
    211
    212        private int endRow;
    213
    214        /**
    215         * Index into the {@link org.apache.tapestry5.grid.GridDataSource}.
    216         */
    217        private int dataRowIndex;
    218
    219        private String propertyName;
    220
    221        @Property(write = false)
    222        private PropertyModel columnModel;
    223
    224        public String getRowClass()
    225        {
    226            List<String> classes = CollectionFactory.newList();
    227
    228            // Not a cached parameter, so careful to only access it once.
    229
    230            String rc = rowClass;
    231
    232            if (rc != null) classes.add(rc);
    233
    234            if (dataRowIndex == startRow) classes.add(GridConstants.FIRST_CLASS);
    235
    236            if (dataRowIndex == endRow) classes.add(GridConstants.LAST_CLASS);
    237
    238            return TapestryInternalUtils.toClassAttributeValue(classes);
    239        }
    240
    241        public String getCellClass()
    242        {
    243            List<String> classes = CollectionFactory.newList();
    244
    245            String id = gridModel.getDataModel().get(propertyName).getId();
    246
    247            if (!lean)
    248            {
    249                classes.add(id);
    250
    251                switch (gridModel.getSortModel().getColumnSort(id))
    252                {
    253                    case ASCENDING:
    254                        classes.add(GridConstants.SORT_ASCENDING_CLASS);
    255                        break;
    256
    257                    case DESCENDING:
    258                        classes.add(GridConstants.SORT_DESCENDING_CLASS);
    259                        break;
    260
    261                    default:
    262                }
    263            }
    264
    265
    266            return TapestryInternalUtils.toClassAttributeValue(classes);
    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    }