diff --git a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/BaseAbilityBot.java b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/BaseAbilityBot.java index aab22a2e..301f2eab 100644 --- a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/BaseAbilityBot.java +++ b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/bot/BaseAbilityBot.java @@ -12,20 +12,21 @@ import org.telegram.abilitybots.api.objects.*; import org.telegram.abilitybots.api.sender.DefaultSender; import org.telegram.abilitybots.api.sender.MessageSender; import org.telegram.abilitybots.api.sender.SilentSender; +import org.telegram.abilitybots.api.util.AbilityExtension; import org.telegram.abilitybots.api.util.AbilityUtils; import org.telegram.abilitybots.api.util.Pair; import org.telegram.abilitybots.api.util.Trio; +import org.telegram.telegrambots.bots.DefaultAbsSender; +import org.telegram.telegrambots.bots.DefaultBotOptions; +import org.telegram.telegrambots.bots.TelegramLongPollingBot; import org.telegram.telegrambots.meta.api.methods.GetFile; import org.telegram.telegrambots.meta.api.methods.groupadministration.GetChatAdministrators; import org.telegram.telegrambots.meta.api.methods.send.SendDocument; import org.telegram.telegrambots.meta.api.objects.Message; +import org.telegram.telegrambots.meta.api.objects.Update; import org.telegram.telegrambots.meta.api.objects.User; -import org.telegram.telegrambots.bots.DefaultAbsSender; -import org.telegram.telegrambots.bots.DefaultBotOptions; -import org.telegram.telegrambots.bots.TelegramLongPollingBot; import org.telegram.telegrambots.meta.exceptions.TelegramApiException; import org.telegram.telegrambots.meta.logging.BotLogger; -import org.telegram.telegrambots.meta.api.objects.Update; import java.io.File; import java.io.FileNotFoundException; @@ -36,7 +37,9 @@ import java.lang.reflect.Method; import java.util.*; import java.util.Map.Entry; import java.util.function.BiFunction; +import java.util.function.Function; import java.util.function.Predicate; +import java.util.stream.Collectors; import java.util.stream.Stream; import static com.google.common.base.Strings.isNullOrEmpty; @@ -93,7 +96,7 @@ import static org.telegram.abilitybots.api.util.AbilityUtils.*; * @author Abbas Abou Daya */ @SuppressWarnings({"ConfusingArgumentToVarargsMethod", "UnusedReturnValue", "WeakerAccess", "unused", "ConstantConditions"}) -public abstract class BaseAbilityBot extends DefaultAbsSender { +public abstract class BaseAbilityBot extends DefaultAbsSender implements AbilityExtension { private static final String TAG = BaseAbilityBot.class.getSimpleName(); // DB objects @@ -611,22 +614,38 @@ public abstract class BaseAbilityBot extends DefaultAbsSender { */ private void registerAbilities() { try { - abilities = stream(this.getClass().getMethods()) - .filter(method -> method.getReturnType().equals(Ability.class)) - .map(this::returnAbility) + // Collect all classes that implement AbilityExtension declared in the bot + List extensions = stream(getClass().getMethods()) + .filter(checkReturnType(AbilityExtension.class)) + .map(returnExtension(this)) + .collect(Collectors.toList()); + + // Add the bot itself as it is an AbilityExtension + extensions.add(this); + + // Extract all abilities from every single extension instance + abilities = extensions.stream() + .flatMap(ext -> stream(ext.getClass().getMethods()) + .filter(checkReturnType(Ability.class)) + .map(returnAbility(ext))) + // Abilities are immutable, build it respectively .collect(ImmutableMap::builder, (b, a) -> b.put(a.name(), a), (b1, b2) -> b1.putAll(b2.build())) .build(); - Stream methodReplies = stream(this.getClass().getMethods()) - .filter(method -> method.getReturnType().equals(Reply.class)) - .map(this::returnReply); + // Extract all replies from every single extension instance + Stream extensionReplies = extensions.stream() + .flatMap(ext -> stream(ext.getClass().getMethods()) + .filter(checkReturnType(Reply.class)) + .map(returnReply(ext))); + // Replies can be standalone or attached to abilities, fetch those too Stream abilityReplies = abilities.values().stream() .flatMap(ability -> ability.replies().stream()); - replies = Stream.concat(methodReplies, abilityReplies).collect( + // Now create the replies registry (list) + replies = Stream.concat(abilityReplies, extensionReplies).collect( ImmutableList::builder, Builder::add, (b1, b2) -> b1.addAll(b2.build())) @@ -635,37 +654,65 @@ public abstract class BaseAbilityBot extends DefaultAbsSender { BotLogger.error(TAG, "Duplicate names found while registering abilities. Make sure that the abilities declared don't clash with the reserved ones.", e); throw propagate(e); } + } + /** + * @param clazz the type to be tested + * @return a predicate testing the return type of the method corresponding to the class parameter + */ + private Predicate checkReturnType(Class clazz) { + return method -> clazz.isAssignableFrom(method.getReturnType()); + } + + /** + * Invokes the method and retrieves its return {@link Reply}. + * + * @param obj an bot or extension that this method is invoked with + * @return a {@link Function} which returns the {@link Reply} returned by the given method + */ + private Function returnExtension(Object obj) { + return method -> { + try { + return (AbilityExtension) method.invoke(obj); + } catch (IllegalAccessException | InvocationTargetException e) { + BotLogger.error("Could not add ability extension", TAG, e); + throw propagate(e); + } + }; } /** * Invokes the method and retrieves its return {@link Ability}. * - * @param method a method that returns an ability - * @return the ability returned by the method + * @param obj an bot or extension that this method is invoked with + * @return a {@link Function} which returns the {@link Ability} returned by the given method */ - private Ability returnAbility(Method method) { - try { - return (Ability) method.invoke(this); - } catch (IllegalAccessException | InvocationTargetException e) { - BotLogger.error("Could not add ability", TAG, e); - throw propagate(e); - } + private Function returnAbility(Object obj) { + return method -> { + try { + return (Ability) method.invoke(obj); + } catch (IllegalAccessException | InvocationTargetException e) { + BotLogger.error("Could not add ability", TAG, e); + throw propagate(e); + } + }; } /** - * Invokes the method and retrieves its returned Reply. + * Invokes the method and retrieves its return {@link Reply}. * - * @param method a method that returns a reply - * @return the reply returned by the method + * @param obj an bot or extension that this method is invoked with + * @return a {@link Function} which returns the {@link Reply} returned by the given method */ - private Reply returnReply(Method method) { - try { - return (Reply) method.invoke(this); - } catch (IllegalAccessException | InvocationTargetException e) { - BotLogger.error("Could not add reply", TAG, e); - throw propagate(e); - } + private Function returnReply(Object obj) { + return method -> { + try { + return (Reply) method.invoke(obj); + } catch (IllegalAccessException | InvocationTargetException e) { + BotLogger.error("Could not add reply", TAG, e); + throw propagate(e); + } + }; } private void postConsumption(Pair pair) { diff --git a/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/util/AbilityExtension.java b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/util/AbilityExtension.java new file mode 100644 index 00000000..357ccebe --- /dev/null +++ b/telegrambots-abilities/src/main/java/org/telegram/abilitybots/api/util/AbilityExtension.java @@ -0,0 +1,7 @@ +package org.telegram.abilitybots.api.util; + +/** + * An interface to mark a class as an extension. Similar to when a method returns an Ability, it is added to the bot, a method which returns an AbilityExtension will add all Abilities or Replies from this Extension to the bot. + */ +public interface AbilityExtension { +} diff --git a/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/ExtensionTest.java b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/ExtensionTest.java new file mode 100644 index 00000000..43bb8da5 --- /dev/null +++ b/telegrambots-abilities/src/test/java/org/telegram/abilitybots/api/bot/ExtensionTest.java @@ -0,0 +1,89 @@ +package org.telegram.abilitybots.api.bot; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.telegram.abilitybots.api.objects.Ability; +import org.telegram.abilitybots.api.util.AbilityExtension; + +import java.io.IOException; + +import static junit.framework.TestCase.assertTrue; +import static org.telegram.abilitybots.api.db.MapDBContext.offlineInstance; +import static org.telegram.abilitybots.api.objects.Locality.ALL; +import static org.telegram.abilitybots.api.objects.Privacy.PUBLIC; + +public class ExtensionTest { + private ExtensionUsingBot bot; + + @Before + public void setUp() { + bot = new ExtensionUsingBot(); + } + + @Test + public void methodReturningAbilities() { + assertTrue("Failed to find Ability in directly declared in root extension/bot", hasAbilityNamed("direct")); + assertTrue("Failed to find Ability in directly declared in extension returned by method returning the AbilityExtension class", hasAbilityNamed("returningSuperClass0abc")); + assertTrue("Failed to find Ability in directly declared in extension returned by method returning the AbilityExtension subclass", hasAbilityNamed("returningSubClass0abc")); + } + + @After + public void tearDown() throws IOException { + bot.db.clear(); + bot.db.close(); + } + + private boolean hasAbilityNamed(String name) { + return bot.abilities().values().stream().map(Ability::name).anyMatch(name::equals); + } + + public static class ExtensionUsingBot extends AbilityBot { + public ExtensionUsingBot() { + super("", "", offlineInstance("testing")); + } + + @Override + public int creatorId() { + return 0; + } + + public AbilityBotExtension methodReturningExtensionSubClass() { + return new AbilityBotExtension("returningSubClass"); + } + + public AbilityExtension methodReturningExtensionSuperClass() { + return new AbilityBotExtension("returningSuperClass"); + } + + public Ability methodReturningAbility() { + return Ability.builder() + .name("direct") + .info("Test ability") + .locality(ALL) + .privacy(PUBLIC) + .action(messageContext -> { + }) + .build(); + } + } + + public static class AbilityBotExtension implements AbilityExtension { + private String name; + + public AbilityBotExtension(String name) { + this.name = name; + } + + public Ability abc() { + return Ability.builder() + .name(name + "0abc") + .info("Test ability") + .locality(ALL) + .privacy(PUBLIC) + .action(ctx -> { + }) + .build(); + } + } +} \ No newline at end of file