package org.tsbs.manager;
import com.github.theholywaffle.teamspeak3.TS3Api;
import com.github.theholywaffle.teamspeak3.TS3Config;
import com.github.theholywaffle.teamspeak3.TS3Query;
import com.github.theholywaffle.teamspeak3.api.wrapper.Channel;
import org.tsbs.core.CallableFunction;
import org.tsbs.core.CommandHandler;
import org.tsbs.core.TS3BotChannelListener;
import org.tsbs.manager.exceptions.DirectoryNotFoundException;
import org.tsbs.manager.exceptions.UnknownChannelException;
import org.tsbs.manager.exceptions.UnknownFunctionException;
import org.tsbs.util.ApiUtils;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FilenameFilter;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.*;
import java.util.logging.FileHandler;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* A BotManager is used to handle multiple TS3ChannelListener, CommandHandler and the functions they use.
*
* @author falx
* @version v0.1a
*/
public class BotManager
{
// -------------------------------------------------------------------------------------------------------------- //
// ------------------------- //
// -- static declarations -- //
// ------------------------- //
// -------------------------------------------------------------------------------------------------------------- //
// logger used by this class
private static final Logger sfLogger = Logger.getLogger( BotManager.class.getName() );
{
sfLogger.setLevel( Level.ALL );
}
// URL to the internal bin - / - package where the internal implemented functions are saved - as class files
private static final URL PACKAGE_DIRECTORY = BotManager.class.getClassLoader().getResource( "org/tsbs/functions" );
// qualifying prefix for the classes which are implemented internally. It is needed to load the classes using the ClassLoader!
private static final String PACKAGE_QUALIFIER = "org.tsbs.functions.";
// -------------------------------------------------------------------------------------------------------------- //
// ------------------------- //
// -- member declarations -- //
// ------------------------- //
// -------------------------------------------------------------------------------------------------------------- //
// URLClassLoader is used to load all functions, defined in class files, from the specified directories
private URLClassLoader mUrlClassLoader;
// contains all functions found in the specified directories
private HashMap<Class<? extends CallableFunction>, FunctionWrapper> mFunctionMap;
// contains all channels, where a TS§BotChannelListener is active
private HashMap<Integer, ChannelWrapper> mChannelMap;
// contains configuration data
private BotConfiguration mBotConfiguration;
// CommandHandler used by the TS3BotChannelListener
private CommandHandler mCommandHandler;
// objects to initially set up the TS3-Query API
// this Query and Api is only used to connect to the server initially and
// to get all needed informations from the server (e.g. channelList)
private TS3Query mQuery;
private TS3Api mApi; // created for a more simple handling
// FileNameFilter used to load the class files which contain the function classes
private FilenameFilter mClassFileFilter = (dir, name) -> { return name.endsWith( ".class" ); };
// indicates it the CommandHandler is running and if the channelListeners are running
private volatile boolean mIsRunning;
// -------------------------------------------------------------------------------------------------------------- //
// ------------------ //
// -- Constructors -- //
// ------------------ //
// -------------------------------------------------------------------------------------------------------------- //
private BotManager( BotConfiguration botConfiguration )
{
this.mFunctionMap = new HashMap<>();
this.mChannelMap = new HashMap<>();
this.mBotConfiguration = botConfiguration;
this.mIsRunning = false;
}
// -------------------------------------------------------------------------------------------------------------- //
// -------------------------------- //
// -- Factory / Creation methods -- //
// -------------------------------- //
// -------------------------------------------------------------------------------------------------------------- //
/**
*
* @param botConfiguration
* @return
* @throws DirectoryNotFoundException
*/
public static BotManager createBotManager( BotConfiguration botConfiguration ) throws FileNotFoundException, MalformedURLException
{
BotManager manager = new BotManager( botConfiguration );
boolean retSuccessful;
String logPath = manager.mBotConfiguration.getLoggingFilePath();
if( logPath != null )
{
try
{
sfLogger.addHandler( new FileHandler( logPath ) );
sfLogger.log( Level.CONFIG, "Started logging to " + logPath );
}
catch( IOException exc )
{
sfLogger.log( Level.WARNING, "Could not start logging to " + logPath, exc );
}
}
else
sfLogger.log( Level.INFO, "Not logging-file defined. Log will printed only on console!" );
sfLogger.log( Level.INFO, "Starting to set up the BotManager..." );
// if there is a path to external define functions: check if the path is valid and if it exist
// and initiated the URLClassLoader with it, so the functions can be loaded.
// if there is not path the class loader should be initiated without any external path
URL externalFunctionURL;
String functionDirPath = manager.mBotConfiguration.getFunctionDirectory();
if( functionDirPath != null )
{
try
{
externalFunctionURL = new URL( functionDirPath );
}
catch ( MalformedURLException exc )
{
sfLogger.log( Level.SEVERE, "MalformedURLException caused by the " + functionDirPath, exc );
throw exc;
}
File f = new File( externalFunctionURL.getFile() );
if( !f.exists() || !f.isDirectory() )
{
FileNotFoundException exc = new FileNotFoundException( "The Directory " + externalFunctionURL.getFile() +
" was not found!" );
sfLogger.log( Level.SEVERE, exc.getMessage(), exc );
}
manager.initiateURLClassLoader( new URL[]{ externalFunctionURL } );
}
else
{
sfLogger.log( Level.INFO, "No directory for external functions found." );
manager.initiateURLClassLoader( new URL[]{ } );
}
manager.loadInternalFunctions();
manager.loadExternalFunctions();
manager.initiateCommandHandler();
manager.initialConnect();
manager.initiateChannelMap();
return manager;
}
// ------------------------------------------- private methods -------------------------------------------------- //
// -------------------------------------------------------------------------------------------------------------- //
// ------------------------ //
// -- initiation methods -- //
// ------------------------ //
// -------------------------------------------------------------------------------------------------------------- //
// initiates the URLClassLoader with the url given as arguments plus the package url where
// all standartfunctions are saved
private void initiateURLClassLoader( URL[] urls )
{
urls = Arrays.copyOf( urls, urls.length + 1 );
urls[ urls.length - 1 ] = PACKAGE_DIRECTORY;
mUrlClassLoader = URLClassLoader.newInstance( urls );
sfLogger.log( Level.INFO, "ClassLoader initiated." );
}
// loading all internal implemented functions
private void loadInternalFunctions()
{
File[] files = new File( PACKAGE_DIRECTORY.getFile() ).listFiles( mClassFileFilter );;
String fileName;
String className;
for ( File f : files )
{
// differentiate between class- and fileName because the className needs to be
// fully qualified, but the name for the function should be the not fully qualified name
fileName = f.getName().substring( 0, f.getName().indexOf( ".class" ) );
className = PACKAGE_QUALIFIER + fileName;
CallableFunction function = loadFunction( className );
if( function != null )
{
mFunctionMap.put( function.getClass(), new FunctionWrapper( function.getClass(), f, fileName, false ) );
sfLogger.log( Level.INFO, "Internal function \"" + fileName + "\" saved to the functionMap." );
}
}
sfLogger.log( Level.INFO, "Internal functions loaded." );
}
// loading all external defined functions, if there are any defined
private void loadExternalFunctions()
{
String externalFunctionPath = mBotConfiguration.getFunctionDirectory();
// if there is not path to external functions exit this method, because there's nothing todo
if( externalFunctionPath == null )
{
sfLogger.log( Level.WARNING, "No path to external define functions found!" );
return;
}
File[] files = new File( externalFunctionPath ).listFiles( mClassFileFilter );
String className;
for( File f : files )
{
className = f.getName().substring( 0, f.getName().indexOf( ".class" ) );
CallableFunction function = loadFunction( className );
if( function != null )
{
mFunctionMap.put( function.getClass(), new FunctionWrapper( function.getClass(), f, className, false ) );
sfLogger.log( Level.INFO, "External function \"" + className + "\" saved to the functionMap." );
}
}
sfLogger.log( Level.INFO, "External functions loaded." );
}
// loads a function using the ClassLoader. Which function should be loaded is specified by className
// returns null and lo gs a failure if there is no function/class like className
private CallableFunction loadFunction( String className )
{
CallableFunction function = null;
try
{
function = (CallableFunction)mUrlClassLoader.loadClass( className ).newInstance();
sfLogger.log( Level.INFO, "Function \"" + className + "\" successful loaded." );
}
catch ( InstantiationException | IllegalAccessException | ClassNotFoundException exc )
{
sfLogger.log( Level.WARNING, "Could not load the function class " + className, exc );
}
return function;
}
// initiates the command handler and sets up the threadpool-size
private void initiateCommandHandler()
{
int amountThreads;
try {
amountThreads = Integer.parseInt( mBotConfiguration.getCmdHandlerProcNo() );
}
catch( NumberFormatException exc ) {
sfLogger.log( Level.WARNING, "Unable to read the cmdHandlerProcNo-property. Using standard amount of threads", exc );
amountThreads = Integer.parseInt( System.getenv( "NUMBER_OF_PROCESSORS" ) );
}
mCommandHandler = new CommandHandler( amountThreads );
sfLogger.log( Level.INFO, "CommandHandler successfully created using " + amountThreads + " Threads." );
}
// initially connects to the ts3-server so the ts3-objects which are needed to manage the ts3 server stuff
private void initialConnect()
{
TS3Config config = getServerConfig();
mQuery = new TS3Query( config );
mQuery.connect();
mApi = mQuery.getApi();
mApi.selectVirtualServerById( 1 );
sfLogger.log( Level.INFO, "Connected to the TS3-server \"" + mBotConfiguration.getHostName() + "\" using " +
"the login name \"" + mBotConfiguration.getLoginName() + "\"" );
}
private void initiateChannelMap()
{
sfLogger.log( Level.INFO, "initiateChannelMap..." );
List<Channel> channelsToJoin = ApiUtils.filterNotJoinableChannels( getAllServerChannels() );
for( Channel c : channelsToJoin )
{
TS3BotChannelListener listener = new TS3BotChannelListener( getServerConfig(), false, mCommandHandler, c.getId() );
ChannelWrapper cw = new ChannelWrapper( c, listener, false );
mChannelMap.put( c.getId(), cw );
sfLogger.log( Level.INFO, String.format( "ChannelName: %s \tChannelId: %d", c.getName(), c.getId() ) );
}
sfLogger.log( Level.INFO, "...initiateChannelMap finished!" );
}
// returns a TS3Config, configurated to connect to the server given by the BotConfiguration
private TS3Config getServerConfig()
{
TS3Config config = new TS3Config();
config.setHost( mBotConfiguration.getHostName() );
config.setLoginCredentials( mBotConfiguration.getLoginName(), mBotConfiguration.getLoginPassword() );
return config;
}
// connects all active channelwrappers to their associated channels
private void connectChannel()
{
for( ChannelWrapper cw : mChannelMap.values() )
if( cw.isActive() )
{
cw.connect();
sfLogger.log( Level.INFO, "connected to " + cw.mfChannel.getName() + " with the id " + cw.mfChannel.getId() );
}
sfLogger.log( Level.INFO, "Connected all listeners to their channel." );
}
// disconnects all active channelwrappers from their associated channels
private void disconnectChannel()
{
for( ChannelWrapper cw : mChannelMap.values() )
if( !cw.isActive() )
cw.disconnect();
sfLogger.log( Level.INFO, "Disconnected all listeners from theixr channel." );
}
// starting the CommandHandler. Before starting there must be the functions given to it,
// important: use the functions override method!
private void startCommandHandler()
{
sfLogger.log( Level.INFO, "Preparing and starting CommandHandler.." );
int i = 0;
Map<String, Class<? extends CallableFunction>> activeFunctions = getAllActiveFunctionNames();
Map<String, CallableFunction> commandMap = new HashMap<>();
for( String s : activeFunctions.keySet() )
{
Class<? extends CallableFunction> clazz = activeFunctions.get( s );
try {
commandMap.put( s, clazz.newInstance() );
i++;
} catch( IllegalAccessException | InstantiationException exc ) {
sfLogger.log( Level.INFO, "Could not instantiate the function: " + clazz.getName(), exc );
}
}
mCommandHandler.overrideFunctions( commandMap );
mCommandHandler.run();
sfLogger.log( Level.INFO, String.format( "..prepared and started CommandHandler using %d Functions.", i ) );
}
// stops the CommandHandler. Nothing extras have to be done here
private void stopCommandHandler()
{
sfLogger.log( Level.INFO, "Stopping CommandHandler.." );
mCommandHandler.stop();
sfLogger.log( Level.INFO, "..finished." ) ;
}
// ------------------------------------------ public methods ---------------------------------------------------- //
// -------------------------------------------------------------------------------------------------------------- //
// ---------------------------------- //
// -- methods for channel handling -- //
// ---------------------------------- //
// -------------------------------------------------------------------------------------------------------------- //
/**
* @return a list with all available channels on the TS3-server
*/
public List<Channel> getAllServerChannels()
{
return mApi.getChannels();
}
/**
* @return a list containing all channels which are currently the bot is listening at - if it is running
*/
public List<Channel> getActiveChannels()
{
ArrayList<Channel> list = new ArrayList<>();
for( ChannelWrapper cw : mChannelMap.values() )
if( cw.isActive() )
list.add( cw.mfChannel );
return list;
}
/**
* @param c the channel to be checked if the bot is currently active in or not
* @return true if the channel is active, else false
*/
public boolean isChannelActive( Channel c )
{
ChannelWrapper cw = mChannelMap.get( c.getId() );
return cw == null ? false : cw.isActive();
}
/**
*
* @param channelId
* @throws org.tsbs.manager.exceptions.UnknownChannelException if there's no channel with the id ChannelId
*/
public void activateChannel( int channelId ) throws UnknownChannelException
{
Channel c = ApiUtils.getChannelById( getAllServerChannels(), channelId );
if( c == null )
throw new UnknownChannelException( "Channel with the id( " + channelId + " ) not found!" );
activateChannel( c );
}
/**
*
* @param channel
* @throws org.tsbs.manager.exceptions.UnknownChannelException
*/
public void activateChannel( Channel channel ) throws UnknownChannelException
{
ChannelWrapper cw = mChannelMap.get( channel.getId() );
if( cw == null )
throw new UnknownChannelException( "Channel with the id " + channel.getId() + " not as active found!" );
cw.setActive();
sfLogger.log( Level.INFO, String.format( "Activated channel with the id: %d, name: %s", cw.mfChannel.getId(),
channel.getId(), channel.getName() ), channel );
}
/**
*
* @param channelId id of the channel to be removed
* @return true when successfully removed/deactivated, else false - e.g. if the channel was not active
*/
public boolean deactivateChannel( int channelId )
{
ChannelWrapper cw = mChannelMap.remove( channelId );
// if the channel was not in the active map, return false,
if( cw == null )
{
sfLogger.log( Level.INFO, "No active channel found with the channeld-id: " + channelId );
return false;
}
cw.setInactive();
sfLogger.log( Level.INFO, String.format( "Deactivated channel id: %d, name: %s.", channelId, cw.mfChannel.getName() ),
cw.mfChannel );
return true;
}
/**
*
* @param channel the channel to be removed/deactivated
* @return true when successfully removed/deactivated, else false - e.g. if the channel was not active
*/
public boolean deactivateChannel( Channel channel )
{
return deactivateChannel( channel.getId() );
}
// -------------------------------------------------------------------------------------------------------------- //
// ------------------------------- //
// -- Command/Function settings -- //
// ------------------------------- //
// -------------------------------------------------------------------------------------------------------------- //
/**
* @return a list containing the classes of all loaded functions
*/
public List<Class<? extends CallableFunction>> getAllFunctionClasses()
{
return new ArrayList<>( mFunctionMap.keySet() );
}
/**
* @return a map containing the function names in reference to the classes they name
*/
public Map<String, Class<? extends CallableFunction>> getAllFunctionNames()
{
HashMap<String, Class<? extends CallableFunction>> map = new HashMap<>();
for( FunctionWrapper fw : mFunctionMap.values() )
map.put( fw.getFunctionName(), fw.getCallableAction() );
return map;
}
/** @return a map containing the function names of all active functions in reference to the classes they name */
public Map<String, Class<? extends CallableFunction>> getAllActiveFunctionNames()
{
HashMap<String, Class<? extends CallableFunction>> map = new HashMap<>();
for( FunctionWrapper fw : mFunctionMap.values() )
if( fw.isActive() )
map.put( fw.getFunctionName(), fw.getCallableAction() );
return map;
}
/**
* This Operation will take effect after the Bot has been restarted!
* @param clazz identifies the function which has to be renamed
* @param newName the new Name for the function
* @return true if rename process was successful, else false
*/
public boolean renameFunction( Class<? extends CallableFunction> clazz, String newName ) throws UnknownFunctionException
{
FunctionWrapper fw = mFunctionMap.get( clazz );
if( clazz == null )
{
UnknownFunctionException exc = new UnknownFunctionException( "Unknown function: " + clazz.getName() );
sfLogger.log( Level.WARNING, "Unknown function tried to rename.", exc );
}
String old = fw.getFunctionName();
fw.setFunctionName( newName );
sfLogger.log( Level.INFO, "Renamed function from " + old + " to " + newName );
return true;
}
/**
* This operation will take effect after the bot has been restarted!
* @param clazz function to activate/deactivate
* @param setActive will be activated if true, else deactivate
* @return true if the operation was successful, else false
*/
public boolean setFunctionActive( Class<? extends CallableFunction> clazz, boolean setActive ) throws UnknownFunctionException
{
FunctionWrapper fw = mFunctionMap.get(clazz);
if( fw == null )
{
UnknownFunctionException exc = new UnknownFunctionException( "UnknownFunction: " + clazz.getName() );
sfLogger.log( Level.WARNING, "Unknown function tried to activate.", exc );
}
fw.setIsActive( setActive );
sfLogger.log( Level.INFO, "Function " + clazz.getName() + " is now activated." );
return true;
}
/**
* This Operation will take effect after the bot has been restarted!
* @param clazz function to activate
* @return true if the operation was successful, else false
*/
public boolean setFunctionActive( Class<? extends CallableFunction> clazz )
{
return setFunctionActive( clazz, true );
}
/**
* This operation will take effect after the bot has been restarted!
* @param clazz function to deactivate
* @return true if the operation was successful, else false
*/
public boolean setFunctionInactive( Class<? extends CallableFunction> clazz )
{
return setFunctionActive( clazz, false );
}
// -------------------------------------------------------------------------------------------------------------- //
// ---------------------------------------------- //
// -- methods to control the managers activity -- //
// ---------------------------------------------- //
// -------------------------------------------------------------------------------------------------------------- //
/**
* Connects all ChannelListener to their TS3-Channels and starts up
* the commandHandler using the functions marked as active.
* @return true if the bot has been set active, else false;
*/
public boolean setBotActive()
{
sfLogger.log( Level.INFO, "Activating Bot ..." );
if( mIsRunning )
{
sfLogger.log( Level.INFO, ".. Bot is already active!" );
return false;
}
connectChannel();
startCommandHandler();
mIsRunning = true;
sfLogger.log( Level.INFO, "... activating bot finished." );
return true;
}
/**
* Disconnects all ChannelListener from their TS3-Channels and stops
* the CommandHandler imidiatly
* @return true if the bot has been set inactive, else false
*/
public boolean setBotInactive()
{
sfLogger.log( Level.INFO, "Deactivating Bot ... " );
if( !mIsRunning )
{
sfLogger.log( Level.INFO, "... Bot is already deactivated!" );
return false;
}
disconnectChannel();
stopCommandHandler();
mIsRunning = false;
sfLogger.log( Level.INFO, "... deactivating finished." );
return true;
}
}