001    /**
002     *
003     * Licensed to the Apache Software Foundation (ASF) under one or more
004     * contributor license agreements.  See the NOTICE file distributed with
005     * this work for additional information regarding copyright ownership.
006     * The ASF licenses this file to You under the Apache License, Version 2.0
007     * (the "License"); you may not use this file except in compliance with
008     * the License.  You may obtain a copy of the License at
009     *
010     *     http://www.apache.org/licenses/LICENSE-2.0
011     *
012     *  Unless required by applicable law or agreed to in writing, software
013     *  distributed under the License is distributed on an "AS IS" BASIS,
014     *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015     *  See the License for the specific language governing permissions and
016     *  limitations under the License.
017     */
018    package org.apache.xbean.recipe;
019    
020    import java.lang.annotation.Annotation;
021    import java.lang.reflect.AccessibleObject;
022    import java.lang.reflect.Constructor;
023    import java.lang.reflect.Field;
024    import java.lang.reflect.InvocationTargetException;
025    import java.lang.reflect.Method;
026    import java.lang.reflect.Modifier;
027    import java.lang.reflect.Type;
028    import java.security.AccessController;
029    import java.security.PrivilegedAction;
030    import java.util.ArrayList;
031    import java.util.Arrays;
032    import java.util.Collections;
033    import java.util.Comparator;
034    import java.util.EnumSet;
035    import java.util.LinkedHashSet;
036    import java.util.LinkedList;
037    import java.util.List;
038    import java.util.Set;
039    
040    import static org.apache.xbean.recipe.RecipeHelper.isAssignableFrom;
041    
042    public final class ReflectionUtil {
043        private static ParameterNameLoader parameterNamesLoader;
044        static {
045            try {
046                Class<? extends ParameterNameLoader> loaderClass = ReflectionUtil.class.getClassLoader().loadClass("org.apache.xbean.recipe.AsmParameterNameLoader").asSubclass(ParameterNameLoader.class);
047                parameterNamesLoader = loaderClass.newInstance();
048            } catch (Throwable ignored) {
049            }
050        }
051    
052        private ReflectionUtil() {
053        }
054    
055        public static Field findField(Class typeClass, String propertyName, Object propertyValue, Set<Option> options) {
056            if (typeClass == null) throw new NullPointerException("typeClass is null");
057            if (propertyName == null) throw new NullPointerException("name is null");
058            if (propertyName.length() == 0) throw new IllegalArgumentException("name is an empty string");
059            if (options == null) options = EnumSet.noneOf(Option.class);
060    
061            int matchLevel = 0;
062            MissingAccessorException missException = null;
063    
064            if (propertyName.contains("/")){
065                String[] strings = propertyName.split("/");
066                if (strings == null || strings.length != 2) throw new IllegalArgumentException("badly formed <class>/<attribute> property name: " + propertyName);
067    
068                String className = strings[0];
069                propertyName = strings[1];
070    
071                boolean found = false;
072                while(!typeClass.equals(Object.class) && !found){
073                    if (typeClass.getName().equals(className)){
074                        found = true;
075                        break;
076                    } else {
077                        typeClass = typeClass.getSuperclass();
078                    }
079                }
080    
081                if (!found) throw new MissingAccessorException("Type not assignable to class: " + className, -1);
082            }
083    
084            List<Field> fields = new ArrayList<Field>(Arrays.asList(typeClass.getDeclaredFields()));
085            Class parent = typeClass.getSuperclass();
086            while (parent != null){
087                fields.addAll(Arrays.asList(parent.getDeclaredFields()));
088                parent = parent.getSuperclass();
089            }
090    
091            boolean allowPrivate = options.contains(Option.PRIVATE_PROPERTIES);
092            boolean allowStatic = options.contains(Option.STATIC_PROPERTIES);
093            boolean caseInsesnitive = options.contains(Option.CASE_INSENSITIVE_PROPERTIES);
094    
095            for (Field field : fields) {
096                if (field.getName().equals(propertyName) || (caseInsesnitive && field.getName().equalsIgnoreCase(propertyName))) {
097    
098                    if (!allowPrivate && !Modifier.isPublic(field.getModifiers())) {
099                        if (matchLevel < 4) {
100                            matchLevel = 4;
101                            missException = new MissingAccessorException("Field is not public: " + field, matchLevel);
102                        }
103                        continue;
104                    }
105    
106                    if (!allowStatic && Modifier.isStatic(field.getModifiers())) {
107                        if (matchLevel < 4) {
108                            matchLevel = 4;
109                            missException = new MissingAccessorException("Field is static: " + field, matchLevel);
110                        }
111                        continue;
112                    }
113    
114                    Class fieldType = field.getType();
115                    if (fieldType.isPrimitive() && propertyValue == null) {
116                        if (matchLevel < 6) {
117                            matchLevel = 6;
118                            missException = new MissingAccessorException("Null can not be assigned to " +
119                                    fieldType.getName() + ": " + field, matchLevel);
120                        }
121                        continue;
122                    }
123    
124    
125                    if (!RecipeHelper.isInstance(fieldType, propertyValue) && !RecipeHelper.isConvertable(fieldType, propertyValue)) {
126                        if (matchLevel < 5) {
127                            matchLevel = 5;
128                            missException = new MissingAccessorException((propertyValue == null ? "null" : propertyValue.getClass().getName()) + " can not be assigned or converted to " +
129                                    fieldType.getName() + ": " + field, matchLevel);
130                        }
131                        continue;
132                    }
133    
134                    if (allowPrivate && !Modifier.isPublic(field.getModifiers())) {
135                        setAccessible(field);
136                    }
137    
138                    return field;
139                }
140    
141            }
142    
143            if (missException != null) {
144                throw missException;
145            } else {
146                StringBuffer buffer = new StringBuffer("Unable to find a valid field: ");
147                buffer.append("public ").append(" ").append(propertyValue == null ? "null" : propertyValue.getClass().getName());
148                buffer.append(" ").append(propertyName).append(";");
149                throw new MissingAccessorException(buffer.toString(), -1);
150            }
151        }
152    
153        public static Method findSetter(Class typeClass, String propertyName, Object propertyValue, Set<Option> options) {
154            List<Method> setters = findAllSetters(typeClass, propertyName, propertyValue, options);
155            return setters.get(0);
156        }
157    
158        /**
159         * Finds all valid setters for the property.  Due to automatic type conversion there may be more than one possible
160         * setter that could be used to set the property.  The setters that do not require type converstion will be a the
161         * head of the returned list of setters.
162         * @param typeClass the class to search for setters
163         * @param propertyName the name of the property
164         * @param propertyValue the value that must be settable either directly or after conversion
165         * @param options controls which setters are considered valid
166         * @return the valid setters; never null or empty
167         */
168        public static List<Method> findAllSetters(Class typeClass, String propertyName, Object propertyValue, Set<Option> options) {
169            if (typeClass == null) throw new NullPointerException("typeClass is null");
170            if (propertyName == null) throw new NullPointerException("name is null");
171            if (propertyName.length() == 0) throw new IllegalArgumentException("name is an empty string");
172            if (options == null) options = EnumSet.noneOf(Option.class);
173    
174            if (propertyName.contains("/")){
175                String[] strings = propertyName.split("/");
176                if (strings == null || strings.length != 2) throw new IllegalArgumentException("badly formed <class>/<attribute> property name: " + propertyName);
177    
178                String className = strings[0];
179                propertyName = strings[1];
180    
181                boolean found = false;
182                while(!typeClass.equals(Object.class) && !found){
183                    if (typeClass.getName().equals(className)){
184                        found = true;
185                        break;
186                    } else {
187                        typeClass = typeClass.getSuperclass();
188                    }
189                }
190    
191                if (!found) throw new MissingAccessorException("Type not assignable to class: " + className, -1);
192            }
193    
194            String setterName = "set" + Character.toUpperCase(propertyName.charAt(0));
195            if (propertyName.length() > 0) {
196                setterName += propertyName.substring(1);
197            }
198    
199    
200            int matchLevel = 0;
201            MissingAccessorException missException = null;
202    
203            boolean allowPrivate = options.contains(Option.PRIVATE_PROPERTIES);
204            boolean allowStatic = options.contains(Option.STATIC_PROPERTIES);
205            boolean caseInsesnitive = options.contains(Option.CASE_INSENSITIVE_PROPERTIES);
206    
207    
208            LinkedList<Method> validSetters = new LinkedList<Method>();
209    
210            List<Method> methods = new ArrayList<Method>(Arrays.asList(typeClass.getMethods()));
211            methods.addAll(Arrays.asList(typeClass.getDeclaredMethods()));
212            for (Method method : methods) {
213                if (method.getName().equals(setterName) || (caseInsesnitive && method.getName().equalsIgnoreCase(setterName))) {
214                    if (method.getParameterTypes().length == 0) {
215                        if (matchLevel < 1) {
216                            matchLevel = 1;
217                            missException = new MissingAccessorException("Setter takes no parameters: " + method, matchLevel);
218                        }
219                        continue;
220                    }
221    
222                    if (method.getParameterTypes().length > 1) {
223                        if (matchLevel < 1) {
224                            matchLevel = 1;
225                            missException = new MissingAccessorException("Setter takes more then one parameter: " + method, matchLevel);
226                        }
227                        continue;
228                    }
229    
230                    if (method.getReturnType() != Void.TYPE) {
231                        if (matchLevel < 2) {
232                            matchLevel = 2;
233                            missException = new MissingAccessorException("Setter returns a value: " + method, matchLevel);
234                        }
235                        continue;
236                    }
237    
238                    if (Modifier.isAbstract(method.getModifiers())) {
239                        if (matchLevel < 3) {
240                            matchLevel = 3;
241                            missException = new MissingAccessorException("Setter is abstract: " + method, matchLevel);
242                        }
243                        continue;
244                    }
245    
246                    if (!allowPrivate && !Modifier.isPublic(method.getModifiers())) {
247                        if (matchLevel < 4) {
248                            matchLevel = 4;
249                            missException = new MissingAccessorException("Setter is not public: " + method, matchLevel);
250                        }
251                        continue;
252                    }
253    
254                    if (!allowStatic && Modifier.isStatic(method.getModifiers())) {
255                        if (matchLevel < 4) {
256                            matchLevel = 4;
257                            missException = new MissingAccessorException("Setter is static: " + method, matchLevel);
258                        }
259                        continue;
260                    }
261    
262                    Class methodParameterType = method.getParameterTypes()[0];
263                    if (methodParameterType.isPrimitive() && propertyValue == null) {
264                        if (matchLevel < 6) {
265                            matchLevel = 6;
266                            missException = new MissingAccessorException("Null can not be assigned to " +
267                                    methodParameterType.getName() + ": " + method, matchLevel);
268                        }
269                        continue;
270                    }
271    
272    
273                    if (!RecipeHelper.isInstance(methodParameterType, propertyValue) && !RecipeHelper.isConvertable(methodParameterType, propertyValue)) {
274                        if (matchLevel < 5) {
275                            matchLevel = 5;
276                            missException = new MissingAccessorException((propertyValue == null ? "null" : propertyValue.getClass().getName()) + " can not be assigned or converted to " +
277                                    methodParameterType.getName() + ": " + method, matchLevel);
278                        }
279                        continue;
280                    }
281    
282                    if (allowPrivate && !Modifier.isPublic(method.getModifiers())) {
283                        setAccessible(method);
284                    }
285    
286                    if (RecipeHelper.isInstance(methodParameterType, propertyValue)) {
287                        // This setter requires no conversion, which means there can not be a conversion error.
288                        // Therefore this setter is perferred and put a the head of the list
289                        validSetters.addFirst(method);
290                    } else {
291                        validSetters.add(method);
292                    }
293                }
294    
295            }
296    
297            if (!validSetters.isEmpty()) {
298                // remove duplicate methods (can happen with inheritance)
299                return new ArrayList<Method>(new LinkedHashSet<Method>(validSetters));
300            }
301            
302            if (missException != null) {
303                throw missException;
304            } else {
305                StringBuffer buffer = new StringBuffer("Unable to find a valid setter method: ");
306                buffer.append("public void ").append(typeClass.getName()).append(".");
307                buffer.append(setterName).append("(");
308                if (propertyValue == null) {
309                    buffer.append("null");
310                } else if (propertyValue instanceof String || propertyValue instanceof Recipe) {
311                    buffer.append("...");
312                } else {
313                    buffer.append(propertyValue.getClass().getName());
314                }
315                buffer.append(")");
316                throw new MissingAccessorException(buffer.toString(), -1);
317            }
318        }
319    
320        public static List<Field> findAllFieldsByType(Class typeClass, Object propertyValue, Set<Option> options) {
321            if (typeClass == null) throw new NullPointerException("typeClass is null");
322            if (options == null) options = EnumSet.noneOf(Option.class);
323    
324            int matchLevel = 0;
325            MissingAccessorException missException = null;
326    
327            List<Field> fields = new ArrayList<Field>(Arrays.asList(typeClass.getDeclaredFields()));
328            Class parent = typeClass.getSuperclass();
329            while (parent != null){
330                fields.addAll(Arrays.asList(parent.getDeclaredFields()));
331                parent = parent.getSuperclass();
332            }
333    
334            boolean allowPrivate = options.contains(Option.PRIVATE_PROPERTIES);
335            boolean allowStatic = options.contains(Option.STATIC_PROPERTIES);
336    
337            LinkedList<Field> validFields = new LinkedList<Field>();
338            for (Field field : fields) {
339                Class fieldType = field.getType();
340                if (RecipeHelper.isInstance(fieldType, propertyValue) || RecipeHelper.isConvertable(fieldType, propertyValue)) {
341                    if (!allowPrivate && !Modifier.isPublic(field.getModifiers())) {
342                        if (matchLevel < 4) {
343                            matchLevel = 4;
344                            missException = new MissingAccessorException("Field is not public: " + field, matchLevel);
345                        }
346                        continue;
347                    }
348    
349                    if (!allowStatic && Modifier.isStatic(field.getModifiers())) {
350                        if (matchLevel < 4) {
351                            matchLevel = 4;
352                            missException = new MissingAccessorException("Field is static: " + field, matchLevel);
353                        }
354                        continue;
355                    }
356    
357    
358                    if (fieldType.isPrimitive() && propertyValue == null) {
359                        if (matchLevel < 6) {
360                            matchLevel = 6;
361                            missException = new MissingAccessorException("Null can not be assigned to " +
362                                    fieldType.getName() + ": " + field, matchLevel);
363                        }
364                        continue;
365                    }
366    
367                    if (allowPrivate && !Modifier.isPublic(field.getModifiers())) {
368                        setAccessible(field);
369                    }
370    
371                    if (RecipeHelper.isInstance(fieldType, propertyValue)) {
372                        // This field requires no conversion, which means there can not be a conversion error.
373                        // Therefore this setter is perferred and put a the head of the list
374                        validFields.addFirst(field);
375                    } else {
376                        validFields.add(field);
377                    }
378                }
379            }
380    
381            if (!validFields.isEmpty()) {
382                // remove duplicate methods (can happen with inheritance)
383                return new ArrayList<Field>(new LinkedHashSet<Field>(validFields));
384            }
385    
386            if (missException != null) {
387                throw missException;
388            } else {
389                StringBuffer buffer = new StringBuffer("Unable to find a valid field ");
390                if (propertyValue instanceof Recipe) {
391                    buffer.append("for ").append(propertyValue == null ? "null" : propertyValue);
392                } else {
393                    buffer.append("of type ").append(propertyValue == null ? "null" : propertyValue.getClass().getName());
394                }
395                buffer.append(" in class ").append(typeClass.getName());
396                throw new MissingAccessorException(buffer.toString(), -1);
397            }
398        }
399        public static List<Method> findAllSettersByType(Class typeClass, Object propertyValue, Set<Option> options) {
400            if (typeClass == null) throw new NullPointerException("typeClass is null");
401            if (options == null) options = EnumSet.noneOf(Option.class);
402    
403            int matchLevel = 0;
404            MissingAccessorException missException = null;
405    
406            boolean allowPrivate = options.contains(Option.PRIVATE_PROPERTIES);
407            boolean allowStatic = options.contains(Option.STATIC_PROPERTIES);
408    
409            LinkedList<Method> validSetters = new LinkedList<Method>();
410            List<Method> methods = new ArrayList<Method>(Arrays.asList(typeClass.getMethods()));
411            methods.addAll(Arrays.asList(typeClass.getDeclaredMethods()));
412            for (Method method : methods) {
413                if (method.getName().startsWith("set") && method.getParameterTypes().length == 1 && (RecipeHelper.isInstance(method.getParameterTypes()[0], propertyValue) || RecipeHelper.isConvertable(method.getParameterTypes()[0], propertyValue))) {
414                    if (method.getReturnType() != Void.TYPE) {
415                        if (matchLevel < 2) {
416                            matchLevel = 2;
417                            missException = new MissingAccessorException("Setter returns a value: " + method, matchLevel);
418                        }
419                        continue;
420                    }
421    
422                    if (Modifier.isAbstract(method.getModifiers())) {
423                        if (matchLevel < 3) {
424                            matchLevel = 3;
425                            missException = new MissingAccessorException("Setter is abstract: " + method, matchLevel);
426                        }
427                        continue;
428                    }
429    
430                    if (!allowPrivate && !Modifier.isPublic(method.getModifiers())) {
431                        if (matchLevel < 4) {
432                            matchLevel = 4;
433                            missException = new MissingAccessorException("Setter is not public: " + method, matchLevel);
434                        }
435                        continue;
436                    }
437    
438                    Class methodParameterType = method.getParameterTypes()[0];
439                    if (methodParameterType.isPrimitive() && propertyValue == null) {
440                        if (matchLevel < 6) {
441                            matchLevel = 6;
442                            missException = new MissingAccessorException("Null can not be assigned to " +
443                                    methodParameterType.getName() + ": " + method, matchLevel);
444                        }
445                        continue;
446                    }
447    
448                    if (!allowStatic && Modifier.isStatic(method.getModifiers())) {
449                        if (matchLevel < 4) {
450                            matchLevel = 4;
451                            missException = new MissingAccessorException("Setter is static: " + method, matchLevel);
452                        }
453                        continue;
454                    }
455    
456                    if (allowPrivate && !Modifier.isPublic(method.getModifiers())) {
457                        setAccessible(method);
458                    }
459    
460                    if (RecipeHelper.isInstance(methodParameterType, propertyValue)) {
461                        // This setter requires no conversion, which means there can not be a conversion error.
462                        // Therefore this setter is perferred and put a the head of the list
463                        validSetters.addFirst(method);
464                    } else {
465                        validSetters.add(method);
466                    }
467                }
468    
469            }
470    
471            if (!validSetters.isEmpty()) {
472                // remove duplicate methods (can happen with inheritance)
473                return new ArrayList<Method>(new LinkedHashSet<Method>(validSetters));
474            }
475    
476            if (missException != null) {
477                throw missException;
478            } else {
479                StringBuffer buffer = new StringBuffer("Unable to find a valid setter ");
480                if (propertyValue instanceof Recipe) {
481                    buffer.append("for ").append(propertyValue == null ? "null" : propertyValue);
482                } else {
483                    buffer.append("of type ").append(propertyValue == null ? "null" : propertyValue.getClass().getName());
484                }
485                buffer.append(" in class ").append(typeClass.getName());
486                throw new MissingAccessorException(buffer.toString(), -1);
487            }
488        }
489    
490        public static ConstructorFactory findConstructor(Class typeClass, List<? extends Class<?>> parameterTypes, Set<Option> options) {
491            return findConstructor(typeClass, null, parameterTypes, null, options);
492    
493        }
494        public static ConstructorFactory findConstructor(Class typeClass, List<String> parameterNames, List<? extends Class<?>> parameterTypes, Set<String> availableProperties, Set<Option> options) {
495            if (typeClass == null) throw new NullPointerException("typeClass is null");
496            if (availableProperties == null) availableProperties = Collections.emptySet();
497            if (options == null) options = EnumSet.noneOf(Option.class);
498    
499            //
500            // verify that it is a class we can construct
501            if (!Modifier.isPublic(typeClass.getModifiers())) {
502                throw new ConstructionException("Class is not public: " + typeClass.getName());
503            }
504            if (Modifier.isInterface(typeClass.getModifiers())) {
505                throw new ConstructionException("Class is an interface: " + typeClass.getName());
506            }
507            if (Modifier.isAbstract(typeClass.getModifiers())) {
508                throw new ConstructionException("Class is abstract: " + typeClass.getName());
509            }
510    
511            // verify parameter names and types are the same length
512            if (parameterNames != null) {
513                if (parameterTypes == null) parameterTypes = Collections.nCopies(parameterNames.size(), null);
514                if (parameterNames.size() != parameterTypes.size()) {
515                    throw new ConstructionException("Invalid ObjectRecipe: recipe has " + parameterNames.size() +
516                            " parameter names and " + parameterTypes.size() + " parameter types");
517                }
518            } else if (!options.contains(Option.NAMED_PARAMETERS)) {
519                // Named parameters are not supported and no explicit parameters were given,
520                // so we will only use the no-arg constructor
521                parameterNames = Collections.emptyList();
522                parameterTypes = Collections.emptyList();
523            }
524    
525    
526            // get all methods sorted so that the methods with the most constructor args are first
527            List<Constructor> constructors = new ArrayList<Constructor>(Arrays.asList(typeClass.getConstructors()));
528            constructors.addAll(Arrays.asList(typeClass.getDeclaredConstructors()));
529            Collections.sort(constructors, new Comparator<Constructor>() {
530                public int compare(Constructor constructor1, Constructor constructor2) {
531                    return constructor2.getParameterTypes().length - constructor1.getParameterTypes().length;
532                }
533            });
534    
535            // as we check each constructor, we remember the closest invalid match so we can throw a nice exception to the user
536            int matchLevel = 0;
537            MissingFactoryMethodException missException = null;
538    
539            boolean allowPrivate = options.contains(Option.PRIVATE_CONSTRUCTOR);
540            for (Constructor constructor : constructors) {
541                // if an explicit constructor is specified (via parameter types), look a constructor that matches
542                if (parameterTypes != null) {
543                    if (constructor.getParameterTypes().length != parameterTypes.size()) {
544                        if (matchLevel < 1) {
545                            matchLevel = 1;
546                            missException = new MissingFactoryMethodException("Constructor has " + constructor.getParameterTypes().length + " arugments " +
547                                    "but expected " + parameterTypes.size() + " arguments: " + constructor);
548                        }
549                        continue;
550                    }
551    
552                    if (!isAssignableFrom(parameterTypes, Arrays.<Class<?>>asList(constructor.getParameterTypes()))) {
553                        if (matchLevel < 2) {
554                            matchLevel = 2;
555                            missException = new MissingFactoryMethodException("Constructor has signature " +
556                                    "public static " + typeClass.getName() + toParameterList(constructor.getParameterTypes()) +
557                                    " but expected signature " +
558                                    "public static " + typeClass.getName() + toParameterList(parameterTypes));
559                        }
560                        continue;
561                    }
562                } else {
563                    // Implicit constructor selection based on named constructor args
564                    //
565                    // Only consider methods where we can supply a value for all of the parameters
566                    parameterNames = getParameterNames(constructor);
567                    if (parameterNames == null || !availableProperties.containsAll(parameterNames)) {
568                        continue;
569                    }
570                }
571    
572                if (Modifier.isAbstract(constructor.getModifiers())) {
573                    if (matchLevel < 4) {
574                        matchLevel = 4;
575                        missException = new MissingFactoryMethodException("Constructor is abstract: " + constructor);
576                    }
577                    continue;
578                }
579    
580                if (!allowPrivate && !Modifier.isPublic(constructor.getModifiers())) {
581                    if (matchLevel < 5) {
582                        matchLevel = 5;
583                        missException = new MissingFactoryMethodException("Constructor is not public: " + constructor);
584                    }
585                    continue;
586                }
587    
588                if (allowPrivate && !Modifier.isPublic(constructor.getModifiers())) {
589                    setAccessible(constructor);
590                }
591    
592                return new ConstructorFactory(constructor, parameterNames);
593            }
594    
595            if (missException != null) {
596                throw missException;
597            } else {
598                StringBuffer buffer = new StringBuffer("Unable to find a valid constructor: ");
599                buffer.append("public void ").append(typeClass.getName()).append(toParameterList(parameterTypes));
600                throw new ConstructionException(buffer.toString());
601            }
602        }
603    
604        public static StaticFactory findStaticFactory(Class typeClass, String factoryMethod, List<? extends Class<?>>  parameterTypes, Set<Option> options) {
605            return findStaticFactory(typeClass, factoryMethod, null, parameterTypes, null, options);
606        }
607    
608        public static StaticFactory findStaticFactory(Class typeClass, String factoryMethod, List<String> parameterNames, List<? extends Class<?>> parameterTypes, Set<String> allProperties, Set<Option> options) {
609            if (typeClass == null) throw new NullPointerException("typeClass is null");
610            if (factoryMethod == null) throw new NullPointerException("name is null");
611            if (factoryMethod.length() == 0) throw new IllegalArgumentException("name is an empty string");
612            if (allProperties == null) allProperties = Collections.emptySet();
613            if (options == null) options = EnumSet.noneOf(Option.class);
614    
615            //
616            // verify that it is a class we can construct
617            if (!Modifier.isPublic(typeClass.getModifiers())) {
618                throw new ConstructionException("Class is not public: " + typeClass.getName());
619            }
620            if (Modifier.isInterface(typeClass.getModifiers())) {
621                throw new ConstructionException("Class is an interface: " + typeClass.getName());
622            }
623    
624            // verify parameter names and types are the same length
625            if (parameterNames != null) {
626                if (parameterTypes == null) parameterTypes = Collections.nCopies(parameterNames.size(), null);
627                if (parameterNames.size() != parameterTypes.size()) {
628                    throw new ConstructionException("Invalid ObjectRecipe: recipe has " + parameterNames.size() +
629                            " parameter names and " + parameterTypes.size() + " parameter types");
630                }
631            } else if (!options.contains(Option.NAMED_PARAMETERS)) {
632                // Named parameters are not supported and no explicit parameters were given,
633                // so we will only use the no-arg constructor
634                parameterNames = Collections.emptyList();
635                parameterTypes = Collections.emptyList();
636            }
637    
638            // get all methods sorted so that the methods with the most constructor args are first
639            List<Method> methods = new ArrayList<Method>(Arrays.asList(typeClass.getMethods()));
640            methods.addAll(Arrays.asList(typeClass.getDeclaredMethods()));
641            Collections.sort(methods, new Comparator<Method>() {
642                public int compare(Method method2, Method method1) {
643                    return method1.getParameterTypes().length - method2.getParameterTypes().length;
644                }
645            });
646    
647    
648            // as we check each constructor, we remember the closest invalid match so we can throw a nice exception to the user
649            int matchLevel = 0;
650            MissingFactoryMethodException missException = null;
651    
652            boolean allowPrivate = options.contains(Option.PRIVATE_FACTORY);
653            boolean caseInsesnitive = options.contains(Option.CASE_INSENSITIVE_FACTORY);
654            for (Method method : methods) {
655                // Only consider methods where the name matches
656                if (!method.getName().equals(factoryMethod) && (!caseInsesnitive || !method.getName().equalsIgnoreCase(method.getName()))) {
657                    continue;
658                }
659    
660                // if an explicit constructor is specified (via parameter types), look a constructor that matches
661                if (parameterTypes != null) {
662                    if (method.getParameterTypes().length != parameterTypes.size()) {
663                        if (matchLevel < 1) {
664                            matchLevel = 1;
665                            missException = new MissingFactoryMethodException("Static factory method has " + method.getParameterTypes().length + " arugments " +
666                                    "but expected " + parameterTypes.size() + " arguments: " + method);
667                        }
668                        continue;
669                    }
670    
671                    if (!isAssignableFrom(parameterTypes, Arrays.asList(method.getParameterTypes()))) {
672                        if (matchLevel < 2) {
673                            matchLevel = 2;
674                            missException = new MissingFactoryMethodException("Static factory method has signature " +
675                                    "public static " + typeClass.getName() + "." + factoryMethod + toParameterList(method.getParameterTypes()) +
676                                    " but expected signature " +
677                                    "public static " + typeClass.getName() + "." + factoryMethod + toParameterList(parameterTypes));
678                        }
679                        continue;
680                    }
681                } else {
682                    // Implicit constructor selection based on named constructor args
683                    //
684                    // Only consider methods where we can supply a value for all of the parameters
685                    parameterNames = getParameterNames(method);
686                    if (parameterNames == null || !allProperties.containsAll(parameterNames)) {
687                        continue;
688                    }
689                }
690    
691                if (method.getReturnType() == Void.TYPE) {
692                    if (matchLevel < 3) {
693                        matchLevel = 3;
694                        missException = new MissingFactoryMethodException("Static factory method does not return a value: " + method);
695                    }
696                    continue;
697                }
698    
699                if (Modifier.isAbstract(method.getModifiers())) {
700                    if (matchLevel < 4) {
701                        matchLevel = 4;
702                        missException = new MissingFactoryMethodException("Static factory method is abstract: " + method);
703                    }
704                    continue;
705                }
706    
707                if (!allowPrivate && !Modifier.isPublic(method.getModifiers())) {
708                    if (matchLevel < 5) {
709                        matchLevel = 5;
710                        missException = new MissingFactoryMethodException("Static factory method is not public: " + method);
711                    }
712                    continue;
713                }
714    
715                if (!Modifier.isStatic(method.getModifiers())) {
716                    if (matchLevel < 6) {
717                        matchLevel = 6;
718                        missException = new MissingFactoryMethodException("Static factory method is not static: " + method);
719                    }
720                    continue;
721                }
722    
723                if (allowPrivate && !Modifier.isPublic(method.getModifiers())) {
724                    setAccessible(method);
725                }
726    
727                return new StaticFactory(method, parameterNames);
728            }
729    
730            if (missException != null) {
731                throw missException;
732            } else {
733                StringBuffer buffer = new StringBuffer("Unable to find a valid factory method: ");
734                buffer.append("public void ").append(typeClass.getName()).append(".");
735                buffer.append(factoryMethod).append(toParameterList(parameterTypes));
736                throw new MissingFactoryMethodException(buffer.toString());
737            }
738        }
739    
740        public static Method findInstanceFactory(Class typeClass, String factoryMethod, Set<Option> options) {
741            if (typeClass == null) throw new NullPointerException("typeClass is null");
742            if (factoryMethod == null) throw new NullPointerException("name is null");
743            if (factoryMethod.length() == 0) throw new IllegalArgumentException("name is an empty string");
744            if (options == null) options = EnumSet.noneOf(Option.class);
745            
746            int matchLevel = 0;
747            MissingFactoryMethodException missException = null;
748    
749            boolean allowPrivate = options.contains(Option.PRIVATE_FACTORY);
750            boolean caseInsesnitive = options.contains(Option.CASE_INSENSITIVE_FACTORY);
751    
752            List<Method> methods = new ArrayList<Method>(Arrays.asList(typeClass.getMethods()));
753            methods.addAll(Arrays.asList(typeClass.getDeclaredMethods()));
754            for (Method method : methods) {
755                if (method.getName().equals(factoryMethod) || (caseInsesnitive && method.getName().equalsIgnoreCase(method.getName()))) {
756                    if (Modifier.isStatic(method.getModifiers())) {
757                        if (matchLevel < 1) {
758                            matchLevel = 1;
759                            missException = new MissingFactoryMethodException("Instance factory method is static: " + method);
760                        }
761                        continue;
762                    }
763    
764                    if (method.getParameterTypes().length != 0) {
765                        if (matchLevel < 2) {
766                            matchLevel = 2;
767                            missException = new MissingFactoryMethodException("Instance factory method has signature " +
768                                    "public " + typeClass.getName() + "." + factoryMethod + toParameterList(method.getParameterTypes()) +
769                                    " but expected signature " +
770                                    "public " + typeClass.getName() + "." + factoryMethod + "()");
771                        }
772                        continue;
773                    }
774    
775                    if (method.getReturnType() == Void.TYPE) {
776                        if (matchLevel < 3) {
777                            matchLevel = 3;
778                            missException = new MissingFactoryMethodException("Instance factory method does not return a value: " + method);
779                        }
780                        continue;
781                    }
782    
783                    if (Modifier.isAbstract(method.getModifiers())) {
784                        if (matchLevel < 4) {
785                            matchLevel = 4;
786                            missException = new MissingFactoryMethodException("Instance factory method is abstract: " + method);
787                        }
788                        continue;
789                    }
790    
791                    if (!allowPrivate && !Modifier.isPublic(method.getModifiers())) {
792                        if (matchLevel < 5) {
793                            matchLevel = 5;
794                            missException = new MissingFactoryMethodException("Instance factory method is not public: " + method);
795                        }
796                        continue;
797                    }
798    
799                    if (allowPrivate && !Modifier.isPublic(method.getModifiers())) {
800                        setAccessible(method);
801                    }
802    
803                    return method;
804                }
805            }
806    
807            if (missException != null) {
808                throw missException;
809            } else {
810                StringBuffer buffer = new StringBuffer("Unable to find a valid factory method: ");
811                buffer.append("public void ").append(typeClass.getName()).append(".");
812                buffer.append(factoryMethod).append("()");
813                throw new MissingFactoryMethodException(buffer.toString());
814            }
815        }
816    
817        public static List<String> getParameterNames(Constructor<?> constructor) {
818            // use reflection to get Java6 ConstructorParameter annotation value
819            try {
820                Class<? extends Annotation> constructorPropertiesClass = ClassLoader.getSystemClassLoader().loadClass("java.beans.ConstructorProperties").asSubclass(Annotation.class);
821                Annotation constructorProperties = constructor.getAnnotation(constructorPropertiesClass);
822                if (constructorProperties != null) {
823                    String[] parameterNames = (String[]) constructorPropertiesClass.getMethod("value").invoke(constructorProperties);
824                    if (parameterNames != null) {
825                        return Arrays.asList(parameterNames);
826                    }
827                }
828            } catch (Throwable e) {
829            }
830    
831            ParameterNames parameterNames = constructor.getAnnotation(ParameterNames.class);
832            if (parameterNames != null && parameterNames.value() != null) {
833                return Arrays.asList(parameterNames.value());
834            }
835            if (parameterNamesLoader != null) {
836                return parameterNamesLoader.get(constructor);
837            }
838            return null;
839        }
840    
841        public static List<String> getParameterNames(Method method) {
842            ParameterNames parameterNames = method.getAnnotation(ParameterNames.class);
843            if (parameterNames != null && parameterNames.value() != null) {
844                return Arrays.asList(parameterNames.value());
845            }
846            if (parameterNamesLoader != null) {
847                return parameterNamesLoader.get(method);
848            }
849            return null;
850        }
851    
852        public static interface Factory {
853            List<String> getParameterNames();
854    
855            List<Type> getParameterTypes();
856    
857            Object create(Object... parameters) throws ConstructionException;
858        }
859    
860        public static class ConstructorFactory implements Factory {
861            private Constructor constructor;
862            private List<String> parameterNames;
863    
864            public ConstructorFactory(Constructor constructor, List<String> parameterNames) {
865                if (constructor == null) throw new NullPointerException("constructor is null");
866                if (parameterNames == null) throw new NullPointerException("parameterNames is null");
867                this.constructor = constructor;
868                this.parameterNames = parameterNames;
869            }
870    
871            public List<String> getParameterNames() {
872                return parameterNames;
873            }
874    
875            public List<Type> getParameterTypes() {
876                return new ArrayList<Type>(Arrays.asList(constructor.getGenericParameterTypes()));
877            }
878    
879            public Object create(Object... parameters) throws ConstructionException {
880                // create the instance
881                try {
882                    Object instance = constructor.newInstance(parameters);
883                    return instance;
884                } catch (Exception e) {
885                    Throwable t = e;
886                    if (e instanceof InvocationTargetException) {
887                        InvocationTargetException invocationTargetException = (InvocationTargetException) e;
888                        if (invocationTargetException.getCause() != null) {
889                            t = invocationTargetException.getCause();
890                        }
891                    }
892                    throw new ConstructionException("Error invoking constructor: " + constructor, t);
893                }
894            }
895        }
896    
897        public static class StaticFactory implements Factory {
898            private Method staticFactory;
899            private List<String> parameterNames;
900    
901            public StaticFactory(Method staticFactory, List<String> parameterNames) {
902                this.staticFactory = staticFactory;
903                this.parameterNames = parameterNames;
904            }
905    
906            public List<String> getParameterNames() {
907                if (parameterNames == null) {
908                    throw new ConstructionException("InstanceFactory has not been initialized");
909                }
910    
911                return parameterNames;
912            }
913    
914            public List<Type> getParameterTypes() {
915                return new ArrayList<Type>(Arrays.asList(staticFactory.getGenericParameterTypes()));
916            }
917    
918            public Object create(Object... parameters) throws ConstructionException {
919                try {
920                    Object instance = staticFactory.invoke(null, parameters);
921                    return instance;
922                } catch (Exception e) {
923                    Throwable t = e;
924                    if (e instanceof InvocationTargetException) {
925                        InvocationTargetException invocationTargetException = (InvocationTargetException) e;
926                        if (invocationTargetException.getCause() != null) {
927                            t = invocationTargetException.getCause();
928                        }
929                    }
930                    throw new ConstructionException("Error invoking factory method: " + staticFactory, t);
931                }
932            }
933        }
934    
935        private static void setAccessible(final AccessibleObject accessibleObject) {
936            AccessController.doPrivileged(new PrivilegedAction<Object>() {
937                public Object run() {
938                    accessibleObject.setAccessible(true);
939                    return null;
940                }
941            });
942        }
943    
944        private static String toParameterList(Class<?>[] parameterTypes) {
945            return toParameterList(parameterTypes != null ? Arrays.asList(parameterTypes) : null);
946        }
947    
948        private static String toParameterList(List<? extends Class<?>> parameterTypes) {
949            StringBuffer buffer = new StringBuffer();
950            buffer.append("(");
951            if (parameterTypes != null) {
952                for (int i = 0; i < parameterTypes.size(); i++) {
953                    Class type = parameterTypes.get(i);
954                    if (i > 0) buffer.append(", ");
955                    buffer.append(type.getName());
956                }
957            } else {
958                buffer.append("...");
959            }
960            buffer.append(")");
961            return buffer.toString();
962        }
963    }