--- /dev/null
+/*
+ * See LICENSE.md
+ */
+package net.rizon.acid.commands.annotations;
+
+/**
+ *
+ * @author Orillion <orillion@rizon.net>
+ */
+public enum ArgumentType
+{
+ /**
+ * Argument must be a duration
+ */
+ DURATION,
+ /**
+ * Argument must be a natural number
+ */
+ NATURAL,
+ /**
+ * Argument must be a positive natural number (including 0)
+ */
+ NATURAL_POSITIVE,
+ /**
+ * Argument must be a negative natural number (including 0)
+ */
+ NATURAL_NEGATIVE,
+ /**
+ * Argument must be a an existing user
+ */
+ USER,
+ /**
+ * Argument must be a mask (n!u@h)
+ */
+ MASK,
+ /**
+ * Argument must be an existing server
+ */
+ SERVER,
+ /**
+ * Argument must match regex, specify with regex = ""
+ */
+ REGEX,
+ /**
+ * Argument must match a flag, specify with flag = ""
+ */
+ FLAG,
+ /**
+ * Argument is a string
+ */
+ STRING,
+}
--- /dev/null
+/*
+ * See LICENSE.md
+ */
+package net.rizon.acid.commands.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ *
+ * @author Orillion <orillion@rizon.net>
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface Command
+{
+ String value();
+}
--- /dev/null
+/*
+ * See LICENSE.md
+ */
+package net.rizon.acid.commands.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ *
+ * @author Orillion <orillion@rizon.net>
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.PARAMETER)
+public @interface Parameter
+{
+ ArgumentType value() default ArgumentType.STRING;
+
+ boolean optional() default false;
+
+ String flag() default "";
+
+ String regex() default "";
+
+ String prefix() default "";
+}
--- /dev/null
+/*
+ * See LICENSE.md
+ */
+package net.rizon.acid.commands.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ *
+ * @author Orillion <orillion@rizon.net>
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface SubCommand
+{
+ String value();
+
+ String description() default "No description available";
+}
--- /dev/null
+/*
+ * See LICENSE.md
+ */
+package net.rizon.acid.commands.special;
+
+import net.rizon.acid.commands.annotations.ArgumentType;
+import net.rizon.acid.commands.annotations.Command;
+import net.rizon.acid.commands.annotations.Parameter;
+import net.rizon.acid.commands.annotations.SubCommand;
+import net.rizon.acid.core.AcidUser;
+import net.rizon.acid.core.Acidictive;
+import net.rizon.acid.core.Channel;
+import net.rizon.acid.core.User;
+
+/**
+ *
+ * @author Orillion <orillion@rizon.net>
+ */
+@Command("CALC")
+public class Calculator extends SpecialCommand
+{
+ public Calculator(final User source, final AcidUser target, final Channel channel)
+ {
+ super(source, target, channel);
+ }
+
+ @SubCommand(
+ value = "ADD",
+ description = "Adds 2 numbers together"
+ )
+ public void handleAdd(
+ @Parameter(ArgumentType.NATURAL) int a,
+ @Parameter(ArgumentType.NATURAL) int b)
+ {
+ Acidictive.reply(source, target, channel, Integer.toString(a + b));
+ }
+
+ @SubCommand(
+ value = "SUBTRACT",
+ description = "Subtracts 2 numbers"
+ )
+ public void handleSubtract(
+ @Parameter(ArgumentType.NATURAL) int a,
+ @Parameter(ArgumentType.NATURAL) int b)
+ {
+ Acidictive.reply(source, target, channel, Integer.toString(a - b));
+ }
+}
--- /dev/null
+/*
+ * See LICENSE.md
+ */
+package net.rizon.acid.commands.special;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.function.Predicate;
+import java.util.stream.Stream;
+
+/**
+ *
+ * @author Orillion <orillion@rizon.net>
+ */
+public class Reflection
+{
+ /**
+ * Get a stream of instances from all the subtypes of the specified
+ * interface or superclass.
+ *
+ * @param interfaceType super class or interface of which this would find
+ * the subtype of.
+ * @param types types of the parameters for the constructor.
+ * @param args arguments of the constructor.
+ * @param packageName name of the package containing the interfaceType and
+ * sub types.
+ * @param <T> type of the object.
+ *
+ * @return stream of the objects.
+ */
+ public static <T> Stream<T> getInstancesOf(Class interfaceType, Class[] types, Object[] args, String packageName)
+ {
+ return getSubTypesOf(interfaceType, packageName)
+ .map(c -> initializeObject(c, types, args));
+ }
+
+ /**
+ * Initialize an object of the given class type with parameters for the
+ * constructor.
+ *
+ * @param classType type of the object.
+ * @param types types of the parameters for the constructor.
+ * @param args arguments of the constructor.
+ * @param <T> type of the object.
+ *
+ * @return Instantiation of the class type.
+ */
+ public static <T> T initializeObject(Class classType, Class[] types, Object[] args)
+ {
+ try
+ {
+ return (T) classType.getDeclaredConstructor(types).newInstance(args);
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ throw new RuntimeException(
+ String.format("Couldn't initialize object '%s' with the specified args", classType.getName())
+ );
+ }
+
+ /**
+ * Initialize an object of the given class type.
+ *
+ * @param classType type of the object.
+ * @param <T> type of the object.
+ *
+ * @return Instantiation of the class type.
+ */
+ public static <T> T initializeObject(Class classType)
+ {
+ try
+ {
+ return (T) classType.getDeclaredConstructor().newInstance();
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ throw new RuntimeException(String.format("Couldn't initialize object '%s'", classType.getName()));
+ }
+
+ /**
+ * Get stream of sub types of the specified super class of interface.
+ *
+ * @param interfaceType super class or interface of which this would find
+ * the subtype of.
+ * @param packageName name of the package containing the interfaceType and
+ * sub types.
+ *
+ * @return
+ */
+ public static Stream<Class> getSubTypesOf(Class interfaceType, String packageName)
+ {
+ try
+ {
+ return Stream.of(getClasses(packageName))
+ .filter(c -> !c.isInterface())
+ .filter(c -> !Modifier.isAbstract(c.getModifiers()))
+ .filter(interfaceType::isAssignableFrom);
+ }
+ catch (IOException | ClassNotFoundException c)
+ {
+ c.printStackTrace();
+ }
+ return Stream.empty();
+ }
+
+ /**
+ * Filter class types on the private static field 'TAG'
+ *
+ * @param classType type of class holding the TAG field.
+ * @param predicate function to apply when testing the class type.
+ *
+ * @return true if predicate return true on the given class type.
+ */
+ public static boolean filterOnTag(Class classType, Predicate<String> predicate)
+ {
+ try
+ {
+ Field f = classType.getDeclaredField("TAG");
+ f.setAccessible(true);
+ return predicate.test((String) f.get(null));
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ return false;
+ }
+
+ /**
+ * Scans all classes accessible from the context class loader which belong
+ * to the given package and sub packages.
+ *
+ * @param packageName The base package
+ *
+ * @return The classes
+ *
+ * @throws ClassNotFoundException
+ * @throws IOException
+ */
+ private static Class[] getClasses(String packageName) throws ClassNotFoundException, IOException
+ {
+ ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
+ assert classLoader != null;
+ String path = packageName.replace('.', '/');
+ Enumeration<URL> resources = classLoader.getResources(path);
+ List<File> dirs = new ArrayList<>();
+ while (resources.hasMoreElements())
+ {
+ URL resource = resources.nextElement();
+ dirs.add(new File(resource.getFile()));
+ }
+ List<Class> classes = new ArrayList<>();
+ for (File directory : dirs)
+ {
+ classes.addAll(findClasses(directory, packageName));
+ }
+ return classes.toArray(new Class[classes.size()]);
+ }
+
+ /**
+ * Recursive method used to find all classes in a given directory and sub
+ * directories.
+ *
+ * @param directory The base directory
+ * @param packageName The package name for classes found inside the base
+ * directory
+ *
+ * @return The classes
+ *
+ * @throws ClassNotFoundException
+ */
+ private static List<Class> findClasses(File directory, String packageName) throws ClassNotFoundException
+ {
+ List<Class> classes = new ArrayList<>();
+ if (!directory.exists())
+ {
+ return classes;
+ }
+ File[] files = directory.listFiles();
+ for (File file : files)
+ {
+ if (file.isDirectory())
+ {
+ assert !file.getName().contains(".");
+ classes.addAll(findClasses(file, packageName + "." + file.getName()));
+ }
+ else if (file.getName().endsWith(".class"))
+ {
+ classes.add(Class.forName(packageName + '.' + file.getName().substring(0, file.getName().length() - 6)));
+ }
+ }
+ return classes;
+ }
+}
--- /dev/null
+/*
+ * See LICENSE.md
+ */
+package net.rizon.acid.commands.special;
+
+import net.rizon.acid.core.AcidUser;
+import net.rizon.acid.core.Channel;
+import net.rizon.acid.core.User;
+
+/**
+ *
+ * @author Orillion <orillion@rizon.net>
+ */
+public class SpecialCommand
+{
+ protected final User source;
+ protected final AcidUser target;
+ protected final Channel channel;
+
+ protected SpecialCommand(final User source, final AcidUser target, final Channel channel)
+ {
+ this.source = source;
+ this.target = target;
+ this.channel = channel;
+ }
+}
--- /dev/null
+/*
+ * See LICENSE.md
+ */
+package net.rizon.acid.commands.special;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.stream.Stream;
+import net.rizon.acid.arguments.ExpiryArgument;
+import net.rizon.acid.commands.annotations.Command;
+import net.rizon.acid.commands.annotations.Parameter;
+import net.rizon.acid.commands.annotations.SubCommand;
+import net.rizon.acid.core.AcidUser;
+import net.rizon.acid.core.Channel;
+import net.rizon.acid.core.User;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ *
+ * @author Orillion <orillion@rizon.net>
+ */
+public class SpecialCommandParser
+{
+ private static final Logger LOGGER = LoggerFactory.getLogger(SpecialCommandParser.class);
+ private static Class[] constructorClasses =
+ {
+ User.class,
+ AcidUser.class,
+ Channel.class
+ };
+
+ public static void parse(String command, String[] args, User source, AcidUser target, Channel channel)
+ {
+ Stream<Class> classes = Reflection.getSubTypesOf(SpecialCommand.class, "net.rizon.acid.commands.special");
+
+ classes
+ .filter(cls -> matchCommandName(cls, command))
+ .findFirst()
+ .ifPresent(cls -> executeCommand(cls, args, source, target, channel));
+ }
+
+ private static void executeCommand(Class cls, String[] args, User source, AcidUser target, Channel channel)
+ {
+ Object[] arguments =
+ {
+ source,
+ target,
+ channel
+ };
+
+ String subCommand = args[0];
+
+ SpecialCommand command = Reflection.initializeObject(cls, constructorClasses, arguments);
+
+ for (Method m : command.getClass().getDeclaredMethods())
+ {
+ boolean validMethod = true;
+ Annotation annotation = m.getAnnotation(SubCommand.class);
+
+ if (annotation == null)
+ {
+ continue;
+ }
+
+ SubCommand sc = (SubCommand) annotation;
+
+ if (!sc.value().equalsIgnoreCase(subCommand))
+ {
+ continue;
+ }
+
+ Annotation[][] parameterAnnotations = m.getParameterAnnotations();
+ Class[] parameterClasses = m.getParameterTypes();
+
+ Object[] params = new Object[m.getParameterCount()];
+
+ for (int i = 0; i < parameterClasses.length && validMethod == true; i++)
+ {
+ Annotation[] annotations = parameterAnnotations[i];
+
+ Parameter param = null;
+
+ for (Annotation a : annotations)
+ {
+ if (a instanceof Parameter)
+ {
+ param = (Parameter) a;
+ break;
+ }
+ }
+
+ if (param == null)
+ {
+ validMethod = false;
+ break;
+ }
+
+ Object o = constructObjectForParameter(param, args[i + 1]);
+
+ if (o == null)
+ {
+ validMethod = false;
+ break;
+ }
+
+ params[i] = o;
+ }
+
+ if (!validMethod)
+ {
+ continue;
+ }
+
+ try
+ {
+ m.invoke(command, params);
+ break;
+ }
+ catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex)
+ {
+ LOGGER.debug("Error while invoking method which looked okay", ex);
+ }
+ }
+ }
+
+ private static Object constructObjectForParameter(Parameter parameter, String argument)
+ {
+ Object param = null;
+ int value;
+
+ if (!argument.startsWith(parameter.prefix()))
+ {
+ return null;
+ }
+
+ String noPrefixArgument = argument.substring(parameter.prefix().length());
+
+ try
+ {
+ switch (parameter.value())
+ {
+ case DURATION:
+ param = ExpiryArgument.parse(noPrefixArgument);
+ break;
+ case NATURAL:
+ param = Integer.parseInt(noPrefixArgument);
+ break;
+ case NATURAL_NEGATIVE:
+ value = Integer.parseInt(noPrefixArgument);
+ if (value <= 0)
+ {
+ param = value;
+ }
+ break;
+ case NATURAL_POSITIVE:
+ value = Integer.parseInt(noPrefixArgument);
+ if (value >= 0)
+ {
+ param = value;
+ }
+ break;
+ case STRING:
+ param = noPrefixArgument;
+ break;
+ default:
+ param = null;
+ break;
+ }
+ }
+ catch (Throwable t)
+ {
+ // Ignore
+ param = null;
+ }
+
+ return param;
+ }
+
+ private static boolean matchCommandName(Class cls, String name)
+ {
+ Annotation annotation = cls.getAnnotation(Command.class);
+
+ if (annotation == null)
+ {
+ return false;
+ }
+
+ Command c = (Command) annotation;
+
+ return c.value().equalsIgnoreCase(name);
+ }
+}
import java.util.List;
import java.util.concurrent.TimeUnit;
import net.rizon.acid.capab.QuitStorm;
+import net.rizon.acid.commands.special.SpecialCommandParser;
import net.rizon.acid.conf.Client;
import net.rizon.acid.conf.Config;
import net.rizon.acid.conf.PluginDesc;
confCommand = to.findConfCommand(command, null);
}
+ SpecialCommandParser.parse(command, args, x, to, c);
+
if (confCommand == null)
return;