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