001 /* 002 Copyright (C) 2003 Renaud Pawlak <renaud@aopsys.com>, 003 Laurent Martelli <laurent@aopsys.com> 004 005 This program is free software; you can redistribute it and/or modify 006 it under the terms of the GNU Lesser General Public License as 007 published by the Free Software Foundation; either version 2 of the 008 License, or (at your option) any later version. 009 010 This program is distributed in the hope that it will be useful, 011 but WITHOUT ANY WARRANTY; without even the implied warranty of 012 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 013 GNU Lesser General Public License for more details. 014 015 You should have received a copy of the GNU Lesser General Public License 016 along with this program; if not, write to the Free Software 017 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ 018 019 package org.objectweb.jac.core.rtti; 020 021 import java.lang.reflect.Array; 022 import java.lang.reflect.Field; 023 import java.lang.reflect.InvocationTargetException; 024 import java.util.Arrays; 025 import java.util.Collection; 026 import java.util.List; 027 import java.util.Map; 028 import java.util.Vector; 029 import org.apache.log4j.Logger; 030 import org.objectweb.jac.util.WrappedThrowableException; 031 import java.util.ArrayList; 032 033 /** 034 * This class defines a meta item that corresponds to a 035 * <code>java.lang.reflect.Field</code> meta element that is of an 036 * array, a collection, or a map type.<p> 037 * 038 * @see java.util.Collection 039 * @see java.util.Map 040 * @see java.util.Arrays 041 * @see java.lang.reflect.Field 042 * 043 * @author Renaud Pawlak 044 * @author Laurent Martelli 045 */ 046 047 public class CollectionItem extends FieldItem { 048 049 static Logger logger = Logger.getLogger("rtti.collection"); 050 051 protected ClassItem componentType; 052 053 /** 054 * Default contructor to create a new collection item object.<p> 055 * 056 * @param delegate the <code>java.lang.reflect.Field</code> actual 057 * meta item */ 058 059 /** 060 * Creates a CollectionItem for a real field 061 * @param delegate the field the collection corresponds to 062 * @param parent the class the collection belongs to 063 */ 064 public CollectionItem(Field delegate, ClassItem parent) 065 throws InvalidDelegateException 066 { 067 super(delegate,parent); 068 type = delegate.getType(); 069 if (!RttiAC.isCollectionType(type)) { 070 throw new InvalidDelegateException(delegate,"not a collection type"); 071 } 072 } 073 074 /** 075 * Creates a CollectionItem for an expression field. 076 * @param name the expression 077 * @param path the list of FieldItem the expression is made of 078 * @param parent the class the collection belongs to 079 */ 080 public CollectionItem(String name, List path, ClassItem parent) 081 throws InvalidDelegateException 082 { 083 super(name,path,parent); 084 if (!RttiAC.isCollectionType(type)) { 085 throw new InvalidDelegateException(parent.getName()+"."+name,"Not a collection type"); 086 } 087 } 088 089 /** 090 * Creates a CollectionItem for a calculated field. 091 * @param name the name of the collection 092 * @param getter the getter of the calculated collection 093 * @param parent the class the collection belongs to 094 */ 095 public CollectionItem(String name, MethodItem getter, ClassItem parent) { 096 super(name,getter,parent); 097 } 098 099 MethodItem[] addingMethods = MethodItem.emptyArray; 100 101 /** 102 * Gets the method items that access this collection for adding.<p> 103 * 104 * @return value of addingMethods. 105 * @see #hasAdder() 106 */ 107 108 public MethodItem[] getAddingMethods() { 109 ((ClassItem)parent).buildFieldInfo(); 110 return addingMethods; 111 } 112 113 /** 114 * Returns true if the collection has at least one adding method. 115 * @see #getAddingMethods() 116 */ 117 public boolean hasAdder() { 118 ((ClassItem)parent).buildFieldInfo(); 119 return addingMethods.length>0; 120 } 121 122 MethodItem adder; 123 /** 124 * Returns <em>the</em> adder of the collection, or null if it has 125 * no adder. 126 */ 127 public MethodItem getAdder() { 128 ((ClassItem)parent).buildFieldInfo(); 129 if (adder!=null) 130 return adder; 131 else if (addingMethods.length>0) 132 return addingMethods[0]; 133 else if (isExpression) 134 return ((CollectionItem)getPathTop()).getAdder(); 135 else 136 return null; 137 } 138 /** 139 * Sets <em>the</em> adder of the collection. 140 * @param adder the adder 141 */ 142 public void setAdder(MethodItem adder) { 143 this.adder = adder; 144 } 145 146 /** 147 * Sets the methods that access this collection for adding.<p> 148 * 149 * @param addingMethods value to assign to addingMethods. 150 */ 151 152 public void setAddingMethods( MethodItem[] addingMethods ) { 153 this.addingMethods = addingMethods; 154 } 155 156 /** 157 * Adds a new adding method for this field.<p> 158 * 159 * @param addingMethod the method to add 160 */ 161 162 public void addAddingMethod(MethodItem addingMethod) { 163 if (addingMethods.length == 0) { 164 addingMethods = new MethodItem[] { addingMethod }; 165 } else { 166 MethodItem[] tmp = new MethodItem[addingMethods.length + 1]; 167 System.arraycopy(addingMethods, 0, tmp, 0, addingMethods.length); 168 tmp[addingMethods.length] = addingMethod; 169 addingMethods = tmp; 170 if (getLongName().startsWith("org.objectweb.jac.")) 171 logger.debug("Adders for "+getLongName()+": "+Arrays.asList(tmp)); 172 else 173 logger.warn("Adders for "+getLongName()+": "+Arrays.asList(tmp)); 174 } 175 } 176 177 /** 178 * Returns the actual collection item. In the case of an expression 179 * field, this is the last element of the path, otherwise it is the 180 * field itself. 181 */ 182 public CollectionItem getCollection() { 183 if (isExpression) { 184 return (CollectionItem)getPathTop(); 185 } else { 186 return this; 187 } 188 } 189 190 /** 191 * Returns the type of the objects contained in this collection. 192 * @return the type of objects in this collection or null if it is 193 * undefined. 194 */ 195 public ClassItem getComponentType() { 196 ((ClassItem)parent).buildFieldInfo(); 197 if (componentType!=null) 198 return componentType; 199 if (isArray()) { 200 componentType = ClassRepository.get().getClass(getType().getComponentType()); 201 } else { 202 203 if (isExpression) { 204 componentType = ((CollectionItem)getPathTop()).getComponentType(); 205 } else { 206 MethodItem adder = getAdder(); 207 if (adder!=null && adder.getParameterCount()>0) { 208 if (isMap()) { 209 if (isIndex()) 210 componentType = adder.getParameterTypeItem( 211 adder.getParameterCount()-1); 212 else { 213 componentType = 214 ClassRepository.get().getClass( 215 "java.util.Map$Entry"); 216 } 217 } else { 218 if (adder.getParameterCount()==1) 219 componentType = adder.getParameterTypeItem(0); 220 else if (adder.getParameterCount()==2) { 221 int itemArg = adder.getCollectionItemArgument(); 222 if (itemArg!=-1) 223 componentType = adder.getParameterTypeItem(itemArg); 224 } 225 if (componentType==null) 226 logger.warn( 227 "Cannot determine component type of "+getName()+ 228 " from adder "+adder.getFullName()); 229 } 230 } 231 } 232 } 233 234 if (componentType==null) { 235 if (!isMap() || isIndex()) 236 logger.warn("Component type of "+this+" is null"); 237 if (adder==null && !isCalculated()) 238 logger.warn("no adder fo "+this+" "+getParent()); 239 } 240 return componentType; 241 } 242 243 /** 244 * Sets the component type of the collection. 245 * @param componentType the component type 246 */ 247 public void setComponentType(ClassItem componentType) { 248 this.componentType = componentType; 249 } 250 251 public boolean isAddingMethod(MethodItem method) { 252 ((ClassItem)parent).buildFieldInfo(); 253 return Arrays.asList(addingMethods).contains(method); 254 } 255 256 /** 257 * Adds an item to the collection, using the adder method if it has one. 258 * @param substance object on which to add 259 * @param value object to add to the collection 260 */ 261 public final void addThroughAdder(Object substance, Object value) { 262 logger.debug("addThroughAdder "+substance+"."+getName()+","+value); 263 MethodItem adder = getAdder(); 264 if (adder!=null) { 265 try { 266 //Log.trace("rtti.field",this+": invoking "+adders[i]); 267 adder.invoke(substance,new Object[] { value }); 268 return; 269 } catch (WrappedThrowableException e) { 270 Throwable target = e.getWrappedThrowable(); 271 if (target instanceof InvocationTargetException) 272 throw e; 273 else 274 logger.error("addThroughAdder "+substance+"."+getName()+","+value,e); 275 } 276 } 277 278 logger.warn("No adder for collection " + this); 279 add(substance,value,null); 280 } 281 282 283 public final void putThroughAdder(Object substance, Object value, Object key) { 284 logger.debug("putThroughAdder "+substance+"."+getName()+","+key+"->"+value); 285 MethodItem adder = getAdder(); 286 if (adder!=null) { 287 try { 288 //Log.trace("rtti.field",this+": invoking "+adders[i]); 289 if (adder.getParameterCount()==2) 290 adder.invoke(substance,new Object[] { key, value }); 291 else if (adder.getParameterCount()==1) 292 adder.invoke(substance,new Object[] { value }); 293 else 294 throw new RuntimeException("putThroughAdder("+substance+","+value+","+key+ 295 ") :Wrong number off parameters for adder "+adder); 296 return; 297 } catch (WrappedThrowableException e) { 298 Throwable target = e.getWrappedThrowable(); 299 if (target instanceof InvocationTargetException) 300 throw e; 301 else 302 logger.error("putThroughAdder "+substance+"."+getName()+","+value,e); 303 } 304 } 305 306 logger.warn("No adder for collection " + this); 307 add(substance,value,key); 308 } 309 310 MethodItem[] removingMethods = MethodItem.emptyArray; 311 312 /** 313 * Gets the methods that access this collection for removing.<p> 314 * 315 * @return value of removingMethods. 316 */ 317 318 public MethodItem[] getRemovingMethods() { 319 ((ClassItem)parent).buildFieldInfo(); 320 return removingMethods; 321 } 322 323 /** 324 * Returns true if the collection has at least one removing method. 325 * @see #getRemovingMethods() 326 */ 327 public boolean hasRemover() { 328 ((ClassItem)parent).buildFieldInfo(); 329 return removingMethods.length>0; 330 } 331 332 MethodItem remover; 333 /** 334 * Returns <em>the</em> remover of the collection, or null if it has 335 * no remover. 336 */ 337 public MethodItem getRemover() { 338 ((ClassItem)parent).buildFieldInfo(); 339 if (remover!=null) 340 return remover; 341 else if (removingMethods.length>0) 342 return removingMethods[0]; 343 else if (isExpression) 344 return ((CollectionItem)getPathTop()).getRemover(); 345 else 346 return null; 347 } 348 349 /** 350 * Sets <em>the</em> remover of the collection. 351 * @param remover the remover 352 */ 353 public void setRemover(MethodItem remover) { 354 this.remover = remover; 355 } 356 357 /** 358 * Sets the methods that access this collection for removing.<p> 359 * 360 * @param removingMethods value to assign to removingMethods. 361 */ 362 363 public void setRemovingMethods( MethodItem[] removingMethods ) { 364 this.removingMethods = removingMethods; 365 } 366 367 /** 368 * Adds a new removing method for this field.<p> 369 * 370 * @param removingMethod the method to add 371 */ 372 373 public void addRemovingMethod( MethodItem removingMethod ) { 374 if (removingMethods == null) { 375 removingMethods = new MethodItem[] { removingMethod }; 376 } else { 377 MethodItem[] tmp = new MethodItem[removingMethods.length + 1]; 378 System.arraycopy(removingMethods, 0, tmp, 0, removingMethods.length); 379 tmp[removingMethods.length] = removingMethod; 380 removingMethods = tmp; 381 } 382 } 383 384 public boolean isRemovingMethod(MethodItem method) { 385 ((ClassItem)parent).buildFieldInfo(); 386 return Arrays.asList(removingMethods).contains(method); 387 } 388 389 /** 390 * Clears all the methods that has been set to be removers or 391 * adders for this collection item. 392 * 393 * @see #addAddingMethod(MethodItem) 394 * @see #setAddingMethods(MethodItem[]) 395 * @see MethodItem#removeAddedCollection(CollectionItem) 396 * @see #addRemovingMethod(MethodItem) 397 * @see #setRemovingMethods(MethodItem[]) 398 * @see MethodItem#removeRemovedCollection(CollectionItem) */ 399 400 public void clearMethods() { 401 super.clearMethods(); 402 if (removingMethods != null) { 403 for (int i=0; i<removingMethods.length; i++) { 404 removingMethods[i].removeRemovedCollection(this); 405 } 406 removingMethods = null; 407 } 408 if (addingMethods != null) { 409 for (int i=0; i<addingMethods.length; i++) { 410 addingMethods[i].removeAddedCollection(this); 411 } 412 addingMethods = null; 413 } 414 } 415 416 /** 417 * Clears the collection on the given object.<p> 418 * 419 * For maps and collections, it delegates to the <code>clear</code> 420 * method. For array, it resets the adding index so that added 421 * elements will crush existing ones.<p> 422 * 423 * @param substance the object where to clean the collection 424 */ 425 426 public void clear(Object substance) { 427 logger.debug(this+".clear("+substance+")"); 428 Field f = (Field) delegate; 429 430 try { 431 Object collection = f.get(substance); 432 logger.debug("collection="+System.identityHashCode(collection)); 433 if (collection!=null) { 434 logger.debug("type="+collection.getClass().getName()); 435 if (collection instanceof Collection) { 436 ((Collection)collection).clear(); 437 } else if (collection instanceof Map) { 438 ((Map)collection).clear(); 439 } else if (getType().isArray()) { 440 f.set(substance, Array.newInstance(getType().getComponentType(),0)); 441 } 442 } 443 } catch (Exception e) { 444 logger.error("clear failed for "+this+" on "+substance,e); 445 } 446 } 447 448 /** 449 * Tells if this collection is actually an array.<p> 450 * 451 * @return true if an array 452 */ 453 454 public boolean isArray() { 455 return getType()!=null && getType().isArray(); 456 } 457 458 /** 459 * Gets the collection object represented by this collection 460 * item. 461 * 462 * <p>It returns a <code>java.util.Collection</code> representation of 463 * the actual collection (i.e. either a <code>Collection</code>, a 464 * <code>Map</code>, or an array). 465 * 466 * <p>The programmer should rather use the methods that gets the 467 * actual collection by using the accessor for this collection if 468 * any since it allows possible aspects applications if needed. 469 * 470 * @return the actual object collection representation 471 * @see #getActualCollectionThroughAccessor(Object) 472 * @see #toCollection(Object) */ 473 474 public Collection getActualCollection(Object substance) { 475 Object value = null; 476 477 try { 478 value = ((Field)delegate).get(substance); 479 } catch (Exception e) { 480 logger.error("getActualCollection("+getName()+") failed",e); 481 return null; 482 } 483 return toCollection(value); 484 } 485 486 /** 487 * Gets the collection object represented by this collection item 488 * by using the accessor defined in the class containing this 489 * collection if any. 490 * 491 * <p>Use this method to be sure that all the aspects will be 492 * applied to the substance when the collection is retrieved. 493 * 494 * <p>It returns a <code>java.util.Collection</code> representation of 495 * the actual collection (i.e. either a <code>Collection</code>, a 496 * <code>Map</code>, or an array). 497 * 498 * @return the actual object collection representation 499 * @see #getActualCollection(Object) 500 * @see #toCollection(Object) 501 */ 502 public Collection getActualCollectionThroughAccessor(Object substance) { 503 Object value = getThroughAccessor(substance); 504 return value!=null ? toCollection(value) : new ArrayList(); 505 } 506 507 /** 508 * A useful method to convert any kind of collection to a 509 * <code>java.util.Collection</code> compatible instance. 510 * 511 * <p>Supported converted value types are 512 * <code>java.util.Collection</code>, <code>java.util.Map</code>, 513 * and Java arrays. 514 * 515 * @param value an object of one of the supported types 516 * @return the collection compliant converted value */ 517 518 public Collection toCollection(Object value) { 519 if (value == null) return null; 520 521 if (value instanceof Collection) { 522 return (Collection)value; 523 } else if (value instanceof Map) { 524 if (isIndex()) 525 return ((Map)value).values(); 526 else 527 return ((Map)value).entrySet(); 528 } else if (value.getClass().isArray()) { 529 if (value.getClass().getComponentType()==Object.class) { 530 return Arrays.asList( (Object[]) value ); 531 } else { 532 int length = Array.getLength(value); 533 Vector result = new Vector(length); 534 for (int i=0; i<length; i++) { 535 result.add(Array.get(value,i)); 536 } 537 return result; 538 } 539 } 540 return null; 541 } 542 543 /** 544 * Adds an item to the collection.<p> 545 * 546 * The <code>extraInfos</code> param must represent a key if the 547 * collection is an hastable. If the collection is a 548 * <code>java.util</code> collection or if it is an array, the 549 * extra informations can be null (in this case, the new item is 550 * added at the end of the collection), and they can be not 551 * null. In this case, it must be an <code>Integer</code> instance 552 * that represents the index of the new item within the 553 * collection (must be a list in this case).<p> 554 * 555 * @param substance the collection where to add the new item 556 * @param newItem the item to add 557 * @param extraInfos this optional parameter must be used when the 558 * collection need some other information when adding a item to it 559 * as, for instance the key for a map or the index for a list 560 */ 561 public void add(Object substance, Object newItem, Object extraInfos) { 562 563 Field f = (Field) delegate; 564 565 try { 566 if (isSet() || isList()) { 567 // Set and List 568 if ( extraInfos == null ) { 569 ((Collection)f.get(substance)).add(newItem); 570 } else { 571 ((List)f.get(substance)).add( 572 ((Integer)extraInfos).intValue(), newItem); 573 } 574 } else if (isMap()) { 575 // Map 576 ((Map)f.get(substance)).put(extraInfos, newItem); 577 } else if (isArray()) { 578 // Arrays 579 // THIS IS BUGGED !!! 580 if (extraInfos == null) { 581 Object oldArray = f.get(substance); 582 int index = 0; 583 if (oldArray != null) { 584 index = Array.getLength(oldArray); 585 } 586 Object newArray; 587 f.set( substance, 588 newArray = Array.newInstance( 589 getType().getComponentType(), index+1 ) ); 590 591 if (oldArray != null) { 592 for(int i=0; i<index; i++) { 593 Array.set(newArray, i, Array.get(oldArray, i)); 594 } 595 } 596 597 Array.set(newArray, index, newItem); 598 599 } else { 600 ((Object[])f.get(substance))[((Integer)extraInfos).intValue()] = newItem; 601 //arrayAddingIndex = ((Integer)extraInfos).intValue() + 1; 602 } 603 } 604 } catch ( Exception e ) { 605 logger.error("add "+substance+"."+getName()+" "+newItem+","+extraInfos,e); 606 } 607 } 608 609 /** 610 * Removes an item from the collection, using the remover method if it has one. 611 * @param substance object on which to remove 612 * @param item object to remove from the collection 613 */ 614 public void removeThroughRemover(Object substance, Object item) { 615 ((ClassItem)parent).buildFieldInfo(); 616 MethodItem remover = getRemover(); 617 if (remover!=null) { 618 remover.invoke(substance,new Object[] {item}); 619 } else { 620 logger.warn("No remover for collection "+this); 621 remove(substance,item,null); 622 } 623 } 624 625 /** 626 * Removes an item from the collection.<p> 627 * 628 * The <code>extraInfos</code> param must represent a key if the 629 * collection is an hastable. If the collection is a 630 * <code>java.util</code> collection or if it is an array, the 631 * extra informations can be null (in this case, the new item is 632 * added at the end of the collection), and they can be not 633 * null. In this case, it must be an <code>Integer</code> instance 634 * that represents the index of the new item within the 635 * collection (must be a list in this case).<p> 636 * 637 * @param substance the collection where to remove the new item 638 * @param item the item to remove (can be null if extra infos are 639 * not null) 640 * @param extraInfos this optional parameter must be used when the 641 * collection need some other information when adding a item to it 642 * as, for instance the key for a map, or the index in a list */ 643 644 public void remove(Object substance, Object item, Object extraInfos) { 645 Field f = (Field)delegate; 646 647 try { 648 649 if (Collection.class.isAssignableFrom(getType())) { 650 // Set and List 651 if (extraInfos == null) { 652 ((Collection)f.get(substance)).remove(item); 653 } else { 654 int index = ((Integer)extraInfos).intValue(); 655 ((List)f.get(substance)).remove(index); 656 } 657 } else if (Map.class.isAssignableFrom(getType())) { 658 // Map 659 ((Map)f.get(substance)).remove(extraInfos); 660 } else if (getType().isArray()) { 661 // Arrays 662 // THIS IS BUGGED !!! 663 if (extraInfos == null) { 664 Object oldArray = f.get(substance); 665 int index = 0; 666 if (oldArray != null) { 667 index = Array.getLength(oldArray); 668 } 669 Object newArray; 670 f.set( substance, 671 newArray = Array.newInstance( 672 getType().getComponentType(), index-1 ) ); 673 674 if (oldArray != null) { 675 for( int i=0; i<index; i++) { 676 Array.set(newArray, i, Array.get(oldArray,i)); 677 } 678 } 679 680 //Array.set( newArray, index, newItem ); 681 682 } else { 683 //((Object[])f.get( substance ))[((Integer)extraInfos).intValue()] = item; 684 //arrayAddingIndex = ((Integer)extraInfos).intValue() + 1; 685 } 686 } 687 } catch (Exception e) { 688 logger.error("remove "+substance+"."+getName()+" "+item+","+extraInfos,e); 689 } 690 691 } 692 693 /** 694 * Tells wether an object's collection contains an given object 695 * @param substance 696 * @param object the object to search 697 * @return true if substance.collection.contains(object) 698 */ 699 public boolean contains(Object substance, Object object) { 700 if (!isArray()) { 701 Collection collection = (Collection)getThroughAccessor(substance); 702 return collection.contains(object); 703 } else { 704 throw new RuntimeException( 705 "CollectionItem.contains(substance,object) is not implemented for arrays"); 706 } 707 } 708 709 public Object getMap(Object substance, Object key) { 710 if (!isMap()) { 711 throw new RuntimeException("Cannot call getMap() on "+this); 712 } 713 try { 714 return getMap(substance).get(key); 715 } catch(IllegalAccessException e) { 716 logger.error("getMap "+substance+"."+getName()+" "+key,e); 717 } 718 return null; 719 } 720 721 protected Map getMap(Object substance) throws IllegalAccessException { 722 return (Map)((Field)delegate).get(substance); 723 } 724 725 /** 726 * Tells if this collection item is compliant with a 727 * <code>java.util.List</code> interface. 728 * 729 * @return true if compliant */ 730 731 public boolean isList() { 732 return List.class.isAssignableFrom(getType()); 733 } 734 735 /** 736 * Tells if this collection item is compliant with a 737 * <code>java.util.Map</code> interface. 738 * 739 * @return true if compliant */ 740 741 public boolean isMap() { 742 return Map.class.isAssignableFrom(getType()); 743 } 744 745 public boolean isIndex() { 746 return RttiAC.isIndex(this); 747 } 748 749 /** 750 * Tells if this collection item is compliant with a 751 * <code>java.util.Set</code> interface. 752 * 753 * @return true if compliant */ 754 755 public boolean isSet() { 756 return java.util.Set.class.isAssignableFrom(getType()); 757 } 758 759 /** 760 * Always returns false for collections (maybe not very 761 * semantically clear). 762 * 763 * @return false */ 764 765 public boolean isPrimitive() { 766 return false; 767 } 768 769 /** 770 * Always returns false for collections (maybe not very 771 * semantically clear). 772 * 773 * @return false */ 774 775 public boolean isReference() { 776 return false; 777 } 778 779 public FieldItem clone(ClassItem parent) { 780 CollectionItem clone = null; 781 try { 782 if (isCalculated) 783 clone = new CollectionItem(name,getter,parent); 784 else 785 clone = new CollectionItem((Field)delegate,parent); 786 clone.setAdder(adder); 787 clone.setRemover(remover); 788 } catch(Exception e) { 789 logger.error("Failed to clone collection "+this); 790 } 791 return clone; 792 } 793 794 public static final CollectionItem[] emptyArray = new CollectionItem[0]; 795 }