1 /*
2  * Copyright (C) 2019 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 
17 package com.android.tools.layoutlib.create;
18 
19 import com.android.tools.layoutlib.create.dataclass.OuterClass;
20 import com.android.tools.layoutlib.create.dataclass.UsageClass;
21 
22 import org.junit.Test;
23 import org.objectweb.asm.ClassReader;
24 import org.objectweb.asm.ClassWriter;
25 
26 import java.io.IOException;
27 import java.lang.reflect.Method;
28 import java.util.HashMap;
29 import java.util.Map;
30 import java.util.Map.Entry;
31 import java.util.Set;
32 
33 import static org.junit.Assert.assertEquals;
34 import static org.junit.Assert.assertNotNull;
35 
36 public class RefactorClassAdapterTest {
37     private static final String OUTER_CLASS_NAME = OuterClass.class.getName();
38     private static final String USAGE_CLASS_NAME = UsageClass.class.getName();
39 
40     @Test
testRefactorWithLambdas()41     public void testRefactorWithLambdas() throws Exception {
42         Map<String, String> refactorMap = new HashMap<>();
43         String originalClassName = OUTER_CLASS_NAME.replace('.', '/');
44         String newClassName = originalClassName + "Refactored";
45         refactorMap.put(originalClassName, newClassName);
46 
47         ClassWriter outerCw = new ClassWriter(0 /*flags*/);
48         RefactorClassAdapter cv = new RefactorClassAdapter(outerCw, refactorMap);
49         ClassReader cr = new ClassReader(OUTER_CLASS_NAME);
50         cr.accept(cv, 0 /* flags */);
51 
52         ClassWriter usageCw = new ClassWriter(0 /*flags*/);
53         cv = new RefactorClassAdapter(usageCw, refactorMap);
54         cr = new ClassReader(USAGE_CLASS_NAME);
55         cr.accept(cv, 0 /* flags */);
56 
57         ClassLoader2 cl2 = new ClassLoader2() {
58             @Override
59             public void testModifiedInstance() throws Exception {
60                 Class<?> clazz2 = loadClass(USAGE_CLASS_NAME);
61                 Object usage = clazz2.newInstance();
62                 assertNotNull(usage);
63                 assertEquals(17, callUsage(usage));
64             }
65         };
66         cl2.add(OUTER_CLASS_NAME + "Refactored", outerCw);
67         cl2.add(USAGE_CLASS_NAME, usageCw);
68         cl2.testModifiedInstance();
69     }
70 
71     //-------
72 
73     /**
74      * A class loader than can define and instantiate our modified classes.
75      * <p/>
76      * Trying to do so in the original class loader generates all sort of link issues because
77      * there are 2 different definitions of the same class name. This class loader will
78      * define and load the class when requested by name and provide helpers to access the
79      * instance methods via reflection.
80      */
81     private abstract static class ClassLoader2 extends ClassLoader {
82 
83         private final Map<String, byte[]> mClassDefs = new HashMap<>();
84 
ClassLoader2()85         public ClassLoader2() {
86             super(null);
87         }
88 
add(String className, ClassWriter rewrittenClass)89         private void add(String className, ClassWriter rewrittenClass) {
90             mClassDefs.put(className, rewrittenClass.toByteArray());
91         }
92 
getByteCode()93         private Set<Entry<String, byte[]>> getByteCode() {
94             return mClassDefs.entrySet();
95         }
96 
97         @Override
findClass(String name)98         protected Class<?> findClass(String name) {
99             try {
100                 return super.findClass(name);
101             } catch (ClassNotFoundException e) {
102 
103                 byte[] def = mClassDefs.get(name);
104                 if (def != null) {
105                     // Load the modified class from its bytes representation.
106                     return defineClass(name, def, 0, def.length);
107                 }
108 
109                 try {
110                     // Load everything else from the original definition into the new class loader.
111                     ClassReader cr = new ClassReader(name);
112                     ClassWriter cw = new ClassWriter(0);
113                     cr.accept(cw, 0);
114                     byte[] bytes = cw.toByteArray();
115                     return defineClass(name, bytes, 0, bytes.length);
116 
117                 } catch (IOException ioe) {
118                     throw new RuntimeException(ioe);
119                 }
120             }
121         }
122 
123         /**
124          * Accesses {@link UsageClass#doSomething} via reflection.
125          */
callUsage(Object instance)126         public int callUsage(Object instance) throws Exception {
127             Method m = instance.getClass().getMethod("doSomething");
128 
129             Object result = m.invoke(instance);
130             return (Integer) result;
131         }
132 
testModifiedInstance()133         public abstract void testModifiedInstance() throws Exception;
134     }
135 }
136