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.security.AccessController;
029import java.security.PrivilegedAction;
030import java.util.ArrayList;
031import java.util.Arrays;
032import java.util.Collections;
033import java.util.Comparator;
034import java.util.EnumSet;
035import java.util.LinkedHashSet;
036import java.util.LinkedList;
037import java.util.List;
038import java.util.Set;
039
040import static org.apache.xbean.recipe.RecipeHelper.isAssignableFrom;
041
042public 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}