0a0c9480d719ff46f58bd55aa3346a39976c22bc
[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.Verify.verify;
11 import static java.util.Objects.requireNonNull;
12
13 import com.google.common.annotations.Beta;
14 import com.google.common.base.Strings;
15 import com.google.common.base.Supplier;
16 import com.google.common.collect.ImmutableMap;
17 import com.google.common.collect.ImmutableSet;
18 import java.io.File;
19 import java.io.IOException;
20 import java.security.AccessController;
21 import java.security.PrivilegedAction;
22 import java.util.Collection;
23 import java.util.HashSet;
24 import java.util.Map.Entry;
25 import java.util.Set;
26 import net.bytebuddy.description.type.TypeDescription;
27 import net.bytebuddy.dynamic.DynamicType.Unloaded;
28 import net.bytebuddy.dynamic.loading.ClassLoadingStrategy;
29 import org.eclipse.jdt.annotation.NonNull;
30 import org.slf4j.Logger;
31 import org.slf4j.LoggerFactory;
32
33 /**
34  * A ClassLoader hosting types generated for a particular type. A root instance is attached to a
35  * BindingCodecContext instance, so any generated classes from it can be garbage-collected when the context
36  * is destroyed, as well as to prevent two contexts trampling over each other.
37  *
38  * <p>
39  * It semantically combines two class loaders: the class loader in which this class is loaded and the class loader in
40  * which a target Binding interface/class is loaded. This inherently supports multi-classloader environments -- the root
41  * instance has visibility only into codec classes and for each classloader we encounter when presented with a binding
42  * class we create a leaf instance and cache it in the root instance. Leaf instances are using the root loader as their
43  * parent, but consult the binding class's class loader if the root loader fails to load a particular class.
44  *
45  * <p>In single-classloader environments, obviously, the root loader can load all binding classes, and hence no leaf
46  * loader is created.
47  *
48  * @author Robert Varga
49  */
50 @Beta
51 public abstract class CodecClassLoader extends ClassLoader {
52     public interface ClassGenerator<T> {
53         /**
54          * Generate a class.
55          *
56          * @param fqcn Generated class Fully-qualified class name
57          * @param bindingInterface Binding interface for which the class is being generated
58          * @return A result.
59          */
60         GeneratorResult<T> generateClass(CodecClassLoader loader, String fqcn, Class<?> bindingInterface);
61
62         /**
63          * Run the specified loader in a customized environment. The environment customizations must be cleaned up by
64          * the time this method returns. The default implementation performs no customization.
65          *
66          * @param loader Class loader to execute
67          * @return Class returned by the loader
68          */
69         default Class<T> customizeLoading(final @NonNull Supplier<Class<T>> loader) {
70             return loader.get();
71         }
72     }
73
74     public static final class GeneratorResult<T> {
75         private final @NonNull ImmutableSet<Class<?>> dependecies;
76         private final @NonNull Unloaded<T> result;
77
78         GeneratorResult(final Unloaded<T> result, final ImmutableSet<Class<?>> dependecies) {
79             this.result = requireNonNull(result);
80             this.dependecies = requireNonNull(dependecies);
81         }
82
83         public static <T> @NonNull GeneratorResult<T> of(final Unloaded<T> result) {
84             return new GeneratorResult<>(result, ImmutableSet.of());
85         }
86
87         public static <T> @NonNull GeneratorResult<T> of(final Unloaded<T> result,
88                 final Collection<Class<?>> dependencies) {
89             return dependencies.isEmpty() ? of(result) : new GeneratorResult<>(result,
90                     ImmutableSet.copyOf(dependencies));
91         }
92
93         @NonNull Unloaded<T> getResult() {
94             return result;
95         }
96
97         @NonNull ImmutableSet<Class<?>> getDependencies() {
98             return dependecies;
99         }
100     }
101
102     private static final ClassLoadingStrategy<CodecClassLoader> STRATEGY = (classLoader, types) -> {
103         verify(types.size() == 1, "Unexpected multiple types", types);
104         final Entry<TypeDescription, byte[]> entry = types.entrySet().iterator().next();
105         return ImmutableMap.of(entry.getKey(), classLoader.loadClass(entry.getKey().getName(), entry.getValue()));
106     };
107
108     static {
109         verify(ClassLoader.registerAsParallelCapable());
110     }
111
112     private static final Logger LOG = LoggerFactory.getLogger(CodecClassLoader.class);
113     private static final File BYTECODE_DIRECTORY;
114
115     static {
116         final String dir = System.getProperty("org.opendaylight.mdsal.binding.dom.codec.loader.bytecodeDumpDirectory");
117         BYTECODE_DIRECTORY = Strings.isNullOrEmpty(dir) ? null : new File(dir);
118     }
119
120     CodecClassLoader(final ClassLoader parentLoader) {
121         super(parentLoader);
122     }
123
124     /**
125      * Instantiate a new CodecClassLoader, which serves as the root of generated code loading.
126      *
127      * @return A new CodecClassLoader.
128      */
129     public static @NonNull CodecClassLoader create() {
130         return AccessController.doPrivileged((PrivilegedAction<CodecClassLoader>)() -> new RootCodecClassLoader());
131     }
132
133     /**
134      * The name of the target class is formed through concatenation of the name of a {@code bindingInterface} and
135      * specified {@code suffix}.
136      *
137      * @param bindingInterface Binding compile-time-generated interface
138      * @param suffix Suffix to use
139      * @param generator Code generator to run
140      * @return A generated class object
141      * @throws NullPointerException if any argument is null
142      */
143     public final <T> Class<T> generateClass(final Class<?> bindingInterface,
144             final String suffix, final ClassGenerator<T> generator)  {
145         return findClassLoader(requireNonNull(bindingInterface)).doGenerateClass(bindingInterface, suffix, generator);
146     }
147
148     /**
149      * Append specified loaders to this class loader for the purposes of looking up generated classes. Note that the
150      * loaders are expected to have required classes already loaded. This is required to support generation of
151      * inter-dependent structures, such as those used for streaming binding interfaces.
152      *
153      * @param newLoaders Loaders to append
154      * @throws NullPointerException if {@code loaders} is null
155      */
156     abstract void appendLoaders(@NonNull Set<LeafCodecClassLoader> newLoaders);
157
158     /**
159      * Find the loader responsible for holding classes related to a binding class.
160      *
161      * @param bindingClass Class to locate
162      * @return a Loader instance
163      * @throws NullPointerException if {@code bindingClass} is null
164      */
165     abstract @NonNull CodecClassLoader findClassLoader(@NonNull Class<?> bindingClass);
166
167     private <T> Class<T> doGenerateClass(final Class<?> bindingInterface, final String suffix,
168             final ClassGenerator<T> generator)  {
169         final String fqcn = bindingInterface.getName() + "$$$" + suffix;
170
171         synchronized (getClassLoadingLock(fqcn)) {
172             // Attempt to find a loaded class
173             final Class<?> existing = findLoadedClass(fqcn);
174             if (existing != null) {
175                 return (Class<T>) existing;
176             }
177
178             final GeneratorResult<T> result = generator.generateClass(this, fqcn, bindingInterface);
179             final Unloaded<T> unloaded = result.getResult();
180             verify(fqcn.equals(unloaded.getTypeDescription().getName()), "Unexpected class in %s", unloaded);
181             verify(unloaded.getAuxiliaryTypes().isEmpty(), "Auxiliary types present in %s", unloaded);
182             dumpBytecode(unloaded);
183
184             processDependencies(result.getDependencies());
185             return generator.customizeLoading(() -> (Class<T>) unloaded.load(this, STRATEGY).getLoaded());
186         }
187     }
188
189     final Class<?> loadClass(final String fqcn, final byte[] byteCode) {
190         synchronized (getClassLoadingLock(fqcn)) {
191             final Class<?> existing = findLoadedClass(fqcn);
192             verify(existing == null, "Attempted to load existing %s", existing);
193             return defineClass(fqcn, byteCode, 0, byteCode.length);
194         }
195     }
196
197     private void processDependencies(final Collection<Class<?>> deps) {
198         final Set<LeafCodecClassLoader> depLoaders = new HashSet<>();
199         for (Class<?> dep : deps) {
200             final ClassLoader depLoader = dep.getClassLoader();
201             verify(depLoader instanceof CodecClassLoader, "Dependency %s is not a generated class", dep);
202             if (this.equals(depLoader)) {
203                 // Same loader, skip
204                 continue;
205             }
206
207             try {
208                 loadClass(dep.getName());
209             } catch (ClassNotFoundException e) {
210                 LOG.debug("Cannot find {} in local loader, attempting to compensate", dep, e);
211                 // Root loader is always visible from a leaf, hence the dependency can only be a leaf
212                 verify(depLoader instanceof LeafCodecClassLoader, "Dependency loader %s is not a leaf", depLoader);
213                 depLoaders.add((LeafCodecClassLoader) depLoader);
214             }
215         }
216
217         if (!depLoaders.isEmpty()) {
218             appendLoaders(depLoaders);
219         }
220     }
221
222     private static void dumpBytecode(final Unloaded<?> unloaded) {
223         if (BYTECODE_DIRECTORY != null) {
224             try {
225                 unloaded.saveIn(BYTECODE_DIRECTORY);
226             } catch (IOException | IllegalArgumentException e) {
227                 LOG.info("Failed to save {}", unloaded.getTypeDescription().getName(), e);
228             }
229         }
230     }
231 }