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