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