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