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