c8b2a60140ab512569a57bb2f5a4d05853414508
[mdsal.git] / binding / mdsal-binding-dom-codec / src / main / java / org / opendaylight / mdsal / binding / dom / codec / loader / CodecClassLoader.java
1 /*
2  * Copyright (c) 2019 PANTHEON.tech, s.r.o. and others.  All rights reserved.
3  *
4  * This program and the accompanying materials are made available under the
5  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6  * and is available at http://www.eclipse.org/legal/epl-v10.html
7  */
8 package org.opendaylight.mdsal.binding.dom.codec.loader;
9
10 import static com.google.common.base.Preconditions.checkArgument;
11 import static com.google.common.base.Verify.verify;
12 import static com.google.common.base.Verify.verifyNotNull;
13 import static java.util.Objects.requireNonNull;
14
15 import com.google.common.annotations.Beta;
16 import com.google.common.base.Strings;
17 import com.google.common.base.Supplier;
18 import java.io.IOException;
19 import java.util.HashSet;
20 import java.util.List;
21 import java.util.Set;
22 import javassist.CannotCompileException;
23 import javassist.ClassPool;
24 import javassist.CtClass;
25 import javassist.LoaderClassPath;
26 import javassist.NotFoundException;
27 import org.eclipse.jdt.annotation.NonNull;
28 import org.opendaylight.mdsal.binding.spec.reflect.BindingReflections;
29 import org.slf4j.Logger;
30 import org.slf4j.LoggerFactory;
31
32 /**
33  * A ClassLoader hosting types generated for a particular type. A root instance is attached to a
34  * BindingCodecContext instance, so any generated classes from it can be garbage-collected when the context
35  * is destroyed, as well as to prevent two contexts trampling over each other.
36  *
37  * <p>
38  * It semantically combines two class loaders: the class loader in which this class is loaded and the class loader in
39  * which a target Binding interface/class is loaded. This inherently supports multi-classloader environments -- the root
40  * instance has visibility only into codec classes and for each classloader we encounter when presented with a binding
41  * class we create a leaf instance and cache it in the root instance. Leaf instances are using the root loader as their
42  * parent, but consult the binding class's class loader if the root loader fails to load a particular class.
43  *
44  * <p>In single-classloader environments, obviously, the root loader can load all binding classes, and hence no leaf
45  * loader is created.
46  *
47  * <p>
48  * Each {@link CodecClassLoader} has a {@link ClassPool} attached to it and can perform operations on it. Leaf loaders
49  * specify the root loader's ClassPool as their parent, but are configured to lookup classes first in themselves.
50  *
51  * @author Robert Varga
52  */
53 @Beta
54 public abstract class CodecClassLoader extends ClassLoader {
55     /**
56      * A customizer allowing a generated class to be modified before it is loader.
57      */
58     @FunctionalInterface
59     public interface Customizer {
60         /**
61          * Customize a generated class before it is instantiated in the loader.
62          *
63          * @param loader CodecClassLoader which will hold the class. It can be used to lookup/instantiate other classes
64          * @param bindingClass Binding class for which the customized class is being generated
65          * @param generated The class being generated
66          * @return A set of generated classes the generated class depends on
67          * @throws CannotCompileException if the customizer cannot perform partial compilation
68          * @throws NotFoundException if the customizer cannot find a required class
69          * @throws IOException if the customizer cannot perform partial loading
70          */
71         @NonNull List<Class<?>> customize(@NonNull CodecClassLoader loader, @NonNull CtClass bindingClass,
72                 @NonNull CtClass generated) throws CannotCompileException, NotFoundException, IOException;
73
74         /**
75          * Run the specified loader in a customized environment. The environment customizations must be cleaned up by
76          * the time this method returns. The default implementation performs no customization.
77          *
78          * @param loader Class loader to execute
79          * @return Class returned by the loader
80          */
81         default Class<?> customizeLoading(final @NonNull Supplier<Class<?>> loader) {
82             return loader.get();
83         }
84     }
85
86     static {
87         verify(ClassLoader.registerAsParallelCapable());
88     }
89
90     private static final Logger LOG = LoggerFactory.getLogger(CodecClassLoader.class);
91
92     private final ClassPool classPool;
93
94     private CodecClassLoader(final ClassLoader parentLoader, final ClassPool parentPool) {
95         super(parentLoader);
96         this.classPool = new ClassPool(parentPool);
97         this.classPool.childFirstLookup = true;
98         this.classPool.appendClassPath(new LoaderClassPath(this));
99     }
100
101     CodecClassLoader() {
102         this(StaticClassPool.LOADER, StaticClassPool.POOL);
103     }
104
105     CodecClassLoader(final CodecClassLoader parent) {
106         this(parent, parent.classPool);
107     }
108
109     /**
110      * Turn a Class instance into a CtClass for referencing it in code generation. This method supports both
111      * generated- and non-generated classes.
112      *
113      * @param clazz Class to be looked up.
114      * @return A CtClass instance
115      * @throws NotFoundException if the class cannot be found
116      * @throws NullPointerException if {@code clazz} is null
117      */
118     public final @NonNull CtClass findClass(final @NonNull Class<?> clazz) throws NotFoundException {
119         return BindingReflections.isBindingClass(clazz) ? findClassLoader(clazz).getLocalFrozen(clazz.getName())
120                 : StaticClassPool.findClass(clazz);
121     }
122
123     /**
124      * Create a new class by subclassing specified class and running a customizer on it. The name of the target class
125      * is formed through concatenation of the name of a {@code bindingInterface} and specified {@code suffix}
126      *
127      * @param superClass Superclass from which to derive
128      * @param bindingInterface Binding compile-time-generated interface
129      * @param suffix Suffix to use
130      * @param customizer Customizer to use to process the class
131      * @return A generated class object
132      * @throws CannotCompileException if the resulting generated class cannot be compiled or customized
133      * @throws NotFoundException if the binding interface cannot be found or the generated class cannot be customized
134      * @throws IOException if the generated class cannot be turned into bytecode or the generator fails with IOException
135      * @throws NullPointerException if any argument is null
136      */
137     public final Class<?> generateSubclass(final CtClass superClass, final Class<?> bindingInterface,
138             final String suffix, final Customizer customizer) throws CannotCompileException, IOException,
139                 NotFoundException {
140         return findClassLoader(requireNonNull(bindingInterface))
141                 .doGenerateSubclass(superClass, bindingInterface, suffix, customizer);
142     }
143
144     final @NonNull CtClass getLocalFrozen(final String name) throws NotFoundException {
145         synchronized (getClassLoadingLock(name)) {
146             final CtClass result = classPool.get(name);
147             result.freeze();
148             return result;
149         }
150     }
151
152     /**
153      * Append specified loaders to this class loader for the purposes of looking up generated classes. Note that the
154      * loaders are expected to have required classes already loaded. This is required to support generation of
155      * inter-dependent structures, such as those used for streaming binding interfaces.
156      *
157      * @param newLoaders Loaders to append
158      * @throws NullPointerException if {@code loaders} is null
159      */
160     abstract void appendLoaders(@NonNull Set<LeafCodecClassLoader> newLoaders);
161
162     /**
163      * Find the loader responsible for holding classes related to a binding class.
164      *
165      * @param bindingClass Class to locate
166      * @return a Loader instance
167      * @throws NullPointerException if {@code bindingClass} is null
168      */
169     abstract @NonNull CodecClassLoader findClassLoader(@NonNull Class<?> bindingClass);
170
171     private Class<?> doGenerateSubclass(final CtClass superClass, final Class<?> bindingInterface, final String suffix,
172             final Customizer customizer) throws CannotCompileException, IOException, NotFoundException {
173         checkArgument(!superClass.isInterface(), "%s must not be an interface", superClass);
174         checkArgument(bindingInterface.isInterface(), "%s is not an interface", bindingInterface);
175         checkArgument(!Strings.isNullOrEmpty(suffix));
176
177         final String bindingName = bindingInterface.getName();
178         final String fqn = bindingName + "$$$" + suffix;
179         synchronized (getClassLoadingLock(fqn)) {
180             // Attempt to find a loaded class
181             final Class<?> loaded = findLoadedClass(fqn);
182             if (loaded != null) {
183                 return loaded;
184             }
185
186             // Get the interface
187             final CtClass bindingCt = getLocalFrozen(bindingName);
188             try {
189                 final byte[] byteCode;
190                 final CtClass generated = verifyNotNull(classPool.makeClass(fqn, superClass));
191                 try {
192                     final List<Class<?>> deps = customizer.customize(this, bindingCt, generated);
193                     final String ctName = generated.getName();
194                     verify(fqn.equals(ctName), "Target class is %s returned result is %s", fqn, ctName);
195                     processDependencies(deps);
196
197                     byteCode = generated.toBytecode();
198                 } finally {
199                     // Always detach the result, as we will never use it again
200                     generated.detach();
201                 }
202
203                 return customizer.customizeLoading(() -> {
204                     final Class<?> newClass = defineClass(fqn, byteCode, 0, byteCode.length);
205                     resolveClass(newClass);
206                     return newClass;
207                 });
208             } finally {
209                 // Binding interfaces are used only a few times, hence it does not make sense to cache them in the class
210                 // pool.
211                 // TODO: this hinders caching, hence we should re-think this
212                 bindingCt.detach();
213             }
214         }
215     }
216
217     private void processDependencies(final List<Class<?>> deps) {
218         final Set<LeafCodecClassLoader> depLoaders = new HashSet<>();
219         for (Class<?> dep : deps) {
220             final ClassLoader depLoader = dep.getClassLoader();
221             verify(depLoader instanceof CodecClassLoader, "Dependency %s is not a generated class", dep);
222             if (this.equals(depLoader)) {
223                 // Same loader, skip
224                 continue;
225             }
226
227             try {
228                 loadClass(dep.getName());
229             } catch (ClassNotFoundException e) {
230                 LOG.debug("Cannot find {} in local loader, attempting to compensate", dep, e);
231                 // Root loader is always visible from a leaf, hence the dependency can only be a leaf
232                 verify(depLoader instanceof LeafCodecClassLoader, "Dependency loader %s is not a leaf", depLoader);
233                 depLoaders.add((LeafCodecClassLoader) depLoader);
234             }
235         }
236
237         if (!depLoaders.isEmpty()) {
238             appendLoaders(depLoaders);
239         }
240     }
241 }