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