1 package org.apache.velocity.tools.generic; 2 3 /* 4 * Licensed to the Apache Software Foundation (ASF) under one 5 * or more contributor license agreements. See the NOTICE file 6 * distributed with this work for additional information 7 * regarding copyright ownership. The ASF licenses this file 8 * to you under the Apache License, Version 2.0 (the 9 * "License"); you may not use this file except in compliance 10 * with the License. You may obtain a copy of the License at 11 * 12 * http://www.apache.org/licenses/LICENSE-2.0 13 * 14 * Unless required by applicable law or agreed to in writing, 15 * software distributed under the License is distributed on an 16 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 17 * KIND, either express or implied. See the License for the 18 * specific language governing permissions and limitations 19 * under the License. 20 */ 21 22 import java.util.ArrayList; 23 import java.util.HashMap; 24 import java.util.Iterator; 25 import java.util.List; 26 import java.util.Map; 27 import java.util.NoSuchElementException; 28 import java.util.Stack; 29 import org.apache.velocity.tools.ClassUtils; 30 import org.apache.velocity.tools.Scope; 31 import org.apache.velocity.tools.config.DefaultKey; 32 import org.apache.velocity.tools.config.ValidScope; 33 34 /** 35 * <p> 36 * A convenience tool to use with #foreach loops. It wraps a list 37 * with a custom iterator to provide additional controls and feedback 38 * for managing loops. 39 * </p> 40 * <p> 41 * This tool was originally inspired the now-deprecated IteratorTool, 42 * which provided similar base functionality but was somewhat more difficult 43 * to understand and use. Rather than try to migrate that implementation 44 * via deprecation and new methods, it was simplest to just create an 45 * entirely new tool that simplified the original API and was easy 46 * to augment with useful new features like support for nested 47 * (and nameable) loops, skipping ahead in loops, synchronizing multiple 48 * iterators, getting the iteration count of loops, identifying if a loop is 49 * on its first or last iteration, and so on. 50 * </p> 51 * <p> 52 * Most functions of this tool will be obsolete with the release of 53 * Velocity 1.7, which will provide $foreach.hasNext, $foreach.isFirst, 54 * $foreach.isLast, $foreach.index and $foreach.count automatically. 55 * However, this will still be useful for the more advanced sync 56 * and skip features. Also, for very complicated nested loops, the 57 * loop naming feature may be easier than doing things like $foreach.parent.parent. 58 * </p> 59 * <p> 60 * Example of use: 61 * <pre> 62 * Template 63 * --- 64 * #set( $list = [1..7] ) 65 * #set( $others = [3..10] ) 66 * #foreach( $item in $loop.watch($list).sync($others, 'other') ) 67 * $item -> $loop.other 68 * #if( $item >= 5 )$loop.stop()#end 69 * #end 70 * 71 * Output 72 * ------ 73 * 1 -> 3 74 * 2 -> 4 75 * 3 -> 5 76 * 4 -> 6 77 * 5 -> 7 78 * 79 * Example tools.xml config (if you want to use this with VelocityView): 80 * <tools> 81 * <toolbox scope="request"> 82 * <tool class="org.apache.velocity.tools.generic.LoopTool"/> 83 * </toolbox> 84 * </tools> 85 * </pre> 86 * </p> 87 * 88 * @author Nathan Bubna 89 * @version $Id: LoopTool.java 590893 2007-11-01 04:40:21Z nbubna $ 90 */ 91 @DefaultKey("loop") 92 @ValidScope(Scope.REQUEST) 93 public class LoopTool 94 { 95 private Stack<ManagedIterator> iterators = new Stack<ManagedIterator>(); 96 private ManagedIterator last; 97 98 /** 99 * <p>Tells the LoopTool to watch the specified Array, Collection, Map, 100 * Iterator, Iterable, Enumeration or POJO with an iterator() method 101 * while the template iterates over the values within it. 102 * </p> 103 * <p>Under the covers, this is returning an iterable wrapper that 104 * is also pushed onto this tool's stack. That allows this tool to 105 * know which iterator to give later commands (i.e. stop() or skip()). 106 * </p> 107 * @param obj an object that Velocity's #foreach directive can iterate over 108 * @return a {@link ManagedIterator} that this tool instance will track 109 */ 110 public ManagedIterator watch(Object obj) 111 { 112 Iterator iterator = getIterator(obj); 113 if (iterator == null) 114 { 115 return null; 116 } 117 118 ManagedIterator managed = manage(iterator, null); 119 iterators.push(managed); 120 this.last = managed; 121 return managed; 122 } 123 124 /** 125 * This is just like {@link #watch(Object)} except that it also takes 126 * a name which is given to the {@link ManagedIterator} that is returned. 127 * This allows the user to send stop or skip commands to that specific 128 * iterator even when there are nested iterators within it that are being 129 * watched. If the given name is {@code null}, then this will return 130 * {@code null} even if the object can be watched. Provided names cannot 131 * be {@code null}. 132 * @see #watch(Object) 133 */ 134 public ManagedIterator watch(Object obj, String name) 135 { 136 // don't mess around with null names 137 if (name == null) 138 { 139 return null; 140 } 141 Iterator iterator = getIterator(obj); 142 if (iterator == null) 143 { 144 return null; 145 } 146 147 ManagedIterator managed = manage(iterator, name); 148 iterators.push(managed); 149 this.last = managed; 150 return managed; 151 } 152 153 public ManagedIterator sync(Object main, Object synced) 154 { 155 return watch(main).sync(synced); 156 } 157 158 protected ManagedIterator manage(Iterator iterator, String name) 159 { 160 return new ManagedIterator(name, iterator, this); 161 } 162 163 /** 164 * This tells the current loop to stop after the current iteration. 165 * This is different from "break" common to most programming languages, 166 * in that it does not immediately cease activity in the current iteration. 167 * Instead, it merely tells the #foreach loop that this is the last time 168 * around. 169 */ 170 public void stop() 171 { 172 // if we have an iterator on the stack 173 if (!iterators.empty()) 174 { 175 // stop the top one, so #foreach doesn't loop again 176 iterators.peek().stop(); 177 } 178 } 179 180 /** 181 * This is just like {@link #stop()} except that the stop command is issued 182 * <strong>only</strong> to the loop/iterator with the specified name. 183 * If no such loop is found with that name, then no stop command is issued. 184 * @see #stop() 185 */ 186 public void stop(String name) 187 { 188 // just stop the matching one 189 for (ManagedIterator iterator : iterators) 190 { 191 if (iterator.getName().equals(name)) 192 { 193 iterator.stop(); 194 break; 195 } 196 } 197 } 198 199 /** 200 * This is just like {@link #stop(String)} except that the stop command is issued 201 * both to the loop/iterator with the specified name and all loops nested within 202 * it. If no such loop is found with that name, then no stop commands are 203 * issued. 204 * @see #stop() 205 * @see #stop(String) 206 */ 207 public void stopTo(String name) 208 { 209 if (!iterators.empty()) 210 { 211 // create a backup stack to put things back as they were 212 Stack<ManagedIterator> backup = new Stack<ManagedIterator>(); 213 // look for the iterator with the specified name 214 boolean found = false; 215 while (!found && !iterators.empty()) 216 { 217 ManagedIterator iterator = iterators.pop(); 218 if (iterator.getName().equals(name)) 219 { 220 found = true; 221 iterator.stop(); 222 } 223 else 224 { 225 // keep a backup of the ones that don't match 226 backup.push(iterator); 227 } 228 } 229 230 while (!backup.empty()) 231 { 232 // push the nested iterators back 233 ManagedIterator iterator = backup.pop(); 234 iterators.push(iterator); 235 if (found) 236 { 237 iterator.stop(); 238 } 239 } 240 } 241 } 242 243 /** 244 * This is just like {@link #stop()} except that the stop command is issued 245 * <strong>all</strong> the loops being watched by this tool. 246 * @see #stop() 247 */ 248 public void stopAll() 249 { 250 // just stop them all 251 for (ManagedIterator iterator : iterators) 252 { 253 iterator.stop(); 254 } 255 } 256 257 258 /** 259 * Skips ahead the specified number of iterations (if possible). 260 * Since this is manual skipping (unlike the automatic skipping 261 * provided by the likes of {@link ManagedIterator#exclude(Object)}, any elements 262 * skipped are still considered in the results returned by {@link #getCount()} 263 * and {@link #isFirst()}. 264 */ 265 public void skip(int number) 266 { 267 // if we have an iterator on the stack 268 if (!iterators.empty()) 269 { 270 // tell the top one to skip the specified number 271 skip(number, iterators.peek()); 272 } 273 } 274 275 /** 276 * This tells the specified loop to skip ahead the specified number of 277 * iterations. 278 * @see #skip(int) 279 */ 280 public void skip(int number, String name) 281 { 282 // just tell the matching one to skip 283 ManagedIterator iterator = findIterator(name); 284 if (iterator != null) 285 { 286 skip(number, iterator); 287 } 288 } 289 290 // does the actual skipping by manually advancing the ManagedIterator 291 private void skip(int number, ManagedIterator iterator) 292 { 293 for (int i=0; i < number; i++) 294 { 295 if (iterator.hasNext()) 296 { 297 iterator.next(); 298 } 299 else 300 { 301 break; 302 } 303 } 304 } 305 306 /** 307 * Returns {@code true} if the current loop is on its first iteration. 308 */ 309 public Boolean isFirst() 310 { 311 if (last != null) 312 { 313 return last.isFirst(); 314 } 315 return null; 316 } 317 318 /** 319 * Returns {@code true} if the loop with the specified name 320 * is on its first iteration. 321 */ 322 public Boolean isFirst(String name) 323 { 324 // just tell the matching one to skip 325 ManagedIterator iterator = findIterator(name); 326 if (iterator != null) 327 { 328 return iterator.isFirst(); 329 } 330 return null; 331 } 332 333 /** 334 * Returns the result of {@link #isFirst}. Exists to allow $loop.first syntax. 335 */ 336 public Boolean getFirst() 337 { 338 return isFirst(); 339 } 340 341 /** 342 * Returns {@code true} if the current loop is on its last iteration. 343 */ 344 public Boolean isLast() 345 { 346 if (last != null) 347 { 348 return last.isLast(); 349 } 350 return null; 351 } 352 353 /** 354 * Returns {@code true} if the loop with the specified name 355 * is on its last iteration. 356 */ 357 public Boolean isLast(String name) 358 { 359 // just tell the matching one to skip 360 ManagedIterator iterator = findIterator(name); 361 if (iterator != null) 362 { 363 return iterator.isLast(); 364 } 365 return null; 366 } 367 368 /** 369 * Returns the result of {@link #isLast}. Exists to allow $loop.last syntax. 370 */ 371 public Boolean getLast() 372 { 373 return isLast(); 374 } 375 376 /** 377 * <p>This serves two purposes: 378 * <ul><li>Getting the current value of a sync'ed iterator</li> 379 * <li>Abbreviate syntax for properties of outer loops</li></ul></p> 380 * <p>First, it searches all the loops being managed for one 381 * with a sync'ed Iterator under the specified name and 382 * returns the current value for that sync'ed iterator, 383 * if any. If there is no sync'ed iterators or none with 384 * that name, then this will check if the specified key 385 * is requesting a "property" of an outer loop (e.g. 386 * {@code $loop.count_foo} or {@code $loop.first_foo}). 387 * This syntax is shorter and clearer than {@code $loop.getCount('foo')}. 388 * If the key starts with a property name and ends with an outer loop 389 * name, then the value of that property for that loop is returned. 390 */ 391 public Object get(String key) 392 { 393 // search all iterators in reverse 394 // (so nested ones take priority) 395 // for one that is responsible for synced 396 for (int i=iterators.size() - 1; i >= 0; i--) 397 { 398 ManagedIterator iterator = iterators.get(i); 399 if (iterator.isSyncedWith(key)) 400 { 401 return iterator.get(key); 402 } 403 } 404 // shortest key would be "last_X" where X is the loop name 405 if (key == null || key.length() < 6) 406 { 407 return null; 408 } 409 if (key.startsWith("last_")) 410 { 411 return isLast(key.substring(5, key.length())); 412 } 413 if (key.startsWith("count_")) 414 { 415 return getCount(key.substring(6, key.length())); 416 } 417 if (key.startsWith("index_")) 418 { 419 return getIndex(key.substring(6, key.length())); 420 } 421 if (key.startsWith("first_")) 422 { 423 return isFirst(key.substring(6, key.length())); 424 } 425 return null; 426 } 427 428 /** 429 * Asks the loop with the specified name for the current value 430 * of the specified sync'ed iterator, if any. 431 */ 432 public Object get(String name, String synced) 433 { 434 // just ask the matching iterator for the sync'ed value 435 ManagedIterator iterator = findIterator(name); 436 if (iterator != null) 437 { 438 return iterator.get(synced); 439 } 440 return null; 441 } 442 443 /** 444 * Returns the 0-based index of the item the current loop is handling. 445 * So, if this is the first iteration, then the index will be 0. If 446 * you {@link #skip} ahead in this loop, those skipped iterations will 447 * still be reflected in the index. If iteration has not begun, this 448 * will return {@code null}. 449 */ 450 public Integer getIndex() 451 { 452 Integer count = getCount(); 453 if (count == null || count == 0) 454 { 455 return null; 456 } 457 return count - 1; 458 } 459 460 /** 461 * Returns the 0-based index of the item the specified loop is handling. 462 * So, if this is the first iteration, then the index will be 0. If 463 * you {@link #skip} ahead in this loop, those skipped iterations will 464 * still be reflected in the index. If iteration has not begun, this 465 * will return {@code null}. 466 */ 467 public Integer getIndex(String name) 468 { 469 Integer count = getCount(name); 470 if (count == null || count == 0) 471 { 472 return null; 473 } 474 return count - 1; 475 } 476 477 /** 478 * Returns the number of items the current loop has handled. So, if this 479 * is the first iteration, then the count will be 1. If you {@link #skip} 480 * ahead in this loop, those skipped iterations will still be included in 481 * the count. 482 */ 483 public Integer getCount() 484 { 485 if (last != null) 486 { 487 return last.getCount(); 488 } 489 return null; 490 } 491 492 /** 493 * Returns the number of items the specified loop has handled. So, if this 494 * is the first iteration, then the count will be 1. If you {@link #skip} 495 * ahead in this loop, those skipped iterations will still be included in 496 * the count. 497 */ 498 public Integer getCount(String name) 499 { 500 // just tell the matching one to skip 501 ManagedIterator iterator = findIterator(name); 502 if (iterator != null) 503 { 504 return iterator.getCount(); 505 } 506 return null; 507 } 508 509 /** 510 * Returns the most recent {@link ManagedIterator} for this instance. 511 * This can be used to access properties like the count, index, 512 * isFirst, isLast, etc which would otherwise fail on the last item 513 * in a loop due to the necessity of popping iterators off the 514 * stack when the last item is retrieved. (See VELTOOLS-124) 515 */ 516 public ManagedIterator getThis() 517 { 518 return last; 519 } 520 521 /** 522 * Returns the number of loops currently on the stack. 523 * This is only useful for debugging, as iterators are 524 * popped off the stack at the start of their final iteration, 525 * making this frequently "incorrect". 526 */ 527 public int getDepth() 528 { 529 return iterators.size(); 530 } 531 532 533 /** 534 * Finds the {@link ManagedIterator} with the specified name 535 * if it is in this instance's iterator stack. 536 */ 537 protected ManagedIterator findIterator(String name) 538 { 539 // look for the one with the specified name 540 for (ManagedIterator iterator : iterators) 541 { 542 if (iterator.getName().equals(name)) 543 { 544 return iterator; 545 } 546 } 547 return null; 548 } 549 550 /** 551 * Don't let templates call this, but allow subclasses 552 * and ManagedIterator to have access. 553 */ 554 protected ManagedIterator pop() 555 { 556 return iterators.pop(); 557 } 558 559 560 /** 561 * Wraps access to {@link ClassUtils#getIterator} is a 562 * nice little try/catch block to prevent exceptions from 563 * escaping into the template. In the case of such problems, 564 * this will return {@code null}. 565 */ 566 protected static Iterator getIterator(Object obj) 567 { 568 if (obj == null) 569 { 570 return null; 571 } 572 try 573 { 574 return ClassUtils.getIterator(obj); 575 } 576 catch (Exception e) 577 { 578 //TODO: pick up a log so we can log this 579 } 580 return null; 581 } 582 583 584 /** 585 * Iterator implementation that wraps a standard {@link Iterator} 586 * and allows it to be prematurely stopped, skipped ahead, and 587 * associated with a name for advanced nested loop control. 588 * This also allows a arbitrary {@link ActionCondition}s to be added 589 * in order to have it automatically skip over or stop before 590 * certain elements in the iterator. 591 */ 592 public static class ManagedIterator implements Iterator 593 { 594 private String name; 595 private Iterator iterator; 596 private LoopTool owner; 597 private boolean stopped = false; 598 private Boolean first = null; 599 private int count = 0; 600 private Object next; 601 private List<ActionCondition> conditions; 602 private Map<String,SyncedIterator> synced; 603 604 public ManagedIterator(String name, Iterator iterator, LoopTool owner) 605 { 606 if (name == null) 607 { 608 this.name = "loop"+owner.getDepth(); 609 } 610 else 611 { 612 this.name = name; 613 } 614 this.iterator = iterator; 615 this.owner = owner; 616 } 617 618 /** 619 * Returns the name of this instance. 620 */ 621 public String getName() 622 { 623 return this.name; 624 } 625 626 /** 627 * Returns true if either 0 or 1 elements have been returned 628 * by {@link #next()}. 629 */ 630 public boolean isFirst() 631 { 632 if (first == null || first.booleanValue()) 633 { 634 return true; 635 } 636 return false; 637 } 638 639 /** 640 * Returns true if the last element returned by {@link #next()} 641 * is the last element available in the iterator being managed 642 * which satisfies any/all {@link ActionCondition}s set for this 643 * instance. Otherwise, returns false. 644 */ 645 public boolean isLast() 646 { 647 return !hasNext(false); 648 } 649 650 /** 651 * Returns the result of {@link #isFirst}. Exists to allow $loop.this.first syntax. 652 */ 653 public boolean getFirst() 654 { 655 return isFirst(); 656 } 657 658 /** 659 * Returns the result of {@link #isLast}. Exists to allow $loop.this.last syntax. 660 */ 661 public boolean getLast() 662 { 663 return isLast(); 664 } 665 666 /** 667 * Returns true if there are more elements in the iterator 668 * being managed by this instance which satisfy all the 669 * {@link ActionCondition}s set for this instance. Returns 670 * false if there are no more valid elements available. 671 */ 672 public boolean hasNext() 673 { 674 return hasNext(true); 675 } 676 677 /** 678 * Returns the result of {@link #hasNext}. Exists to allow $loop.this.hasNext syntax. 679 */ 680 public boolean getHasNext() 681 { 682 return hasNext(false);//no need to pop, #foreach will always call hasNext() 683 } 684 685 // version that lets isLast check w/o popping this from the stack 686 private boolean hasNext(boolean popWhenDone) 687 { 688 // we don't if we've stopped 689 if (stopped) 690 { 691 return false; 692 } 693 // we're not stopped, so do we have a next cached? 694 if (this.next != null) 695 { 696 return true; 697 } 698 // try to get a next that satisfies the conditions 699 // if there isn't one, return false; if there is, return true 700 return cacheNext(popWhenDone); 701 } 702 703 // Tries to get a next that satisfies the conditions. 704 // Returns true if there is a next to get. 705 private boolean cacheNext(boolean popWhenDone) 706 { 707 // ok, let's see if we can get a next 708 if (!iterator.hasNext()) 709 { 710 if (popWhenDone) 711 { 712 // this iterator is done, pop it from the owner's stack 713 owner.pop(); 714 // and make sure we don't pop twice 715 stop(); 716 } 717 return false; 718 } 719 720 // ok, the iterator has more, but do they work for us? 721 this.next = iterator.next(); 722 if (conditions != null) 723 { 724 for (ActionCondition condition : conditions) 725 { 726 if (condition.matches(this.next)) 727 { 728 switch (condition.action) 729 { 730 case EXCLUDE: 731 // recurse on to the next one 732 return cacheNext(popWhenDone); 733 case STOP: 734 stop(); 735 return false; 736 default: 737 throw new IllegalStateException("ActionConditions should never have a null Action"); 738 } 739 } 740 } 741 } 742 743 // ok, looks like we have a next that met all the conditions 744 shiftSynced(); 745 return true; 746 } 747 748 private void shiftSynced() 749 { 750 if (synced != null) 751 { 752 for (SyncedIterator parallel : synced.values()) 753 { 754 parallel.shift(); 755 } 756 } 757 } 758 759 /** 760 * Returns {@code true} if this ManagedIterator has a sync'ed 761 * iterator with the specified name. 762 */ 763 public boolean isSyncedWith(String name) 764 { 765 if (synced == null) 766 { 767 return false; 768 } 769 return synced.containsKey(name); 770 } 771 772 /** 773 * Returns the parallel value from the specified sync'ed iterator. 774 * If no sync'ed iterator exists with that name or that iterator 775 * is finished, this will return {@code null}. 776 */ 777 public Object get(String name) 778 { 779 if (synced == null) 780 { 781 return null; 782 } 783 SyncedIterator parallel = synced.get(name); 784 if (parallel == null) 785 { 786 return null; 787 } 788 return parallel.get(); 789 } 790 791 /** 792 * Returns the number of elements returned by {@link #next()} so far. 793 */ 794 public int getCount() 795 { 796 return count; 797 } 798 799 /** 800 * Returns the 0-based index of the current item. 801 */ 802 public int getIndex() 803 { 804 return count - 1; 805 } 806 807 /** 808 * Returns the next element that meets the set {@link ActionCondition}s 809 * (if any) in the iterator being managed. If there are none left, then 810 * this will throw a {@link NoSuchElementException}. 811 */ 812 public Object next() 813 { 814 // if no next is cached... 815 if (this.next == null) 816 { 817 // try to cache one 818 if (!cacheNext(true)) 819 { 820 // naughty! calling next() without knowing if there is one! 821 throw new NoSuchElementException("There are no more valid elements in this iterator"); 822 } 823 } 824 825 // if we haven't returned any elements, first = true 826 if (first == null) 827 { 828 first = Boolean.TRUE; 829 } 830 // or if we've only returned one, first = false 831 else if (first.booleanValue()) 832 { 833 first = Boolean.FALSE; 834 } 835 // update the number of iterations made 836 count++; 837 838 // get the cached next value 839 Object value = this.next; 840 // clear the cache 841 this.next = null; 842 // return the no-longer-cached value 843 return value; 844 } 845 846 /** 847 * This operation is unsupported. 848 */ 849 public void remove() 850 { 851 // at this point, i don't see any use for this, so... 852 throw new UnsupportedOperationException("remove is not currently supported"); 853 } 854 855 /** 856 * Stops this iterator from doing any further iteration. 857 */ 858 public void stop() 859 { 860 this.stopped = true; 861 this.next = null; 862 } 863 864 /** 865 * Directs this instance to completely exclude 866 * any elements equal to the specified Object. 867 * @return This same {@link ManagedIterator} instance 868 */ 869 public ManagedIterator exclude(Object compare) 870 { 871 return condition(new ActionCondition(Action.EXCLUDE, new Equals(compare))); 872 } 873 874 875 /** 876 * Directs this instance to stop iterating immediately prior to 877 * any element equal to the specified Object. 878 * @return This same {@link ManagedIterator} instance 879 */ 880 public ManagedIterator stop(Object compare) 881 { 882 return condition(new ActionCondition(Action.STOP, new Equals(compare))); 883 } 884 885 /** 886 * Adds a new {@link ActionCondition} for this instance to check 887 * against the elements in the iterator being managed. 888 * @return This same {@link ManagedIterator} instance 889 */ 890 public ManagedIterator condition(ActionCondition condition) 891 { 892 if (condition == null) 893 { 894 return null; 895 } 896 if (conditions == null) 897 { 898 conditions = new ArrayList<ActionCondition>(); 899 } 900 conditions.add(condition); 901 return this; 902 } 903 904 /** 905 * <p>Adds another iterator to be kept in sync with the one 906 * being managed by this instance. The values of the parallel 907 * iterator can be retrieved from the LoopTool under the 908 * name s"synced" (e.g. $loop.synched or $loop.get('synced')) 909 * and are automatically updated for each iteration by this instance. 910 * </p><p><b>NOTE</b>: if you are sync'ing multiple iterators 911 * with the same managed iterator, you must use 912 * {@link #sync(Object,String)} or else your the later iterators 913 * will simply replace the earlier ones under the default 914 * 'synced' key.</p> 915 * 916 * @return This same {@link ManagedIterator} instance 917 * @see SyncedIterator 918 * @see #get(String) 919 */ 920 public ManagedIterator sync(Object iterable) 921 { 922 return sync(iterable, "synced"); 923 } 924 925 /** 926 * Adds another iterator to be kept in sync with the one 927 * being managed by this instance. The values of the parallel 928 * iterator can be retrieved from the LoopTool under the 929 * name specified here (e.g. $loop.name or $loop.get('name')) 930 * and are automatically updated for each iteration by this instance. 931 * 932 * @return This same {@link ManagedIterator} instance 933 * @see SyncedIterator 934 * @see #get(String) 935 */ 936 public ManagedIterator sync(Object iterable, String name) 937 { 938 Iterator parallel = LoopTool.getIterator(iterable); 939 if (parallel == null) 940 { 941 return null; 942 } 943 if (synced == null) 944 { 945 synced = new HashMap<String,SyncedIterator>(); 946 } 947 synced.put(name, new SyncedIterator(parallel)); 948 return this; 949 } 950 951 @Override 952 public String toString() 953 { 954 return ManagedIterator.class.getSimpleName()+':'+getName(); 955 } 956 } 957 958 /** 959 * Represents an automatic action taken by a {@link ManagedIterator} 960 * when a {@link Condition} is satisfied by the subsequent element. 961 */ 962 public static enum Action 963 { 964 EXCLUDE, STOP; 965 } 966 967 /** 968 * Composition class which associates an {@link Action} and {@link Condition} 969 * for a {@link ManagedIterator}. 970 */ 971 public static class ActionCondition 972 { 973 protected Condition condition; 974 protected Action action; 975 976 public ActionCondition(Action action, Condition condition) 977 { 978 if (condition == null || action == null) 979 { 980 throw new IllegalArgumentException("Condition and Action must both not be null"); 981 } 982 this.condition = condition; 983 this.action = action; 984 } 985 986 /** 987 * Returns true if the specified value meets the set {@link Condition} 988 */ 989 public boolean matches(Object value) 990 { 991 return condition.test(value); 992 } 993 } 994 995 /** 996 * Represents a function into which a {@link ManagedIterator} can 997 * pass it's next element to see if an {@link Action} should be taken. 998 */ 999 public static interface Condition 1000 { 1001 public boolean test(Object value); 1002 } 1003 1004 /** 1005 * Base condition class for conditions (assumption here is that 1006 * conditions are all comparative. Not much else makes sense to me 1007 * for this context at this point. 1008 */ 1009 public static abstract class Comparison implements Condition 1010 { 1011 protected Object compare; 1012 1013 public Comparison(Object compare) 1014 { 1015 if (compare == null) 1016 { 1017 throw new IllegalArgumentException("Condition must have something to compare to"); 1018 } 1019 this.compare = compare; 1020 } 1021 } 1022 1023 /** 1024 * Simple condition that checks elements in the iterator 1025 * for equality to a specified Object. 1026 */ 1027 public static class Equals extends Comparison 1028 { 1029 public Equals(Object compare) 1030 { 1031 super(compare); 1032 } 1033 1034 public boolean test(Object value) 1035 { 1036 if (value == null) 1037 { 1038 return false; 1039 } 1040 if (compare.equals(value)) 1041 { 1042 return true; 1043 } 1044 if (value.getClass().equals(compare.getClass())) 1045 { 1046 // no point in going on to string comparison 1047 // if the classes are the same 1048 return false; 1049 } 1050 // compare them as strings as a last resort 1051 return String.valueOf(value).equals(String.valueOf(compare)); 1052 } 1053 } 1054 1055 1056 /** 1057 * Simple wrapper to make it easy to keep an arbitray Iterator 1058 * in sync with a {@link ManagedIterator}. 1059 */ 1060 public static class SyncedIterator 1061 { 1062 private Iterator iterator; 1063 private Object current; 1064 1065 public SyncedIterator(Iterator iterator) 1066 { 1067 if (iterator == null) 1068 { 1069 // do we really care? perhaps we should just keep quiet... 1070 throw new NullPointerException("Cannot synchronize a null Iterator"); 1071 } 1072 this.iterator = iterator; 1073 } 1074 1075 /** 1076 * If the sync'ed iterator has any more values, 1077 * this sets the next() value as the current one. 1078 * If there are no more values, this sets the current 1079 * one to {@code null}. 1080 */ 1081 public void shift() 1082 { 1083 if (iterator.hasNext()) 1084 { 1085 current = iterator.next(); 1086 } 1087 else 1088 { 1089 current = null; 1090 } 1091 } 1092 1093 /** 1094 * Returns the currently parallel value, if any. 1095 */ 1096 public Object get() 1097 { 1098 return current; 1099 } 1100 } 1101 1102 }