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    }