001 /** 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017 package org.apache.xbean.recipe; 018 019 import java.lang.reflect.Field; 020 import java.lang.reflect.InvocationTargetException; 021 import java.lang.reflect.Method; 022 import java.lang.reflect.Modifier; 023 import java.lang.reflect.Type; 024 import java.util.ArrayList; 025 import java.util.Arrays; 026 import java.util.Collections; 027 import java.util.EnumSet; 028 import java.util.LinkedHashMap; 029 import java.util.List; 030 import java.util.Map; 031 import java.util.Set; 032 import org.apache.xbean.recipe.ReflectionUtil.*; 033 034 /** 035 * @version $Rev: 6688 $ $Date: 2005-12-29T02:08:29.200064Z $ 036 */ 037 public class ObjectRecipe extends AbstractRecipe { 038 private String typeName; 039 private Class typeClass; 040 private String factoryMethod; 041 private List<String> constructorArgNames; 042 private List<Class<?>> constructorArgTypes; 043 private final LinkedHashMap<Property,Object> properties = new LinkedHashMap<Property,Object>(); 044 private final EnumSet<Option> options = EnumSet.of(Option.FIELD_INJECTION); 045 private final Map<String,Object> unsetProperties = new LinkedHashMap<String,Object>(); 046 047 public ObjectRecipe(Class typeClass) { 048 this(typeClass, null, null, null, null); 049 } 050 051 public ObjectRecipe(Class typeClass, String factoryMethod) { 052 this(typeClass, factoryMethod, null, null, null); 053 } 054 055 public ObjectRecipe(Class typeClass, Map<String,Object> properties) { 056 this(typeClass, null, null, null, properties); 057 } 058 059 public ObjectRecipe(Class typeClass, String[] constructorArgNames) { 060 this(typeClass, null, constructorArgNames, null, null); 061 } 062 063 public ObjectRecipe(Class typeClass, String[] constructorArgNames, Class[] constructorArgTypes) { 064 this(typeClass, null, constructorArgNames, constructorArgTypes, null); 065 } 066 067 public ObjectRecipe(Class type, String factoryMethod, String[] constructorArgNames) { 068 this(type, factoryMethod, constructorArgNames, null, null); 069 } 070 071 public ObjectRecipe(Class type, String factoryMethod, String[] constructorArgNames, Class[] constructorArgTypes) { 072 this(type, factoryMethod, constructorArgNames, constructorArgTypes, null); 073 } 074 075 public ObjectRecipe(Class typeClass, String factoryMethod, String[] constructorArgNames, Class[] constructorArgTypes, Map<String,Object> properties) { 076 this.typeClass = typeClass; 077 this.factoryMethod = factoryMethod; 078 this.constructorArgNames = constructorArgNames != null ? Arrays.asList(constructorArgNames) : null; 079 this.constructorArgTypes = constructorArgTypes != null ? Arrays.<Class<?>>asList(constructorArgTypes) : null; 080 if (properties != null) { 081 setAllProperties(properties); 082 } 083 } 084 085 public ObjectRecipe(String typeName) { 086 this(typeName, null, null, null, null); 087 } 088 089 public ObjectRecipe(String typeName, String factoryMethod) { 090 this(typeName, factoryMethod, null, null, null); 091 } 092 093 public ObjectRecipe(String typeName, Map<String,Object> properties) { 094 this(typeName, null, null, null, properties); 095 } 096 097 public ObjectRecipe(String typeName, String[] constructorArgNames) { 098 this(typeName, null, constructorArgNames, null, null); 099 } 100 101 public ObjectRecipe(String typeName, String[] constructorArgNames, Class[] constructorArgTypes) { 102 this(typeName, null, constructorArgNames, constructorArgTypes, null); 103 } 104 105 public ObjectRecipe(String typeName, String factoryMethod, String[] constructorArgNames) { 106 this(typeName, factoryMethod, constructorArgNames, null, null); 107 } 108 109 public ObjectRecipe(String typeName, String factoryMethod, String[] constructorArgNames, Class[] constructorArgTypes) { 110 this(typeName, factoryMethod, constructorArgNames, constructorArgTypes, null); 111 } 112 113 public ObjectRecipe(String typeName, String factoryMethod, String[] constructorArgNames, Class[] constructorArgTypes, Map<String,Object> properties) { 114 this.typeName = typeName; 115 this.factoryMethod = factoryMethod; 116 this.constructorArgNames = constructorArgNames != null ? Arrays.asList(constructorArgNames) : null; 117 this.constructorArgTypes = constructorArgTypes != null ? Arrays.<Class<?>>asList(constructorArgTypes) : null; 118 if (properties != null) { 119 setAllProperties(properties); 120 } 121 } 122 123 public void allow(Option option){ 124 options.add(option); 125 } 126 127 public void disallow(Option option){ 128 options.remove(option); 129 } 130 131 public Set<Option> getOptions() { 132 return Collections.unmodifiableSet(options); 133 } 134 135 public List<String> getConstructorArgNames() { 136 return constructorArgNames; 137 } 138 139 public void setConstructorArgNames(String[] constructorArgNames) { 140 this.constructorArgNames = constructorArgNames != null ? Arrays.asList(constructorArgNames) : null; 141 } 142 143 public void setConstructorArgNames(List<String> constructorArgNames) { 144 this.constructorArgNames = constructorArgNames; 145 } 146 147 public List<Class<?>> getConstructorArgTypes() { 148 return constructorArgTypes; 149 } 150 151 public void setConstructorArgTypes(Class[] constructorArgTypes) { 152 this.constructorArgTypes = constructorArgTypes != null ? Arrays.<Class<?>>asList(constructorArgTypes) : null; 153 } 154 155 public void setConstructorArgTypes(List<? extends Class<?>> constructorArgTypes) { 156 this.constructorArgTypes = new ArrayList<Class<?>>(constructorArgTypes); 157 } 158 159 public String getFactoryMethod() { 160 return factoryMethod; 161 } 162 163 public void setFactoryMethod(String factoryMethod) { 164 this.factoryMethod = factoryMethod; 165 } 166 167 public Object getProperty(String name) { 168 Object value = properties.get(new Property(name)); 169 return value; 170 } 171 172 public Map<String, Object> getProperties() { 173 LinkedHashMap<String, Object> properties = new LinkedHashMap<String, Object>(); 174 for (Map.Entry<Property, Object> entry : this.properties.entrySet()) { 175 properties.put(entry.getKey().name, entry.getValue()); 176 } 177 return properties; 178 } 179 180 public void setProperty(String name, Object value) { 181 setProperty(new Property(name), value); 182 } 183 184 public void setFieldProperty(String name, Object value){ 185 setProperty(new FieldProperty(name), value); 186 options.add(Option.FIELD_INJECTION); 187 } 188 189 public void setMethodProperty(String name, Object value){ 190 setProperty(new SetterProperty(name), value); 191 } 192 193 public void setAutoMatchProperty(String type, Object value){ 194 setProperty(new AutoMatchProperty(type), value); 195 } 196 197 public void setCompoundProperty(String name, Object value) { 198 setProperty(new CompoundProperty(name), value); 199 } 200 201 private void setProperty(Property key, Object value) { 202 if (value instanceof UnsetPropertiesRecipe) { 203 allow(Option.IGNORE_MISSING_PROPERTIES); 204 } 205 properties.put(key, value); 206 } 207 208 209 public void setAllProperties(Map<?,?> map) { 210 if (map == null) throw new NullPointerException("map is null"); 211 for (Map.Entry<?, ?> entry : map.entrySet()) { 212 String name = (String) entry.getKey(); 213 Object value = entry.getValue(); 214 setProperty(name, value); 215 } 216 } 217 218 public Map<String,Object> getUnsetProperties() { 219 return unsetProperties; 220 } 221 222 public List<Recipe> getNestedRecipes() { 223 List<Recipe> nestedRecipes = new ArrayList<Recipe>(properties.size()); 224 for (Object o : properties.values()) { 225 if (o instanceof Recipe) { 226 Recipe recipe = (Recipe) o; 227 nestedRecipes.add(recipe); 228 } 229 } 230 return nestedRecipes; 231 } 232 233 public List<Recipe> getConstructorRecipes() { 234 // find the factory that will be used to create the class instance 235 Factory factory = findFactory(Object.class); 236 237 // if we are NOT using an instance factory to create the object 238 // (we have a factory method and it is not a static factory method) 239 if (factoryMethod != null && !(factory instanceof StaticFactory)) { 240 // only include recipes used in the construcor args 241 List<String> parameterNames = factory.getParameterNames(); 242 List<Recipe> nestedRecipes = new ArrayList<Recipe>(parameterNames.size()); 243 for (Map.Entry<Property, Object> entry : properties.entrySet()) { 244 if (parameterNames.contains(entry.getKey().name) && entry.getValue() instanceof Recipe) { 245 Recipe recipe = (Recipe) entry.getValue(); 246 nestedRecipes.add(recipe); 247 } 248 } 249 return nestedRecipes; 250 } else { 251 // when there is an instance factory all nested recipes are used in the constructor 252 return getNestedRecipes(); 253 } 254 } 255 256 public boolean canCreate(Type type) { 257 Class myType = getType(); 258 return RecipeHelper.isAssignable(type, myType) || RecipeHelper.isAssignable(type, myType); 259 } 260 261 protected Object internalCreate(Type expectedType, boolean lazyRefAllowed) throws ConstructionException { 262 unsetProperties.clear(); 263 264 // 265 // load the type class 266 Class typeClass = getType(); 267 268 // 269 // clone the properties so they can be used again 270 Map<Property,Object> propertyValues = new LinkedHashMap<Property,Object>(properties); 271 272 // 273 // create the instance 274 Factory factory = findFactory(expectedType); 275 Object[] parameters = extractConstructorArgs(propertyValues, factory); 276 Object instance = factory.create(parameters); 277 278 // 279 // add to execution context if name is specified 280 if (getName() != null) { 281 ExecutionContext.getContext().addObject(getName(), instance); 282 } 283 284 // 285 // set the properties 286 setProperties(propertyValues, instance, instance.getClass()); 287 288 // 289 // call instance factory method 290 291 // if we have a factory method name and did not find a static factory, 292 // then we have an instance factory 293 if (factoryMethod != null && !(factory instanceof StaticFactory)) { 294 // find the instance factory method 295 Method instanceFactory = ReflectionUtil.findInstanceFactory(instance.getClass(), factoryMethod, null); 296 297 try { 298 instance = instanceFactory.invoke(instance); 299 } catch (Exception e) { 300 Throwable t = e; 301 if (e instanceof InvocationTargetException) { 302 InvocationTargetException invocationTargetException = (InvocationTargetException) e; 303 if (invocationTargetException.getCause() != null) { 304 t = invocationTargetException.getCause(); 305 } 306 } 307 throw new ConstructionException("Error calling instance factory method: " + instanceFactory, t); 308 } 309 } 310 311 return instance; 312 } 313 314 public void setProperties(Object instance) throws ConstructionException { 315 unsetProperties.clear(); 316 317 // clone the properties so they can be used again 318 Map<Property,Object> propertyValues = new LinkedHashMap<Property,Object>(properties); 319 320 setProperties(propertyValues, instance, instance.getClass()); 321 } 322 323 public Class setStaticProperties() throws ConstructionException { 324 unsetProperties.clear(); 325 326 // load the type class 327 Class typeClass = getType(); 328 329 // verify that it is a class we can construct 330 if (!Modifier.isPublic(typeClass.getModifiers())) { 331 throw new ConstructionException("Class is not public: " + typeClass.getName()); 332 } 333 if (Modifier.isInterface(typeClass.getModifiers())) { 334 throw new ConstructionException("Class is an interface: " + typeClass.getName()); 335 } 336 if (Modifier.isAbstract(typeClass.getModifiers())) { 337 throw new ConstructionException("Class is abstract: " + typeClass.getName()); 338 } 339 340 // clone the properties so they can be used again 341 Map<Property,Object> propertyValues = new LinkedHashMap<Property,Object>(properties); 342 343 setProperties(propertyValues, null, typeClass); 344 345 return typeClass; 346 } 347 348 public Class getType() { 349 if (typeClass != null || typeName != null) { 350 Class type = typeClass; 351 if (type == null) { 352 try { 353 type = RecipeHelper.loadClass(typeName); 354 } catch (ClassNotFoundException e) { 355 throw new ConstructionException("Type class could not be found: " + typeName); 356 } 357 } 358 359 return type; 360 } 361 362 return null; 363 } 364 365 private void setProperties(Map<Property, Object> propertyValues, Object instance, Class clazz) { 366 // set remaining properties 367 for (Map.Entry<Property, Object> entry : RecipeHelper.prioritizeProperties(propertyValues)) { 368 Property propertyName = entry.getKey(); 369 Object propertyValue = entry.getValue(); 370 371 setProperty(instance, clazz, propertyName, propertyValue); 372 } 373 374 } 375 376 private void setProperty(Object instance, Class clazz, Property propertyName, Object propertyValue) { 377 378 List<Member> members = new ArrayList<Member>(); 379 try { 380 if (propertyName instanceof SetterProperty){ 381 List<Method> setters = ReflectionUtil.findAllSetters(clazz, propertyName.name, propertyValue, options); 382 for (Method setter : setters) { 383 MethodMember member = new MethodMember(setter); 384 members.add(member); 385 } 386 } else if (propertyName instanceof FieldProperty){ 387 FieldMember member = new FieldMember(ReflectionUtil.findField(clazz, propertyName.name, propertyValue, options)); 388 members.add(member); 389 } else if (propertyName instanceof AutoMatchProperty){ 390 MissingAccessorException noField = null; 391 if (options.contains(Option.FIELD_INJECTION)) { 392 List<Field> fieldsByType = null; 393 try { 394 fieldsByType = ReflectionUtil.findAllFieldsByType(clazz, propertyValue, options); 395 FieldMember member = new FieldMember(fieldsByType.iterator().next()); 396 members.add(member); 397 } catch (MissingAccessorException e) { 398 noField = e; 399 } 400 401 // if we got more then one matching field, that is an immidate error 402 if (fieldsByType != null && fieldsByType.size() > 1) { 403 List<String> matches = new ArrayList<String>(); 404 for (Field field : fieldsByType) { 405 matches.add(field.getName()); 406 } 407 throw new MissingAccessorException("Property of type " + propertyValue.getClass().getName() + " can be mapped to more then one field: " + matches, 0); 408 } 409 } 410 411 // if we didn't find any fields, try the setters 412 if (members.isEmpty()) { 413 List<Method> settersByType; 414 try { 415 settersByType = ReflectionUtil.findAllSettersByType(clazz, propertyValue, options); 416 MethodMember member = new MethodMember(settersByType.iterator().next()); 417 members.add(member); 418 } catch (MissingAccessorException noSetter) { 419 throw (noField == null || noSetter.getMatchLevel() > noField.getMatchLevel())? noSetter: noField; 420 } 421 422 // if we got more then one matching field, that is an immidate error 423 if (settersByType != null && settersByType.size() > 1) { 424 List<String> matches = new ArrayList<String>(); 425 for (Method setter : settersByType) { 426 matches.add(setter.getName()); 427 } 428 throw new MissingAccessorException("Property of type " + propertyValue.getClass().getName() + " can be mapped to more then one setter: " + matches, 0); 429 } 430 } 431 } else if (propertyName instanceof CompoundProperty) { 432 String[] names = propertyName.name.split("\\."); 433 for (int i = 0; i < names.length - 1; i++) { 434 Method getter = ReflectionUtil.findGetter(clazz, names[i], options); 435 if (getter != null) { 436 try { 437 instance = getter.invoke(instance); 438 clazz = instance.getClass(); 439 } catch (Exception e) { 440 Throwable t = e; 441 if (e instanceof InvocationTargetException) { 442 InvocationTargetException invocationTargetException = (InvocationTargetException) e; 443 if (invocationTargetException.getCause() != null) { 444 t = invocationTargetException.getCause(); 445 } 446 } 447 throw new ConstructionException("Error setting property: " + names[i], t); 448 } 449 } else { 450 throw new ConstructionException("No getter for " + names[i] + " property"); 451 } 452 } 453 List<Method> setters = ReflectionUtil.findAllSetters(clazz, names[names.length - 1], propertyValue, options); 454 for (Method setter : setters) { 455 MethodMember member = new MethodMember(setter); 456 members.add(member); 457 } 458 } else { 459 // add setter members 460 MissingAccessorException noSetter = null; 461 try { 462 List<Method> setters = ReflectionUtil.findAllSetters(clazz, propertyName.name, propertyValue, options); 463 for (Method setter : setters) { 464 MethodMember member = new MethodMember(setter); 465 members.add(member); 466 } 467 } catch (MissingAccessorException e) { 468 noSetter = e; 469 if (!options.contains(Option.FIELD_INJECTION)) { 470 throw noSetter; 471 } 472 } 473 474 if (options.contains(Option.FIELD_INJECTION)) { 475 try { 476 FieldMember member = new FieldMember(ReflectionUtil.findField(clazz, propertyName.name, propertyValue, options)); 477 members.add(member); 478 } catch (MissingAccessorException noField) { 479 if (members.isEmpty()) { 480 throw (noSetter == null || noField.getMatchLevel() > noSetter.getMatchLevel())? noField: noSetter; 481 } 482 } 483 } 484 } 485 } catch (MissingAccessorException e) { 486 if (options.contains(Option.IGNORE_MISSING_PROPERTIES)) { 487 unsetProperties.put(propertyName.name, propertyValue); 488 return; 489 } 490 throw e; 491 } 492 493 ConstructionException conversionException = null; 494 for (Member member : members) { 495 // convert the value to type of setter/field 496 try { 497 propertyValue = RecipeHelper.convert(member.getType(), propertyValue, false); 498 } catch (Exception e) { 499 // save off first conversion exception, in case setting failed 500 if (conversionException == null) { 501 String valueType = propertyValue == null ? "null" : propertyValue.getClass().getName(); 502 String memberType = member.getType() instanceof Class ? ((Class) member.getType()).getName() : member.getType().toString(); 503 conversionException = new ConstructionException("Unable to convert property value" + 504 " from " + valueType + 505 " to " + memberType + 506 " for injection " + member, e); 507 } 508 continue; 509 } 510 try { 511 // set value 512 member.setValue(instance, propertyValue); 513 } catch (Exception e) { 514 Throwable t = e; 515 if (e instanceof InvocationTargetException) { 516 InvocationTargetException invocationTargetException = (InvocationTargetException) e; 517 if (invocationTargetException.getCause() != null) { 518 t = invocationTargetException.getCause(); 519 } 520 } 521 throw new ConstructionException("Error setting property: " + member, t); 522 } 523 524 // value set successfully 525 return; 526 } 527 528 throw conversionException; 529 } 530 531 private Factory findFactory(Type expectedType) { 532 Class type = getType(); 533 534 // 535 // attempt to find a static factory 536 if (factoryMethod != null) { 537 try { 538 StaticFactory staticFactory = ReflectionUtil.findStaticFactory( 539 type, 540 factoryMethod, 541 constructorArgNames, 542 constructorArgTypes, 543 getProperties().keySet(), 544 options); 545 return staticFactory; 546 } catch (MissingFactoryMethodException ignored) { 547 } 548 549 } 550 551 // 552 // factory was not found, look for a constuctor 553 554 // if expectedType is a subclass of the assigned type, we create 555 // the sub class instead 556 Class consturctorClass; 557 if (RecipeHelper.isAssignable(type, expectedType)) { 558 consturctorClass = RecipeHelper.toClass(expectedType); 559 } else { 560 consturctorClass = type; 561 } 562 563 ConstructorFactory constructor = ReflectionUtil.findConstructor( 564 consturctorClass, 565 constructorArgNames, 566 constructorArgTypes, 567 getProperties().keySet(), 568 options); 569 570 return constructor; 571 } 572 573 private Object[] extractConstructorArgs(Map propertyValues, Factory factory) { 574 List<String> parameterNames = factory.getParameterNames(); 575 List<Type> parameterTypes = factory.getParameterTypes(); 576 577 Object[] parameters = new Object[parameterNames.size()]; 578 for (int i = 0; i < parameterNames.size(); i++) { 579 Property name = new Property(parameterNames.get(i)); 580 Type type = parameterTypes.get(i); 581 582 Object value; 583 if (propertyValues.containsKey(name)) { 584 value = propertyValues.remove(name); 585 if (!RecipeHelper.isInstance(type, value) && !RecipeHelper.isConvertable(type, value)) { 586 throw new ConstructionException("Invalid and non-convertable constructor parameter type: " + 587 "name=" + name + ", " + 588 "index=" + i + ", " + 589 "expected=" + RecipeHelper.toClass(type).getName() + ", " + 590 "actual=" + (value == null ? "null" : value.getClass().getName())); 591 } 592 value = RecipeHelper.convert(type, value, false); 593 } else { 594 value = getDefaultValue(RecipeHelper.toClass(type)); 595 } 596 597 598 parameters[i] = value; 599 } 600 return parameters; 601 } 602 603 private static Object getDefaultValue(Class type) { 604 if (type.equals(Boolean.TYPE)) { 605 return Boolean.FALSE; 606 } else if (type.equals(Character.TYPE)) { 607 return (char) 0; 608 } else if (type.equals(Byte.TYPE)) { 609 return (byte) 0; 610 } else if (type.equals(Short.TYPE)) { 611 return (short) 0; 612 } else if (type.equals(Integer.TYPE)) { 613 return 0; 614 } else if (type.equals(Long.TYPE)) { 615 return (long) 0; 616 } else if (type.equals(Float.TYPE)) { 617 return (float) 0; 618 } else if (type.equals(Double.TYPE)) { 619 return (double) 0; 620 } 621 return null; 622 } 623 624 public static interface Member { 625 Type getType(); 626 void setValue(Object instance, Object value) throws Exception; 627 } 628 629 public static class MethodMember implements Member { 630 private final Method setter; 631 632 public MethodMember(Method method) { 633 this.setter = method; 634 } 635 636 public Type getType() { 637 return setter.getGenericParameterTypes()[0]; 638 } 639 640 public void setValue(Object instance, Object value) throws Exception { 641 setter.invoke(instance, value); 642 } 643 644 public String toString() { 645 return setter.toString(); 646 } 647 } 648 649 public static class FieldMember implements Member { 650 private final Field field; 651 652 public FieldMember(Field field) { 653 this.field = field; 654 } 655 656 public Type getType() { 657 return field.getGenericType(); 658 } 659 660 public void setValue(Object instance, Object value) throws Exception { 661 field.set(instance, value); 662 } 663 664 public String toString() { 665 return field.toString(); 666 } 667 } 668 669 public static class Property { 670 private final String name; 671 672 public Property(String name) { 673 if (name == null) throw new NullPointerException("name is null"); 674 this.name = name; 675 } 676 677 public boolean equals(Object o) { 678 if (this == o) return true; 679 if (o == null) return false; 680 if (o instanceof String){ 681 return this.name.equals(o); 682 } 683 if (o instanceof Property) { 684 Property property = (Property) o; 685 return this.name.equals(property.name); 686 } 687 return false; 688 } 689 690 public int hashCode() { 691 return name.hashCode(); 692 } 693 694 public String toString() { 695 return name; 696 } 697 } 698 699 public static class SetterProperty extends Property { 700 public SetterProperty(String name) { 701 super(name); 702 } 703 public int hashCode() { 704 return super.hashCode()+2; 705 } 706 public String toString() { 707 return "[setter] "+super.toString(); 708 } 709 710 } 711 712 public static class FieldProperty extends Property { 713 public FieldProperty(String name) { 714 super(name); 715 } 716 717 public int hashCode() { 718 return super.hashCode()+1; 719 } 720 public String toString() { 721 return "[field] "+ super.toString(); 722 } 723 } 724 725 public static class AutoMatchProperty extends Property { 726 public AutoMatchProperty(String type) { 727 super(type); 728 } 729 730 public int hashCode() { 731 return super.hashCode()+1; 732 } 733 public String toString() { 734 return "[auto-match] "+ super.toString(); 735 } 736 } 737 738 public static class CompoundProperty extends Property { 739 public CompoundProperty(String type) { 740 super(type); 741 } 742 743 public int hashCode() { 744 return super.hashCode()+1; 745 } 746 public String toString() { 747 return "[compound] "+ super.toString(); 748 } 749 } 750 }