]> jfr.im git - irc/rizon/acid.git/commitdiff
Add example for reflection based argument parser origin/acid/command
authorOrillion <redacted>
Sun, 19 Nov 2017 15:19:07 +0000 (16:19 +0100)
committerOrillion <redacted>
Sun, 19 Nov 2017 15:19:07 +0000 (16:19 +0100)
acid/src/main/java/net/rizon/acid/commands/annotations/ArgumentType.java [new file with mode: 0644]
acid/src/main/java/net/rizon/acid/commands/annotations/Command.java [new file with mode: 0644]
acid/src/main/java/net/rizon/acid/commands/annotations/Parameter.java [new file with mode: 0644]
acid/src/main/java/net/rizon/acid/commands/annotations/SubCommand.java [new file with mode: 0644]
acid/src/main/java/net/rizon/acid/commands/special/Calculator.java [new file with mode: 0644]
acid/src/main/java/net/rizon/acid/commands/special/Reflection.java [new file with mode: 0644]
acid/src/main/java/net/rizon/acid/commands/special/SpecialCommand.java [new file with mode: 0644]
acid/src/main/java/net/rizon/acid/commands/special/SpecialCommandParser.java [new file with mode: 0644]
acid/src/main/java/net/rizon/acid/core/Acidictive.java

diff --git a/acid/src/main/java/net/rizon/acid/commands/annotations/ArgumentType.java b/acid/src/main/java/net/rizon/acid/commands/annotations/ArgumentType.java
new file mode 100644 (file)
index 0000000..a9b7f2c
--- /dev/null
@@ -0,0 +1,52 @@
+/*
+ * 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,
+}
diff --git a/acid/src/main/java/net/rizon/acid/commands/annotations/Command.java b/acid/src/main/java/net/rizon/acid/commands/annotations/Command.java
new file mode 100644 (file)
index 0000000..4a4560d
--- /dev/null
@@ -0,0 +1,20 @@
+/*
+ * 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();
+}
diff --git a/acid/src/main/java/net/rizon/acid/commands/annotations/Parameter.java b/acid/src/main/java/net/rizon/acid/commands/annotations/Parameter.java
new file mode 100644 (file)
index 0000000..b3836a3
--- /dev/null
@@ -0,0 +1,28 @@
+/*
+ * 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 "";
+}
diff --git a/acid/src/main/java/net/rizon/acid/commands/annotations/SubCommand.java b/acid/src/main/java/net/rizon/acid/commands/annotations/SubCommand.java
new file mode 100644 (file)
index 0000000..3ca8dce
--- /dev/null
@@ -0,0 +1,22 @@
+/*
+ * 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";
+}
diff --git a/acid/src/main/java/net/rizon/acid/commands/special/Calculator.java b/acid/src/main/java/net/rizon/acid/commands/special/Calculator.java
new file mode 100644 (file)
index 0000000..c419527
--- /dev/null
@@ -0,0 +1,48 @@
+/*
+ * 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));
+       }
+}
diff --git a/acid/src/main/java/net/rizon/acid/commands/special/Reflection.java b/acid/src/main/java/net/rizon/acid/commands/special/Reflection.java
new file mode 100644 (file)
index 0000000..ec9ef0e
--- /dev/null
@@ -0,0 +1,204 @@
+/*
+ * 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;
+       }
+}
diff --git a/acid/src/main/java/net/rizon/acid/commands/special/SpecialCommand.java b/acid/src/main/java/net/rizon/acid/commands/special/SpecialCommand.java
new file mode 100644 (file)
index 0000000..3346da9
--- /dev/null
@@ -0,0 +1,26 @@
+/*
+ * 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;
+       }
+}
diff --git a/acid/src/main/java/net/rizon/acid/commands/special/SpecialCommandParser.java b/acid/src/main/java/net/rizon/acid/commands/special/SpecialCommandParser.java
new file mode 100644 (file)
index 0000000..dbc41c5
--- /dev/null
@@ -0,0 +1,194 @@
+/*
+ * 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);
+       }
+}
index 1342c7c594a6f0f2acca42e9b0a45547c7ef3644..328cad5bf65f0a49d56658144b05c11b7371381e 100644 (file)
@@ -12,6 +12,7 @@ import java.util.Date;
 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;
@@ -337,6 +338,8 @@ public class Acidictive extends AcidCore
                                confCommand = to.findConfCommand(command, null);
                        }
 
+                       SpecialCommandParser.parse(command, args, x, to, c);
+
                        if (confCommand == null)
                                return;