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 <tr> element) within the <tbody>). 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 <TD> 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 }