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.io.IOException;
021import java.io.InputStream;
022import java.lang.reflect.Constructor;
023import java.lang.reflect.Method;
024import java.lang.reflect.Modifier;
025import java.util.ArrayList;
026import java.util.Collections;
027import java.util.HashMap;
028import java.util.List;
029import java.util.Map;
030import java.util.WeakHashMap;
031import java.util.Arrays;
032
033import org.objectweb.asm.ClassReader;
034import org.objectweb.asm.Label;
035import org.objectweb.asm.MethodVisitor;
036import org.objectweb.asm.Type;
037import org.objectweb.asm.commons.EmptyVisitor;
038
039/**
040 * Implementation of ParameterNameLoader that uses ASM to read the parameter names from the local variable table in the
041 * class byte code.
042 *
043 * This wonderful piece of code was taken from org.springframework.core.LocalVariableTableParameterNameDiscover
044 */
045public class AsmParameterNameLoader implements ParameterNameLoader {
046    /**
047     * Weak map from Constructor to List<String>.
048     */
049    private final WeakHashMap<Constructor,List<String>> constructorCache = new WeakHashMap<Constructor,List<String>>();
050
051    /**
052     * Weak map from Method to List&lt;String&gt;.
053     */
054    private final WeakHashMap<Method,List<String>> methodCache = new WeakHashMap<Method,List<String>>();
055
056    /**
057     * Gets the parameter names of the specified method or null if the class was compiled without debug symbols on.
058     * @param method the method for which the parameter names should be retrieved
059     * @return the parameter names or null if the class was compilesd without debug symbols on
060     */
061    public List<String> get(Method method) {
062        // check the cache
063        if (methodCache.containsKey(method)) {
064            return methodCache.get(method);
065        }
066
067        Map<Method,List<String>> allMethodParameters = getAllMethodParameters(method.getDeclaringClass(), method.getName());
068        return allMethodParameters.get(method);
069    }
070
071    /**
072     * Gets the parameter names of the specified constructor or null if the class was compiled without debug symbols on.
073     * @param constructor the constructor for which the parameters should be retrieved
074     * @return the parameter names or null if the class was compiled without debug symbols on
075     */
076    public List<String> get(Constructor constructor) {
077        // check the cache
078        if (constructorCache.containsKey(constructor)) {
079            return constructorCache.get(constructor);
080        }
081
082        Map<Constructor,List<String>> allConstructorParameters = getAllConstructorParameters(constructor.getDeclaringClass());
083        return allConstructorParameters.get(constructor);
084    }
085
086    /**
087     * Gets the parameter names of all constructor or null if the class was compiled without debug symbols on.
088     * @param clazz the class for which the constructor parameter names should be retrieved
089     * @return a map from Constructor object to the parameter names or null if the class was compiled without debug symbols on
090     */
091    public Map<Constructor,List<String>> getAllConstructorParameters(Class clazz) {
092        // Determine the constructors?
093        List<Constructor> constructors = new ArrayList<Constructor>(Arrays.asList(clazz.getConstructors()));
094        constructors.addAll(Arrays.asList(clazz.getDeclaredConstructors()));
095        if (constructors.isEmpty()) {
096            return Collections.emptyMap();
097        }
098
099        // Check the cache
100        if (constructorCache.containsKey(constructors.get(0))) {
101            Map<Constructor,List<String>> constructorParameters = new HashMap<Constructor,List<String>>();
102            for (Constructor constructor : constructors) {
103                constructorParameters.put(constructor, constructorCache.get(constructor));
104            }
105            return constructorParameters;
106        }
107
108        // Load the parameter names using ASM
109        Map<Constructor,List<String>> constructorParameters = new HashMap<Constructor,List<String>> ();
110        try {
111            ClassReader reader = AsmParameterNameLoader.createClassReader(clazz);
112
113            AsmParameterNameLoader.AllParameterNamesDiscoveringVisitor visitor = new AsmParameterNameLoader.AllParameterNamesDiscoveringVisitor(clazz);
114            reader.accept(visitor, 0);
115
116            Map exceptions = visitor.getExceptions();
117            if (exceptions.size() == 1) {
118                throw new RuntimeException((Exception)exceptions.values().iterator().next());
119            }
120            if (!exceptions.isEmpty()) {
121                throw new RuntimeException(exceptions.toString());
122            }
123
124            constructorParameters = visitor.getConstructorParameters();
125        } catch (IOException ex) {
126        }
127
128        // Cache the names
129        for (Constructor constructor : constructors) {
130            constructorCache.put(constructor, constructorParameters.get(constructor));
131        }
132        return constructorParameters;
133    }
134
135    /**
136     * Gets the parameter names of all methods with the specified name or null if the class was compiled without debug symbols on.
137     * @param clazz the class for which the method parameter names should be retrieved
138     * @param methodName the of the method for which the parameters should be retrieved
139     * @return a map from Method object to the parameter names or null if the class was compiled without debug symbols on
140     */
141    public Map<Method,List<String>> getAllMethodParameters(Class clazz, String methodName) {
142        // Determine the constructors?
143        Method[] methods = getMethods(clazz, methodName);
144        if (methods.length == 0) {
145            return Collections.emptyMap();
146        }
147
148        // Check the cache
149        if (methodCache.containsKey(methods[0])) {
150            Map<Method,List<String>> methodParameters = new HashMap<Method,List<String>>();
151            for (Method method : methods) {
152                methodParameters.put(method, methodCache.get(method));
153            }
154            return methodParameters;
155        }
156
157        // Load the parameter names using ASM
158        Map<Method,List<String>>  methodParameters = new HashMap<Method,List<String>>();
159        try {
160            ClassReader reader = AsmParameterNameLoader.createClassReader(clazz);
161
162            AsmParameterNameLoader.AllParameterNamesDiscoveringVisitor visitor = new AsmParameterNameLoader.AllParameterNamesDiscoveringVisitor(clazz, methodName);
163            reader.accept(visitor, 0);
164
165            Map exceptions = visitor.getExceptions();
166            if (exceptions.size() == 1) {
167                throw new RuntimeException((Exception)exceptions.values().iterator().next());
168            }
169            if (!exceptions.isEmpty()) {
170                throw new RuntimeException(exceptions.toString());
171            }
172
173            methodParameters = visitor.getMethodParameters();
174        } catch (IOException ex) {
175        }
176
177        // Cache the names
178        for (Method method : methods) {
179            methodCache.put(method, methodParameters.get(method));
180        }
181        return methodParameters;
182    }
183
184    private Method[] getMethods(Class clazz, String methodName) {
185        List<Method> methods = new ArrayList<Method>(Arrays.asList(clazz.getMethods()));
186        methods.addAll(Arrays.asList(clazz.getDeclaredMethods()));
187        List<Method> matchingMethod = new ArrayList<Method>(methods.size());
188        for (Method method : methods) {
189            if (method.getName().equals(methodName)) {
190                matchingMethod.add(method);
191            }
192        }
193        return matchingMethod.toArray(new Method[matchingMethod.size()]);
194    }
195
196    private static ClassReader createClassReader(Class declaringClass) throws IOException {
197        InputStream in = null;
198        try {
199            ClassLoader classLoader = declaringClass.getClassLoader();
200            in = classLoader.getResourceAsStream(declaringClass.getName().replace('.', '/') + ".class");
201            ClassReader reader = new ClassReader(in);
202            return reader;
203        } finally {
204            if (in != null) {
205                try {
206                    in.close();
207                } catch (IOException ignored) {
208                }
209            }
210        }
211    }
212
213    private static class AllParameterNamesDiscoveringVisitor extends EmptyVisitor {
214        private final Map<Constructor,List<String>> constructorParameters = new HashMap<Constructor,List<String>>();
215        private final Map<Method,List<String>> methodParameters = new HashMap<Method,List<String>>();
216        private final Map<String,Exception> exceptions = new HashMap<String,Exception>();
217        private final String methodName;
218        private final Map<String,Method> methodMap = new HashMap<String,Method>();
219        private final Map<String,Constructor> constructorMap = new HashMap<String,Constructor>();
220
221        public AllParameterNamesDiscoveringVisitor(Class type, String methodName) {
222            this.methodName = methodName;
223
224            List<Method> methods = new ArrayList<Method>(Arrays.asList(type.getMethods()));
225            methods.addAll(Arrays.asList(type.getDeclaredMethods()));
226            for (Method method : methods) {
227                if (method.getName().equals(methodName)) {
228                    methodMap.put(Type.getMethodDescriptor(method), method);
229                }
230            }
231        }
232
233        public AllParameterNamesDiscoveringVisitor(Class type) {
234            this.methodName = "<init>";
235
236            List<Constructor> constructors = new ArrayList<Constructor>(Arrays.asList(type.getConstructors()));
237            constructors.addAll(Arrays.asList(type.getDeclaredConstructors()));
238            for (Constructor constructor : constructors) {
239                Type[] types = new Type[constructor.getParameterTypes().length];
240                for (int j = 0; j < types.length; j++) {
241                    types[j] = Type.getType(constructor.getParameterTypes()[j]);
242                }
243                constructorMap.put(Type.getMethodDescriptor(Type.VOID_TYPE, types), constructor);
244            }
245        }
246
247        public Map<Constructor, List<String>> getConstructorParameters() {
248            return constructorParameters;
249        }
250
251        public Map<Method, List<String>> getMethodParameters() {
252            return methodParameters;
253        }
254
255        public Map<String,Exception> getExceptions() {
256            return exceptions;
257        }
258
259        public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
260            if (!name.equals(this.methodName)) {
261                return null;
262            }
263
264            try {
265                final List<String> parameterNames;
266                final boolean isStaticMethod;
267
268                if (methodName.equals("<init>")) {
269                    Constructor constructor = constructorMap.get(desc);
270                    if (constructor == null) {
271                        return null;
272                    }
273                    parameterNames = new ArrayList<String>(constructor.getParameterTypes().length);
274                    parameterNames.addAll(Collections.<String>nCopies(constructor.getParameterTypes().length, null));
275                    constructorParameters.put(constructor, parameterNames);
276                    isStaticMethod = false;
277                } else {
278                    Method method = methodMap.get(desc);
279                    if (method == null) {
280                        return null;
281                    }
282                    parameterNames = new ArrayList<String>(method.getParameterTypes().length);
283                    parameterNames.addAll(Collections.<String>nCopies(method.getParameterTypes().length, null));
284                    methodParameters.put(method, parameterNames);
285                    isStaticMethod = Modifier.isStatic(method.getModifiers());
286                }
287
288                return new EmptyVisitor() {
289                    // assume static method until we get a first parameter name
290                    public void visitLocalVariable(String name, String description, String signature, Label start, Label end, int index) {
291                        if (isStaticMethod) {
292                            parameterNames.set(index, name);
293                        } else if (index > 0) {
294                            // for non-static the 0th arg is "this" so we need to offset by -1
295                            parameterNames.set(index - 1, name);
296                        }
297                    }
298                };
299            } catch (Exception e) {
300                this.exceptions.put(signature, e);
301            }
302            return null;
303        }
304    }
305}