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