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 }