From c7026c1299a80f2759ddb0878bda3e6ade59e7e1 Mon Sep 17 00:00:00 2001 From: Alexey Andreev Date: Mon, 12 Oct 2015 18:39:29 +0300 Subject: [PATCH] Add plugin ordering annotations and implementation --- .../java/org/teavm/vm/TeaVMPluginLoader.java | 266 ++++++++++++++++++ .../src/main/java/org/teavm/vm/spi/After.java | 31 ++ .../main/java/org/teavm/vm/spi/Before.java | 31 ++ .../main/java/org/teavm/vm/spi/Requires.java | 31 ++ .../java/org/teavm/vm/PluginLoaderTest.java | 60 ++++ 5 files changed, 419 insertions(+) create mode 100644 core/src/main/java/org/teavm/vm/TeaVMPluginLoader.java create mode 100644 core/src/main/java/org/teavm/vm/spi/After.java create mode 100644 core/src/main/java/org/teavm/vm/spi/Before.java create mode 100644 core/src/main/java/org/teavm/vm/spi/Requires.java create mode 100644 core/src/test/java/org/teavm/vm/PluginLoaderTest.java diff --git a/core/src/main/java/org/teavm/vm/TeaVMPluginLoader.java b/core/src/main/java/org/teavm/vm/TeaVMPluginLoader.java new file mode 100644 index 000000000..1994c26cf --- /dev/null +++ b/core/src/main/java/org/teavm/vm/TeaVMPluginLoader.java @@ -0,0 +1,266 @@ +/* + * Copyright 2015 Alexey Andreev. + * + * 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 org.teavm.vm; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import org.objectweb.asm.AnnotationVisitor; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.teavm.vm.spi.After; +import org.teavm.vm.spi.Before; +import org.teavm.vm.spi.Requires; +import org.teavm.vm.spi.TeaVMPlugin; + +/** + * + * @author Alexey Andreev + */ +public final class TeaVMPluginLoader { + private static final String DESCRIPTOR_LOCATION = "META-INF/services/org.teavm.vm.spi.TeaVMPlugin"; + private static final String REQUIRES_DESC = Type.getDescriptor(Requires.class); + private static final String BEFORE_DESC = Type.getDescriptor(Before.class); + private static final String AFTER_DESC = Type.getDescriptor(After.class); + + private TeaVMPluginLoader() { + } + + public static List load(ClassLoader classLoader) { + Set unorderedPlugins = new HashSet<>(); + try { + Enumeration resourceFiles = classLoader.getResources(DESCRIPTOR_LOCATION); + while (resourceFiles.hasMoreElements()) { + URL resourceFile = resourceFiles.nextElement(); + try (BufferedReader input = new BufferedReader( + new InputStreamReader(resourceFile.openStream(), "UTF-8"))) { + readPlugins(input, unorderedPlugins); + } + } + } catch (IOException e) { + throw new IllegalStateException("Error loading plugins", e); + } + + return orderPlugins(classLoader, unorderedPlugins).stream().map(p -> instantiate(classLoader, p)) + .collect(Collectors.toList()); + } + + public static List orderPlugins(ClassLoader classLoader, Set classNames) { + Map descriptors = new HashMap<>(); + try { + for (String className : classNames) { + PluginDescriptor descriptor = new PluginDescriptor(); + descriptor.name = className; + if (readDescriptor(classLoader, className, descriptor)) { + descriptors.put(className, descriptor); + } + } + } catch (IOException e) { + throw new IllegalStateException("Error ordering plugins", e); + } + + findReachableDescriptors(descriptors); + processDescriptors(descriptors); + List plugins = new ArrayList<>(); + Set visited = new HashSet<>(); + Set emmited = new HashSet<>(); + for (PluginDescriptor descriptor : descriptors.values()) { + orderDescriptors(descriptor, descriptors, plugins, visited, emmited, new ArrayList<>()); + } + + return plugins; + } + + private static void readPlugins(BufferedReader input, Set plugins) throws IOException { + while (true) { + String line = input.readLine(); + if (line == null) { + break; + } + line = line.trim(); + if (line.isEmpty() || line.startsWith("#")) { + continue; + } + + plugins.add(line); + } + } + + private static boolean readDescriptor(ClassLoader classLoader, String className, PluginDescriptor descriptor) + throws IOException { + try (InputStream input = classLoader.getResourceAsStream(className.replace('.', '/') + ".class")) { + if (input == null) { + return false; + } + ClassReader reader = new ClassReader(input); + PluginDescriptorFiller filler = new PluginDescriptorFiller(descriptor); + reader.accept(filler, 0); + return true; + } + } + + private static void findReachableDescriptors(Map descriptors) { + Set visited = new HashSet<>(); + for (String plugin : descriptors.keySet()) { + isReachable(plugin, visited, descriptors); + } + } + + private static boolean isReachable(String plugin, Set visited, Map descriptors) { + PluginDescriptor descriptor = descriptors.get(plugin); + if (descriptor == null) { + return false; + } + if (!visited.add(plugin)) { + return descriptor.reachable; + } + + boolean reachable = true; + for (String required : descriptor.requires) { + if (!isReachable(required, visited, descriptors)) { + reachable = false; + break; + } + } + descriptor.reachable = reachable; + return reachable; + } + + private static void processDescriptors(Map descriptors) { + for (PluginDescriptor descriptor : descriptors.values()) { + if (descriptor.after.length > 0 && descriptor.before.length > 0) { + throw new IllegalStateException("Plugin " + descriptor.name + + " has both before and after annotations"); + } + descriptor.beforeList.addAll(Arrays.asList(descriptor.before)); + for (String after : descriptor.after) { + PluginDescriptor afterDescriptor = descriptors.get(after); + if (afterDescriptor != null && afterDescriptor.reachable) { + afterDescriptor.beforeList.add(descriptor.name); + } + } + } + } + + private static void orderDescriptors(PluginDescriptor descriptor, Map descriptors, + List list, Set visited, Set emmited, List path) { + if (!descriptor.reachable) { + return; + } + if (!visited.add(descriptor.name)) { + return; + } + path.add(descriptor.name); + + for (String before : descriptor.beforeList) { + PluginDescriptor beforeDescriptor = descriptors.get(before); + if (beforeDescriptor != null) { + if (visited.contains(before) && !emmited.contains(before)) { + List loop = new ArrayList<>(); + for (String pathElem : path) { + loop.add(pathElem); + if (pathElem.equals(descriptor.name)) { + break; + } + } + Collections.reverse(loop); + throw new IllegalStateException("Circular dependency found: " + loop.stream() + .collect(Collectors.joining(" -> "))); + } + orderDescriptors(beforeDescriptor, descriptors, list, visited, emmited, path); + } + } + + emmited.add(descriptor.name); + path.remove(descriptor.name); + list.add(descriptor.name); + } + + private static TeaVMPlugin instantiate(ClassLoader classLoader, String className) { + try { + return (TeaVMPlugin) Class.forName(className, true, classLoader).newInstance(); + } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) { + throw new IllegalStateException("Can't instantiate plugin " + className, e); + } + } + + static class PluginDescriptorFiller extends ClassVisitor { + PluginDescriptor descriptor; + + public PluginDescriptorFiller(PluginDescriptor descriptor) { + super(Opcodes.ASM5); + this.descriptor = descriptor; + } + + @Override + public AnnotationVisitor visitAnnotation(String desc, boolean visible) { + if (desc.equals(REQUIRES_DESC)) { + return readClassArray(arr -> descriptor.requires = arr); + } else if (desc.equals(BEFORE_DESC)) { + return readClassArray(arr -> descriptor.before = arr); + } else if (desc.equals(AFTER_DESC)) { + return readClassArray(arr -> descriptor.after = arr); + } + return null; + } + + private AnnotationVisitor readClassArray(Consumer resultConsumer) { + return new AnnotationVisitor(Opcodes.ASM5) { + @Override + public AnnotationVisitor visitArray(String name) { + List values = new ArrayList<>(); + if (name.equals("value")) { + return new AnnotationVisitor(Opcodes.ASM5) { + @Override + public void visit(String name, Object value) { + values.add(((Type) value).getClassName()); + } + @Override + public void visitEnd() { + resultConsumer.accept(values.toArray(new String[0])); + } + }; + } + return null; + } + }; + } + } + + static class PluginDescriptor { + String name; + String[] requires = new String[0]; + String[] before = new String[0]; + String[] after = new String[0]; + List beforeList = new ArrayList<>(); + boolean reachable; + } +} diff --git a/core/src/main/java/org/teavm/vm/spi/After.java b/core/src/main/java/org/teavm/vm/spi/After.java new file mode 100644 index 000000000..0fe87a08f --- /dev/null +++ b/core/src/main/java/org/teavm/vm/spi/After.java @@ -0,0 +1,31 @@ +/* + * Copyright 2015 Alexey Andreev. + * + * 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 org.teavm.vm.spi; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * + * @author Alexey Andreev + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface After { + Class[] value(); +} diff --git a/core/src/main/java/org/teavm/vm/spi/Before.java b/core/src/main/java/org/teavm/vm/spi/Before.java new file mode 100644 index 000000000..6705678f6 --- /dev/null +++ b/core/src/main/java/org/teavm/vm/spi/Before.java @@ -0,0 +1,31 @@ +/* + * Copyright 2015 Alexey Andreev. + * + * 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 org.teavm.vm.spi; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * + * @author Alexey Andreev + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface Before { + Class[] value(); +} diff --git a/core/src/main/java/org/teavm/vm/spi/Requires.java b/core/src/main/java/org/teavm/vm/spi/Requires.java new file mode 100644 index 000000000..a30ac96a5 --- /dev/null +++ b/core/src/main/java/org/teavm/vm/spi/Requires.java @@ -0,0 +1,31 @@ +/* + * Copyright 2015 Alexey Andreev. + * + * 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 org.teavm.vm.spi; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * + * @author Alexey Andreev + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface Requires { + Class[] value(); +} diff --git a/core/src/test/java/org/teavm/vm/PluginLoaderTest.java b/core/src/test/java/org/teavm/vm/PluginLoaderTest.java new file mode 100644 index 000000000..aea0b4fc6 --- /dev/null +++ b/core/src/test/java/org/teavm/vm/PluginLoaderTest.java @@ -0,0 +1,60 @@ +/* + * Copyright 2015 Alexey Andreev. + * + * 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 org.teavm.vm; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import org.junit.Test; +import org.teavm.vm.spi.Before; +import org.teavm.vm.spi.Requires; +import org.teavm.vm.spi.TeaVMHost; +import org.teavm.vm.spi.TeaVMPlugin; + +/** + * + * @author Alexey Andreev + */ +public class PluginLoaderTest { + @Test + public void loadsPlugins() { + List plugins = order(A.class, B.class); + assertThat(plugins.size(), is(2)); + assertThat(plugins.get(0), is(B.class.getName())); + assertThat(plugins.get(1), is(A.class.getName())); + } + + private List order(Class... classes) { + return TeaVMPluginLoader.orderPlugins(PluginLoaderTest.class.getClassLoader(), + Arrays.stream(classes).map(Class::getName).collect(Collectors.toSet())); + } + + static class A implements TeaVMPlugin { + @Override + public void install(TeaVMHost host) { + } + } + + @Before(A.class) + @Requires(A.class) + static class B implements TeaVMPlugin { + @Override + public void install(TeaVMHost host) { + } + } +}