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