1/*
2 * Javassist, a Java-bytecode translator toolkit.
3 * Copyright (C) 1999-2007 Shigeru Chiba. All Rights Reserved.
4 *
5 * The contents of this file are subject to the Mozilla Public License Version
6 * 1.1 (the "License"); you may not use this file except in compliance with
7 * the License.  Alternatively, the contents of this file may be used under
8 * the terms of the GNU Lesser General Public License Version 2.1 or later.
9 *
10 * Software distributed under the License is distributed on an "AS IS" basis,
11 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
12 * for the specific language governing rights and limitations under the
13 * License.
14 */
15
16package javassist.bytecode.annotation;
17
18import java.lang.reflect.InvocationHandler;
19import java.lang.reflect.Method;
20import java.lang.reflect.Proxy;
21
22import javassist.ClassPool;
23import javassist.CtClass;
24import javassist.NotFoundException;
25import javassist.bytecode.AnnotationDefaultAttribute;
26import javassist.bytecode.ClassFile;
27import javassist.bytecode.MethodInfo;
28
29/**
30 * Internal-use only.  This is a helper class internally used for implementing
31 * <code>toAnnotationType()</code> in <code>Annotation</code>.
32 *
33 * @author Shigeru Chiba
34 * @author <a href="mailto:bill@jboss.org">Bill Burke</a>
35 * @author <a href="mailto:adrian@jboss.org">Adrian Brock</a>
36 */
37public class AnnotationImpl implements InvocationHandler {
38    private static final String JDK_ANNOTATION_CLASS_NAME = "java.lang.annotation.Annotation";
39    private static Method JDK_ANNOTATION_TYPE_METHOD = null;
40
41    private Annotation annotation;
42    private ClassPool pool;
43    private ClassLoader classLoader;
44    private transient Class annotationType;
45    private transient int cachedHashCode = Integer.MIN_VALUE;
46
47    static {
48        // Try to resolve the JDK annotation type method
49        try {
50            Class clazz = Class.forName(JDK_ANNOTATION_CLASS_NAME);
51            JDK_ANNOTATION_TYPE_METHOD = clazz.getMethod("annotationType", (Class[])null);
52        }
53        catch (Exception ignored) {
54            // Probably not JDK5+
55        }
56    }
57
58    /**
59     * Constructs an annotation object.
60     *
61     * @param cl        class loader for obtaining annotation types.
62     * @param clazz     the annotation type.
63     * @param cp        class pool for containing an annotation
64     *                  type (or null).
65     * @param anon      the annotation.
66     * @return the annotation
67     */
68    public static Object make(ClassLoader cl, Class clazz, ClassPool cp,
69                              Annotation anon) {
70        AnnotationImpl handler = new AnnotationImpl(anon, cp, cl);
71        return Proxy.newProxyInstance(cl, new Class[] { clazz }, handler);
72    }
73
74    private AnnotationImpl(Annotation a, ClassPool cp, ClassLoader loader) {
75        annotation = a;
76        pool = cp;
77        classLoader = loader;
78    }
79
80    /**
81     * Obtains the name of the annotation type.
82     *
83     * @return the type name
84     */
85    public String getTypeName() {
86        return annotation.getTypeName();
87    }
88
89    /**
90     * Get the annotation type
91     *
92     * @return the annotation class
93     * @throws NoClassDefFoundError when the class could not loaded
94     */
95    private Class getAnnotationType() {
96        if (annotationType == null) {
97            String typeName = annotation.getTypeName();
98            try {
99                annotationType = classLoader.loadClass(typeName);
100            }
101            catch (ClassNotFoundException e) {
102                NoClassDefFoundError error = new NoClassDefFoundError("Error loading annotation class: " + typeName);
103                error.setStackTrace(e.getStackTrace());
104                throw error;
105            }
106        }
107        return annotationType;
108    }
109
110    /**
111     * Obtains the internal data structure representing the annotation.
112     *
113     * @return the annotation
114     */
115    public Annotation getAnnotation() {
116        return annotation;
117    }
118
119    /**
120     * Executes a method invocation on a proxy instance.
121     * The implementations of <code>toString()</code>, <code>equals()</code>,
122     * and <code>hashCode()</code> are directly supplied by the
123     * <code>AnnotationImpl</code>.  The <code>annotationType()</code> method
124     * is also available on the proxy instance.
125     */
126    public Object invoke(Object proxy, Method method, Object[] args)
127        throws Throwable
128    {
129        String name = method.getName();
130        if (Object.class == method.getDeclaringClass()) {
131            if ("equals".equals(name)) {
132                Object obj = args[0];
133                return new Boolean(checkEquals(obj));
134            }
135            else if ("toString".equals(name))
136                return annotation.toString();
137            else if ("hashCode".equals(name))
138                return new Integer(hashCode());
139        }
140        else if ("annotationType".equals(name)
141                 && method.getParameterTypes().length == 0)
142           return getAnnotationType();
143
144        MemberValue mv = annotation.getMemberValue(name);
145        if (mv == null)
146            return getDefault(name, method);
147        else
148            return mv.getValue(classLoader, pool, method);
149    }
150
151    private Object getDefault(String name, Method method)
152        throws ClassNotFoundException, RuntimeException
153    {
154        String classname = annotation.getTypeName();
155        if (pool != null) {
156            try {
157                CtClass cc = pool.get(classname);
158                ClassFile cf = cc.getClassFile2();
159                MethodInfo minfo = cf.getMethod(name);
160                if (minfo != null) {
161                    AnnotationDefaultAttribute ainfo
162                        = (AnnotationDefaultAttribute)
163                          minfo.getAttribute(AnnotationDefaultAttribute.tag);
164                    if (ainfo != null) {
165                        MemberValue mv = ainfo.getDefaultValue();
166                        return mv.getValue(classLoader, pool, method);
167                    }
168                }
169            }
170            catch (NotFoundException e) {
171                throw new RuntimeException("cannot find a class file: "
172                                           + classname);
173            }
174        }
175
176        throw new RuntimeException("no default value: " + classname + "."
177                                   + name + "()");
178    }
179
180    /**
181     * Returns a hash code value for this object.
182     */
183    public int hashCode() {
184        if (cachedHashCode == Integer.MIN_VALUE) {
185            int hashCode = 0;
186
187            // Load the annotation class
188            getAnnotationType();
189
190            Method[] methods = annotationType.getDeclaredMethods();
191            for (int i = 0; i < methods.length; ++ i) {
192                String name = methods[i].getName();
193                int valueHashCode = 0;
194
195                // Get the value
196                MemberValue mv = annotation.getMemberValue(name);
197                Object value = null;
198                try {
199                   if (mv != null)
200                       value = mv.getValue(classLoader, pool, methods[i]);
201                   if (value == null)
202                       value = getDefault(name, methods[i]);
203                }
204                catch (RuntimeException e) {
205                    throw e;
206                }
207                catch (Exception e) {
208                    throw new RuntimeException("Error retrieving value " + name + " for annotation " + annotation.getTypeName(), e);
209                }
210
211                // Calculate the hash code
212                if (value != null) {
213                    if (value.getClass().isArray())
214                        valueHashCode = arrayHashCode(value);
215                    else
216                        valueHashCode = value.hashCode();
217                }
218                hashCode += 127 * name.hashCode() ^ valueHashCode;
219            }
220
221            cachedHashCode = hashCode;
222        }
223        return cachedHashCode;
224    }
225
226    /**
227     * Check that another annotation equals ourselves.
228     *
229     * @param obj the other annotation
230     * @return the true when equals false otherwise
231     * @throws Exception for any problem
232     */
233    private boolean checkEquals(Object obj) throws Exception {
234        if (obj == null)
235            return false;
236
237        // Optimization when the other is one of ourselves
238        if (obj instanceof Proxy) {
239            InvocationHandler ih = Proxy.getInvocationHandler(obj);
240            if (ih instanceof AnnotationImpl) {
241                AnnotationImpl other = (AnnotationImpl) ih;
242                return annotation.equals(other.annotation);
243            }
244        }
245
246        Class otherAnnotationType = (Class) JDK_ANNOTATION_TYPE_METHOD.invoke(obj, (Object[])null);
247        if (getAnnotationType().equals(otherAnnotationType) == false)
248           return false;
249
250        Method[] methods = annotationType.getDeclaredMethods();
251        for (int i = 0; i < methods.length; ++ i) {
252            String name = methods[i].getName();
253
254            // Get the value
255            MemberValue mv = annotation.getMemberValue(name);
256            Object value = null;
257            Object otherValue = null;
258            try {
259               if (mv != null)
260                   value = mv.getValue(classLoader, pool, methods[i]);
261               if (value == null)
262                   value = getDefault(name, methods[i]);
263               otherValue = methods[i].invoke(obj, (Object[])null);
264            }
265            catch (RuntimeException e) {
266                throw e;
267            }
268            catch (Exception e) {
269                throw new RuntimeException("Error retrieving value " + name + " for annotation " + annotation.getTypeName(), e);
270            }
271
272            if (value == null && otherValue != null)
273                return false;
274            if (value != null && value.equals(otherValue) == false)
275                return false;
276        }
277
278        return true;
279    }
280
281    /**
282     * Calculates the hashCode of an array using the same
283     * algorithm as java.util.Arrays.hashCode()
284     *
285     * @param object the object
286     * @return the hashCode
287     */
288    private static int arrayHashCode(Object object)
289    {
290       if (object == null)
291          return 0;
292
293       int result = 1;
294
295       Object[] array = (Object[]) object;
296       for (int i = 0; i < array.length; ++i) {
297           int elementHashCode = 0;
298           if (array[i] != null)
299              elementHashCode = array[i].hashCode();
300           result = 31 * result + elementHashCode;
301       }
302       return result;
303    }
304}
305