package net.md_5.bungee.api.plugin;
import com.google.common.base.Preconditions;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import com.google.common.eventbus.Subscribe;
import java.io.File;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.logging.Level;
import java.util.regex.Pattern;
import lombok.RequiredArgsConstructor;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.CommandSender;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.event.EventBus;
import net.md_5.bungee.event.EventHandler;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.Constructor;
import org.yaml.snakeyaml.introspector.PropertyUtils;
/**
* Class to manage bridging between plugin duties and implementation duties, for
* example event handling and plugin management.
*/
@RequiredArgsConstructor
public class PluginManager
{
private static final Pattern argsSplit = Pattern.compile( " " );
/*========================================================================*/
private final ProxyServer proxy;
/*========================================================================*/
private final Yaml yaml;
private final EventBus eventBus;
private final Map<String, Plugin> plugins = new LinkedHashMap<>();
private final Map<String, Command> commandMap = new HashMap<>();
private Map<String, PluginDescription> toLoad = new HashMap<>();
private final Multimap<Plugin, Command> commandsByPlugin = ArrayListMultimap.create();
private final Multimap<Plugin, Listener> listenersByPlugin = ArrayListMultimap.create();
@SuppressWarnings("unchecked")
public PluginManager(ProxyServer proxy)
{
this.proxy = proxy;
// Ignore unknown entries in the plugin descriptions
Constructor yamlConstructor = new Constructor();
PropertyUtils propertyUtils = yamlConstructor.getPropertyUtils();
propertyUtils.setSkipMissingProperties( true );
yamlConstructor.setPropertyUtils( propertyUtils );
yaml = new Yaml( yamlConstructor );
eventBus = new EventBus( proxy.getLogger() );
}
/**
* Register a command so that it may be executed.
*
* @param plugin the plugin owning this command
* @param command the command to register
*/
public void registerCommand(Plugin plugin, Command command)
{
commandMap.put( command.getName().toLowerCase(), command );
for ( String alias : command.getAliases() )
{
commandMap.put( alias.toLowerCase(), command );
}
commandsByPlugin.put( plugin, command );
}
/**
* Unregister a command so it will no longer be executed.
*
* @param command the command to unregister
*/
public void unregisterCommand(Command command)
{
while ( commandMap.values().remove( command ) );
commandsByPlugin.values().remove( command );
}
/**
* Unregister all commands owned by a {@link Plugin}
*
* @param plugin the plugin to register the commands of
*/
public void unregisterCommands(Plugin plugin)
{
for ( Iterator<Command> it = commandsByPlugin.get( plugin ).iterator(); it.hasNext(); )
{
Command command = it.next();
while ( commandMap.values().remove( command ) );
it.remove();
}
}
public boolean dispatchCommand(CommandSender sender, String commandLine)
{
return dispatchCommand( sender, commandLine, null );
}
/**
* Execute a command if it is registered, else return false.
*
* @param sender the sender executing the command
* @param commandLine the complete command line including command name and
* arguments
* @return whether the command was handled
*/
public boolean dispatchCommand(CommandSender sender, String commandLine, List<String> tabResults)
{
String[] split = argsSplit.split( commandLine, -1 );
// Check for chat that only contains " "
if ( split.length == 0 )
{
return false;
}
String commandName = split[0].toLowerCase();
if ( sender instanceof ProxiedPlayer && proxy.getDisabledCommands().contains( commandName ) )
{
return false;
}
Command command = commandMap.get( commandName );
if ( command == null )
{
return false;
}
String permission = command.getPermission();
if ( permission != null && !permission.isEmpty() && !sender.hasPermission( permission ) )
{
if ( !( command instanceof TabExecutor ) || tabResults == null )
{
sender.sendMessage( proxy.getTranslation( "no_permission" ) );
}
return true;
}
String[] args = Arrays.copyOfRange( split, 1, split.length );
try
{
if ( tabResults == null )
{
if ( proxy.getConfig().isLogCommands() )
{
proxy.getLogger().log( Level.INFO, "{0} executed command: /{1}", new Object[]
{
sender.getName(), commandLine
} );
}
command.execute( sender, args );
} else if ( commandLine.contains( " " ) && command instanceof TabExecutor )
{
for ( String s : ( (TabExecutor) command ).onTabComplete( sender, args ) )
{
tabResults.add( s );
}
}
} catch ( Exception ex )
{
sender.sendMessage( ChatColor.RED + "An internal error occurred whilst executing this command, please check the console log for details." );
ProxyServer.getInstance().getLogger().log( Level.WARNING, "Error in dispatching command", ex );
}
return true;
}
/**
* Returns the {@link Plugin} objects corresponding to all loaded plugins.
*
* @return the set of loaded plugins
*/
public Collection<Plugin> getPlugins()
{
return plugins.values();
}
/**
* Returns a loaded plugin identified by the specified name.
*
* @param name of the plugin to retrieve
* @return the retrieved plugin or null if not loaded
*/
public Plugin getPlugin(String name)
{
return plugins.get( name );
}
public void loadPlugins()
{
Map<PluginDescription, Boolean> pluginStatuses = new HashMap<>();
for ( Map.Entry<String, PluginDescription> entry : toLoad.entrySet() )
{
PluginDescription plugin = entry.getValue();
if ( !enablePlugin( pluginStatuses, new Stack<PluginDescription>(), plugin ) )
{
ProxyServer.getInstance().getLogger().log( Level.WARNING, "Failed to enable {0}", entry.getKey() );
}
}
toLoad.clear();
toLoad = null;
}
public void enablePlugins()
{
for ( Plugin plugin : plugins.values() )
{
try
{
plugin.onEnable();
ProxyServer.getInstance().getLogger().log( Level.INFO, "Enabled plugin {0} version {1} by {2}", new Object[]
{
plugin.getDescription().getName(), plugin.getDescription().getVersion(), plugin.getDescription().getAuthor()
} );
} catch ( Throwable t )
{
ProxyServer.getInstance().getLogger().log( Level.WARNING, "Exception encountered when loading plugin: " + plugin.getDescription().getName(), t );
}
}
}
private boolean enablePlugin(Map<PluginDescription, Boolean> pluginStatuses, Stack<PluginDescription> dependStack, PluginDescription plugin)
{
if ( pluginStatuses.containsKey( plugin ) )
{
return pluginStatuses.get( plugin );
}
// combine all dependencies for 'for loop'
Set<String> dependencies = new HashSet<>();
dependencies.addAll( plugin.getDepends() );
dependencies.addAll( plugin.getSoftDepends() );
// success status
boolean status = true;
// try to load dependencies first
for ( String dependName : dependencies )
{
PluginDescription depend = toLoad.get( dependName );
Boolean dependStatus = ( depend != null ) ? pluginStatuses.get( depend ) : Boolean.FALSE;
if ( dependStatus == null )
{
if ( dependStack.contains( depend ) )
{
StringBuilder dependencyGraph = new StringBuilder();
for ( PluginDescription element : dependStack )
{
dependencyGraph.append( element.getName() ).append( " -> " );
}
dependencyGraph.append( plugin.getName() ).append( " -> " ).append( dependName );
ProxyServer.getInstance().getLogger().log( Level.WARNING, "Circular dependency detected: {0}", dependencyGraph );
status = false;
} else
{
dependStack.push( plugin );
dependStatus = this.enablePlugin( pluginStatuses, dependStack, depend );
dependStack.pop();
}
}
if ( dependStatus == Boolean.FALSE && plugin.getDepends().contains( dependName ) ) // only fail if this wasn't a soft dependency
{
ProxyServer.getInstance().getLogger().log( Level.WARNING, "{0} (required by {1}) is unavailable", new Object[]
{
String.valueOf( dependName ), plugin.getName()
} );
status = false;
}
if ( !status )
{
break;
}
}
// do actual loading
if ( status )
{
try
{
URLClassLoader loader = new PluginClassloader( new URL[]
{
plugin.getFile().toURI().toURL()
} );
Class<?> main = loader.loadClass( plugin.getMain() );
Plugin clazz = (Plugin) main.getDeclaredConstructor().newInstance();
clazz.init( proxy, plugin );
plugins.put( plugin.getName(), clazz );
clazz.onLoad();
ProxyServer.getInstance().getLogger().log( Level.INFO, "Loaded plugin {0} version {1} by {2}", new Object[]
{
plugin.getName(), plugin.getVersion(), plugin.getAuthor()
} );
} catch ( Throwable t )
{
proxy.getLogger().log( Level.WARNING, "Error enabling plugin " + plugin.getName(), t );
}
}
pluginStatuses.put( plugin, status );
return status;
}
/**
* Load all plugins from the specified folder.
*
* @param folder the folder to search for plugins in
*/
public void detectPlugins(File folder)
{
Preconditions.checkNotNull( folder, "folder" );
Preconditions.checkArgument( folder.isDirectory(), "Must load from a directory" );
for ( File file : folder.listFiles() )
{
if ( file.isFile() && file.getName().endsWith( ".jar" ) )
{
try ( JarFile jar = new JarFile( file ) )
{
JarEntry pdf = jar.getJarEntry( "bungee.yml" );
if ( pdf == null )
{
pdf = jar.getJarEntry( "plugin.yml" );
}
Preconditions.checkNotNull( pdf, "Plugin must have a plugin.yml or bungee.yml" );
try ( InputStream in = jar.getInputStream( pdf ) )
{
PluginDescription desc = yaml.loadAs( in, PluginDescription.class );
Preconditions.checkNotNull( desc.getName(), "Plugin from %s has no name", file );
Preconditions.checkNotNull( desc.getMain(), "Plugin from %s has no main", file );
desc.setFile( file );
toLoad.put( desc.getName(), desc );
}
} catch ( Exception ex )
{
ProxyServer.getInstance().getLogger().log( Level.WARNING, "Could not load plugin from file " + file, ex );
}
}
}
}
/**
* Dispatch an event to all subscribed listeners and return the event once
* it has been handled by these listeners.
*
* @param <T> the type bounds, must be a class which extends event
* @param event the event to call
* @return the called event
*/
public <T extends Event> T callEvent(T event)
{
Preconditions.checkNotNull( event, "event" );
long start = System.nanoTime();
eventBus.post( event );
event.postCall();
long elapsed = System.nanoTime() - start;
if ( elapsed > 250000000 )
{
ProxyServer.getInstance().getLogger().log( Level.WARNING, "Event {0} took {1}ns to process!", new Object[]
{
event, elapsed
} );
}
return event;
}
/**
* Register a {@link Listener} for receiving called events. Methods in this
* Object which wish to receive events must be annotated with the
* {@link EventHandler} annotation.
*
* @param plugin the owning plugin
* @param listener the listener to register events for
*/
public void registerListener(Plugin plugin, Listener listener)
{
for ( Method method : listener.getClass().getDeclaredMethods() )
{
Preconditions.checkArgument( !method.isAnnotationPresent( Subscribe.class ),
"Listener %s has registered using deprecated subscribe annotation! Please update to @EventHandler.", listener );
}
eventBus.register( listener );
listenersByPlugin.put( plugin, listener );
}
/**
* Unregister a {@link Listener} so that the events do not reach it anymore.
*
* @param listener the listener to unregister
*/
public void unregisterListener(Listener listener)
{
eventBus.unregister( listener );
listenersByPlugin.values().remove( listener );
}
/**
* Unregister all of a Plugin's listener.
*/
public void unregisterListeners(Plugin plugin)
{
for ( Iterator<Listener> it = listenersByPlugin.get( plugin ).iterator(); it.hasNext(); )
{
eventBus.unregister( it.next() );
it.remove();
}
}
}