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    }