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
045    return modules.containsKey(moduleName);
046  }
047
048  public boolean hasModuleWithoutCase(String moduleName) {
049
050    for(String key : modules.keySet()) {
051      if(key.equalsIgnoreCase(moduleName)) return true;
052    }
053    return false;
054  }
055
056  public ModuleWrapper getModule(String name) {
057
058    return modules.get(name);
059  }
060
061  public Map<String, ModuleWrapper> getModules() {
062
063    return modules;
064  }
065
066  public void load() {
067
068    final File directory = new File(PluginCore.directory(), "modules");
069
070    if(directory.exists()) {
071
072      final File[] jars = directory.listFiles((dir, name)->name.endsWith(".jar"));
073
074      if(jars != null) {
075        for(File jar : jars) {
076
077          try {
078            final ModuleWrapper wrapper = loadModuleWrapper(jar.getAbsolutePath());
079
080            if(wrapper.getModule() == null) {
081              PluginCore.log().inform("Skipping file due to invalid module: " + jar.getName());
082              continue;
083            }
084
085            if(jar.getName().contains("old-")) continue;
086
087            if(!wrapper.getModule().getClass().isAnnotationPresent(ModuleInfo.class)) {
088              PluginCore.log().inform("Invalid module format! ModuleOld File: " + jar.getName());
089              continue;
090            }
091
092            wrapper.setInfo(wrapper.getModule().getClass().getAnnotation(ModuleInfo.class));
093            PluginCore.log().inform("Found module: " + wrapper.name() + " version: " + wrapper.version());
094            modules.put(wrapper.name(), wrapper);
095
096            if(!wrapper.getInfo().updateURL().trim().equalsIgnoreCase("")) {
097              PluginCore.log().inform("Checking for updates for module " + wrapper.info.name());
098              ModuleUpdateChecker checker = new ModuleUpdateChecker(wrapper.info.name(), wrapper.info.updateURL(), wrapper.version());
099              checker.check();
100            }
101          } catch(Exception ignore) {
102            PluginCore.log().inform("Unable to load module file: " + jar.getName() + ". Are you sure it's up to date?");
103          }
104        }
105      }
106    } else {
107      if(directory.mkdir()) {
108        PluginCore.log().inform("Created module directory: " + directory.getAbsolutePath());
109      }
110    }
111  }
112
113  public boolean load(String moduleName) {
114
115    final String path = findPath(moduleName);
116    if(path != null) {
117      try {
118        final ModuleWrapper wrapper = loadModuleWrapper(path);
119        if(!wrapper.getModule().getClass().isAnnotationPresent(ModuleInfo.class)) {
120          PluginCore.log().inform("Invalid module format! ModuleOld File: " + moduleName);
121          return false;
122        }
123
124        wrapper.setInfo(wrapper.getModule().getClass().getAnnotation(ModuleInfo.class));
125
126        if(!wrapper.getInfo().pluginVersion().equalsIgnoreCase("0.0.0.0") &&
127           new Semver(wrapper.info.pluginVersion()).isGreaterThan(PluginCore.engine().version())) {
128
129          PluginCore.log().error("Unable to load module: " + wrapper.name() + " Requires a higher plugin version. Required version: " + PluginCore.engine().version());
130          return false;
131        }
132
133        PluginCore.log().inform("Found module: " + wrapper.name() + " version: " + wrapper.version());
134        modules.put(wrapper.name(), wrapper);
135
136        /*if(!wrapper.getInfo().updateURL().trim().equalsIgnoreCase("")) {
137          PluginCore.log().inform("Checking for updates for module " + moduleName);
138          ModuleUpdateChecker checker = new ModuleUpdateChecker(moduleName, wrapper.info.updateURL(), wrapper.version());
139          checker.check();
140        }*/
141        return true;
142      } catch(Exception ignore) {
143        PluginCore.log().inform("Unable to load module: " + moduleName + ". Are you sure it exists?");
144      }
145    }
146    return false;
147  }
148
149  public void unload(String moduleName) {
150
151    if(hasModule(moduleName)) {
152      ModuleWrapper wrapper = getModule(moduleName);
153      //TODO: Command and configuration unloading.
154      wrapper.getModule().disable(PluginCore.instance());
155
156      try {
157        Field f = ClassLoader.class.getDeclaredField("classes");
158        f.setAccessible(true);
159
160        Vector<Class> classes = (Vector<Class>)f.get(PluginCore.loader().getModule(moduleName).getLoader());
161        for(Class clazz : classes) {
162          PluginCore.log().debug("Loaded: " + clazz.getName());
163        }
164      } catch(Exception e) {
165        e.printStackTrace();
166      }
167      wrapper.unload();
168      wrapper.setModule(null);
169      wrapper.setInfo(null);
170      wrapper.setLoader(null);
171      wrapper = null;
172      System.gc();
173
174      modules.remove(moduleName);
175    }
176  }
177
178  protected String findPath(String moduleName) {
179
180    final File directory = new File(PluginCore.directory(), "modules");
181    final File[] jars = directory.listFiles((dir, name)->name.endsWith(".jar"));
182
183    if(jars != null) {
184      for(File jar : jars) {
185        if(jar.getAbsolutePath().toLowerCase().contains(moduleName.toLowerCase() + ".jar")) {
186          return jar.getAbsolutePath();
187        }
188      }
189    }
190    return null;
191  }
192
193  private ModuleWrapper loadModuleWrapper(String modulePath) {
194
195    ModuleWrapper wrapper;
196
197    Module module = null;
198
199    final File file = new File(modulePath);
200    Class<? extends Module> moduleClass;
201
202    URLClassLoader classLoader = null;
203    try {
204      classLoader = new URLClassLoader(new URL[]{ file.toURI().toURL() }, PluginCore.instance().getClass().getClassLoader());
205      moduleClass = classLoader.loadClass(getModuleMain(new File(modulePath))).asSubclass(Module.class);
206      module = moduleClass.newInstance();
207    } catch(Exception ignore) {
208      PluginCore.log().inform("Unable to locate module main class for file " + file.getName());
209    }
210    wrapper = new ModuleWrapper(module);
211    wrapper.setLoader(classLoader);
212    return wrapper;
213  }
214
215  public boolean downloadModule(String module) {
216
217    if(modules.containsKey(module)) {
218      try {
219        final String fileURL = modules.get(module).info.updateURL();
220        final URL url = new URL(fileURL);
221        final HttpURLConnection httpConn = (HttpURLConnection)url.openConnection();
222        final int responseCode = httpConn.getResponseCode();
223        if(responseCode == HttpURLConnection.HTTP_OK) {
224          final String fileName = fileURL.substring(fileURL.lastIndexOf("/") + 1, fileURL.length());
225
226          final InputStream in = httpConn.getInputStream();
227          final File file = new File(PluginCore.directory() + File.separator + "modules", fileName);
228
229          if(file.exists()) {
230            if(!file.renameTo(new File(PluginCore.directory() + File.separator + "modules", "outdated-" + fileName))) {
231              return false;
232            }
233          }
234
235          final FileOutputStream out = new FileOutputStream(file);
236
237          int bytesRead = -1;
238          final byte[] buffer = new byte[4096];
239          while((bytesRead = in.read(buffer)) != -1) {
240            out.write(buffer, 0, bytesRead);
241          }
242
243          out.close();
244          in.close();
245        }
246      } catch(Exception ignore) {
247        return false;
248      }
249      return true;
250    }
251    return false;
252  }
253
254  private String getModuleMain(File jarFile) {
255
256    String main = "";
257    JarFile jar = null;
258    InputStream in = null;
259    BufferedReader reader = null;
260
261    try {
262      jar = new JarFile(jarFile);
263      final JarEntry infoFile = jar.getJarEntry("module.tne");
264
265      if(infoFile == null) {
266        PluginCore.log().inform("TNE encountered an error while loading a module! No module.tne file!");
267        return "";
268      }
269
270      in = jar.getInputStream(infoFile);
271      reader = new BufferedReader(new InputStreamReader(in));
272
273      main = reader.readLine().split("=")[1].trim();
274    } catch(IOException e) {
275      PluginCore.log().debug(e.toString());
276    } finally {
277      if(jar != null) {
278        try {
279          jar.close();
280        } catch(IOException e) {
281          PluginCore.log().debug(e.toString());
282        }
283      }
284
285      if(in != null) {
286        try {
287          in.close();
288        } catch(IOException e) {
289          PluginCore.log().debug(e.toString());
290        }
291      }
292
293      if(reader != null) {
294        try {
295          reader.close();
296        } catch(IOException e) {
297          PluginCore.log().debug(e.toString());
298        }
299      }
300    }
301    return main;
302  }
303}