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 013package org.apache.tapestry5.corelib.components; 014 015import org.apache.tapestry5.*; 016import org.apache.tapestry5.annotations.*; 017import org.apache.tapestry5.commons.util.CollectionFactory; 018import org.apache.tapestry5.corelib.LoopFormState; 019import org.apache.tapestry5.ioc.annotations.Inject; 020import org.apache.tapestry5.services.ComponentDefaultProvider; 021import org.apache.tapestry5.services.FormSupport; 022import org.apache.tapestry5.services.Heartbeat; 023 024import java.util.Iterator; 025import java.util.List; 026 027/** 028 * A basic looping component; loops over a number of items (provided by its source parameter), rendering its body for each 029 * one. When a Loop is inside a {@link Form}, it records quite a bit of state into the Form to coordinate access 030 * to the same (or equivalent) objects during the form submission as during the render. This is controlled by 031 * the formState parameter (of type {@link LoopFormState}) and can be 'none' (nothing stored into the form), 'values' 032 * (which stores the individual values looped over, or via a {@link ValueEncoder}, just the value's ids), and 033 * 'iteration' (which just stores indexes to the values within the source parameter, which means that the source 034 * parameter will be accessed during the form submission). 035 * 036 * For a non-volatile Loop inside the form, the Loop stores a series of commands that start and end 037 * {@linkplain Heartbeat heartbeats}, and stores state for each value in the source parameter (either as full objects 038 * when the encoder parameter is not bound, or as client-side objects when there is an encoder). For a Loop that doesn't 039 * need to be aware of the enclosing Form (if any), the formState parameter should be bound to 'none'. 040 * 041 * When the Loop is used inside a Form, it will generate an 042 * {@link org.apache.tapestry5.EventConstants#SYNCHRONIZE_VALUES} event to inform its container what values were 043 * submitted and in what order; this can allow the container to pre-load the values in a single batch form external 044 * storage, if that is appropriate. 045 * 046 * @tapestrydoc 047 */ 048@SupportsInformalParameters 049@Events(EventConstants.SYNCHRONIZE_VALUES) 050public class Loop<T> 051{ 052 /** 053 * Setup command for non-volatile rendering. 054 */ 055 private static final ComponentAction<Loop> RESET_INDEX = new ComponentAction<Loop>() 056 { 057 private static final long serialVersionUID = 6477493424977597345L; 058 059 public void execute(Loop component) 060 { 061 component.resetIndex(); 062 } 063 064 @Override 065 public String toString() 066 { 067 return "Loop.ResetIndex"; 068 } 069 }; 070 071 /** 072 * Setup command for volatile rendering. Volatile rendering relies on re-acquiring the Iterator and working our way 073 * through it (and hoping for the best!). 074 */ 075 private static final ComponentAction<Loop> SETUP_FOR_VOLATILE = new ComponentAction<Loop>() 076 { 077 private static final long serialVersionUID = -977168791667037377L; 078 079 public void execute(Loop component) 080 { 081 component.setupForVolatile(); 082 } 083 084 @Override 085 public String toString() 086 { 087 return "Loop.SetupForVolatile"; 088 } 089 }; 090 091 /** 092 * Advances to next value in a volatile way. So, the <em>number</em> of steps is intrinsically stored in the Form 093 * (as the number of ADVANCE_VOLATILE commands), but the actual values are expressly stored only on the server. 094 */ 095 private static final ComponentAction<Loop> ADVANCE_VOLATILE = new ComponentAction<Loop>() 096 { 097 private static final long serialVersionUID = -4600281573714776832L; 098 099 public void execute(Loop component) 100 { 101 component.advanceVolatile(); 102 } 103 104 @Override 105 public String toString() 106 { 107 return "Loop.AdvanceVolatile"; 108 } 109 }; 110 111 /** 112 * Used in both volatile and non-volatile mode to end the current heartbeat (started by either ADVANCE_VOLATILE or 113 * one of the RestoreState commands). Also increments the index. 114 */ 115 private static final ComponentAction<Loop> END_HEARTBEAT = new ComponentAction<Loop>() 116 { 117 private static final long serialVersionUID = -977168791667037377L; 118 119 public void execute(Loop component) 120 { 121 component.endHeartbeat(); 122 } 123 124 @Override 125 public String toString() 126 { 127 return "Loop.EndHeartbeat"; 128 } 129 }; 130 131 /** 132 * Restores a state value (this is the case when there is no encoder and the complete value is stored). 133 */ 134 static class RestoreState implements ComponentAction<Loop> 135 { 136 private static final long serialVersionUID = -3926831611368720764L; 137 138 private final Object storedValue; 139 140 public RestoreState(final Object storedValue) 141 { 142 this.storedValue = storedValue; 143 } 144 145 public void execute(Loop component) 146 { 147 component.restoreState(storedValue); 148 } 149 150 @Override 151 public String toString() 152 { 153 return String.format("Loop.RestoreState[%s]", storedValue); 154 } 155 } 156 157 /** 158 * Restores the value using a stored primary key via {@link ValueEncoder#toValue(String)}. 159 */ 160 static class RestoreStateFromStoredClientValue implements ComponentAction<Loop> 161 { 162 private final String clientValue; 163 164 public RestoreStateFromStoredClientValue(final String clientValue) 165 { 166 this.clientValue = clientValue; 167 } 168 169 public void execute(Loop component) 170 { 171 component.restoreStateFromStoredClientValue(clientValue); 172 } 173 174 @Override 175 public String toString() 176 { 177 return String.format("Loop.RestoreStateFromStoredClientValue[%s]", clientValue); 178 } 179 } 180 181 /** 182 * Start of processing event that allows the Loop to set up internal bookeeping, to track which values have come up 183 * in the form submission. 184 */ 185 static final ComponentAction<Loop> PREPARE_FOR_SUBMISSION = new ComponentAction<Loop>() 186 { 187 public void execute(Loop component) 188 { 189 component.prepareForSubmission(); 190 } 191 192 @Override 193 public String toString() 194 { 195 return "Loop.PrepareForSubmission"; 196 } 197 }; 198 199 static final ComponentAction<Loop> NOTIFY_CONTAINER = new ComponentAction<Loop>() 200 { 201 public void execute(Loop component) 202 { 203 component.notifyContainer(); 204 } 205 206 @Override 207 public String toString() 208 { 209 return "Loop.NotifyContainer"; 210 } 211 }; 212 213 /** 214 * Defines the collection of values for the loop to iterate over. If not specified, defaults to a property of the 215 * container whose name matches the Loop cmponent's id. 216 */ 217 @Parameter(required = true, principal = true, autoconnect = true) 218 private Iterable<T> source; 219 220 /** 221 * A ValueEncoder used to convert server-side objects (provided by the 222 * "value" parameter) into unique client-side strings (typically IDs) and 223 * back. In general, when using a non-volatile Loop in a Form, you should 224 * either provide a ValueEncoder with the encoder parameter or use a "value" 225 * type for which Tapestry is configured to provide a ValueEncoder 226 * automatically. Otherwise Tapestry must fall back to using the plain 227 * index of each loop iteration, rather than the ValueEncoder-provided 228 * unique ID, for recording state into the form. 229 */ 230 @Parameter 231 private ValueEncoder<T> encoder; 232 233 /** 234 * Controls what information, if any, is encoded into an enclosing Form. The default value 235 * is {@link org.apache.tapestry5.corelib.LoopFormState#VALUES}. This parameter 236 * is only used if the component is enclosed by a Form. 237 */ 238 @Parameter(allowNull = false, defaultPrefix = BindingConstants.LITERAL) 239 private LoopFormState formState = LoopFormState.VALUES; 240 241 @Environmental(false) 242 private FormSupport formSupport; 243 244 /** 245 * The element to render. If not null, then the loop will render the indicated element around its body (on each pass 246 * through the loop). The default is derived from the component template. 247 */ 248 @Parameter(defaultPrefix = BindingConstants.LITERAL) 249 private String element; 250 251 /** 252 * The current value, set before the component renders its body. 253 */ 254 @Parameter(principal = true) 255 private T value; 256 257 /** 258 * The index into the source items. 259 */ 260 @Parameter 261 private int index; 262 263 /** 264 * A Block to render instead of the loop when the source is empty. The default is to render nothing. 265 */ 266 @Parameter(defaultPrefix = BindingConstants.LITERAL) 267 private Block empty; 268 269 private Iterator<T> iterator; 270 271 @Environmental 272 private Heartbeat heartbeat; 273 274 private boolean storeValuesInForm, storeIncrementsInForm, storeHeartbeatsInForm; 275 276 @Inject 277 private ComponentResources resources; 278 279 @Inject 280 private ComponentDefaultProvider defaultProvider; 281 282 private Block cleanupBlock; 283 284 /** 285 * Objects that have been recovered via {@link org.apache.tapestry5.ValueEncoder#toValue(String)} during the 286 * processing of the loop. These are sent to the container via an event. 287 */ 288 private List<T> synchonizedValues; 289 290 LoopFormState defaultFormState() 291 { 292 return LoopFormState.VALUES; 293 } 294 295 String defaultElement() 296 { 297 return resources.getElementName(); 298 } 299 300 ValueEncoder defaultEncoder() 301 { 302 return defaultProvider.defaultValueEncoder("value", resources); 303 } 304 305 @SetupRender 306 boolean setup() 307 { 308 index = 0; 309 310 iterator = source == null ? null : source.iterator(); 311 312 boolean insideForm = formSupport != null; 313 314 storeValuesInForm = insideForm && formState == LoopFormState.VALUES; 315 storeIncrementsInForm = insideForm && formState == LoopFormState.ITERATION; 316 317 storeHeartbeatsInForm = insideForm && formState != LoopFormState.NONE; 318 319 if (storeValuesInForm) 320 formSupport.store(this, PREPARE_FOR_SUBMISSION); 321 322 // Only render the body if there is something to iterate over 323 324 boolean hasContent = iterator != null && iterator.hasNext(); 325 326 if (insideForm && hasContent) 327 { 328 if (storeValuesInForm) 329 formSupport.store(this, RESET_INDEX); 330 if (storeIncrementsInForm) 331 formSupport.store(this, SETUP_FOR_VOLATILE); 332 } 333 334 cleanupBlock = hasContent ? null : empty; 335 336 // Jump directly to cleanupRender if there is no content 337 338 return hasContent; 339 } 340 341 /** 342 * Returns the empty block, or null, after the render has finished. It will only be the empty block (which itself 343 * may be null) if the source was null or empty. 344 */ 345 Block cleanupRender() 346 { 347 if (storeValuesInForm) 348 formSupport.store(this, NOTIFY_CONTAINER); 349 350 return cleanupBlock; 351 } 352 353 private void setupForVolatile() 354 { 355 index = 0; 356 iterator = source.iterator(); 357 } 358 359 private void advanceVolatile() 360 { 361 value = iterator.next(); 362 363 startHeartbeat(); 364 } 365 366 /** 367 * Begins a new heartbeat. 368 */ 369 @BeginRender 370 void begin(MarkupWriter writer) 371 { 372 value = iterator.next(); 373 374 if (storeValuesInForm) 375 { 376 if (encoder == null) 377 { 378 formSupport.store(this, new RestoreState(value)); 379 } 380 else 381 { 382 String clientValue = encoder.toClient(value); 383 384 formSupport.store(this, new RestoreStateFromStoredClientValue(clientValue)); 385 } 386 } 387 388 if (storeIncrementsInForm) 389 { 390 formSupport.store(this, ADVANCE_VOLATILE); 391 } 392 393 startHeartbeat(); 394 395 if (element != null) 396 { 397 writer.element(element); 398 resources.renderInformalParameters(writer); 399 } 400 } 401 402 private void startHeartbeat() 403 { 404 heartbeat.begin(); 405 } 406 407 /** 408 * Ends the current heartbeat. 409 */ 410 @AfterRender 411 Boolean after(MarkupWriter writer) 412 { 413 if (element != null) 414 writer.end(); 415 416 endHeartbeat(); 417 418 if (storeHeartbeatsInForm) 419 { 420 formSupport.store(this, END_HEARTBEAT); 421 } 422 423 return iterator.hasNext() ? false : null; 424 } 425 426 private void endHeartbeat() 427 { 428 heartbeat.end(); 429 430 index++; 431 } 432 433 private void resetIndex() 434 { 435 index = 0; 436 } 437 438 /** 439 * Restores state previously stored by the Loop into a Form. 440 */ 441 private void restoreState(T storedValue) 442 { 443 value = storedValue; 444 445 startHeartbeat(); 446 } 447 448 /** 449 * Restores state previously encoded by the Loop and stored into the Form. 450 */ 451 private void restoreStateFromStoredClientValue(String clientValue) 452 { 453 // We assume that if an encoder is available when we rendered, that one will be available 454 // when the form is submitted. 455 456 T restoredValue = encoder.toValue(clientValue); 457 458 restoreState(restoredValue); 459 460 synchonizedValues.add(restoredValue); 461 } 462 463 private void prepareForSubmission() 464 { 465 synchonizedValues = CollectionFactory.newList(); 466 } 467 468 private void notifyContainer() 469 { 470 Object[] values = synchonizedValues.toArray(); 471 472 resources.triggerEvent(EventConstants.SYNCHRONIZE_VALUES, values, null); 473 } 474 475 // For testing: 476 477 public int getIndex() 478 { 479 return index; 480 } 481 482 public T getValue() 483 { 484 return value; 485 } 486 487 void setSource(Iterable<T> source) 488 { 489 this.source = source; 490 } 491 492 void setHeartbeat(Heartbeat heartbeat) 493 { 494 this.heartbeat = heartbeat; 495 } 496}