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        private void setProperty(Property key, Object value) {
198            if (value instanceof UnsetPropertiesRecipe) {
199                allow(Option.IGNORE_MISSING_PROPERTIES);
200            }
201            properties.put(key, value);
202        }
203    
204    
205        public void setAllProperties(Map<?,?> map) {
206            if (map == null) throw new NullPointerException("map is null");
207            for (Map.Entry<?, ?> entry : map.entrySet()) {
208                String name = (String) entry.getKey();
209                Object value = entry.getValue();
210                setProperty(name, value);
211            }
212        }
213    
214        public Map<String,Object> getUnsetProperties() {
215            return unsetProperties;
216        }
217    
218        public List<Recipe> getNestedRecipes() {
219            List<Recipe> nestedRecipes = new ArrayList<Recipe>(properties.size());
220            for (Object o : properties.values()) {
221                if (o instanceof Recipe) {
222                    Recipe recipe = (Recipe) o;
223                    nestedRecipes.add(recipe);
224                }
225            }
226            return nestedRecipes;
227        }
228    
229        public List<Recipe> getConstructorRecipes() {
230            // find the factory that will be used to create the class instance
231            Factory factory = findFactory(Object.class);
232    
233            // if we are NOT using an instance factory to create the object
234            // (we have a factory method and it is not a static factory method)
235            if (factoryMethod != null && !(factory instanceof StaticFactory)) {
236                // only include recipes used in the construcor args
237                List<String> parameterNames = factory.getParameterNames();
238                List<Recipe> nestedRecipes = new ArrayList<Recipe>(parameterNames.size());
239                for (Map.Entry<Property, Object> entry : properties.entrySet()) {
240                    if (parameterNames.contains(entry.getKey().name) && entry.getValue() instanceof Recipe) {
241                        Recipe recipe = (Recipe) entry.getValue();
242                        nestedRecipes.add(recipe);
243                    }
244                }
245                return nestedRecipes;
246            } else {
247                // when there is an instance factory all nested recipes are used in the constructor
248                return getNestedRecipes();
249            }
250        }
251    
252        public boolean canCreate(Type type) {
253            Class myType = getType();
254            return RecipeHelper.isAssignable(type, myType) || RecipeHelper.isAssignable(type, myType);
255        }
256    
257        protected Object internalCreate(Type expectedType, boolean lazyRefAllowed) throws ConstructionException {
258            unsetProperties.clear();
259    
260            //
261            // load the type class
262            Class typeClass = getType();
263    
264            //
265            // clone the properties so they can be used again
266            Map<Property,Object> propertyValues = new LinkedHashMap<Property,Object>(properties);
267    
268            //
269            // create the instance
270            Factory factory = findFactory(expectedType);
271            Object[] parameters = extractConstructorArgs(propertyValues, factory);
272            Object instance = factory.create(parameters);
273    
274            //
275            // add to execution context if name is specified
276            if (getName() != null) {
277                ExecutionContext.getContext().addObject(getName(), instance);
278            }
279    
280            //
281            // set the properties
282            setProperties(propertyValues, instance, instance.getClass());
283    
284            //
285            // call instance factory method
286    
287            // if we have a factory method name and did not find a static factory,
288            // then we have an instance factory
289            if (factoryMethod != null && !(factory instanceof StaticFactory)) {
290                // find the instance factory method
291                Method instanceFactory = ReflectionUtil.findInstanceFactory(instance.getClass(), factoryMethod, null);
292    
293                try {
294                    instance = instanceFactory.invoke(instance);
295                } catch (Exception e) {
296                    Throwable t = e;
297                    if (e instanceof InvocationTargetException) {
298                        InvocationTargetException invocationTargetException = (InvocationTargetException) e;
299                        if (invocationTargetException.getCause() != null) {
300                            t = invocationTargetException.getCause();
301                        }
302                    }
303                    throw new ConstructionException("Error calling instance factory method: " + instanceFactory, t);
304                }
305            }
306    
307            return instance;
308        }
309    
310        public void setProperties(Object instance) throws ConstructionException {
311            unsetProperties.clear();
312    
313            // clone the properties so they can be used again
314            Map<Property,Object> propertyValues = new LinkedHashMap<Property,Object>(properties);
315    
316            setProperties(propertyValues, instance, instance.getClass());
317        }
318    
319        public Class setStaticProperties() throws ConstructionException {
320            unsetProperties.clear();
321    
322            // load the type class
323            Class typeClass = getType();
324    
325            // verify that it is a class we can construct
326            if (!Modifier.isPublic(typeClass.getModifiers())) {
327                throw new ConstructionException("Class is not public: " + typeClass.getName());
328            }
329            if (Modifier.isInterface(typeClass.getModifiers())) {
330                throw new ConstructionException("Class is an interface: " + typeClass.getName());
331            }
332            if (Modifier.isAbstract(typeClass.getModifiers())) {
333                throw new ConstructionException("Class is abstract: " + typeClass.getName());
334            }
335    
336            // clone the properties so they can be used again
337            Map<Property,Object> propertyValues = new LinkedHashMap<Property,Object>(properties);
338    
339            setProperties(propertyValues, null, typeClass);
340    
341            return typeClass;
342        }
343    
344        public Class getType() {
345            if (typeClass != null || typeName != null) {
346                Class type = typeClass;
347                if (type == null) {
348                    try {
349                        type = RecipeHelper.loadClass(typeName);
350                    } catch (ClassNotFoundException e) {
351                        throw new ConstructionException("Type class could not be found: " + typeName);
352                    }
353                }
354    
355                return type;
356            }
357    
358            return null;
359        }
360    
361        private void setProperties(Map<Property, Object> propertyValues, Object instance, Class clazz) {
362            // set remaining properties
363            for (Map.Entry<Property, Object> entry : RecipeHelper.prioritizeProperties(propertyValues)) {
364                Property propertyName = entry.getKey();
365                Object propertyValue = entry.getValue();
366    
367                setProperty(instance, clazz, propertyName, propertyValue);
368            }
369    
370        }
371    
372        private void setProperty(Object instance, Class clazz, Property propertyName, Object propertyValue) {
373    
374            List<Member> members = new ArrayList<Member>();
375            try {
376                if (propertyName instanceof SetterProperty){
377                    List<Method> setters = ReflectionUtil.findAllSetters(clazz, propertyName.name, propertyValue, options);
378                    for (Method setter : setters) {
379                        MethodMember member = new MethodMember(setter);
380                        members.add(member);
381                    }
382                } else if (propertyName instanceof FieldProperty){
383                    FieldMember member = new FieldMember(ReflectionUtil.findField(clazz, propertyName.name, propertyValue, options));
384                    members.add(member);
385                } else if (propertyName instanceof AutoMatchProperty){
386                    MissingAccessorException noField = null;
387                    if (options.contains(Option.FIELD_INJECTION)) {
388                        List<Field> fieldsByType = null;
389                        try {
390                            fieldsByType = ReflectionUtil.findAllFieldsByType(clazz, propertyValue, options);
391                            FieldMember member = new FieldMember(fieldsByType.iterator().next());
392                            members.add(member);
393                        } catch (MissingAccessorException e) {
394                            noField = e;
395                        }
396    
397                        // if we got more then one matching field, that is an immidate error
398                        if (fieldsByType != null && fieldsByType.size() > 1) {
399                            List<String> matches = new ArrayList<String>();
400                            for (Field field : fieldsByType) {
401                                matches.add(field.getName());
402                            }
403                            throw new MissingAccessorException("Property of type " + propertyValue.getClass().getName() + " can be mapped to more then one field: " + matches, 0);
404                        }
405                    }
406    
407                    // if we didn't find any fields, try the setters
408                    if (members.isEmpty()) {
409                        List<Method> settersByType;
410                        try {
411                            settersByType = ReflectionUtil.findAllSettersByType(clazz, propertyValue, options);
412                            MethodMember member = new MethodMember(settersByType.iterator().next());
413                            members.add(member);
414                        } catch (MissingAccessorException noSetter) {
415                            throw (noField == null || noSetter.getMatchLevel() > noField.getMatchLevel())? noSetter: noField;
416                        }
417    
418                        // if we got more then one matching field, that is an immidate error
419                        if (settersByType != null && settersByType.size() > 1) {
420                            List<String> matches = new ArrayList<String>();
421                            for (Method setter : settersByType) {
422                                matches.add(setter.getName());
423                            }
424                            throw new MissingAccessorException("Property of type " + propertyValue.getClass().getName() + " can be mapped to more then one setter: " + matches, 0);
425                        }
426                    }
427                } else {
428                    // add setter members
429                    MissingAccessorException noSetter = null;
430                    try {
431                        List<Method> setters = ReflectionUtil.findAllSetters(clazz, propertyName.name, propertyValue, options);
432                        for (Method setter : setters) {
433                            MethodMember member = new MethodMember(setter);
434                            members.add(member);
435                        }
436                    } catch (MissingAccessorException e) {
437                        noSetter = e;
438                        if (!options.contains(Option.FIELD_INJECTION)) {
439                            throw noSetter;
440                        }
441                    }
442    
443                    if (options.contains(Option.FIELD_INJECTION)) {
444                        try {
445                            FieldMember member = new FieldMember(ReflectionUtil.findField(clazz, propertyName.name, propertyValue, options));
446                            members.add(member);
447                        } catch (MissingAccessorException noField) {
448                            if (members.isEmpty()) {
449                                throw (noSetter == null || noField.getMatchLevel() > noSetter.getMatchLevel())? noField: noSetter;
450                            }
451                        }
452                    }
453                }
454            } catch (MissingAccessorException e) {
455                if (options.contains(Option.IGNORE_MISSING_PROPERTIES)) {
456                    unsetProperties.put(propertyName.name, propertyValue);
457                    return;
458                }
459                throw e;
460            }
461    
462            ConstructionException conversionException = null;
463            for (Member member : members) {
464                // convert the value to type of setter/field
465                try {
466                    propertyValue = RecipeHelper.convert(member.getType(), propertyValue, false);
467                } catch (Exception e) {
468                    // save off first conversion exception, in case setting failed
469                    if (conversionException == null) {
470                        String valueType = propertyValue == null ? "null" : propertyValue.getClass().getName();
471                        String memberType = member.getType() instanceof Class ? ((Class) member.getType()).getName() : member.getType().toString();
472                        conversionException = new ConstructionException("Unable to convert property value" +
473                                " from " + valueType +
474                                " to " + memberType +
475                                " for injection " + member, e);
476                    }
477                    continue;
478                }
479                try {
480                    // set value
481                    member.setValue(instance, propertyValue);
482                } catch (Exception e) {
483                    Throwable t = e;
484                    if (e instanceof InvocationTargetException) {
485                        InvocationTargetException invocationTargetException = (InvocationTargetException) e;
486                        if (invocationTargetException.getCause() != null) {
487                            t = invocationTargetException.getCause();
488                        }
489                    }
490                    throw new ConstructionException("Error setting property: " + member, t);
491                }
492    
493                // value set successfully
494                return;
495            }
496    
497            throw conversionException;
498        }
499    
500        private Factory findFactory(Type expectedType) {
501            Class type = getType();
502    
503            //
504            // attempt to find a static factory
505            if (factoryMethod != null) {
506                try {
507                    StaticFactory staticFactory = ReflectionUtil.findStaticFactory(
508                            type,
509                            factoryMethod,
510                            constructorArgNames,
511                            constructorArgTypes,
512                            getProperties().keySet(),
513                            options);
514                    return staticFactory;
515                } catch (MissingFactoryMethodException ignored) {
516                }
517    
518            }
519    
520            //
521            // factory was not found, look for a constuctor
522    
523            // if expectedType is a subclass of the assigned type, we create
524            // the sub class instead
525            Class consturctorClass;
526            if (RecipeHelper.isAssignable(type, expectedType)) {
527                consturctorClass = RecipeHelper.toClass(expectedType);
528            } else {
529                consturctorClass = type;
530            }
531    
532            ConstructorFactory constructor = ReflectionUtil.findConstructor(
533                    consturctorClass,
534                    constructorArgNames,
535                    constructorArgTypes,
536                    getProperties().keySet(),
537                    options);
538    
539            return constructor;
540        }
541    
542        private Object[] extractConstructorArgs(Map propertyValues, Factory factory) {
543            List<String> parameterNames = factory.getParameterNames();
544            List<Type> parameterTypes = factory.getParameterTypes();
545    
546            Object[] parameters = new Object[parameterNames.size()];
547            for (int i = 0; i < parameterNames.size(); i++) {
548                Property name = new Property(parameterNames.get(i));
549                Type type = parameterTypes.get(i);
550    
551                Object value;
552                if (propertyValues.containsKey(name)) {
553                    value = propertyValues.remove(name);
554                    if (!RecipeHelper.isInstance(type, value) && !RecipeHelper.isConvertable(type, value)) {
555                        throw new ConstructionException("Invalid and non-convertable constructor parameter type: " +
556                                "name=" + name + ", " +
557                                "index=" + i + ", " +
558                                "expected=" + RecipeHelper.toClass(type).getName() + ", " +
559                                "actual=" + (value == null ? "null" : value.getClass().getName()));
560                    }
561                    value = RecipeHelper.convert(type, value, false);
562                } else {
563                    value = getDefaultValue(RecipeHelper.toClass(type));
564                }
565    
566    
567                parameters[i] = value;
568            }
569            return parameters;
570        }
571    
572        private static Object getDefaultValue(Class type) {
573            if (type.equals(Boolean.TYPE)) {
574                return Boolean.FALSE;
575            } else if (type.equals(Character.TYPE)) {
576                return (char) 0;
577            } else if (type.equals(Byte.TYPE)) {
578                return (byte) 0;
579            } else if (type.equals(Short.TYPE)) {
580                return (short) 0;
581            } else if (type.equals(Integer.TYPE)) {
582                return 0;
583            } else if (type.equals(Long.TYPE)) {
584                return (long) 0;
585            } else if (type.equals(Float.TYPE)) {
586                return (float) 0;
587            } else if (type.equals(Double.TYPE)) {
588                return (double) 0;
589            }
590            return null;
591        }
592    
593        public static interface Member {
594            Type getType();
595            void setValue(Object instance, Object value) throws Exception;
596        }
597    
598        public static class MethodMember implements Member {
599            private final Method setter;
600    
601            public MethodMember(Method method) {
602                this.setter = method;
603            }
604    
605            public Type getType() {
606                return setter.getGenericParameterTypes()[0];
607            }
608    
609            public void setValue(Object instance, Object value) throws Exception {
610                setter.invoke(instance, value);
611            }
612    
613            public String toString() {
614                return setter.toString();
615            }
616        }
617    
618        public static class FieldMember implements Member {
619            private final Field field;
620    
621            public FieldMember(Field field) {
622                this.field = field;
623            }
624    
625            public Type getType() {
626                return field.getGenericType();
627            }
628    
629            public void setValue(Object instance, Object value) throws Exception {
630                field.set(instance, value);
631            }
632    
633            public String toString() {
634                return field.toString();
635            }
636        }
637    
638        public static class Property {
639            private final String name;
640    
641            public Property(String name) {
642                if (name == null) throw new NullPointerException("name is null");
643                this.name = name;
644            }
645    
646            public boolean equals(Object o) {
647                if (this == o) return true;
648                if (o == null) return false;
649                if (o instanceof String){
650                    return this.name.equals(o);
651                }
652                if (o instanceof Property) {
653                    Property property = (Property) o;
654                    return this.name.equals(property.name);
655                }
656                return false;
657            }
658    
659            public int hashCode() {
660                return name.hashCode();
661            }
662    
663            public String toString() {
664                return name;
665            }
666        }
667    
668        public static class SetterProperty extends Property {
669            public SetterProperty(String name) {
670                super(name);
671            }
672            public int hashCode() {
673                return super.hashCode()+2;
674            }
675            public String toString() {
676                return "[setter] "+super.toString();
677            }
678    
679        }
680    
681        public static class FieldProperty extends Property {
682            public FieldProperty(String name) {
683                super(name);
684            }
685    
686            public int hashCode() {
687                return super.hashCode()+1;
688            }
689            public String toString() {
690                return "[field] "+ super.toString();
691            }
692        }
693    
694        public static class AutoMatchProperty extends Property {
695            public AutoMatchProperty(String type) {
696                super(type);
697            }
698    
699            public int hashCode() {
700                return super.hashCode()+1;
701            }
702            public String toString() {
703                return "[auto-match] "+ super.toString();
704            }
705        }
706    }