ProtocolLib/src/main/java/com/comphenix/protocol/injector/PluginVerifier.java

239 lines
6.9 KiB
Java

package com.comphenix.protocol.injector;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.PluginLoadOrder;
import com.google.common.collect.Sets;
/**
* Determine if a plugin using ProtocolLib is correct.
*
* @author Kristian
*/
class PluginVerifier {
/**
* A named plugin cannot be found.
* @author Kristian
*/
public static class PluginNotFoundException extends RuntimeException {
/**
* Generated by Eclipse.
*/
private static final long serialVersionUID = 8956699101336877611L;
public PluginNotFoundException() {
super();
}
public PluginNotFoundException(String message) {
super(message);
}
}
public enum VerificationResult {
VALID,
/**
* The plugin doesn't depend on ProtocolLib directly or indirectly.
*/
NO_DEPEND;
/**
* Determine if the verification was valid.
*/
public boolean isValid() {
return this == VALID;
}
}
/**
* Contains a list of plugins that will detect and use ProtocolLib dynamically, instead of relying on the dependency system.
*/
private static final Set<String> DYNAMIC_DEPENDENCY = Sets.newHashSet("mcore", "MassiveCore");
/**
* Set of plugins that have been loaded after ProtocolLib.
*/
private final Set<String> loadedAfter = new HashSet<String>();
/**
* Reference to ProtocolLib.
*/
private final Plugin dependency;
/**
* Construct a new plugin verifier.
* @param dependency - reference to ProtocolLib, a dependency we require of plugins.
*/
public PluginVerifier(Plugin dependency) {
if (dependency == null)
throw new IllegalArgumentException("dependency cannot be NULL.");
try {
// This would screw with the assumption in hasDependency(Plugin, Plugin)
if (safeConversion(dependency.getDescription().getLoadBefore()).size() > 0)
throw new IllegalArgumentException("dependency cannot have a load directives.");
} catch (LinkageError e) {
// They're probably using an ancient version of Bukkit
dependency.getLogger().log(Level.WARNING, "Failed to determine loadBefore: " + e);
}
this.dependency = dependency;
}
/**
* Retrieve a plugin by name.
* @param pluginName - the non-null name of the plugin to retrieve.
* @return The retrieved plugin.
* @throws PluginNotFoundException If a plugin with the given name cannot be found.
*/
private Plugin getPlugin(String pluginName) {
Plugin plugin = getPluginOrDefault(pluginName);
// Ensure that the plugin exists
if (plugin != null)
return plugin;
else
throw new PluginNotFoundException("Cannot find plugin " + pluginName);
}
/**
* Retrieve a plugin by name.
* @param pluginName - the non-null name of the plugin to retrieve.
* @return The retrieved plugin, or NULL if not found.
*/
private Plugin getPluginOrDefault(String pluginName) {
return dependency.getServer().getPluginManager().getPlugin(pluginName);
}
/**
* Performs simple verifications on the given plugin.
* <p>
* Results may be cached.
* @param pluginName - the plugin to verify.
* @return A verification result.
* @throws IllegalArgumentException If plugin name is NULL.
* @throws PluginNotFoundException If a plugin with the given name cannot be found.
*/
public VerificationResult verify(String pluginName) {
if (pluginName == null)
throw new IllegalArgumentException("pluginName cannot be NULL.");
return verify(getPlugin(pluginName));
}
/**
* Performs simple verifications on the given plugin.
* <p>
* Results may be cached.
* @param plugin - the plugin to verify.
* @return A verification result.
* @throws IllegalArgumentException If plugin name is NULL.
* @throws PluginNotFoundException If a plugin with the given name cannot be found.
*/
public VerificationResult verify(Plugin plugin) {
if (plugin == null)
throw new IllegalArgumentException("plugin cannot be NULL.");
String name = plugin.getName();
// Skip the load order check for ProtocolLib itself
if (!dependency.equals(plugin)) {
if (!loadedAfter.contains(name) && !DYNAMIC_DEPENDENCY.contains(name)) {
if (verifyLoadOrder(dependency, plugin)) {
// Memorize
loadedAfter.add(plugin.getName());
} else {
return VerificationResult.NO_DEPEND;
}
}
}
// Everything seems to be in order
return VerificationResult.VALID;
}
/**
* Determine if a given plugin is guarenteed to be loaded before the other.
* <p>
* Note that the before plugin is assumed to have no "load" directives - that is, plugins to be
* loaded after itself. The after plugin may have "load" directives, but it is irrelevant for our purposes.
* @param beforePlugin - the plugin that is loaded first.
* @param afterPlugin - the plugin that is loaded last.
* @return TRUE if it will, FALSE if it may or must load in the opposite other.
*/
private boolean verifyLoadOrder(Plugin beforePlugin, Plugin afterPlugin) {
// If a plugin has a dependency, it will be loaded after its dependency
if (hasDependency(afterPlugin, beforePlugin)) {
return true;
}
// No dependency - check the load order
if (beforePlugin.getDescription().getLoad() == PluginLoadOrder.STARTUP &&
afterPlugin.getDescription().getLoad() == PluginLoadOrder.POSTWORLD) {
return true;
}
return false;
}
/**
* Determine if a plugin has a given dependency, either directly or indirectly.
* @param plugin - the plugin to check.
* @param dependency - the dependency to find.
* @return TRUE if the plugin has the given dependency, FALSE otherwise.
*/
private boolean hasDependency(Plugin plugin, Plugin dependency) {
return hasDependency(plugin, dependency, Sets.<String>newHashSet());
}
/**
* Convert a list to a set.
* <p>
* A null list will be converted to an empty set.
* @param list - the list to convert.
* @return The converted list.
*/
private Set<String> safeConversion(List<String> list) {
if (list == null)
return Collections.emptySet();
else
return Sets.newHashSet(list);
}
// Avoid cycles. DFS.
private boolean hasDependency(Plugin plugin, Plugin dependency, Set<String> checking) {
Set<String> childNames = Sets.union(
safeConversion(plugin.getDescription().getDepend()),
safeConversion(plugin.getDescription().getSoftDepend())
);
// Ensure that the same plugin isn't processed twice
if (!checking.add(plugin.getName())) {
throw new IllegalStateException("Cycle detected in dependency graph: " + plugin);
}
// Look for the dependency in the immediate children
if (childNames.contains(dependency.getName())) {
return true;
}
// Recurse through their dependencies
for (String childName : childNames) {
Plugin childPlugin = getPluginOrDefault(childName);
if (childPlugin != null && hasDependency(childPlugin, dependency, checking)) {
return true;
}
}
// Cross edges are permitted
checking.remove(plugin.getName());
// No dependency found!
return false;
}
}