1/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.layoutlib.bridge;
18
19import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
20import com.android.tools.layoutlib.create.CreateInfo;
21
22import java.lang.reflect.Method;
23import java.lang.reflect.Modifier;
24import java.util.ArrayList;
25import java.util.List;
26
27import junit.framework.TestCase;
28
29/**
30 * Tests that native delegate classes implement all the required methods.
31 *
32 * This looks at {@link CreateInfo#DELEGATE_CLASS_NATIVES} to get the list of classes that
33 * have their native methods reimplemented through a delegate.
34 *
35 * Since the reimplemented methods are not native anymore, we look for the annotation
36 * {@link LayoutlibDelegate}, and look for a matching method in the delegate (named the same
37 * as the modified class with _Delegate added as a suffix).
38 * If the original native method is not static, then we make sure the delegate method also
39 * include the original class as first parameter (to access "this").
40 *
41 */
42public class TestDelegates extends TestCase {
43
44    private List<String> mErrors = new ArrayList<String>();
45
46    public void testNativeDelegates() {
47
48        final String[] classes = CreateInfo.DELEGATE_CLASS_NATIVES;
49        mErrors.clear();
50        for (String clazz : classes) {
51            loadAndCompareClasses(clazz, clazz + "_Delegate");
52        }
53        assertTrue(getErrors(), mErrors.isEmpty());
54    }
55
56    public void testMethodDelegates() {
57        final String[] methods = CreateInfo.DELEGATE_METHODS;
58        mErrors.clear();
59        for (String methodName : methods) {
60            // extract the class name
61            String className = methodName.substring(0, methodName.indexOf('#'));
62            String targetClassName = className.replace('$', '_') + "_Delegate";
63
64            loadAndCompareClasses(className, targetClassName);
65        }
66        assertTrue(getErrors(), mErrors.isEmpty());
67    }
68
69    private void loadAndCompareClasses(String originalClassName, String delegateClassName) {
70        // load the classes
71        try {
72            ClassLoader classLoader = TestDelegates.class.getClassLoader();
73            Class<?> originalClass = classLoader.loadClass(originalClassName);
74            Class<?> delegateClass = classLoader.loadClass(delegateClassName);
75
76            compare(originalClass, delegateClass);
77        } catch (ClassNotFoundException e) {
78            mErrors.add("Failed to load class: " + e.getMessage());
79        } catch (SecurityException e) {
80            mErrors.add("Failed to load class: " + e.getMessage());
81        }
82    }
83
84    private void compare(Class<?> originalClass, Class<?> delegateClass) throws SecurityException {
85        List<Method> checkedDelegateMethods = new ArrayList<Method>();
86
87        // loop on the methods of the original class, and for the ones that are annotated
88        // with @LayoutlibDelegate, look for a matching method in the delegate class.
89        // The annotation is automatically added by layoutlib_create when it replace a method
90        // by a call to a delegate
91        Method[] originalMethods = originalClass.getDeclaredMethods();
92        for (Method originalMethod : originalMethods) {
93            // look for methods that are delegated: they have the LayoutlibDelegate annotation
94            if (originalMethod.getAnnotation(LayoutlibDelegate.class) == null) {
95                continue;
96            }
97
98            // get the signature.
99            Class<?>[] parameters = originalMethod.getParameterTypes();
100
101            // if the method is not static, then the class is added as the first parameter
102            // (for "this")
103            if ((originalMethod.getModifiers() & Modifier.STATIC) == 0) {
104
105                Class<?>[] newParameters = new Class<?>[parameters.length + 1];
106                newParameters[0] = originalClass;
107                System.arraycopy(parameters, 0, newParameters, 1, parameters.length);
108                parameters = newParameters;
109            }
110
111            // if the original class is an inner class that's not static, then
112            // we add this on the enclosing class at the beginning
113            if (originalClass.getEnclosingClass() != null &&
114                    (originalClass.getModifiers() & Modifier.STATIC) == 0) {
115                Class<?>[] newParameters = new Class<?>[parameters.length + 1];
116                newParameters[0] = originalClass.getEnclosingClass();
117                System.arraycopy(parameters, 0, newParameters, 1, parameters.length);
118                parameters = newParameters;
119            }
120
121            try {
122                // try to load the method with the given parameter types.
123                Method delegateMethod = delegateClass.getDeclaredMethod(originalMethod.getName(),
124                        parameters);
125
126                // check the return type of the methods match.
127                if (delegateMethod.getReturnType() != originalMethod.getReturnType()) {
128                    mErrors.add(
129                            String.format("Delegate method %1$s.%2$s does not match the " +
130                                    "corresponding framework method which returns %3$s",
131                            delegateClass.getName(),
132                            getMethodName(delegateMethod),
133                            originalMethod.getReturnType().getName()));
134                }
135
136                // check that the method has the annotation
137                if (delegateMethod.getAnnotation(LayoutlibDelegate.class) == null) {
138                    mErrors.add(
139                            String.format("Delegate method %1$s for class %2$s does not have the " +
140                                            "@LayoutlibDelegate annotation",
141                                    delegateMethod.getName(),
142                                    originalClass.getName()));
143                }
144
145                // check that the method is static
146                if ((delegateMethod.getModifiers() & Modifier.STATIC) != Modifier.STATIC) {
147                    mErrors.add(
148                            String.format(
149                                    "Delegate method %1$s for class %2$s is not static",
150                                    delegateMethod.getName(),
151                                    originalClass.getName())
152                    );
153                }
154
155                // add the method as checked.
156                checkedDelegateMethods.add(delegateMethod);
157            } catch (NoSuchMethodException e) {
158                String name = getMethodName(originalMethod, parameters);
159                mErrors.add(String.format("Missing %1$s.%2$s", delegateClass.getName(), name));
160            }
161        }
162
163        // look for dead (delegate) code.
164        // This looks for all methods in the delegate class, and if they have the
165        // @LayoutlibDelegate annotation, make sure they have been previously found as a
166        // match for a method in the original class.
167        // If not, this means the method is a delegate for a method that either doesn't exist
168        // anymore or is not delegated anymore.
169        Method[] delegateMethods = delegateClass.getDeclaredMethods();
170        for (Method delegateMethod : delegateMethods) {
171            // look for methods that are delegates: they have the LayoutlibDelegate annotation
172            if (delegateMethod.getAnnotation(LayoutlibDelegate.class) == null) {
173                continue;
174            }
175
176            if (!checkedDelegateMethods.contains(delegateMethod)) {
177                mErrors.add(String.format(
178                        "Delegate method %1$s.%2$s is not used anymore and must be removed",
179                        delegateClass.getName(),
180                        getMethodName(delegateMethod)));
181            }
182        }
183
184    }
185
186    private String getMethodName(Method method) {
187        return getMethodName(method, method.getParameterTypes());
188    }
189
190    private String getMethodName(Method method, Class<?>[] parameters) {
191        // compute a full class name that's long but not too long.
192        StringBuilder sb = new StringBuilder(method.getName() + "(");
193        for (int j = 0; j < parameters.length; j++) {
194            Class<?> theClass = parameters[j];
195            int dimensions = 0;
196            while (theClass.isArray()) {
197                dimensions++;
198                theClass = theClass.getComponentType();
199            }
200            sb.append(theClass.getName());
201            for (int i = 0; i < dimensions; i++) {
202                sb.append("[]");
203            }
204            if (j < (parameters.length - 1)) {
205                sb.append(",");
206            }
207        }
208        sb.append(")");
209
210        return sb.toString();
211    }
212
213    private String getErrors() {
214        StringBuilder s = new StringBuilder();
215        for (String error : mErrors) {
216            s.append(error).append('\n');
217        }
218        return s.toString();
219    }
220}
221