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}