com.diffplug.gradle.autosgi.PluginMetadataGen.java Source code

Java tutorial

Introduction

Here is the source code for com.diffplug.gradle.autosgi.PluginMetadataGen.java

Source

/*
 * Copyright 2018 DiffPlug
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.diffplug.gradle.autosgi;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.Vector;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.Opcodes;

import com.diffplug.common.base.Box;
import com.diffplug.common.base.Errors;
import com.diffplug.common.base.Preconditions;
import com.diffplug.common.base.Throwables;
import com.diffplug.common.collect.ImmutableList;
import com.diffplug.gradle.JavaExecable;

public class PluginMetadataGen {
    /**
     * {@link PluginMetadataGen#generate(File, Set, Set)} in a {@link JavaExecable} form.
     */
    public static class External implements JavaExecable {
        private static final long serialVersionUID = -3051458941508147719L;

        // inputs
        File toSearch;
        Set<File> toLinkAgainst;

        public External(File toSearch, Set<File> toLinkAgainst) {
            this.toSearch = toSearch;
            this.toLinkAgainst = toLinkAgainst;
        }

        // outputs
        Map<String, String> osgiInf;

        @Override
        public void run() throws Throwable {
            PluginMetadataGen metadataGen = new PluginMetadataGen(toSearch, toLinkAgainst);
            osgiInf = metadataGen.osgiInf;
        }
    }

    /**
     * Returns a Map from a plugin's name to its OSGI-INF content.
     *
     * @param toSearch         a directory containing class files where we will look for plugin implementations 
     * @param toLinkAgainst      the classes that these plugins implementations need
     * @param pluginInterfaces   the plugin interfaces that we're look for implementations of
     * @return a map from component name to is OSGI-INF string content
     */
    public static Map<String, String> generate(File toSearch, Set<File> toLinkAgainst) {
        GcTracker tracker = new GcTracker();
        try {
            PluginMetadataGen metadataGen = new PluginMetadataGen(toSearch, toLinkAgainst);
            tracker.track(metadataGen.classLoader);
            // save our results, with no reference to the guts of what happened inside PluginMetadataGen
            Map<String, String> result = metadataGen.osgiInf;
            if (!metadataGen.loadedJni.isEmpty()) {
                // if we loaded JNI, that means that future loads will fail if we don't GC the classloader we created inside PluginMetadataGen
                // null out all reference to whatever was in PluginMetadataGen, and try to force a GC
                metadataGen = null;
                tracker.tryGcUntilEmpty(10, 250, TimeUnit.MILLISECONDS);
            }
            return result;
        } catch (Exception e) {
            if (Throwables.getRootCause(e) instanceof UnsatisfiedLinkError) {
                throw new RuntimeException(
                        "This is probably caused by a classpath sticking around from a previous invocation.  Run `gradlew --stop` and try again.",
                        e);
            } else {
                throw Errors.asRuntime(e);
            }
        }
    }

    private final URLClassLoader classLoader;
    private final Map<String, String> osgiInf = new HashMap<>();
    private final ImmutableList<String> loadedJni;

    private static final String EXT_CLASS = ".class";

    private PluginMetadataGen(File toSearch, Set<File> toLinkAgainst)
            throws IOException, ClassNotFoundException, NoSuchFieldException, SecurityException {
        // create a classloader which looks in toSearch first, then each of the jars in toLinkAgainst
        URL[] urls = Stream.concat(Stream.of(toSearch), toLinkAgainst.stream())
                .map(Errors.rethrow().wrapFunction(file -> file.toURI().toURL())).collect(Collectors.toList())
                .toArray(new URL[0]);

        ClassLoader parent = null; // explicitly set parent to null so that the classloader is completely isolated
        classLoader = new URLClassLoader(urls, parent);
        try {
            // walk toSearch, passing each classfile to load()
            Files.walkFileTree(toSearch.toPath(), new SimpleFileVisitor<Path>() {
                @Override
                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
                    if (file.toString().endsWith(EXT_CLASS)) {
                        Errors.rethrow().run(() -> maybeGeneratePlugin(file));
                    }
                    return FileVisitResult.CONTINUE;
                }
            });
        } finally {
            loadedJni = JniAccess.getLoadedLibraries(classLoader);
            classLoader.close();
        }
    }

    private static final String PLUG = "Lcom/diffplug/autosgi/Plug;";

    static String asmToJava(String className) {
        return className.replace("/", ".");
    }

    /** Loads a class by its FQN.  If it's concrete and implements a plugin, then we'll call generatePlugin. */
    private void maybeGeneratePlugin(Path path)
            throws ClassNotFoundException, InstantiationException, IllegalAccessException, IOException {
        ClassReader reader = new ClassReader(Files.readAllBytes(path));
        String plugClassName = asmToJava(reader.getClassName());
        Box.Nullable<String> socketClassName = Box.Nullable.ofNull();
        reader.accept(new ClassVisitor(Opcodes.ASM5) {
            @Override
            public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
                if (PLUG.equals(desc)) {
                    return new AnnotationVisitor(Opcodes.ASM5) {
                        @Override
                        public void visit(String name, Object value) {
                            Preconditions.checkArgument(name.equals("value"),
                                    "For @Plug %s, expected 'value' but was '%s'", plugClassName, name);
                            Preconditions.checkArgument(socketClassName.get() == null,
                                    "For @Plug %s, multiple annotations: '%s' and '%s'", plugClassName,
                                    socketClassName.get(), value);
                            socketClassName.set(((org.objectweb.asm.Type) value).getClassName());
                        }
                    };
                } else {
                    return null;
                }
            }
        }, ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG | ClassReader.SKIP_CODE);
        if (socketClassName.get() == null) {
            return;
        }

        Class<?> plugClass = classLoader.loadClass(plugClassName);
        Class<?> socketClass = classLoader.loadClass(socketClassName.get());
        if (Modifier.isAbstract(plugClass.getModifiers())) {
            throw new IllegalArgumentException(
                    "Class " + plugClass + " has @Plug(" + socketClass + ") but it is abstract.");
        }

        String osgiInfContent = generatePlugin(plugClass, socketClass);
        osgiInf.put(plugClass.getName(), osgiInfContent);
    }

    /** A cache from a plugin interface to a function that converts a class into its metadata. */
    private Map<Class<?>, Function<Class<?>, String>> metadataCreatorCache = new HashMap<>();
    /** If a plugin class is named com.package.Socket, then it must have a DeclarativeMatadataCreator<Socket> at com.package.Socket$MetadataCreator. */
    private static final String METADATA_CREATOR = "$MetadataCreator";
    private static final String METADATA_CREATOR_METHOD = "configFor";

    @SuppressWarnings("unchecked")
    private <SocketT, PlugT extends SocketT> String generatePlugin(Class<?> plugClass, Class<?> socketClass)
            throws IllegalArgumentException, IllegalAccessException {
        Preconditions.checkArgument(socketClass.isAssignableFrom(plugClass),
                "Socket " + socketClass + " is not a supertype of @Plug " + plugClass);
        return generatePluginTyped((Class<PlugT>) plugClass, (Class<SocketT>) socketClass);
    }

    /**
     * @param plugClass            The class for which we are generating plugin metadata.
     * @param socketClass   The interface which is the socket for the metadata.
     * @return               A string containing the content of OSGI-INF as appropriate for clazz.
     */
    private <SocketT, PlugT extends SocketT> String generatePluginTyped(Class<PlugT> plugClass,
            Class<SocketT> socketClass) {
        Function<Class<?>, String> metadataCreator = metadataCreatorCache.computeIfAbsent(socketClass,
                interfase -> {
                    try {
                        // DeclarativeMetadataCreator<Socket>
                        Class<?> creatorClazz = classLoader.loadClass(socketClass.getName() + METADATA_CREATOR);
                        Object instance = instantiate(creatorClazz);
                        // String DeclarativeMetadataCreator<Socket>::configFor(Class<? extends Socket> clazz)
                        Method method = creatorClazz.getMethod(METADATA_CREATOR_METHOD, Class.class);
                        method.setAccessible(true);
                        return (Class<?> instanceClass) -> {
                            try {
                                return (String) method.invoke(instance, instanceClass);
                            } catch (Exception e) {
                                throw new RuntimeException(
                                        "Unable to generate metadata for " + instanceClass + ", "
                                                + " make sure that its metadata methods return simple constants.",
                                        e);
                            }
                        };
                    } catch (Exception e) {
                        throw new RuntimeException("To create plugin metadata around " + plugClass + " for socket "
                                + socketClass + ", we look for an instance of DeclarativeMetadataCreator<"
                                + socketClass + "> which must be named " + socketClass + METADATA_CREATOR, e);
                    }
                });
        return metadataCreator.apply(plugClass);
    }

    /** Calls the no-arg constructor of the given class, even if it is private. */
    static <T> T instantiate(Class<? extends T> clazz) throws Exception {
        Constructor<?> constructor = null;
        for (Constructor<?> candidate : clazz.getDeclaredConstructors()) {
            if (candidate.getParameterCount() == 0) {
                constructor = candidate;
                break;
            }
        }
        Objects.requireNonNull(constructor, "Class must have a no-arg constructor, but it didn't.  " + clazz + " "
                + Arrays.asList(clazz.getDeclaredConstructors()));
        @SuppressWarnings("unchecked")
        T instance = (T) constructor.newInstance();
        return instance;
    }

    /** Provids access to JNI libraries, compliments of http://stackoverflow.com/a/1008631/1153071. */
    public static class JniAccess {
        private static final java.lang.reflect.Field LIBRARIES;

        static {
            try {
                LIBRARIES = ClassLoader.class.getDeclaredField("loadedLibraryNames");
                LIBRARIES.setAccessible(true);
            } catch (NoSuchFieldException | SecurityException e) {
                throw Errors.asRuntime(e);
            }
        }

        public static ImmutableList<String> getLoadedLibraries(ClassLoader loader) {
            return Errors.rethrow().get(() -> {
                @SuppressWarnings("unchecked")
                Vector<String> libraries = (Vector<String>) LIBRARIES.get(loader);
                return ImmutableList.copyOf(libraries);
            });
        }
    }
}