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