001package net.tnemc.plugincore.core.module;
002
003import com.vdurmont.semver4j.Semver;
004import net.tnemc.plugincore.PluginCore;
005
006import java.io.BufferedReader;
007import java.io.File;
008import java.io.FileOutputStream;
009import java.io.IOException;
010import java.io.InputStream;
011import java.io.InputStreamReader;
012import java.lang.reflect.Field;
013import java.net.HttpURLConnection;
014import java.net.URL;
015import java.net.URLClassLoader;
016import java.util.HashMap;
017import java.util.Map;
018import java.util.Vector;
019import java.util.jar.JarEntry;
020import java.util.jar.JarFile;
021
022/*
023 * The New Plugin Core
024 * Copyright (C) 2022 - 2024 Daniel "creatorfromhell" Vidmar
025 *
026 * This program is free software: you can redistribute it and/or modify
027 * it under the terms of the GNU Affero General Public License as published by
028 * the Free Software Foundation, either version 3 of the License, or
029 * (at your option) any later version.
030 *
031 * This program is distributed in the hope that it will be useful,
032 * but WITHOUT ANY WARRANTY; without even the implied warranty of
033 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
034 * GNU Affero General Public License for more details.
035 *
036 * You should have received a copy of the GNU Affero General Public License
037 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
038 */
039public class ModuleLoader {
040
041  private final Map<String, ModuleWrapper> modules = new HashMap<>();
042
043  public boolean hasModule(String moduleName) {
044    return modules.containsKey(moduleName);
045  }
046
047  public boolean hasModuleWithoutCase(String moduleName) {
048    for (String key : modules.keySet()) {
049      if(key.equalsIgnoreCase(moduleName)) return true;
050    }
051    return false;
052  }
053
054  public ModuleWrapper getModule(String name) {
055    return modules.get(name);
056  }
057
058  public Map<String, ModuleWrapper> getModules() {
059    return modules;
060  }
061
062  public void load() {
063    final File directory = new File(PluginCore.directory(), "modules");
064
065    if(directory.exists()) {
066
067      final File[] jars = directory.listFiles((dir, name) -> name.endsWith(".jar"));
068
069      if(jars != null) {
070        for(File jar : jars) {
071
072          try {
073            final ModuleWrapper wrapper = loadModuleWrapper(jar.getAbsolutePath());
074
075            if(wrapper.getModule() == null) {
076              PluginCore.log().inform("Skipping file due to invalid module: " + jar.getName());
077              continue;
078            }
079
080            if(jar.getName().contains("old-")) continue;
081
082            if(!wrapper.getModule().getClass().isAnnotationPresent(ModuleInfo.class)) {
083              PluginCore.log().inform("Invalid module format! ModuleOld File: " + jar.getName());
084              continue;
085            }
086
087            wrapper.setInfo(wrapper.getModule().getClass().getAnnotation(ModuleInfo.class));
088            PluginCore.log().inform("Found module: " + wrapper.name() + " version: " + wrapper.version());
089            modules.put(wrapper.name(), wrapper);
090
091            if (!wrapper.getInfo().updateURL().trim().equalsIgnoreCase("")) {
092              PluginCore.log().inform("Checking for updates for module " + wrapper.info.name());
093              ModuleUpdateChecker checker = new ModuleUpdateChecker(wrapper.info.name(), wrapper.info.updateURL(), wrapper.version());
094              checker.check();
095            }
096          } catch(Exception ignore) {
097            PluginCore.log().inform("Unable to load module file: " + jar.getName() + ". Are you sure it's up to date?");
098          }
099        }
100      }
101    } else {
102      if(directory.mkdir()) {
103        PluginCore.log().inform("Created module directory: " + directory.getAbsolutePath());
104      }
105    }
106  }
107
108  public boolean load(String moduleName) {
109    final String path = findPath(moduleName);
110    if(path != null) {
111      try {
112        final ModuleWrapper wrapper = loadModuleWrapper(path);
113        if(!wrapper.getModule().getClass().isAnnotationPresent(ModuleInfo.class)) {
114          PluginCore.log().inform("Invalid module format! ModuleOld File: " + moduleName);
115          return false;
116        }
117
118        wrapper.setInfo(wrapper.getModule().getClass().getAnnotation(ModuleInfo.class));
119
120        if(!wrapper.getInfo().pluginVersion().equalsIgnoreCase("0.0.0.0") &&
121           new Semver(wrapper.info.pluginVersion()).isGreaterThan(PluginCore.engine().version())) {
122
123          PluginCore.log().error("Unable to load module: " + wrapper.name() + " Requires a higher plugin version. Required version: " + PluginCore.engine().version());
124          return false;
125        }
126
127        PluginCore.log().inform("Found module: " + wrapper.name() + " version: " + wrapper.version());
128        modules.put(wrapper.name(), wrapper);
129
130        /*if(!wrapper.getInfo().updateURL().trim().equalsIgnoreCase("")) {
131          PluginCore.log().inform("Checking for updates for module " + moduleName);
132          ModuleUpdateChecker checker = new ModuleUpdateChecker(moduleName, wrapper.info.updateURL(), wrapper.version());
133          checker.check();
134        }*/
135        return true;
136      } catch(Exception ignore) {
137        PluginCore.log().inform("Unable to load module: " + moduleName + ". Are you sure it exists?");
138      }
139    }
140    return false;
141  }
142
143  public void unload(String moduleName) {
144    if(hasModule(moduleName)) {
145      ModuleWrapper wrapper = getModule(moduleName);
146      //TODO: Command and configuration unloading.
147      wrapper.getModule().disable(PluginCore.instance());
148
149      try {
150        Field f = ClassLoader.class.getDeclaredField("classes");
151        f.setAccessible(true);
152
153        Vector<Class> classes =  (Vector<Class>) f.get(PluginCore.loader().getModule(moduleName).getLoader());
154        for(Class clazz : classes) {
155          PluginCore.log().debug("Loaded: " + clazz.getName());
156        }
157      } catch (Exception e) {
158        e.printStackTrace();
159      }
160      wrapper.unload();
161      wrapper.setModule(null);
162      wrapper.setInfo(null);
163      wrapper.setLoader(null);
164      wrapper = null;
165      System.gc();
166
167      modules.remove(moduleName);
168    }
169  }
170
171  protected String findPath(String moduleName) {
172    final File directory = new File(PluginCore.directory(), "modules");
173    final File[] jars = directory.listFiles((dir, name) -> name.endsWith(".jar"));
174
175    if(jars != null) {
176      for(File jar : jars) {
177        if(jar.getAbsolutePath().toLowerCase().contains(moduleName.toLowerCase() + ".jar")) {
178          return jar.getAbsolutePath();
179        }
180      }
181    }
182    return null;
183  }
184
185  private ModuleWrapper loadModuleWrapper(String modulePath) {
186    ModuleWrapper wrapper;
187
188    Module module = null;
189
190    final File file = new File(modulePath);
191    Class<? extends Module> moduleClass;
192
193    URLClassLoader classLoader = null;
194    try {
195      classLoader = new URLClassLoader(new URL[]{ file.toURI().toURL() }, PluginCore.instance().getClass().getClassLoader());
196      moduleClass = classLoader.loadClass(getModuleMain(new File(modulePath))).asSubclass(Module.class);
197      module = moduleClass.newInstance();
198    } catch (Exception ignore) {
199      PluginCore.log().inform("Unable to locate module main class for file " + file.getName());
200    }
201    wrapper = new ModuleWrapper(module);
202    wrapper.setLoader(classLoader);
203    return wrapper;
204  }
205
206  public boolean downloadModule(String module) {
207    if(modules.containsKey(module)) {
208      try {
209        final String fileURL = modules.get(module).info.updateURL();
210        final URL url = new URL(fileURL);
211        final HttpURLConnection httpConn = (HttpURLConnection) url.openConnection();
212        final int responseCode = httpConn.getResponseCode();
213        if(responseCode == HttpURLConnection.HTTP_OK) {
214          final String fileName = fileURL.substring(fileURL.lastIndexOf("/") + 1, fileURL.length());
215
216          final InputStream in = httpConn.getInputStream();
217          final File file = new File(PluginCore.directory() + File.separator + "modules", fileName);
218
219          if(file.exists()) {
220            if(!file.renameTo(new File(PluginCore.directory() + File.separator + "modules", "outdated-" + fileName))) {
221              return false;
222            }
223          }
224
225          final FileOutputStream out = new FileOutputStream(file);
226
227          int bytesRead = -1;
228          final byte[] buffer = new byte[4096];
229          while ((bytesRead = in.read(buffer)) != -1) {
230            out.write(buffer, 0, bytesRead);
231          }
232
233          out.close();
234          in.close();
235        }
236      } catch(Exception ignore) {
237        return false;
238      }
239      return true;
240    }
241    return false;
242  }
243
244  private String getModuleMain(File jarFile) {
245    String main = "";
246    JarFile jar = null;
247    InputStream in = null;
248    BufferedReader reader = null;
249
250    try {
251      jar = new JarFile(jarFile);
252      final JarEntry infoFile = jar.getJarEntry("module.tne");
253
254      if(infoFile == null) {
255        PluginCore.log().inform("TNE encountered an error while loading a module! No module.tne file!");
256        return "";
257      }
258
259      in = jar.getInputStream(infoFile);
260      reader = new BufferedReader(new InputStreamReader(in));
261
262      main = reader.readLine().split("=")[1].trim();
263    } catch(IOException e) {
264      PluginCore.log().debug(e.toString());
265    } finally {
266      if(jar != null) {
267        try {
268          jar.close();
269        } catch(IOException e) {
270          PluginCore.log().debug(e.toString());
271        }
272      }
273
274      if(in != null) {
275        try {
276          in.close();
277        } catch(IOException e) {
278          PluginCore.log().debug(e.toString());
279        }
280      }
281
282      if(reader != null) {
283        try {
284          reader.close();
285        } catch(IOException e) {
286          PluginCore.log().debug(e.toString());
287        }
288      }
289    }
290    return main;
291  }
292}