001/*
002 * Trident - A Multithreaded Server Alternative
003 * Copyright 2014 The TridentSDK Team
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *    http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017
018package net.tridentsdk.server.plugin;
019
020import com.google.common.collect.ForwardingList;
021import com.google.common.collect.ImmutableList;
022import com.google.common.collect.Maps;
023import net.tridentsdk.Trident;
024import net.tridentsdk.concurrent.HeldValueLatch;
025import net.tridentsdk.concurrent.SelectableThread;
026import net.tridentsdk.docs.InternalUseOnly;
027import net.tridentsdk.event.Listener;
028import net.tridentsdk.plugin.Plugin;
029import net.tridentsdk.plugin.PluginLoadException;
030import net.tridentsdk.plugin.Plugins;
031import net.tridentsdk.plugin.annotation.IgnoreRegistration;
032import net.tridentsdk.plugin.annotation.PluginDesc;
033import net.tridentsdk.plugin.cmd.Command;
034import net.tridentsdk.registry.Registered;
035import net.tridentsdk.server.concurrent.ConcurrentTaskExecutor;
036import net.tridentsdk.server.concurrent.TickSync;
037import net.tridentsdk.util.TridentLogger;
038
039import java.io.File;
040import java.io.IOException;
041import java.lang.reflect.Constructor;
042import java.lang.reflect.InvocationTargetException;
043import java.lang.reflect.Modifier;
044import java.util.Enumeration;
045import java.util.List;
046import java.util.Map;
047import java.util.jar.JarEntry;
048import java.util.jar.JarFile;
049
050/**
051 * Handles server plugins, loading and unloading, class management, and lifecycle management for plugins
052 * <p>
053 * <p>To access this handler, use this code:
054 * <pre><code>
055 *     PluginHandler handler = Registered.plugins();
056 * </code></pre></p>
057 *
058 * @author The TridentSDK Team
059 * @since 0.3-alpha-DP
060 */
061public class PluginHandler extends ForwardingList<Plugin> implements Plugins {
062    private static final SelectableThread EXECUTOR = ConcurrentTaskExecutor.create(1, "Plugins").selectCore();
063    final Map<String, Plugin> plugins = Maps.newConcurrentMap(); // This need not be concurrent... but TridentLogger >.<
064
065    /**
066     * Do not instantiate this without being Trident
067     * <p>
068     * <p>To access this handler, use this code:
069     * <pre><code>
070     *     TridentPluginHandler handler = Handler.plugins();
071     * </code></pre></p>
072     */
073    public PluginHandler() {
074        if (!Trident.isTrident())
075            throw new RuntimeException(new IllegalAccessException("Can only be instantiated by Trident"));
076    }
077
078    @Override
079    @InternalUseOnly
080    public Plugin load(final File pluginFile) {
081        HeldValueLatch<Plugin> latch = HeldValueLatch.create();
082
083        TickSync.sync(new Runnable() {
084            @Override
085            public void run() {
086                JarFile jarFile = null;
087                try {
088                    // load all classes
089                    jarFile = new JarFile(pluginFile);
090                    PluginClassLoader loader = new PluginClassLoader(pluginFile, getClass().getClassLoader());
091                    Class<? extends Plugin> pluginClass = null;
092
093                    Enumeration<JarEntry> entries = jarFile.entries();
094                    while (entries.hasMoreElements()) {
095                        JarEntry entry = entries.nextElement();
096
097                        if (entry.isDirectory() || !entry.getName().endsWith(".class")) {
098                            continue;
099                        }
100
101                        String name = entry.getName().replace(".class", "").replace('/', '.');
102                        Class<?> loadedClass = loader.loadClass(name);
103
104                        loader.putClass(loadedClass);
105
106                        if (Plugin.class.isAssignableFrom(loadedClass)) {
107                            if (pluginClass != null)
108                                TridentLogger.get().error(new PluginLoadException("Plugin has more than one main class!"));
109
110                            pluginClass = loadedClass.asSubclass(Plugin.class);
111                        }
112                    }
113
114                    // start initiating the plugin class and registering commands and listeners
115                    if (pluginClass == null) {
116                        TridentLogger.get().error(new PluginLoadException("Plugin does not have a main class"));
117                        loader.unloadClasses();
118                        loader = null; // help gc
119                        return;
120                    }
121
122                    PluginDesc description = pluginClass.getAnnotation(PluginDesc.class);
123
124                    if (description == null) {
125                        TridentLogger.get().error(new PluginLoadException("PluginDesc annotation does not exist!"));
126                        loader.unloadClasses();
127                        loader = null; // help gc
128                        return;
129                    }
130
131                    if (plugins.containsKey(description.name())) {
132                        TridentLogger.get().error(new PluginLoadException("Plugin with name " + description.name() +
133                                " has been loaded"));
134                        loader.unloadClasses();
135                        loader = null; // help gc
136                        return;
137                    }
138
139                    TridentLogger.get().log("Loading " + description.name() + " version " + description.version());
140
141                    Plugin plugin = pluginClass.newInstance();
142                    plugin.init(pluginFile, description, loader);
143                    plugins.put(description.name(), plugin);
144                    plugin.load();
145                    latch.countDown(plugin);
146                    TridentLogger.get().success("Loaded " + description.name() + " version " + description.version());
147                } catch (IOException | IllegalAccessException | InstantiationException | ClassNotFoundException ex) { // UNLOAD PLUGIN
148                    TridentLogger.get().error(new PluginLoadException(ex));
149                } finally {
150                    if (jarFile != null)
151                        try {
152                            jarFile.close();
153                        } catch (IOException e) {
154                            TridentLogger.get().error(e);
155                        }
156                }
157            }
158        });
159
160        try {
161            return latch.await();
162        } catch (InterruptedException e) {
163            e.printStackTrace();
164            return null;
165        }
166    }
167
168    @Override
169    public void enable(Plugin plugin) {
170        TridentLogger.get().log("Enabling " + plugin.description().name() + " version " + plugin.description().version());
171        for (Class<?> cls : plugin.classLoader.loadedClasses().values()) {
172            try {
173                register(plugin, cls, EXECUTOR);
174            } catch (InstantiationException e) {
175                e.printStackTrace();
176            }
177        }
178
179        TickSync.sync(plugin::enable);
180        TridentLogger.get().success("Enabled " + plugin.description().name() + " version " + plugin.description().version());
181    }
182
183    private void register(Plugin plugin, Class<?> cls, SelectableThread executor) throws InstantiationException {
184        if (Modifier.isAbstract(cls.getModifiers()))
185            return;
186
187        Object instance = null;
188        Constructor<?> c = null;
189
190        try {
191            if (!cls.isAnnotationPresent(IgnoreRegistration.class)) {
192                if (Listener.class.isAssignableFrom(cls)) {
193                    c = cls.getConstructor();
194                    Registered.events().registerListener(plugin, (Listener) (instance = c.newInstance()));
195                }
196
197                if (Command.class.isAssignableFrom(cls)) {
198                    if (c == null)
199                        c = cls.getConstructor();
200                    Registered.commands().register(plugin, (Command) (instance == null ? c.newInstance() : instance));
201                }
202            }
203        } catch (NoSuchMethodException e) {
204            TridentLogger.get().error(
205                    new PluginLoadException("A no-arg constructor for class " + cls.getName() + " does not exist"));
206        } catch (IllegalAccessException e) {
207            TridentLogger.get().error(
208                    new PluginLoadException("A no-arg constructor for class " + cls.getName() + " is not accessible"));
209        } catch (InvocationTargetException e) {
210            e.printStackTrace();
211        }
212    }
213
214    @Override
215    public void disable(final Plugin plugin) {
216        TickSync.sync(() -> {
217            // Perform disabling first, we don't want to unload everything
218            // then disable it
219            // State checking could be performed which breaks the class loader
220            plugin.disable();
221
222            plugins.remove(plugin.description().name());
223
224            plugin.classLoader.unloadClasses();
225            plugin.classLoader = null;
226        });
227    }
228
229    @Override
230    protected List<Plugin> delegate() {
231        return ImmutableList.copyOf(plugins.values());
232    }
233
234    @Override
235    public SelectableThread executor() {
236        return EXECUTOR;
237    }
238}