/*
* This file is part of NucleusFramework for Bukkit, licensed under the MIT License (MIT).
*
* Copyright (c) JCThePants (www.jcwhatever.com)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.jcwhatever.nucleus.utils.modules;
import com.jcwhatever.nucleus.mixins.IPluginOwned;
import com.jcwhatever.nucleus.utils.validate.IValidator;
import com.jcwhatever.nucleus.utils.PreCon;
import com.jcwhatever.nucleus.utils.file.FileUtils;
import com.jcwhatever.nucleus.utils.file.FileUtils.DirectoryTraversal;
import com.jcwhatever.nucleus.utils.file.FileUtils.ITextLineProducer;
import org.bukkit.plugin.Plugin;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
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.jar.JarEntry;
import java.util.jar.JarFile;
import javax.annotation.Nullable;
/**
* Loads jar file modules.
*/
public abstract class JarModuleLoader<T> implements IPluginOwned {
private final Plugin _plugin;
private final Class<T> _moduleClass;
private Set<String> _loadedClasses = new HashSet<>(1000);
// keyed to jar file absolute path
private Map<String, List<Class<T>>> _moduleClassMap = new HashMap<>(100);
// keyed to name specified in jar module info
private Map<String, T> _modules = new HashMap<>(20);
private Map<T, IModuleInfo> _moduleInfo = new HashMap<>(20);
/**
* Constructor.
*
* @param plugin The owning plugin.
* @param moduleClass The super type of a module class.
*/
public JarModuleLoader(Plugin plugin, Class<T> moduleClass) {
PreCon.notNull(plugin);
PreCon.notNull(moduleClass);
_plugin = plugin;
_moduleClass = moduleClass;
}
@Override
public Plugin getPlugin() {
return _plugin;
}
/**
* Get the module class or super class.
*/
public Class<T> getModuleClass() {
return _moduleClass;
}
/**
* Get the module folder.
*/
public abstract File getModuleFolder();
/**
* Get the directory traversal used to find jar files.
*/
public abstract DirectoryTraversal getDirectoryTraversal();
/**
* Get loaded modules.
*/
public List<T> getModules() {
return new ArrayList<>(_modules.values());
}
/**
* Get a module by the name specified in its {@link IModuleInfo} data object.
*/
@Nullable
public T getModule(String moduleName) {
PreCon.notNullOrEmpty(moduleName);
return _modules.get(moduleName.toLowerCase());
}
/**
* Get a modules {@link IModuleInfo} data object.
*/
@Nullable
public <I extends IModuleInfo> I getModuleInfo(T module) {
IModuleInfo moduleInfo = _moduleInfo.get(module);
if (moduleInfo == null)
return null;
@SuppressWarnings("unchecked")
I result = (I)_moduleInfo.get(module);
return result;
}
/**
* Get all module classes from jar files in the module folder and instantiate them.
*/
public void loadModules() {
File folder = getModuleFolder();
if (!folder.exists())
return;
if (!folder.isDirectory())
throw new RuntimeException("Module folder must be a folder.");
List<File> files = FileUtils.getFiles(folder, getDirectoryTraversal(),
new IValidator<File>() {
@Override
public boolean isValid(File element) {
return element.getName().endsWith(".jar");
}
});
List<Class<T>> moduleClasses = getModuleClasses(files);
for (Class<T> clazz : moduleClasses) {
try {
// create instance of module
T instance = instantiateModule(clazz);
if (instance == null)
continue;
IModuleInfo moduleInfo = createModuleInfo(instance);
if (moduleInfo == null)
continue;
addModule(moduleInfo, instance);
}
catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* Invoked after a module is loaded to add it to the appropriate collections.
*/
protected void addModule(IModuleInfo info, T instance) {
_moduleInfo.put(instance, info);
_modules.put(info.getSearchName(), instance);
}
/**
* Invoked to remove a module from the appropriate collections.
*
* @param name The search name of the module.
*/
protected void removeModule(String name) {
T module = _modules.remove(name);
if (module != null) {
_moduleInfo.remove(module);
}
}
/**
* Get all module classes from jar files in the modules folder.
*/
protected List<Class<T>> getModuleClasses(Collection<File> files) {
if (files.isEmpty())
return new ArrayList<>(0);
List<Class<T>> results = new ArrayList<>(30);
for (File file : files) {
ClassLoadMethod method = getLoadMethod(file);
if (method == null)
continue;
switch (method) {
case DIRECT_OR_SEARCH:
// fall through
case DIRECT:
Class<T> module;
try {
module = getModuleClass(file, method);
} catch (IOException e) {
e.printStackTrace();
continue;
}
if (module != null) {
results.add(module);
break;
}
else if (method != ClassLoadMethod.DIRECT_OR_SEARCH) {
break;
}
// else fall through
case SEARCH:
List<Class<T>> modules;
// get module classes from jar file
try {
modules = searchModuleClasses(file, method);
} catch (IOException e) {
e.printStackTrace();
continue;
}
results.addAll(modules);
break;
default:
throw new AssertionError();
}
}
return results;
}
/**
* Find module classes in the specified jar file.
*
* @param file The jar file to search in.
*
* @throws IOException
* @throws ClassNotFoundException
*/
protected List<Class<T>> searchModuleClasses(File file, ClassLoadMethod loadMethod) throws IOException {
List<Class<T>> moduleClasses = _moduleClassMap.get(file.getAbsolutePath());
if (moduleClasses != null)
return moduleClasses;
JarFile jarFile = new JarFile(file);
// validate jar file
if (!isValidJarFile(jarFile))
return new ArrayList<>(0);
Enumeration<JarEntry> entries = jarFile.entries();
URL[] urls = { new URL("jar:file:" + file + "!/") };
// a search loader to be discarded when the search is complete
URLClassLoader classLoader = URLClassLoader.newInstance(urls, getClass().getClassLoader());
// set to cache class names, if a module is found
// these are added to _loadedClasses set.
Set<String> searchResults = new HashSet<>(10);
moduleClasses = new ArrayList<>(10);
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
// make sure entry is a class file
if (entry.isDirectory() || !entry.getName().endsWith(".class"))
continue;
// get the class name
String className = entry.getName().substring(0, entry.getName().length() - 6);
className = className.replace('/', '.');
// check if the class has already been loaded
if (_loadedClasses.contains(className))
continue;
// validate the class before loading it
if (!isValidClassName(className))
continue;
Class<?> c;
try {
c = classLoader.loadClass(className);
} catch (ClassNotFoundException e) {
e.printStackTrace();
continue;
}
searchResults.add(className);
// check if type is a module
if (getModuleClass().isAssignableFrom(c)) {
@SuppressWarnings("unchecked")
Class<T> clazz = (Class<T>)c;
// validate type
if (isValidType(clazz)) {
moduleClasses.add(clazz);
}
}
}
jarFile.close();
// module class not found
if (moduleClasses.isEmpty()) {
return moduleClasses;
}
// cache module class
_moduleClassMap.put(file.getAbsolutePath(), moduleClasses);
// add class names to prevent reloading
_loadedClasses.addAll(searchResults);
if (moduleClasses.size() == 1 &&
loadMethod == ClassLoadMethod.DIRECT_OR_SEARCH) {
// save class name for direct load next time
saveClassName(file, moduleClasses.get(0));
}
return moduleClasses;
}
/**
* Find a specific module class in the specified jar file.
*
* @param file The jar file to search in.
*
* @return The class or null if not found.
*
* @throws IOException
* @throws ClassNotFoundException
*/
@Nullable
protected Class<T> getModuleClass(File file, ClassLoadMethod loadMethod) throws IOException {
PreCon.notNull(file);
JarFile jarFile = new JarFile(file);
// validate jar file
if (!isValidJarFile(jarFile))
return null;
String className = getModuleClassName(jarFile);
if (className == null) {
if (loadMethod == ClassLoadMethod.DIRECT_OR_SEARCH) {
// see if a saved class path exists
className = getSavedClassName(file);
}
if (className == null)
return null;
}
return getModuleClass(jarFile, className);
}
/**
* Find a specific module class in the specified jar file.
*
* @param jarFile The jar file to search in.
* @param className The module class name.
*
* @return Null if class not found.
*
* @throws IOException
* @throws ClassNotFoundException
*/
@Nullable
protected Class<T> getModuleClass(JarFile jarFile, String className) throws IOException {
PreCon.notNull(jarFile);
// validate the class before loading it
if (!isValidClassName(className))
return null;
URL[] urls = { new URL("jar:file:" + jarFile.getName() + "!/") };
URLClassLoader classLoader = URLClassLoader.newInstance(urls, getClass().getClassLoader());
Class<?> c;
try {
c = classLoader.loadClass(className);
} catch (ClassNotFoundException e) {
e.printStackTrace();
return null;
}
// check if type is a module
if (!getModuleClass().isAssignableFrom(c)) {
return null;
}
// add class names to prevent reloading
_loadedClasses.add(className);
@SuppressWarnings("unchecked")
Class<T> result = (Class<T>)c;
return result;
}
/**
* Get a modules saved class name.
*
* @param file The module jar file.
*
* @return The class name or null if not found.
*/
@Nullable
protected String getSavedClassName(File file) {
File saveFile = new File(file.getParent(), file.getName() + ".classpath");
if (!saveFile.exists())
return null;
return FileUtils.scanTextFile(saveFile, StandardCharsets.UTF_8, new IValidator<String>() {
int index = 0;
@Override
public boolean isValid(String element) {
index++;
return index == 1;
}
});
}
/**
* Save a modules class name.
*
* @param file The module jar file.
* @param clazz The module class.
*/
protected void saveClassName(File file, final Class<? extends T> clazz) {
File saveFile = new File(file.getParent(), file.getName() + ".classpath");
FileUtils.writeTextFile(saveFile, StandardCharsets.UTF_8, 1, new ITextLineProducer() {
@Nullable
@Override
public String nextLine() {
return clazz.getCanonicalName();
}
});
}
/**
* Invoked to validate a jar file.
*
* <p>Intended for optional override.</p>
*
* @param jarFile The jar file to check.
*
* @return True if valid, false to deny.
*/
protected boolean isValidJarFile(JarFile jarFile) {
return true;
}
/**
* Invoked to validate a class name before loading the class.
*
* <p>Intended for optional override.</p>
*
* @param className The class name to validate.
*
* @return True if valid, false to deny.
*/
protected boolean isValidClassName(String className) {
return true;
}
/**
* Invoked to valid a module type before instantiation.
*
* <p>Intended for optional override.</p>
*
* @param type The type to validate
*
* @return True if valid, false to deny.
*/
protected boolean isValidType(Class<T> type) {
return true;
}
/**
* Get the class load method to use to load a jar file.
*
* @param file The file that will be loaded.
*
* @return The load method or null to cancel loading the file.
*/
protected abstract ClassLoadMethod getLoadMethod(File file);
/**
* Get the name of the module class to load from the jar file.
*
* <p>Only invoked when the {@link ClassLoadMethod} is {@link ClassLoadMethod#DIRECT}
* or {@link ClassLoadMethod#DIRECT_OR_SEARCH}.</p>
*
* @param jarFile The jar file being loaded.
*
* @return The name of the class to load or null to cancel.
*/
protected abstract String getModuleClassName(JarFile jarFile);
/**
* Create a new instance of {@link IModuleInfo} and fill with information
* about the specified module.
*
* @param moduleInstance The module instance.
*
* @return Null if failed or to cancel loading the module.
*/
@Nullable
protected abstract IModuleInfo createModuleInfo(T moduleInstance);
/**
* Create a new instance of a module.
*
* @param clazz The module class.
*
* @return Null if failed or to cancel loading of module.
*/
@Nullable
protected abstract T instantiateModule(Class<T> clazz);
}