001 // Copyright 2004, 2005 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.tapestry.components; 016 017 import java.util.ArrayList; 018 import java.util.Collections; 019 import java.util.HashMap; 020 import java.util.Iterator; 021 import java.util.List; 022 import java.util.Map; 023 024 import org.apache.tapestry.IBinding; 025 import org.apache.tapestry.IForm; 026 import org.apache.tapestry.IMarkupWriter; 027 import org.apache.tapestry.IRequestCycle; 028 import org.apache.tapestry.Tapestry; 029 import org.apache.tapestry.TapestryUtils; 030 import org.apache.tapestry.coerce.ValueConverter; 031 import org.apache.tapestry.form.AbstractFormComponent; 032 import org.apache.tapestry.services.DataSqueezer; 033 import org.apache.tapestry.services.ExpressionEvaluator; 034 035 /** 036 * @author mb 037 */ 038 public abstract class ForBean extends AbstractFormComponent { 039 // constants 040 private static final char DESC_VALUE = 'V'; 041 private static final char DESC_PRIMARY_KEY = 'P'; 042 043 private final RepSource COMPLETE_REP_SOURCE = new CompleteRepSource(); 044 private final RepSource KEY_EXPRESSION_REP_SOURCE = new KeyExpressionRepSource(); 045 046 // parameters 047 public abstract String getElement(); 048 public abstract String getKeyExpression(); 049 public abstract IPrimaryKeyConverter getConverter(); 050 public abstract Object getDefaultValue(); 051 public abstract boolean getMatch(); 052 public abstract boolean getVolatile(); 053 054 // injects 055 public abstract DataSqueezer getDataSqueezer(); 056 public abstract ValueConverter getValueConverter(); 057 public abstract ExpressionEvaluator getExpressionEvaluator(); 058 059 // intermediate members 060 private Object _value; 061 private int _index; 062 private boolean _rendering; 063 064 065 /** 066 * Gets the source binding and iterates through 067 * its values. For each, it updates the value binding and render's its wrapped elements. 068 * 069 **/ 070 protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle) 071 { 072 // form may be null if component is not located in a form 073 IForm form = (IForm) cycle.getAttribute(TapestryUtils.FORM_ATTRIBUTE); 074 075 // If the cycle is rewinding, but not this particular form, 076 // then do nothing (don't even render the body). 077 boolean cycleRewinding = cycle.isRewinding(); 078 if (cycleRewinding && form != null && !form.isRewinding()) 079 return; 080 081 // Get the data to be iterated upon. Store in form if needed. 082 Iterator dataSource = getData(cycle, form); 083 084 // Do not iterate if dataSource is null. 085 // The dataSource was either not convertable to Iterator, or was empty. 086 if (dataSource == null) 087 return; 088 089 String element = getElement(); 090 091 // Perform the iterations 092 try 093 { 094 _index = 0; 095 _rendering = true; 096 097 while (dataSource.hasNext()) 098 { 099 // Get current value 100 _value = dataSource.next(); 101 102 // Update output component parameters 103 updateOutputParameters(); 104 105 // Render component 106 if (element != null) 107 { 108 writer.begin(element); 109 renderInformalParameters(writer, cycle); 110 } 111 112 renderBody(writer, cycle); 113 114 if (element != null) 115 writer.end(); 116 117 _index++; 118 } 119 } 120 finally 121 { 122 _rendering = false; 123 _value = null; 124 } 125 } 126 127 128 /** 129 * Returns the most recent value extracted from the source parameter. 130 * 131 * @throws org.apache.tapestry.ApplicationRuntimeException if the For is not currently rendering. 132 * 133 **/ 134 135 public final Object getValue() 136 { 137 if (!_rendering) 138 throw Tapestry.createRenderOnlyPropertyException(this, "value"); 139 140 return _value; 141 } 142 143 /** 144 * The index number, within the {@link #getSource() source}, of the 145 * the current value. 146 * 147 * @throws org.apache.tapestry.ApplicationRuntimeException if the For is not currently rendering. 148 * 149 **/ 150 151 public int getIndex() 152 { 153 if (!_rendering) 154 throw Tapestry.createRenderOnlyPropertyException(this, "index"); 155 156 return _index; 157 } 158 159 public boolean isDisabled() 160 { 161 return false; 162 } 163 164 /** 165 * Updates the index and value output parameters if bound. 166 */ 167 protected void updateOutputParameters() 168 { 169 IBinding indexBinding = getBinding("index"); 170 if (indexBinding != null) 171 indexBinding.setObject(new Integer(_index)); 172 173 IBinding valueBinding = getBinding("value"); 174 if (valueBinding != null) 175 valueBinding.setObject(_value); 176 } 177 178 /** 179 * Updates the primaryKeys parameter if bound. 180 */ 181 protected void updatePrimaryKeysParameter(String[] stringReps) 182 { 183 IBinding primaryKeysBinding = getBinding("primaryKeys"); 184 if (primaryKeysBinding == null) 185 return; 186 187 DataSqueezer squeezer = getDataSqueezer(); 188 189 int repsCount = stringReps.length; 190 List primaryKeys = new ArrayList(repsCount); 191 for (int i = 0; i < stringReps.length; i++) { 192 String rep = stringReps[i]; 193 if (rep.length() == 0 || rep.charAt(0) != DESC_PRIMARY_KEY) 194 continue; 195 Object primaryKey = squeezer.unsqueeze(rep.substring(1)); 196 primaryKeys.add(primaryKey); 197 } 198 199 primaryKeysBinding.setObject(primaryKeys); 200 } 201 202 // Do nothing in those methods, but make the JVM happy 203 protected void renderFormComponent(IMarkupWriter writer, IRequestCycle cycle) { } 204 protected void rewindFormComponent(IMarkupWriter writer, IRequestCycle cycle) { } 205 206 207 208 209 /** 210 * Returns a list with the values to be iterated upon. 211 * 212 * The list is obtained in different ways: 213 * - If the component is not located in a form or 'volatile' is set to true, 214 * then the simply the values passed to 'source' are returned (same as Foreach) 215 * - If the component is in a form, and the form is rewinding, the values stored 216 * in the form are returned -- rewind is then always the same as render. 217 * - If the component is in a form, and the form is being rendered, the values 218 * are stored in the form as Hidden fields. 219 * 220 * @param cycle The current request cycle 221 * @param form The form within which the component is located (if any) 222 * @return An iterator with the values to be cycled upon 223 **/ 224 private Iterator getData(IRequestCycle cycle, IForm form) { 225 if (form == null || getVolatile()) 226 return evaluateSourceIterator(); 227 228 String name = form.getElementId(this); 229 if (cycle.isRewinding()) 230 return getStoredData(cycle, name); 231 return storeSourceData(form, name); 232 } 233 234 /** 235 * Returns a list of the values stored as Hidden fields in the form. 236 * A conversion is performed if the primary key of the value is stored. 237 * 238 * @param cycle The current request cycle 239 * @param name The name of the HTTP parameter whether the values 240 * @return an iterator with the values stored in the provided Hidden fields 241 **/ 242 protected Iterator getStoredData(IRequestCycle cycle, String name) 243 { 244 String[] stringReps = cycle.getParameters(name); 245 if (stringReps == null) 246 return null; 247 248 updatePrimaryKeysParameter(stringReps); 249 250 Iterator sourceIterator = evaluateSourceIterator(); 251 Iterator fullSourceIterator = evaluateFullSourceIterator(); 252 Map repToValueMap = new HashMap(); 253 254 int valueCount = stringReps.length; 255 List values = new ArrayList(valueCount); 256 for (int i = 0; i < valueCount; i++) { 257 String rep = stringReps[i]; 258 Object value = getValueFromStringRep(sourceIterator, fullSourceIterator, repToValueMap, rep); 259 values.add(value); 260 } 261 262 return values.iterator(); 263 } 264 265 /** 266 * Stores the provided data in the form and then returns the data as an iterator. 267 * If the primary key of the value can be determined, 268 * then that primary key is saved instead. 269 * 270 * @param form The form where the data will be stored 271 * @param name The name under which the data will be stored 272 * @return an iterator with the bound values stored in the form 273 **/ 274 protected Iterator storeSourceData(IForm form, String name) 275 { 276 List values = new ArrayList(); 277 278 Iterator it = evaluateSourceIterator(); 279 while (it.hasNext()) { 280 Object value = it.next(); 281 values.add(value); 282 283 String rep = getStringRepFromValue(value); 284 form.addHiddenValue(name, rep); 285 } 286 287 return values.iterator(); 288 } 289 290 291 /** 292 * Returns the string representation of the value. 293 * 294 * The first letter of the string representation shows whether a value 295 * or a primary key is being described. 296 * 297 * @param value 298 * @return 299 */ 300 protected String getStringRepFromValue(Object value) { 301 String rep; 302 DataSqueezer squeezer = getDataSqueezer(); 303 304 // try to extract the primary key from the value 305 Object pk = getPrimaryKeyFromValue(value); 306 if (pk != null) 307 // Primary key was extracted successfully. 308 rep = DESC_PRIMARY_KEY + squeezer.squeeze(pk); 309 else 310 // primary key could not be extracted. squeeze value. 311 rep = DESC_VALUE + squeezer.squeeze(value); 312 313 return rep; 314 } 315 316 /** 317 * Returns the primary key of the given value. 318 * Uses the 'keyExpression' or the 'converter' (if either is provided). 319 * 320 * @param value The value from which the primary key should be extracted 321 * @return The primary key of the value, or null if such cannot be extracted. 322 */ 323 protected Object getPrimaryKeyFromValue(Object value) { 324 if (value == null) 325 return null; 326 327 Object primaryKey = getKeyExpressionFromValue(value); 328 if (primaryKey == null) 329 primaryKey = getConverterFromValue(value); 330 331 return primaryKey; 332 } 333 334 /** 335 * Uses the 'keyExpression' parameter to determine the primary key of the given value 336 * 337 * @param value The value from which the primary key should be extracted 338 * @return The primary key of the value as defined by 'keyExpression', 339 * or null if such cannot be extracted. 340 */ 341 protected Object getKeyExpressionFromValue(Object value) { 342 String keyExpression = getKeyExpression(); 343 if (keyExpression == null) 344 return null; 345 346 Object primaryKey = getExpressionEvaluator().read(value, keyExpression); 347 return primaryKey; 348 } 349 350 /** 351 * Uses the 'converter' parameter to determine the primary key of the given value 352 * 353 * @param value The value from which the primary key should be extracted 354 * @return The primary key of the value as provided by the converter, 355 * or null if such cannot be extracted. 356 */ 357 protected Object getConverterFromValue(Object value) { 358 IPrimaryKeyConverter converter = getConverter(); 359 if (converter == null) 360 return null; 361 362 Object primaryKey = converter.getPrimaryKey(value); 363 return primaryKey; 364 } 365 366 /** 367 * Determines the value that corresponds to the given string representation. 368 * 369 * If the 'match' parameter is true, attempt to find a value in 'source' 370 * or 'fullSource' that generates the same string representation. 371 * 372 * Otherwise, create a new value from the string representation. 373 * 374 * @param rep the string representation for which a value should be returned 375 * @return the value that corresponds to the provided string representation 376 */ 377 protected Object getValueFromStringRep(Iterator sourceIterator, Iterator fullSourceIterator, 378 Map repToValueMap, String rep) { 379 Object value = null; 380 DataSqueezer squeezer = getDataSqueezer(); 381 382 // Check if the string rep is empty. If so, just return the default value. 383 if (rep == null || rep.length() == 0) 384 return getDefaultValue(); 385 386 // If required, find a value with an equivalent string representation and return it 387 boolean match = getMatch(); 388 if (match) { 389 value = findValueWithStringRep(sourceIterator, fullSourceIterator, repToValueMap, rep, COMPLETE_REP_SOURCE); 390 if (value != null) 391 return value; 392 } 393 394 // Matching of the string representation was not successful or was disabled. 395 // Use the standard approaches to obtain the value from the rep. 396 char desc = rep.charAt(0); 397 String squeezed = rep.substring(1); 398 switch (desc) { 399 case DESC_VALUE: 400 // If the string rep is just the value itself, unsqueeze it 401 value = squeezer.unsqueeze(squeezed); 402 break; 403 404 case DESC_PRIMARY_KEY: 405 // Perform keyExpression match if not already attempted 406 if (!match && getKeyExpression() != null) 407 value = findValueWithStringRep(sourceIterator, fullSourceIterator, repToValueMap, rep, KEY_EXPRESSION_REP_SOURCE); 408 409 // If 'converter' is defined, try to perform conversion from primary key to value 410 if (value == null) { 411 IPrimaryKeyConverter converter = getConverter(); 412 if (converter != null) { 413 Object pk = squeezer.unsqueeze(squeezed); 414 value = converter.getValue(pk); 415 } 416 } 417 break; 418 } 419 420 if (value == null) 421 value = getDefaultValue(); 422 423 return value; 424 } 425 426 /** 427 * Attempt to find a value in 'source' or 'fullSource' that generates 428 * the provided string representation. 429 * 430 * Use the RepSource interface to determine what the string representation 431 * of a particular value is. 432 * 433 * @param rep the string representation for which a value should be returned 434 * @param repSource an interface providing the string representation of a given value 435 * @return the value in 'source' or 'fullSource' that corresponds 436 * to the provided string representation 437 */ 438 protected Object findValueWithStringRep(Iterator sourceIterator, Iterator fullSourceIterator, 439 Map repToValueMap, String rep, RepSource repSource) { 440 Object value = repToValueMap.get(rep); 441 if (value != null) 442 return value; 443 444 value = findValueWithStringRepInIterator(sourceIterator, repToValueMap, rep, repSource); 445 if (value != null) 446 return value; 447 448 value = findValueWithStringRepInIterator(fullSourceIterator, repToValueMap, rep, repSource); 449 return value; 450 } 451 452 /** 453 * Attempt to find a value in the provided collection that generates 454 * the required string representation. 455 * 456 * Use the RepSource interface to determine what the string representation 457 * of a particular value is. 458 * 459 * @param rep the string representation for which a value should be returned 460 * @param repSource an interface providing the string representation of a given value 461 * @param it the iterator of the collection in which a value should be searched 462 * @return the value in the provided collection that corresponds 463 * to the required string representation 464 */ 465 protected Object findValueWithStringRepInIterator(Iterator it, Map repToValueMap, String rep, RepSource repSource) { 466 while (it.hasNext()) { 467 Object sourceValue = it.next(); 468 if (sourceValue == null) 469 continue; 470 471 String sourceRep = repSource.getStringRep(sourceValue); 472 repToValueMap.put(sourceRep, sourceValue); 473 474 if (rep.equals(sourceRep)) 475 return sourceValue; 476 } 477 478 return null; 479 } 480 481 /** 482 * Returns a new iterator of the values in 'source'. 483 * 484 * @return the 'source' iterator 485 */ 486 protected Iterator evaluateSourceIterator() 487 { 488 Iterator it = null; 489 Object source = null; 490 491 IBinding sourceBinding = getBinding("source"); 492 if (sourceBinding != null) 493 source = sourceBinding.getObject(); 494 495 if (source != null) 496 it = (Iterator) getValueConverter().coerceValue(source, Iterator.class); 497 498 if (it == null) 499 it = Collections.EMPTY_LIST.iterator(); 500 501 return it; 502 } 503 504 /** 505 * Returns a new iterator of the values in 'fullSource'. 506 * 507 * @return the 'fullSource' iterator 508 */ 509 protected Iterator evaluateFullSourceIterator() 510 { 511 Iterator it = null; 512 Object fullSource = null; 513 514 IBinding fullSourceBinding = getBinding("fullSource"); 515 if (fullSourceBinding != null) 516 fullSource = fullSourceBinding.getObject(); 517 518 if (fullSource != null) 519 it = (Iterator) getValueConverter().coerceValue(fullSource, Iterator.class); 520 521 if (it == null) 522 it = Collections.EMPTY_LIST.iterator(); 523 524 return it; 525 } 526 527 /** 528 * An interface that provides the string representation of a given value 529 */ 530 protected interface RepSource { 531 String getStringRep(Object value); 532 } 533 534 /** 535 * An implementation of RepSource that provides the string representation 536 * of the given value using all methods. 537 */ 538 protected class CompleteRepSource implements RepSource { 539 public String getStringRep(Object value) { 540 return getStringRepFromValue(value); 541 } 542 } 543 544 /** 545 * An implementation of RepSource that provides the string representation 546 * of the given value using just the 'keyExpression' parameter. 547 */ 548 protected class KeyExpressionRepSource implements RepSource { 549 public String getStringRep(Object value) { 550 Object pk = getKeyExpressionFromValue(value); 551 return DESC_PRIMARY_KEY + getDataSqueezer().squeeze(pk); 552 } 553 } 554 555 /** 556 * For component can not take focus. 557 */ 558 protected boolean getCanTakeFocus() { 559 return false; 560 } 561 562 public String getClientId() 563 { 564 return null; 565 } 566 567 public String getDisplayName() 568 { 569 return null; 570 } 571 572 573 }