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