From 45c3cd89e9229e0c64ffdbfbabb39b211ee36e81 Mon Sep 17 00:00:00 2001 From: Simon Zaidelman Date: Wed, 14 Oct 2020 17:37:19 +0500 Subject: [PATCH 01/15] refactor: preparing first problem to be done --- src/main/java/org/tbplusc/app/validator/WordDistancePair.java | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 src/main/java/org/tbplusc/app/validator/WordDistancePair.java diff --git a/src/main/java/org/tbplusc/app/validator/WordDistancePair.java b/src/main/java/org/tbplusc/app/validator/WordDistancePair.java new file mode 100644 index 0000000..b8f19a3 --- /dev/null +++ b/src/main/java/org/tbplusc/app/validator/WordDistancePair.java @@ -0,0 +1,4 @@ +package org.tbplusc.app.validator; + +public class WordDistancePair { +} From 3845b27d4168da9edcf989ca93b556fd312d7aaa Mon Sep 17 00:00:00 2001 From: Simon Zaidelman Date: Wed, 14 Oct 2020 18:06:10 +0500 Subject: [PATCH 02/15] refactor: Validator cleaned --- src/main/java/org/tbplusc/app/Main.java | 11 +++++ .../org/tbplusc/app/validator/Validator.java | 44 +++++++++---------- .../app/validator/WordDistancePair.java | 7 +++ .../app/validatortests/ValidatorTests.java | 6 ++- 4 files changed, 44 insertions(+), 24 deletions(-) diff --git a/src/main/java/org/tbplusc/app/Main.java b/src/main/java/org/tbplusc/app/Main.java index de179cb..1f5014c 100644 --- a/src/main/java/org/tbplusc/app/Main.java +++ b/src/main/java/org/tbplusc/app/Main.java @@ -7,9 +7,15 @@ import org.slf4j.LoggerFactory; import org.tbplusc.app.discordinteraction.DefaultChatState; import org.tbplusc.app.discordinteraction.MessageHandler; +import org.tbplusc.app.validator.Validator; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; public class Main { private static final Logger logger = LoggerFactory.getLogger(Main.class); + private static final Validator validator = new Validator(getHeroList()); public static void main(String[] args) { logger.info("Application started"); @@ -26,4 +32,9 @@ public static void main(String[] args) { .subscribe(messageHandler::handleMessage); gateway.on(DisconnectEvent.class).blockLast(); } + + public static List getHeroList() { + // In future this ArrayList should be replaced with getting HeroList via http://hotsapi.net/api/v1/heroes + return new ArrayList<>(Arrays.asList("Hero1", "Hero2", "Hero3")); + } } diff --git a/src/main/java/org/tbplusc/app/validator/Validator.java b/src/main/java/org/tbplusc/app/validator/Validator.java index 17fee31..4e80422 100644 --- a/src/main/java/org/tbplusc/app/validator/Validator.java +++ b/src/main/java/org/tbplusc/app/validator/Validator.java @@ -2,41 +2,41 @@ import org.apache.commons.text.similarity.LevenshteinDistance; -import java.util.Arrays; import java.util.Comparator; +import java.util.List; public class Validator { - private final String[] charactersNames; + private final List charactersNames; private final LevenshteinDistance levenshteinComparer = new LevenshteinDistance(); - private Comparator getComporator(String s){ - return Comparator.comparing(t -> levenshteinComparer.apply(t.toLowerCase(), s)); + private Comparator getComporator(String s) { + return Comparator.comparing(t -> applyComparing(t, s)); } - public Validator(){ - charactersNames = new String[]{"Mei", "Deathwing", "Qhira", "Anduin", "Imperius", "Orphea", "Mal'Ganis", "Mephisto", - "Whitemane", "Yrel", "Deckard", "Fenix", "Maiev", "Blaze", "Hanzo", "Alexstrasza", "Junkrat", - "Ana", "Kel'Thuzad", "Garrosh", "Stukov", "Malthael", "D.Va", "Genji", "Cassia", "Probius", - "Lucio", "Valeera", "Zul'jin", "Ragnaros", "Varian", "Samuro", "Zarya", "Alarak", "Auriel", - "Gul'dan", "Medivh", "Chromie", "Tracer", "Dehaka", "Xul", "Li-Ming", "Greymane", "Lunara", "Cho", - "Gall", "Artanis", "Lt. Morales", "Rexxar", "Kharazim", "Leoric", "The Butcher", "Johanna", "Kael'thas", - "Sylvanas", "The Lost Vikings", "Thrall", "Jaina", "Azmodan", "Anub'arak", "Chen", "Rehgar", "Zagara", - "Murky", "Brightwing", "Li Li", "Tychus", "Abathur", "Arthas", "Diablo", "Illidan", "Kerrigan", - "Malfurion", "Muradin", "Nazeebo", "Nova", "Raynor", "Sgt. Hammer", "Sonya", "Tyrael", "Tyrande", - "Uther", "Valla", "Zeratul", "E.T.C.", "Falstad", "Gazlowe", "Stitches", "Tassadar"}; + private int applyComparing(String s1, String s2) { + return levenshteinComparer.apply(s1, s2); } - public String getClosestToInput(String userInput){ - return Arrays.stream(charactersNames) - .min(getComporator(userInput)) + public Validator(List heroes) throws IllegalArgumentException { + if (heroes == null || heroes.isEmpty()) + throw new IllegalArgumentException(); + charactersNames = heroes; + } + + public String getClosestToInput(String userInput) { + var loweredInput = userInput.toLowerCase(); + return charactersNames.stream() + .min(getComporator(loweredInput)) .get(); } - public String[] getSomeCosestToInput(String userInput, int length){ - return Arrays.stream(charactersNames) - .sorted(getComporator(userInput)) + public WordDistancePair[] getSomeCosestToInput(String userInput, int length) { + var loweredInput = userInput.toLowerCase(); + return charactersNames.stream() + .map(s -> new WordDistancePair(s, applyComparing(s, loweredInput))) + .sorted(Comparator.comparingInt(s -> s.distance)) .limit(length) - .toArray(String[]::new); + .toArray(WordDistancePair[]::new); } } diff --git a/src/main/java/org/tbplusc/app/validator/WordDistancePair.java b/src/main/java/org/tbplusc/app/validator/WordDistancePair.java index b8f19a3..1bdb8fe 100644 --- a/src/main/java/org/tbplusc/app/validator/WordDistancePair.java +++ b/src/main/java/org/tbplusc/app/validator/WordDistancePair.java @@ -1,4 +1,11 @@ package org.tbplusc.app.validator; public class WordDistancePair { + public final String word; + public final int distance; + + public WordDistancePair(String word, int distance) { + this.distance = distance; + this.word = word; + } } diff --git a/src/test/java/org/tbplusc/app/validatortests/ValidatorTests.java b/src/test/java/org/tbplusc/app/validatortests/ValidatorTests.java index a1de20d..4a12075 100644 --- a/src/test/java/org/tbplusc/app/validatortests/ValidatorTests.java +++ b/src/test/java/org/tbplusc/app/validatortests/ValidatorTests.java @@ -1,13 +1,15 @@ package org.tbplusc.app.validatortests; import org.junit.Test; -import org.tbplusc.app.Main; import org.tbplusc.app.validator.Validator; +import java.util.ArrayList; +import java.util.Arrays; + import static org.junit.Assert.assertEquals; public class ValidatorTests { - protected Validator validator = new Validator(); + protected Validator validator = new Validator(new ArrayList<>(Arrays.asList("Mei", "Rexxar", "Abathur", "Chen", "Cho", "Raynor"))); @Test public void testRetursCorrectAnswerOnCompleteInput() { From d2f2c572e50c920788b95e7e5bacf28167553e73 Mon Sep 17 00:00:00 2001 From: Simon Zaidelman Date: Wed, 14 Oct 2020 18:21:06 +0500 Subject: [PATCH 03/15] refactor: Validator is not connected with outer world now --- .../app/discordinteraction/DefaultChatState.java | 12 ++++++------ .../java/org/tbplusc/app/validator/Validator.java | 5 +++-- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/tbplusc/app/discordinteraction/DefaultChatState.java b/src/main/java/org/tbplusc/app/discordinteraction/DefaultChatState.java index 5e388d9..c21ea87 100644 --- a/src/main/java/org/tbplusc/app/discordinteraction/DefaultChatState.java +++ b/src/main/java/org/tbplusc/app/discordinteraction/DefaultChatState.java @@ -42,12 +42,12 @@ public static void registerDefaultCommands() { .block(); return new DefaultChatState(); }); - registerCommand("build", (args, message) -> { - final var validator = new Validator(); - logger.info("Typed hero name: {}", args); - final var possibleHeroNames = validator.getSomeCosestToInput(args, 10); - return new HeroSelectionState(Arrays.asList(possibleHeroNames.clone()), message); - }); +// registerCommand("build", (args, message) -> { +// final var validator = new Validator(); +// logger.info("Typed hero name: {}", args); +// final var possibleHeroNames = validator.getSomeCosestToInput(args, 10); +// return new HeroSelectionState(Arrays.asList(possibleHeroNames.clone()), message); +// }); } public DefaultChatState() { diff --git a/src/main/java/org/tbplusc/app/validator/Validator.java b/src/main/java/org/tbplusc/app/validator/Validator.java index 4e80422..268d4e5 100644 --- a/src/main/java/org/tbplusc/app/validator/Validator.java +++ b/src/main/java/org/tbplusc/app/validator/Validator.java @@ -11,7 +11,7 @@ public class Validator { private final LevenshteinDistance levenshteinComparer = new LevenshteinDistance(); private Comparator getComporator(String s) { - return Comparator.comparing(t -> applyComparing(t, s)); + return Comparator.comparing(t -> applyComparing(t.toLowerCase(), s)); } private int applyComparing(String s1, String s2) { @@ -19,8 +19,9 @@ private int applyComparing(String s1, String s2) { } public Validator(List heroes) throws IllegalArgumentException { - if (heroes == null || heroes.isEmpty()) + if (heroes == null || heroes.isEmpty()) { throw new IllegalArgumentException(); + } charactersNames = heroes; } From 1c5478c500be8f84e79be4895b18aafe66f9454b Mon Sep 17 00:00:00 2001 From: Pam Date: Wed, 14 Oct 2020 18:51:23 +0500 Subject: [PATCH 04/15] aux: removed abbreviation check in interfaces --- checkstyle.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/checkstyle.xml b/checkstyle.xml index 53692a3..59b01c1 100644 --- a/checkstyle.xml +++ b/checkstyle.xml @@ -220,7 +220,7 @@ - From 905d146ff06181dea29e464179de63e1380bb819 Mon Sep 17 00:00:00 2001 From: Pam Date: Fri, 16 Oct 2020 20:16:50 +0500 Subject: [PATCH 05/15] refactor: restructured talenthelper and icyveinsparser --- .../IcyVeinsBuild.java => HeroBuild.java} | 6 +-- ...cyVeinsHeroBuilds.java => HeroBuilds.java} | 10 ++-- .../tbplusc/app/talenthelper/HeroConsts.java | 2 +- .../icyveinsparser/IcyVeinsParser.java | 51 ------------------- .../parsers/IIcyVeinsDataProvider.java | 7 +++ .../talenthelper/parsers/ITalentProvider.java | 8 +++ .../parsers/IcyVeinsRemoteDataProvider.java | 13 +++++ .../parsers/IcyVeinsTalentProvider.java | 42 +++++++++++++++ 8 files changed, 79 insertions(+), 60 deletions(-) rename src/main/java/org/tbplusc/app/talenthelper/{icyveinsparser/IcyVeinsBuild.java => HeroBuild.java} (79%) rename src/main/java/org/tbplusc/app/talenthelper/{icyveinsparser/IcyVeinsHeroBuilds.java => HeroBuilds.java} (60%) delete mode 100644 src/main/java/org/tbplusc/app/talenthelper/icyveinsparser/IcyVeinsParser.java create mode 100644 src/main/java/org/tbplusc/app/talenthelper/parsers/IIcyVeinsDataProvider.java create mode 100644 src/main/java/org/tbplusc/app/talenthelper/parsers/ITalentProvider.java create mode 100644 src/main/java/org/tbplusc/app/talenthelper/parsers/IcyVeinsRemoteDataProvider.java create mode 100644 src/main/java/org/tbplusc/app/talenthelper/parsers/IcyVeinsTalentProvider.java diff --git a/src/main/java/org/tbplusc/app/talenthelper/icyveinsparser/IcyVeinsBuild.java b/src/main/java/org/tbplusc/app/talenthelper/HeroBuild.java similarity index 79% rename from src/main/java/org/tbplusc/app/talenthelper/icyveinsparser/IcyVeinsBuild.java rename to src/main/java/org/tbplusc/app/talenthelper/HeroBuild.java index 5185350..a8c5895 100644 --- a/src/main/java/org/tbplusc/app/talenthelper/icyveinsparser/IcyVeinsBuild.java +++ b/src/main/java/org/tbplusc/app/talenthelper/HeroBuild.java @@ -1,14 +1,14 @@ -package org.tbplusc.app.talenthelper.icyveinsparser; +package org.tbplusc.app.talenthelper; import java.util.ArrayList; import java.util.List; -public class IcyVeinsBuild { +public class HeroBuild { private final String name; private final String description; private final List talents; - public IcyVeinsBuild(String name, String description, List talents) { + public HeroBuild(String name, String description, List talents) { this.name = name; this.description = description; this.talents = new ArrayList<>(talents); diff --git a/src/main/java/org/tbplusc/app/talenthelper/icyveinsparser/IcyVeinsHeroBuilds.java b/src/main/java/org/tbplusc/app/talenthelper/HeroBuilds.java similarity index 60% rename from src/main/java/org/tbplusc/app/talenthelper/icyveinsparser/IcyVeinsHeroBuilds.java rename to src/main/java/org/tbplusc/app/talenthelper/HeroBuilds.java index 2915a65..bba90cb 100644 --- a/src/main/java/org/tbplusc/app/talenthelper/icyveinsparser/IcyVeinsHeroBuilds.java +++ b/src/main/java/org/tbplusc/app/talenthelper/HeroBuilds.java @@ -1,13 +1,13 @@ -package org.tbplusc.app.talenthelper.icyveinsparser; +package org.tbplusc.app.talenthelper; import java.util.ArrayList; import java.util.List; -public class IcyVeinsHeroBuilds { +public class HeroBuilds { private final String heroName; - private final List builds; + private final List builds; - public IcyVeinsHeroBuilds(String heroName, List builds) { + public HeroBuilds(String heroName, List builds) { this.heroName = heroName; this.builds = new ArrayList<>(builds); } @@ -16,7 +16,7 @@ public String getHeroName() { return heroName; } - public List getBuilds() { + public List getBuilds() { return new ArrayList<>(builds); } diff --git a/src/main/java/org/tbplusc/app/talenthelper/HeroConsts.java b/src/main/java/org/tbplusc/app/talenthelper/HeroConsts.java index 7399270..a07d267 100644 --- a/src/main/java/org/tbplusc/app/talenthelper/HeroConsts.java +++ b/src/main/java/org/tbplusc/app/talenthelper/HeroConsts.java @@ -5,5 +5,5 @@ private HeroConsts() throws IllegalAccessException { throw new IllegalAccessException("Utility class"); } - public final static int[] HERO_TALENTS_LEVELS = new int[] {1, 4, 7, 10, 13, 16, 20}; + public static final int[] HERO_TALENTS_LEVELS = new int[] {1, 4, 7, 10, 13, 16, 20}; } diff --git a/src/main/java/org/tbplusc/app/talenthelper/icyveinsparser/IcyVeinsParser.java b/src/main/java/org/tbplusc/app/talenthelper/icyveinsparser/IcyVeinsParser.java deleted file mode 100644 index a06a125..0000000 --- a/src/main/java/org/tbplusc/app/talenthelper/icyveinsparser/IcyVeinsParser.java +++ /dev/null @@ -1,51 +0,0 @@ -package org.tbplusc.app.talenthelper.icyveinsparser; - -import java.io.IOException; -import java.util.List; -import java.util.ArrayList; - -import org.tbplusc.app.util.HttpGetter; - -public class IcyVeinsParser { - private static final String ADDRESS_PREFIX = "https://www.icy-veins.com/heroes/"; - private static final String ADDRESS_POSTFIX = "-talents"; - - private IcyVeinsParser() { - throw new IllegalStateException("Utility class"); - } - - public static IcyVeinsHeroBuilds getBuildsByHeroName(String heroName) throws IOException { - var talentPage = getDocumentFromHeroName(heroName); - return new IcyVeinsHeroBuilds(heroName, getBuildsListFromDocument(talentPage)); - } - - public static List getBuildsListFromDocument(org.jsoup.nodes.Document document) { - var outputBuilds = new ArrayList(); - var buildElems = document.getElementsByClass("heroes_builds").first() - .getElementsByClass("heroes_build"); - - for (var build : buildElems) { - var buildName = build.getElementsByClass("toc_no_parsing").first().text(); - var buildDesc = build.getElementsByClass("heroes_build_text").first().text(); - var talents = new ArrayList(); - build.getElementsByClass("heroes_build_talent_tier").forEach(tier -> { - var talent = tier.selectFirst("img").attr("alt"); - talent = talent.substring(0, talent.lastIndexOf(" ")); - talents.add(talent); - }); - outputBuilds.add(new IcyVeinsBuild(buildName, buildDesc, talents)); - } - - return outputBuilds; - - } - - public static String normalizeHeroName(String heroName) { - return heroName.replace(".", "").replace(" ", "-").replace("'", "").toLowerCase(); - } - - private static org.jsoup.nodes.Document getDocumentFromHeroName(String heroName) - throws IOException { - return HttpGetter.getDocumentFromUrl(ADDRESS_PREFIX + heroName + ADDRESS_POSTFIX); - } -} diff --git a/src/main/java/org/tbplusc/app/talenthelper/parsers/IIcyVeinsDataProvider.java b/src/main/java/org/tbplusc/app/talenthelper/parsers/IIcyVeinsDataProvider.java new file mode 100644 index 0000000..a955d40 --- /dev/null +++ b/src/main/java/org/tbplusc/app/talenthelper/parsers/IIcyVeinsDataProvider.java @@ -0,0 +1,7 @@ +package org.tbplusc.app.talenthelper.parsers; + +import java.io.IOException; + +public interface IIcyVeinsDataProvider { + public org.jsoup.nodes.Document getDocument(String heroName) throws IOException; +} diff --git a/src/main/java/org/tbplusc/app/talenthelper/parsers/ITalentProvider.java b/src/main/java/org/tbplusc/app/talenthelper/parsers/ITalentProvider.java new file mode 100644 index 0000000..c44828f --- /dev/null +++ b/src/main/java/org/tbplusc/app/talenthelper/parsers/ITalentProvider.java @@ -0,0 +1,8 @@ +package org.tbplusc.app.talenthelper.parsers; + +import java.io.IOException; +import org.tbplusc.app.talenthelper.HeroBuilds; + +public interface ITalentProvider { + public HeroBuilds getBuilds(String heroName) throws IOException; +} diff --git a/src/main/java/org/tbplusc/app/talenthelper/parsers/IcyVeinsRemoteDataProvider.java b/src/main/java/org/tbplusc/app/talenthelper/parsers/IcyVeinsRemoteDataProvider.java new file mode 100644 index 0000000..0ca38ee --- /dev/null +++ b/src/main/java/org/tbplusc/app/talenthelper/parsers/IcyVeinsRemoteDataProvider.java @@ -0,0 +1,13 @@ +package org.tbplusc.app.talenthelper.parsers; + +import java.io.IOException; +import org.tbplusc.app.util.HttpGetter; + +public class IcyVeinsRemoteDataProvider implements IIcyVeinsDataProvider { + private static final String ADDRESS_PREFIX = "https://www.icy-veins.com/heroes/"; + private static final String ADDRESS_POSTFIX = "-talents"; + + public org.jsoup.nodes.Document getDocument(String heroName) throws IOException { + return HttpGetter.getDocumentFromUrl(ADDRESS_PREFIX + heroName + ADDRESS_POSTFIX); + } +} diff --git a/src/main/java/org/tbplusc/app/talenthelper/parsers/IcyVeinsTalentProvider.java b/src/main/java/org/tbplusc/app/talenthelper/parsers/IcyVeinsTalentProvider.java new file mode 100644 index 0000000..2bb6054 --- /dev/null +++ b/src/main/java/org/tbplusc/app/talenthelper/parsers/IcyVeinsTalentProvider.java @@ -0,0 +1,42 @@ +package org.tbplusc.app.talenthelper.parsers; + +import java.io.IOException; +import java.util.ArrayList; + +import org.tbplusc.app.talenthelper.HeroBuild; +import org.tbplusc.app.talenthelper.HeroBuilds; + +public class IcyVeinsTalentProvider implements ITalentProvider { + private final IIcyVeinsDataProvider dataProvider; + + public IcyVeinsTalentProvider(IIcyVeinsDataProvider dataProvider) { + this.dataProvider = dataProvider; + } + + public HeroBuilds getBuilds(String heroName) throws IOException { + var document = this.dataProvider.getDocument(heroName); + var hero = document.getElementsByTag("h1").first().text(); + var outputBuilds = new ArrayList(); + var buildElems = document.getElementsByClass("heroes_builds").first() + .getElementsByClass("heroes_build"); + + for (var build : buildElems) { + var buildName = build.getElementsByClass("toc_no_parsing").first().text(); + var buildDesc = build.getElementsByClass("heroes_build_text").first().text(); + var talents = new ArrayList(); + build.getElementsByClass("heroes_build_talent_tier").forEach(tier -> { + var talent = tier.selectFirst("img").attr("alt"); + talent = talent.substring(0, talent.lastIndexOf(" ")); + talents.add(talent); + }); + outputBuilds.add(new HeroBuild(buildName, buildDesc, talents)); + } + + return new HeroBuilds(hero, outputBuilds); + + } + + private String normalizeHeroName(String heroName) { + return heroName.replace(".", "").replace(" ", "-").replace("'", "").toLowerCase(); + } +} From 4fae96c8a5ade7278b018b99dfcef099ca1d3148 Mon Sep 17 00:00:00 2001 From: Pam Date: Fri, 16 Oct 2020 20:18:46 +0500 Subject: [PATCH 06/15] test: adapted tests for refactored icyveinsparser --- .../IcyVeinsParserTests.java | 39 ++++++++++++------- .../html/icyveins_example_build.html | 1 + .../icyveins_example_multiple_builds.html | 1 + 3 files changed, 27 insertions(+), 14 deletions(-) diff --git a/src/test/java/org/tbplusc/app/talenthelpertests/icyveinsparsertests/IcyVeinsParserTests.java b/src/test/java/org/tbplusc/app/talenthelpertests/icyveinsparsertests/IcyVeinsParserTests.java index 5e580ed..ce2bd12 100644 --- a/src/test/java/org/tbplusc/app/talenthelpertests/icyveinsparsertests/IcyVeinsParserTests.java +++ b/src/test/java/org/tbplusc/app/talenthelpertests/icyveinsparsertests/IcyVeinsParserTests.java @@ -1,24 +1,27 @@ package org.tbplusc.app.talenthelpertests.icyveinsparsertests; import org.junit.Test; +import org.tbplusc.app.talenthelper.parsers.IIcyVeinsDataProvider; +import org.tbplusc.app.talenthelper.parsers.IcyVeinsTalentProvider; import org.junit.Before; +import org.mockito.Mockito; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; -import java.io.File; import java.io.IOException; import org.jsoup.Jsoup; -import org.tbplusc.app.talenthelper.icyveinsparser.IcyVeinsParser; - public class IcyVeinsParserTests { private static final String TEST_HTML_PATH = "html"; private org.jsoup.nodes.Document docWithOneBuild; private org.jsoup.nodes.Document docWithMultipleBuilds; + private IcyVeinsTalentProvider parser; @Before public void setUp() throws IOException { + var mock = Mockito.mock(IIcyVeinsDataProvider.class); + parser = new IcyVeinsTalentProvider(mock); docWithOneBuild = Jsoup.parse(new String(getClass().getClassLoader() .getResourceAsStream(TEST_HTML_PATH + "/icyveins_example_build.html") .readAllBytes())); @@ -26,33 +29,41 @@ public void setUp() throws IOException { .getResourceAsStream( TEST_HTML_PATH + "/icyveins_example_multiple_builds.html") .readAllBytes())); + Mockito.when(mock.getDocument("testheroone")).thenReturn(docWithOneBuild); + Mockito.when(mock.getDocument("testherotwo")).thenReturn(docWithMultipleBuilds); } @Test - public void testCorrectBuildNumbers() { - var oneBuild = IcyVeinsParser.getBuildsListFromDocument(docWithOneBuild); - var twoBuilds = IcyVeinsParser.getBuildsListFromDocument(docWithMultipleBuilds); - assertEquals(1, oneBuild.size()); - assertEquals(2, twoBuilds.size()); + public void testCorrectBuildSize() throws IOException { + var oneBuild = parser.getBuilds("testheroone"); + var twoBuilds = parser.getBuilds("testherotwo"); + assertEquals(1, oneBuild.getBuilds().size()); + assertEquals(2, twoBuilds.getBuilds().size()); } @Test - public void testCorrectBuildName() { - var buildToTest = IcyVeinsParser.getBuildsListFromDocument(docWithMultipleBuilds).get(1); + public void testCorrectBuildName() throws IOException { + var buildToTest = parser.getBuilds("testherotwo").getBuilds().get(1); assertEquals("Situational Test Build", buildToTest.getName()); } @Test - public void testCorrectOrderOfTalents() { - var buildToTest = IcyVeinsParser.getBuildsListFromDocument(docWithOneBuild).get(0); + public void testCorrectOrderOfTalents() throws IOException { + var buildToTest = parser.getBuilds("testheroone").getBuilds().get(0); assertArrayEquals(new String[] {"Single Talent 1", "Single Talent 2", "Single Talent 3", "Single Talent 4", "Single Talent 5", "Single Talent 6", "Single Talent 7"}, buildToTest.getTalents().toArray()); } @Test - public void testCorrectDescription() { - var buildToTest = IcyVeinsParser.getBuildsListFromDocument(docWithMultipleBuilds).get(0); + public void testCorrectDescription() throws IOException { + var buildToTest = parser.getBuilds("testherotwo").getBuilds().get(0); assertEquals("Generic multi-purpose build for test hero.", buildToTest.getDescription()); } + + @Test + public void testCorrectHeroName() throws IOException { + var heroBuildsToTest = parser.getBuilds("testheroone"); + assertEquals("Hero with one build", heroBuildsToTest.getHeroName()); + } } diff --git a/src/test/resources/html/icyveins_example_build.html b/src/test/resources/html/icyveins_example_build.html index 2a14dc1..7d32165 100644 --- a/src/test/resources/html/icyveins_example_build.html +++ b/src/test/resources/html/icyveins_example_build.html @@ -2,6 +2,7 @@ +

Hero with one build

diff --git a/src/test/resources/html/icyveins_example_multiple_builds.html b/src/test/resources/html/icyveins_example_multiple_builds.html index 9242e9b..c008e23 100644 --- a/src/test/resources/html/icyveins_example_multiple_builds.html +++ b/src/test/resources/html/icyveins_example_multiple_builds.html @@ -2,6 +2,7 @@ +

Hero with two builds

From 6d48273fa8438997617a3243acf4197bd04a70c3 Mon Sep 17 00:00:00 2001 From: ninele7 Date: Fri, 16 Oct 2020 22:19:00 +0500 Subject: [PATCH 07/15] refactor: changed commands implementations based on recent refactors --- src/main/java/org/tbplusc/app/Main.java | 36 ++++++++++++++----- .../discordinteraction/DefaultChatState.java | 31 ++++++++-------- .../HeroSelectionState.java | 32 ++++++++++------- .../discordinteraction/MessageHandler.java | 6 ++-- .../parsers/IcyVeinsTalentProvider.java | 5 +-- .../org/tbplusc/app/validator/Validator.java | 4 +-- 6 files changed, 73 insertions(+), 41 deletions(-) diff --git a/src/main/java/org/tbplusc/app/Main.java b/src/main/java/org/tbplusc/app/Main.java index 1f5014c..72aaf2d 100644 --- a/src/main/java/org/tbplusc/app/Main.java +++ b/src/main/java/org/tbplusc/app/Main.java @@ -7,22 +7,30 @@ import org.slf4j.LoggerFactory; import org.tbplusc.app.discordinteraction.DefaultChatState; import org.tbplusc.app.discordinteraction.MessageHandler; +import org.tbplusc.app.talenthelper.parsers.ITalentProvider; +import org.tbplusc.app.talenthelper.parsers.IcyVeinsRemoteDataProvider; +import org.tbplusc.app.talenthelper.parsers.IcyVeinsTalentProvider; +import org.tbplusc.app.util.JsonDeserializer; import org.tbplusc.app.validator.Validator; -import java.util.ArrayList; +import java.io.IOException; import java.util.Arrays; -import java.util.List; public class Main { private static final Logger logger = LoggerFactory.getLogger(Main.class); - private static final Validator validator = new Validator(getHeroList()); public static void main(String[] args) { logger.info("Application started"); final var token = System.getenv("DISCORD_TOKEN"); final var client = DiscordClient.create(token); - DefaultChatState.registerDefaultCommands(); - final var messageHandler = new MessageHandler(); + final MessageHandler messageHandler; + try { + messageHandler = createMessageHandler(); + } catch (IOException e) { + logger.error("Can't create message handler", e); + return; + } + logger.info("Message handler is ready"); final var gateway = client.login().block(); if (gateway == null) { logger.error("Can't connect to discord"); @@ -33,8 +41,20 @@ public static void main(String[] args) { gateway.on(DisconnectEvent.class).blockLast(); } - public static List getHeroList() { - // In future this ArrayList should be replaced with getting HeroList via http://hotsapi.net/api/v1/heroes - return new ArrayList<>(Arrays.asList("Hero1", "Hero2", "Hero3")); + private static Validator createValidator() throws IOException { + final var heroes = JsonDeserializer + .deserializeHeroList(org.tbplusc.app.util.HttpGetter + .getBodyFromUrl("https://hotsapi.net/api/v1/heroes")); + return new Validator(Arrays.asList(heroes.stream().map((hero) -> hero.name) + .toArray(String[]::new))); + } + + private static ITalentProvider createIcyVeinsTalentProvider() { + return new IcyVeinsTalentProvider(new IcyVeinsRemoteDataProvider()); + } + + private static MessageHandler createMessageHandler() throws IOException { + DefaultChatState.registerDefaultCommands(createValidator(), createIcyVeinsTalentProvider()); + return new MessageHandler(); } } diff --git a/src/main/java/org/tbplusc/app/discordinteraction/DefaultChatState.java b/src/main/java/org/tbplusc/app/discordinteraction/DefaultChatState.java index c21ea87..75f6d94 100644 --- a/src/main/java/org/tbplusc/app/discordinteraction/DefaultChatState.java +++ b/src/main/java/org/tbplusc/app/discordinteraction/DefaultChatState.java @@ -3,13 +3,11 @@ import discord4j.core.object.entity.Message; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.tbplusc.app.talenthelper.HeroConsts; -import org.tbplusc.app.talenthelper.icyveinsparser.IcyVeinsParser; +import org.tbplusc.app.talenthelper.parsers.ITalentProvider; import org.tbplusc.app.validator.Validator; import static org.tbplusc.app.discordinteraction.DiscordUtil.getChannelForMessage; -import java.io.IOException; import java.util.Arrays; import java.util.HashMap; import java.util.Map; @@ -27,12 +25,11 @@ public static void registerCommand(String name, BiFunction { final var channel = getChannelForMessage(message); - channel.createMessage( - args.equals("") ? "Не могу заэхоть пустую строку" : args - ).block(); + channel.createMessage(args.equals("") ? "Не могу заэхоть пустую строку" : args).block(); return new DefaultChatState(); }); registerCommand("authors", (args, message) -> { @@ -42,20 +39,24 @@ public static void registerDefaultCommands() { .block(); return new DefaultChatState(); }); -// registerCommand("build", (args, message) -> { -// final var validator = new Validator(); -// logger.info("Typed hero name: {}", args); -// final var possibleHeroNames = validator.getSomeCosestToInput(args, 10); -// return new HeroSelectionState(Arrays.asList(possibleHeroNames.clone()), message); -// }); + registerCommand("build", (args, message) -> { + logger.info("Typed hero name: {}", args); + final var possibleHeroNames = validator.getSomeClosestToInput(args, 10); + if (possibleHeroNames[0].distance == 0) { + HeroSelectionState.showHeroBuildToDiscord(message, possibleHeroNames[0].word, + talentProvider); + return new DefaultChatState(); + } + return new HeroSelectionState(Arrays.asList(possibleHeroNames.clone()), message, + talentProvider); + }); } public DefaultChatState() { prefix = System.getenv("DISCORD_PREFIX"); } - @Override - public ChatState handleMessage(Message message) { + @Override public ChatState handleMessage(Message message) { final var content = message.getContent(); if (!content.startsWith(prefix)) { return this; diff --git a/src/main/java/org/tbplusc/app/discordinteraction/HeroSelectionState.java b/src/main/java/org/tbplusc/app/discordinteraction/HeroSelectionState.java index 5d72109..dd5bd9c 100644 --- a/src/main/java/org/tbplusc/app/discordinteraction/HeroSelectionState.java +++ b/src/main/java/org/tbplusc/app/discordinteraction/HeroSelectionState.java @@ -4,7 +4,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.tbplusc.app.talenthelper.HeroConsts; -import org.tbplusc.app.talenthelper.icyveinsparser.IcyVeinsParser; +import org.tbplusc.app.talenthelper.parsers.ITalentProvider; +import org.tbplusc.app.talenthelper.parsers.IcyVeinsTalentProvider; +import org.tbplusc.app.validator.WordDistancePair; import java.io.IOException; import java.util.List; @@ -14,32 +16,38 @@ public class HeroSelectionState implements ChatState { private static final Logger logger = LoggerFactory.getLogger(HeroSelectionState.class); - private final List availableHeroes; + private final List availableHeroes; private final Message message; + private final ITalentProvider talentProvider; - public HeroSelectionState(List availableHeroes, Message message) { + public HeroSelectionState(List availableHeroes, Message message, + ITalentProvider talentProvider) { this.availableHeroes = availableHeroes; this.message = message; + this.talentProvider = talentProvider; showInitMessage(); } - public void showInitMessage() { + private void showInitMessage() { final var channel = getChannelForMessage(message); final var heroes = new StringBuilder(); for (var i = 0; i < availableHeroes.size(); i++) { - heroes.append(String.format("%3d. %s \n", i + 1, availableHeroes.get(i))); + heroes.append(String.format("%3d. %s \n", i + 1, availableHeroes.get(i).word)); } channel.createMessage(String.format("Choice hero (type number): \n ```md\n%s```", heroes)) .block(); } - private static void showHeroBuildToDiscord(Message message, String heroName) { - logger.info("Normalized hero name: {}", heroName); + public static void showHeroBuildToDiscord(Message message, String heroName, + ITalentProvider talentProvider) { + final var normalizedHeroName = IcyVeinsTalentProvider.normalizeHeroName(heroName); + logger.info("Normalized hero name: {}", normalizedHeroName); try { - final var builds = IcyVeinsParser.getBuildsByHeroName(heroName); + final var builds = talentProvider.getBuilds(normalizedHeroName); final var channel = getChannelForMessage(message); - channel.createMessage(String.format("Selected hero is **%s**", heroName)).block(); + channel.createMessage(String.format("Selected hero is **%s**", normalizedHeroName)) + .block(); builds.getBuilds().stream().map((build) -> { final var talents = new StringBuilder(); for (var i = 0; i < build.getTalents().size(); i++) { @@ -56,12 +64,12 @@ private static void showHeroBuildToDiscord(Message message, String heroName) { @Override public ChatState handleMessage(Message message) { var number = Integer.parseInt(message.getContent()); - if (number <= 1 || number >= 10) { + if (number < 1 || number >= 10) { getChannelForMessage(message).createMessage("Wrong number").block(); return this; } - final var heroName = IcyVeinsParser.normalizeHeroName(availableHeroes.get(number - 1)); - showHeroBuildToDiscord(message, heroName); + final var heroName = availableHeroes.get(number - 1).word; + showHeroBuildToDiscord(message, heroName, talentProvider); return new DefaultChatState(); } } diff --git a/src/main/java/org/tbplusc/app/discordinteraction/MessageHandler.java b/src/main/java/org/tbplusc/app/discordinteraction/MessageHandler.java index 538b6fa..a8c5392 100644 --- a/src/main/java/org/tbplusc/app/discordinteraction/MessageHandler.java +++ b/src/main/java/org/tbplusc/app/discordinteraction/MessageHandler.java @@ -8,9 +8,11 @@ public class MessageHandler { private final Map states = new HashMap<>(); + public MessageHandler() { + } + public void handleMessage(Message message) { - final var thread = new Thread(() -> processMessage(message), - "Discord command processor"); + final var thread = new Thread(() -> processMessage(message), "Discord command processor"); thread.start(); } diff --git a/src/main/java/org/tbplusc/app/talenthelper/parsers/IcyVeinsTalentProvider.java b/src/main/java/org/tbplusc/app/talenthelper/parsers/IcyVeinsTalentProvider.java index 2bb6054..c677bfa 100644 --- a/src/main/java/org/tbplusc/app/talenthelper/parsers/IcyVeinsTalentProvider.java +++ b/src/main/java/org/tbplusc/app/talenthelper/parsers/IcyVeinsTalentProvider.java @@ -36,7 +36,8 @@ public HeroBuilds getBuilds(String heroName) throws IOException { } - private String normalizeHeroName(String heroName) { - return heroName.replace(".", "").replace(" ", "-").replace("'", "").toLowerCase(); + public static String normalizeHeroName(String heroName) { + return heroName.replace(".", "").replace(" ", "-").replace("'", "").replace("ú", "u") + .toLowerCase(); } } diff --git a/src/main/java/org/tbplusc/app/validator/Validator.java b/src/main/java/org/tbplusc/app/validator/Validator.java index 268d4e5..0f9167b 100644 --- a/src/main/java/org/tbplusc/app/validator/Validator.java +++ b/src/main/java/org/tbplusc/app/validator/Validator.java @@ -32,10 +32,10 @@ public String getClosestToInput(String userInput) { .get(); } - public WordDistancePair[] getSomeCosestToInput(String userInput, int length) { + public WordDistancePair[] getSomeClosestToInput(String userInput, int length) { var loweredInput = userInput.toLowerCase(); return charactersNames.stream() - .map(s -> new WordDistancePair(s, applyComparing(s, loweredInput))) + .map(s -> new WordDistancePair(s, applyComparing(s.toLowerCase(), loweredInput))) .sorted(Comparator.comparingInt(s -> s.distance)) .limit(length) .toArray(WordDistancePair[]::new); From 61ace8786187e954d0271ac7d732e14560dd0421 Mon Sep 17 00:00:00 2001 From: ninele7 Date: Fri, 16 Oct 2020 22:37:31 +0500 Subject: [PATCH 08/15] fix: now using fixed thread pool to process message instead of creating new threads --- .../org/tbplusc/app/discordinteraction/MessageHandler.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/tbplusc/app/discordinteraction/MessageHandler.java b/src/main/java/org/tbplusc/app/discordinteraction/MessageHandler.java index a8c5392..745cc32 100644 --- a/src/main/java/org/tbplusc/app/discordinteraction/MessageHandler.java +++ b/src/main/java/org/tbplusc/app/discordinteraction/MessageHandler.java @@ -4,16 +4,19 @@ import java.util.HashMap; import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; public class MessageHandler { private final Map states = new HashMap<>(); + private final ExecutorService threadPool; public MessageHandler() { + this.threadPool = Executors.newFixedThreadPool(24); } public void handleMessage(Message message) { - final var thread = new Thread(() -> processMessage(message), "Discord command processor"); - thread.start(); + threadPool.execute(() -> processMessage(message)); } private void processMessage(Message message) { From edb6df61b6078844dfe2a146dcddec975896871b Mon Sep 17 00:00:00 2001 From: ninele7 Date: Sat, 17 Oct 2020 00:03:34 +0500 Subject: [PATCH 09/15] test: added not working test for MessageHandler --- .../HeroSelectionState.java | 2 +- .../MessageHandlerTests.java | 28 +++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 src/test/java/org/tbplusc/app/discordinteraction/MessageHandlerTests.java diff --git a/src/main/java/org/tbplusc/app/discordinteraction/HeroSelectionState.java b/src/main/java/org/tbplusc/app/discordinteraction/HeroSelectionState.java index dd5bd9c..417b071 100644 --- a/src/main/java/org/tbplusc/app/discordinteraction/HeroSelectionState.java +++ b/src/main/java/org/tbplusc/app/discordinteraction/HeroSelectionState.java @@ -35,7 +35,7 @@ private void showInitMessage() { for (var i = 0; i < availableHeroes.size(); i++) { heroes.append(String.format("%3d. %s \n", i + 1, availableHeroes.get(i).word)); } - channel.createMessage(String.format("Choice hero (type number): \n ```md\n%s```", heroes)) + channel.createMessage(String.format("Choose hero (type number): \n ```md\n%s```", heroes)) .block(); } diff --git a/src/test/java/org/tbplusc/app/discordinteraction/MessageHandlerTests.java b/src/test/java/org/tbplusc/app/discordinteraction/MessageHandlerTests.java new file mode 100644 index 0000000..56d15a9 --- /dev/null +++ b/src/test/java/org/tbplusc/app/discordinteraction/MessageHandlerTests.java @@ -0,0 +1,28 @@ +package org.tbplusc.app.discordinteraction; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.util.concurrent.atomic.AtomicBoolean; + +public class MessageHandlerTests { + private MessageHandler messageHandler; + + @Before + public void setUp() { + messageHandler = new MessageHandler(); + } + + @Test + public void testHandlingCommand() { + var called = new AtomicBoolean(false); + DefaultChatState.registerCommand("test", (args, message) -> { + called.set(true); + return new DefaultChatState(); + }); + + + Assert.assertTrue(called.get()); + } +} From f8d1f733b9552ba7e95b34a3a3fc265094ae653e Mon Sep 17 00:00:00 2001 From: ninele7 Date: Sat, 17 Oct 2020 00:21:50 +0500 Subject: [PATCH 10/15] refactor: added wrapper around discord message --- src/main/java/org/tbplusc/app/Main.java | 9 ++--- .../app/discordinteraction/ChatState.java | 2 +- .../discordinteraction/DefaultChatState.java | 15 +++----- .../HeroSelectionState.java | 20 +++++------ .../discordinteraction/MessageHandler.java | 16 ++------- .../WrappedDiscordMessage.java | 36 +++++++++++++++++++ .../discordinteraction/WrappedMessage.java | 9 +++++ 7 files changed, 67 insertions(+), 40 deletions(-) create mode 100644 src/main/java/org/tbplusc/app/discordinteraction/WrappedDiscordMessage.java create mode 100644 src/main/java/org/tbplusc/app/discordinteraction/WrappedMessage.java diff --git a/src/main/java/org/tbplusc/app/Main.java b/src/main/java/org/tbplusc/app/Main.java index 72aaf2d..49f37b8 100644 --- a/src/main/java/org/tbplusc/app/Main.java +++ b/src/main/java/org/tbplusc/app/Main.java @@ -7,6 +7,7 @@ import org.slf4j.LoggerFactory; import org.tbplusc.app.discordinteraction.DefaultChatState; import org.tbplusc.app.discordinteraction.MessageHandler; +import org.tbplusc.app.discordinteraction.WrappedDiscordMessage; import org.tbplusc.app.talenthelper.parsers.ITalentProvider; import org.tbplusc.app.talenthelper.parsers.IcyVeinsRemoteDataProvider; import org.tbplusc.app.talenthelper.parsers.IcyVeinsTalentProvider; @@ -37,14 +38,14 @@ public static void main(String[] args) { return; } gateway.on(MessageCreateEvent.class).map(MessageCreateEvent::getMessage) - .subscribe(messageHandler::handleMessage); + .subscribe(message -> messageHandler + .handleMessage(new WrappedDiscordMessage(message))); gateway.on(DisconnectEvent.class).blockLast(); } private static Validator createValidator() throws IOException { - final var heroes = JsonDeserializer - .deserializeHeroList(org.tbplusc.app.util.HttpGetter - .getBodyFromUrl("https://hotsapi.net/api/v1/heroes")); + final var heroes = JsonDeserializer.deserializeHeroList(org.tbplusc.app.util.HttpGetter + .getBodyFromUrl("https://hotsapi.net/api/v1/heroes")); return new Validator(Arrays.asList(heroes.stream().map((hero) -> hero.name) .toArray(String[]::new))); } diff --git a/src/main/java/org/tbplusc/app/discordinteraction/ChatState.java b/src/main/java/org/tbplusc/app/discordinteraction/ChatState.java index 5c9fcca..f725b04 100644 --- a/src/main/java/org/tbplusc/app/discordinteraction/ChatState.java +++ b/src/main/java/org/tbplusc/app/discordinteraction/ChatState.java @@ -3,5 +3,5 @@ import discord4j.core.object.entity.Message; public interface ChatState { - ChatState handleMessage(Message message); + ChatState handleMessage(WrappedMessage message); } diff --git a/src/main/java/org/tbplusc/app/discordinteraction/DefaultChatState.java b/src/main/java/org/tbplusc/app/discordinteraction/DefaultChatState.java index 75f6d94..9363307 100644 --- a/src/main/java/org/tbplusc/app/discordinteraction/DefaultChatState.java +++ b/src/main/java/org/tbplusc/app/discordinteraction/DefaultChatState.java @@ -1,6 +1,5 @@ package org.tbplusc.app.discordinteraction; -import discord4j.core.object.entity.Message; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.tbplusc.app.talenthelper.parsers.ITalentProvider; @@ -18,25 +17,21 @@ public class DefaultChatState implements ChatState { private final String prefix; - private static final Map> commands = + private static final Map> commands = new HashMap<>(); - public static void registerCommand(String name, BiFunction action) { + public static void registerCommand(String name, BiFunction action) { commands.put(name, action); } public static void registerDefaultCommands(Validator validator, ITalentProvider talentProvider) { registerCommand("echo", (args, message) -> { - final var channel = getChannelForMessage(message); - channel.createMessage(args.equals("") ? "Не могу заэхоть пустую строку" : args).block(); + message.respond(args.equals("") ? "Не могу заэхоть пустую строку" : args); return new DefaultChatState(); }); registerCommand("authors", (args, message) -> { - final var channel = getChannelForMessage(message); - channel.createMessage( - "Код писали: Александ Жмышенко, Олег Белахахлий и Semen Зайдельман") - .block(); + message.respond("Код писали: Александ Жмышенко, Олег Белахахлий и Semen Зайдельман"); return new DefaultChatState(); }); registerCommand("build", (args, message) -> { @@ -56,7 +51,7 @@ public DefaultChatState() { prefix = System.getenv("DISCORD_PREFIX"); } - @Override public ChatState handleMessage(Message message) { + @Override public ChatState handleMessage(WrappedMessage message) { final var content = message.getContent(); if (!content.startsWith(prefix)) { return this; diff --git a/src/main/java/org/tbplusc/app/discordinteraction/HeroSelectionState.java b/src/main/java/org/tbplusc/app/discordinteraction/HeroSelectionState.java index 417b071..64729b8 100644 --- a/src/main/java/org/tbplusc/app/discordinteraction/HeroSelectionState.java +++ b/src/main/java/org/tbplusc/app/discordinteraction/HeroSelectionState.java @@ -18,10 +18,10 @@ public class HeroSelectionState implements ChatState { private final List availableHeroes; - private final Message message; + private final WrappedMessage message; private final ITalentProvider talentProvider; - public HeroSelectionState(List availableHeroes, Message message, + public HeroSelectionState(List availableHeroes, WrappedMessage message, ITalentProvider talentProvider) { this.availableHeroes = availableHeroes; this.message = message; @@ -30,24 +30,20 @@ public HeroSelectionState(List availableHeroes, Message messag } private void showInitMessage() { - final var channel = getChannelForMessage(message); final var heroes = new StringBuilder(); for (var i = 0; i < availableHeroes.size(); i++) { heroes.append(String.format("%3d. %s \n", i + 1, availableHeroes.get(i).word)); } - channel.createMessage(String.format("Choose hero (type number): \n ```md\n%s```", heroes)) - .block(); + message.respond(String.format("Choose hero (type number): \n ```md\n%s```", heroes)); } - public static void showHeroBuildToDiscord(Message message, String heroName, + public static void showHeroBuildToDiscord(WrappedMessage message, String heroName, ITalentProvider talentProvider) { final var normalizedHeroName = IcyVeinsTalentProvider.normalizeHeroName(heroName); logger.info("Normalized hero name: {}", normalizedHeroName); try { final var builds = talentProvider.getBuilds(normalizedHeroName); - final var channel = getChannelForMessage(message); - channel.createMessage(String.format("Selected hero is **%s**", normalizedHeroName)) - .block(); + message.respond(String.format("Selected hero is **%s**", normalizedHeroName)); builds.getBuilds().stream().map((build) -> { final var talents = new StringBuilder(); for (var i = 0; i < build.getTalents().size(); i++) { @@ -56,16 +52,16 @@ public static void showHeroBuildToDiscord(Message message, String heroName, } return String.format("**%s**: ```md\n%s```**Description:** %s", build.getName(), talents, build.getDescription()); - }).forEach(build -> channel.createMessage(build).block()); + }).forEach(message::respond); } catch (IOException e) { logger.error("Hero was not loaded", e); } } - @Override public ChatState handleMessage(Message message) { + @Override public ChatState handleMessage(WrappedMessage message) { var number = Integer.parseInt(message.getContent()); if (number < 1 || number >= 10) { - getChannelForMessage(message).createMessage("Wrong number").block(); + message.respond("Wrong number"); return this; } final var heroName = availableHeroes.get(number - 1).word; diff --git a/src/main/java/org/tbplusc/app/discordinteraction/MessageHandler.java b/src/main/java/org/tbplusc/app/discordinteraction/MessageHandler.java index 745cc32..eacbc66 100644 --- a/src/main/java/org/tbplusc/app/discordinteraction/MessageHandler.java +++ b/src/main/java/org/tbplusc/app/discordinteraction/MessageHandler.java @@ -15,22 +15,12 @@ public MessageHandler() { this.threadPool = Executors.newFixedThreadPool(24); } - public void handleMessage(Message message) { + public void handleMessage(WrappedMessage message) { threadPool.execute(() -> processMessage(message)); } - private void processMessage(Message message) { - final var authorOptional = message.getAuthor(); - if (authorOptional.isEmpty()) { - throw new NullPointerException("Message had no author"); - } - final var authorId = authorOptional.get().getId(); - final var channel = message.getChannel().block(); - if (channel == null) { - throw new NullPointerException("No channel for the message"); - } - final var channelId = channel.getId(); - final var key = authorId.asString() + channelId.asString(); + private void processMessage(WrappedMessage message) { + final var key = message.getContextKey(); if (!states.containsKey(key)) { states.put(key, new DefaultChatState()); } diff --git a/src/main/java/org/tbplusc/app/discordinteraction/WrappedDiscordMessage.java b/src/main/java/org/tbplusc/app/discordinteraction/WrappedDiscordMessage.java new file mode 100644 index 0000000..e09a368 --- /dev/null +++ b/src/main/java/org/tbplusc/app/discordinteraction/WrappedDiscordMessage.java @@ -0,0 +1,36 @@ +package org.tbplusc.app.discordinteraction; + +import discord4j.core.object.entity.Message; +import static org.tbplusc.app.discordinteraction.DiscordUtil.getChannelForMessage; + +public class WrappedDiscordMessage implements WrappedMessage { + private final Message message; + + public WrappedDiscordMessage(Message message) { + this.message = message; + } + + @Override + public String getContextKey() { + final var authorOptional = message.getAuthor(); + if (authorOptional.isEmpty()) { + throw new NullPointerException("Message had no author"); + } + final var authorId = authorOptional.get().getId(); + final var channel = message.getChannel().block(); + if (channel == null) { + throw new NullPointerException("No channel for the message"); + } + final var channelId = channel.getId(); + return authorId.asString() + channelId.asString(); + } + + @Override public String getContent() { + return message.getContent(); + } + + @Override public void respond(String text) { + var channel = getChannelForMessage(message); + channel.createMessage(text).block(); + } +} diff --git a/src/main/java/org/tbplusc/app/discordinteraction/WrappedMessage.java b/src/main/java/org/tbplusc/app/discordinteraction/WrappedMessage.java new file mode 100644 index 0000000..738669e --- /dev/null +++ b/src/main/java/org/tbplusc/app/discordinteraction/WrappedMessage.java @@ -0,0 +1,9 @@ +package org.tbplusc.app.discordinteraction; + +public interface WrappedMessage { + String getContextKey(); + + String getContent(); + + void respond(String text); +} From aaed7e2d446bc17009ed12d9cfe51257f107d024 Mon Sep 17 00:00:00 2001 From: ninele7 Date: Sat, 17 Oct 2020 00:50:59 +0500 Subject: [PATCH 11/15] refactor: added wrapper above environmental variables; test: create first test for discord features --- src/main/java/org/tbplusc/app/Main.java | 9 ++++- .../discordinteraction/DefaultChatState.java | 3 +- .../discordinteraction/MessageHandler.java | 6 ++-- .../java/org/tbplusc/app/util/EnvWrapper.java | 16 +++++++++ .../MessageHandlerTests.java | 36 ++++++++++++++++++- 5 files changed, 64 insertions(+), 6 deletions(-) create mode 100644 src/main/java/org/tbplusc/app/util/EnvWrapper.java diff --git a/src/main/java/org/tbplusc/app/Main.java b/src/main/java/org/tbplusc/app/Main.java index 49f37b8..92c2903 100644 --- a/src/main/java/org/tbplusc/app/Main.java +++ b/src/main/java/org/tbplusc/app/Main.java @@ -11,6 +11,7 @@ import org.tbplusc.app.talenthelper.parsers.ITalentProvider; import org.tbplusc.app.talenthelper.parsers.IcyVeinsRemoteDataProvider; import org.tbplusc.app.talenthelper.parsers.IcyVeinsTalentProvider; +import org.tbplusc.app.util.EnvWrapper; import org.tbplusc.app.util.JsonDeserializer; import org.tbplusc.app.validator.Validator; @@ -22,7 +23,8 @@ public class Main { public static void main(String[] args) { logger.info("Application started"); - final var token = System.getenv("DISCORD_TOKEN"); + registerEnvVariables(); + final var token = EnvWrapper.getValue("DISCORD_TOKEN"); final var client = DiscordClient.create(token); final MessageHandler messageHandler; try { @@ -58,4 +60,9 @@ private static MessageHandler createMessageHandler() throws IOException { DefaultChatState.registerDefaultCommands(createValidator(), createIcyVeinsTalentProvider()); return new MessageHandler(); } + + private static void registerEnvVariables() { + EnvWrapper.registerValue("DISCORD_TOKEN", System.getenv("DISCORD_TOKEN")); + EnvWrapper.registerValue("DISCORD_PREFIX", System.getenv("DISCORD_PREFIX")); + } } diff --git a/src/main/java/org/tbplusc/app/discordinteraction/DefaultChatState.java b/src/main/java/org/tbplusc/app/discordinteraction/DefaultChatState.java index 9363307..12efac2 100644 --- a/src/main/java/org/tbplusc/app/discordinteraction/DefaultChatState.java +++ b/src/main/java/org/tbplusc/app/discordinteraction/DefaultChatState.java @@ -3,6 +3,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.tbplusc.app.talenthelper.parsers.ITalentProvider; +import org.tbplusc.app.util.EnvWrapper; import org.tbplusc.app.validator.Validator; import static org.tbplusc.app.discordinteraction.DiscordUtil.getChannelForMessage; @@ -48,7 +49,7 @@ public static void registerDefaultCommands(Validator validator, } public DefaultChatState() { - prefix = System.getenv("DISCORD_PREFIX"); + prefix = EnvWrapper.getValue("DISCORD_PREFIX"); } @Override public ChatState handleMessage(WrappedMessage message) { diff --git a/src/main/java/org/tbplusc/app/discordinteraction/MessageHandler.java b/src/main/java/org/tbplusc/app/discordinteraction/MessageHandler.java index eacbc66..cedfe0c 100644 --- a/src/main/java/org/tbplusc/app/discordinteraction/MessageHandler.java +++ b/src/main/java/org/tbplusc/app/discordinteraction/MessageHandler.java @@ -1,11 +1,11 @@ package org.tbplusc.app.discordinteraction; -import discord4j.core.object.entity.Message; import java.util.HashMap; import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.Future; public class MessageHandler { private final Map states = new HashMap<>(); @@ -15,8 +15,8 @@ public MessageHandler() { this.threadPool = Executors.newFixedThreadPool(24); } - public void handleMessage(WrappedMessage message) { - threadPool.execute(() -> processMessage(message)); + public Future handleMessage(WrappedMessage message) { + return threadPool.submit(() -> processMessage(message)); } private void processMessage(WrappedMessage message) { diff --git a/src/main/java/org/tbplusc/app/util/EnvWrapper.java b/src/main/java/org/tbplusc/app/util/EnvWrapper.java new file mode 100644 index 0000000..d422c11 --- /dev/null +++ b/src/main/java/org/tbplusc/app/util/EnvWrapper.java @@ -0,0 +1,16 @@ +package org.tbplusc.app.util; + +import java.util.HashMap; +import java.util.Map; + +public class EnvWrapper { + private static final Map registeredValues = new HashMap<>(); + + public static void registerValue(String key, String value) { + registeredValues.put(key, value); + } + + public static String getValue(String key) { + return registeredValues.get(key); + } +} diff --git a/src/test/java/org/tbplusc/app/discordinteraction/MessageHandlerTests.java b/src/test/java/org/tbplusc/app/discordinteraction/MessageHandlerTests.java index 56d15a9..019f8b0 100644 --- a/src/test/java/org/tbplusc/app/discordinteraction/MessageHandlerTests.java +++ b/src/test/java/org/tbplusc/app/discordinteraction/MessageHandlerTests.java @@ -3,10 +3,35 @@ import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.tbplusc.app.util.EnvWrapper; +import java.util.concurrent.ExecutionException; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; public class MessageHandlerTests { + private static class TestDiscordMessage implements WrappedMessage { + + private final Consumer callback; + private final String content; + + public TestDiscordMessage(Consumer callback, String content) { + this.callback = callback; + this.content = content; + } + + @Override public String getContextKey() { + return "AA"; + } + + @Override public String getContent() { + return content; + } + + @Override public void respond(String text) { + callback.accept(text); + } + } private MessageHandler messageHandler; @Before @@ -15,13 +40,22 @@ public void setUp() { } @Test - public void testHandlingCommand() { + public void testHandlingCommand() throws ExecutionException, InterruptedException { + EnvWrapper.registerValue("DISCORD_PREFIX", "!"); + var called = new AtomicBoolean(false); DefaultChatState.registerCommand("test", (args, message) -> { called.set(true); + message.respond("TEST"); return new DefaultChatState(); }); + var messageStub = new TestDiscordMessage((text) -> { + Assert.assertEquals(text, "TEST"); + }, "!test"); + + messageHandler.handleMessage(messageStub).get(); + Assert.assertTrue(called.get()); } From ec83f1047bd1e9928a3e70dfecd876e44b29b096 Mon Sep 17 00:00:00 2001 From: ninele7 Date: Sun, 18 Oct 2020 21:47:14 +0500 Subject: [PATCH 12/15] test: added full tests for discordIntegration --- .../app/discordinteraction/ChatState.java | 1 - .../discordinteraction/DefaultChatState.java | 2 - .../HeroSelectionState.java | 2 - .../MessageHandlerTests.java | 90 +++++++++++++++++-- 4 files changed, 84 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/tbplusc/app/discordinteraction/ChatState.java b/src/main/java/org/tbplusc/app/discordinteraction/ChatState.java index f725b04..bcce9d5 100644 --- a/src/main/java/org/tbplusc/app/discordinteraction/ChatState.java +++ b/src/main/java/org/tbplusc/app/discordinteraction/ChatState.java @@ -1,6 +1,5 @@ package org.tbplusc.app.discordinteraction; -import discord4j.core.object.entity.Message; public interface ChatState { ChatState handleMessage(WrappedMessage message); diff --git a/src/main/java/org/tbplusc/app/discordinteraction/DefaultChatState.java b/src/main/java/org/tbplusc/app/discordinteraction/DefaultChatState.java index 12efac2..cd7bbd4 100644 --- a/src/main/java/org/tbplusc/app/discordinteraction/DefaultChatState.java +++ b/src/main/java/org/tbplusc/app/discordinteraction/DefaultChatState.java @@ -6,8 +6,6 @@ import org.tbplusc.app.util.EnvWrapper; import org.tbplusc.app.validator.Validator; -import static org.tbplusc.app.discordinteraction.DiscordUtil.getChannelForMessage; - import java.util.Arrays; import java.util.HashMap; import java.util.Map; diff --git a/src/main/java/org/tbplusc/app/discordinteraction/HeroSelectionState.java b/src/main/java/org/tbplusc/app/discordinteraction/HeroSelectionState.java index 64729b8..0a7d382 100644 --- a/src/main/java/org/tbplusc/app/discordinteraction/HeroSelectionState.java +++ b/src/main/java/org/tbplusc/app/discordinteraction/HeroSelectionState.java @@ -11,8 +11,6 @@ import java.io.IOException; import java.util.List; -import static org.tbplusc.app.discordinteraction.DiscordUtil.getChannelForMessage; - public class HeroSelectionState implements ChatState { private static final Logger logger = LoggerFactory.getLogger(HeroSelectionState.class); diff --git a/src/test/java/org/tbplusc/app/discordinteraction/MessageHandlerTests.java b/src/test/java/org/tbplusc/app/discordinteraction/MessageHandlerTests.java index 019f8b0..6b0101e 100644 --- a/src/test/java/org/tbplusc/app/discordinteraction/MessageHandlerTests.java +++ b/src/test/java/org/tbplusc/app/discordinteraction/MessageHandlerTests.java @@ -3,13 +3,27 @@ import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.mockito.Mockito; +import org.tbplusc.app.talenthelper.HeroBuild; +import org.tbplusc.app.talenthelper.HeroBuilds; +import org.tbplusc.app.talenthelper.parsers.ITalentProvider; import org.tbplusc.app.util.EnvWrapper; +import org.tbplusc.app.validator.Validator; +import org.tbplusc.app.validator.WordDistancePair; +import java.awt.*; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.ExecutionException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; public class MessageHandlerTests { + private Validator validatorMock; + private ITalentProvider talentHelperMock; + + private static class TestDiscordMessage implements WrappedMessage { private final Consumer callback; @@ -32,17 +46,19 @@ public TestDiscordMessage(Consumer callback, String content) { callback.accept(text); } } + + private MessageHandler messageHandler; - @Before - public void setUp() { + @Before public void setUp() { messageHandler = new MessageHandler(); - } - - @Test - public void testHandlingCommand() throws ExecutionException, InterruptedException { + validatorMock = Mockito.mock(Validator.class); + talentHelperMock = Mockito.mock(ITalentProvider.class); + DefaultChatState.registerDefaultCommands(validatorMock, talentHelperMock); EnvWrapper.registerValue("DISCORD_PREFIX", "!"); + } + @Test public void testHandlingCommand() throws ExecutionException, InterruptedException { var called = new AtomicBoolean(false); DefaultChatState.registerCommand("test", (args, message) -> { called.set(true); @@ -59,4 +75,66 @@ public void testHandlingCommand() throws ExecutionException, InterruptedExceptio Assert.assertTrue(called.get()); } + + @Test public void testBuildsCommandWithExactMatch() + throws IOException, ExecutionException, InterruptedException { + Mockito.when(validatorMock.getSomeClosestToInput("test", 10)) + .thenReturn(new WordDistancePair[] {new WordDistancePair("test", 0), + new WordDistancePair("test1", 1),}); + Mockito.when(talentHelperMock.getBuilds("test")).thenReturn(new HeroBuilds("test", + List.of(new HeroBuild("main", "main", + List.of("a", "b", "c", "d", "CHECK", "f", "g"))))); + var results = new ArrayList(); + messageHandler.handleMessage(new TestDiscordMessage(results::add, "!build test")).get(); + + Assert.assertTrue(results.get(0).contains("test")); + Assert.assertTrue(results.get(1).contains("main")); + Assert.assertTrue(results.get(1).contains("CHECK")); + } + + @Test public void testBuildsCommandWithNonExactMatch() + throws IOException, ExecutionException, InterruptedException { + Mockito.when(validatorMock.getSomeClosestToInput("test1", 10)) + .thenReturn(new WordDistancePair[] {new WordDistancePair("test2", 1), + new WordDistancePair("test23", 2),}); + Mockito.when(talentHelperMock.getBuilds("test23")).thenReturn(new HeroBuilds("test23", + List.of(new HeroBuild("main", "main", + List.of("a", "b", "c", "d", "CHECK", "f", "g"))))); + + var firstResults = new ArrayList(); + messageHandler.handleMessage(new TestDiscordMessage(firstResults::add, "!build test1")) + .get(); + + Assert.assertTrue(firstResults.get(0).contains("test2")); + Assert.assertTrue(firstResults.get(0).contains("test23")); + + var secondResults = new ArrayList(); + messageHandler.handleMessage(new TestDiscordMessage(secondResults::add, "2")).get(); + + Assert.assertTrue(secondResults.get(0).contains("test23")); + Assert.assertTrue(secondResults.get(1).contains("CHECK")); + } + + @Test public void testIgnoreNonCommands() throws ExecutionException, InterruptedException { + var results = new ArrayList(); + messageHandler.handleMessage(new TestDiscordMessage(results::add, "KEKLOLARBIDOL")).get(); + + Assert.assertEquals(0, results.size()); + } + + @Test public void testEcho() throws ExecutionException, InterruptedException { + var results = new ArrayList(); + messageHandler.handleMessage(new TestDiscordMessage(results::add, "!echo KEKLOLARBIDOL")) + .get(); + + Assert.assertEquals(results.get(0), "KEKLOLARBIDOL"); + } + + @Test public void testWrongCommand() throws ExecutionException, InterruptedException { + var results = new ArrayList(); + messageHandler.handleMessage(new TestDiscordMessage(results::add, "!keklolarbidol7623765")) + .get(); + + Assert.assertEquals(0, results.size()); + } } From ceca00925545c268c5445dbcbefaeb628d368927 Mon Sep 17 00:00:00 2001 From: ninele7 Date: Sun, 18 Oct 2020 21:57:50 +0500 Subject: [PATCH 13/15] docs: added docstrings to not obvious methods in discord interaction --- .../app/discordinteraction/DefaultChatState.java | 9 +++++++++ .../discordinteraction/HeroSelectionState.java | 16 +++++++++++++++- .../java/org/tbplusc/app/util/EnvWrapper.java | 3 +++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/tbplusc/app/discordinteraction/DefaultChatState.java b/src/main/java/org/tbplusc/app/discordinteraction/DefaultChatState.java index cd7bbd4..dd21e8d 100644 --- a/src/main/java/org/tbplusc/app/discordinteraction/DefaultChatState.java +++ b/src/main/java/org/tbplusc/app/discordinteraction/DefaultChatState.java @@ -11,6 +11,10 @@ import java.util.Map; import java.util.function.BiFunction; + +/** + * ChatState that responsible for handling commands on first step + */ public class DefaultChatState implements ChatState { private static final Logger logger = LoggerFactory.getLogger(DefaultChatState.class); @@ -23,6 +27,11 @@ public static void registerCommand(String name, BiFunction { diff --git a/src/main/java/org/tbplusc/app/discordinteraction/HeroSelectionState.java b/src/main/java/org/tbplusc/app/discordinteraction/HeroSelectionState.java index 0a7d382..ddce7b3 100644 --- a/src/main/java/org/tbplusc/app/discordinteraction/HeroSelectionState.java +++ b/src/main/java/org/tbplusc/app/discordinteraction/HeroSelectionState.java @@ -1,6 +1,5 @@ package org.tbplusc.app.discordinteraction; -import discord4j.core.object.entity.Message; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.tbplusc.app.talenthelper.HeroConsts; @@ -11,6 +10,9 @@ import java.io.IOException; import java.util.List; +/** + * ChatState to allow user to select hero from closest to his text. + */ public class HeroSelectionState implements ChatState { private static final Logger logger = LoggerFactory.getLogger(HeroSelectionState.class); @@ -19,6 +21,12 @@ public class HeroSelectionState implements ChatState { private final WrappedMessage message; private final ITalentProvider talentProvider; + /** + * ChatState to allow user to select hero from closest to his text. + * @param availableHeroes heroes list provided by Validator + * @param message message with command from user + * @param talentProvider object to get talents from somewhere + */ public HeroSelectionState(List availableHeroes, WrappedMessage message, ITalentProvider talentProvider) { this.availableHeroes = availableHeroes; @@ -35,6 +43,12 @@ private void showInitMessage() { message.respond(String.format("Choose hero (type number): \n ```md\n%s```", heroes)); } + /** + * Format selected hero build for discord and send it as message response. + * @param message message to respond + * @param heroName + * @param talentProvider object to get talents from somewhere + */ public static void showHeroBuildToDiscord(WrappedMessage message, String heroName, ITalentProvider talentProvider) { final var normalizedHeroName = IcyVeinsTalentProvider.normalizeHeroName(heroName); diff --git a/src/main/java/org/tbplusc/app/util/EnvWrapper.java b/src/main/java/org/tbplusc/app/util/EnvWrapper.java index d422c11..96d9da1 100644 --- a/src/main/java/org/tbplusc/app/util/EnvWrapper.java +++ b/src/main/java/org/tbplusc/app/util/EnvWrapper.java @@ -3,6 +3,9 @@ import java.util.HashMap; import java.util.Map; +/** + * Acts as middleware for system environment. + */ public class EnvWrapper { private static final Map registeredValues = new HashMap<>(); From 396593fe5f4ded9ce714b93495423bc1d9486ab9 Mon Sep 17 00:00:00 2001 From: ninele7 <33155080+ninele7@users.noreply.github.com> Date: Sun, 18 Oct 2020 22:10:30 +0500 Subject: [PATCH 14/15] doc: improved discord interaction decomposition --- README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5947320..9768e4d 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,10 @@ # Задачи - [ ] Билды героев Heroes of the storm в дискорде - - [ ] Базовое взаимодействие с дискордом + - [x] Базовое взаимодействие с дискордом + - [x] Получение сообщений от дискорда + - [x] Хранение состояний для связки канал+пользователь + - [x] Обработка "echo", "authors" + - [x] Обработка "build" - [ ] Загрузка талантов героев с icyVeins - - [ ] Толерантность к ошибкам \ No newline at end of file + - [ ] Толерантность к ошибкам From 29f92df3067bca1512f067f9944db083dae4a8ec Mon Sep 17 00:00:00 2001 From: pamhome21 <42671363+pamhome21@users.noreply.github.com> Date: Sun, 18 Oct 2020 22:40:28 +0500 Subject: [PATCH 15/15] aux: updated version in pom.xml --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 6e81fd7..53ca858 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ org.tbplusc boostio-bot - 0.0.1 + 0.1.0