diff --git a/README.md b/README.md index e2ac493..bd64192 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,9 @@ Navigate to https://tibiawiki.dev to view the Swagger API of this project. ## Run locally Clone this git project to your local computer and compile it using: `mvn clean install` from your favourite command line terminal. Then execute: `mvn spring-boot:run` and open your browser on http://localhost:8080 + +Note that you need to add the [sample settings.xml](.travis.settings.xml) to your $HOME/.m2/settings.xml directory +with a valid username and github token with read packages scope. You can now access the REST resources using your browser or any REST client such as Postman or curl from your command line. E.g. navigating to http://localhost:8080/api/corpses should give you a list of corpses. diff --git a/pom.xml b/pom.xml index d525d79..1be1f18 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ com.tibiawiki TibiaWikiApi - 1.7.2 + 1.8.0 jar TibiaWikiApi https://github.com/benjaminkomen/TibiaWikiApi diff --git a/src/it/java/com/tibiawiki/serviceinterface/LootStatisticsV2ResourceIT.java b/src/it/java/com/tibiawiki/serviceinterface/LootStatisticsV2ResourceIT.java new file mode 100644 index 0000000..3f641b6 --- /dev/null +++ b/src/it/java/com/tibiawiki/serviceinterface/LootStatisticsV2ResourceIT.java @@ -0,0 +1,167 @@ +package com.tibiawiki.serviceinterface; + +import benjaminkomen.jwiki.core.NS; +import com.tibiawiki.domain.enums.InfoboxTemplate; +import com.tibiawiki.domain.repositories.ArticleRepository; +import org.json.JSONObject; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static com.tibiawiki.process.RetrieveAny.CATEGORY_LISTS; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.doReturn; + +@ExtendWith(SpringExtension.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class LootStatisticsV2ResourceIT { + + private static final NS LOOT_NAMESPACE = new NS(112); + @Autowired + private TestRestTemplate restTemplate; + + @MockBean + private ArticleRepository articleRepository; // don't instantiate this real class, but use a mock implementation + + private static final String LOOT_AMAZON_TEXT = "{{Loot2\n" + + "|version=8.6\n" + + "|kills=22009\n" + + "|name=Amazon\n" + + "|Empty, times:253\n" + + "|Dagger, times:17626, amount:1, total:17626\n" + + "|Skull, times:17604, amount:1-2, total:26348\n" + + "|Gold Coin, times:8829, amount:1-20, total:93176\n" + + "|Brown Bread, times:6496, amount:1, total:6496\n" + + "|Sabre, times:5098, amount:1, total:5098\n" + + "|Girlish Hair Decoration, times:2179, amount:1, total:2179\n" + + "|Protective Charm, times:1154, amount:1, total:1154\n" + + "|Torch, times:223, amount:1, total:223\n" + + "|Crystal Necklace, times:56, amount:1, total:56\n" + + "|Small Ruby, times:27, amount:1, total:27\n" + + "}}\n"; + + private static final String LOOT_FERUMBRAS_TEXT = "__NOWYSIWYG__\n\n" + + "{{Loot2\n" + + "|version=8.6\n" + + "|kills=49\n" + + "|name=Ferumbras\n" + + "|Ferumbras' Hat, times:49, total:3\n" + + "|Gold Coin, times:48, amount:18-184, total:4751\n" + + "|Gold Ingot, times:37, amount:1-2, total:52\n" + + "|Great Shield, times:13, amount:1, total:13\n" + + "|Spellbook of Lost Souls, times:13, amount:1, total:13\n" + + "|Golden Armor, times:12, amount:1, total:12\n" + + "}}\n" + + "\n" + + "{{Loot2_RC\n" + + "|version=8.6\n" + + "|kills=1\n" + + "|name=Ferumbras\n" + + "|Blue Gem, times:1, amount:1, total:1\n" + + "|Giant Shimmering Pearl, times:1, amount:1, total:1\n" + + "|Gold Coin, times:1, amount:100, total:100\n" + + "|Golden Armor, times:1, amount:1, total:1\n" + + "|Lightning Legs, times:1\n" + + "|Runed Sword, times:1, amount:1, total:1\n" + + "|Small Emerald, times:1, amount:10, total:10\n" + + "}}\n" + + "\n" + + "{{Loot\n" + + "|version=8.54\n" + + "|kills=4\n" + + "|name=Ferumbras\n" + + "|[[Gold Coin]], 399\n" + + "|[[Small Ruby]], 126\n" + + "|[[Small Diamond]], 45\n" + + "|[[Gold Ingot]], 6\n" + + "|[[Ferumbras' Hat]], 4\n" + + "|[[Small Topaz]], 3\n" + + "|[[Spellbook of Lost Souls]], 3\n" + + "}}\n" + + "
Average gold: 99.75"; + + @Test + void givenGetLootsNotExpanded_whenCorrectRequest_thenResponseIsOkAndContainsTwoLootNames() { + doReturn(Arrays.asList("foo", "bar")).when(articleRepository).getPageNamesFromCategory(InfoboxTemplate.LOOT.getCategoryName(), LOOT_NAMESPACE); + + final ResponseEntity result = restTemplate.getForEntity("/api/v2/loot?expand=false", List.class); + + assertThat(result.getStatusCode(), is(HttpStatus.OK)); + assertThat(result.getBody().size(), is(2)); + assertThat(result.getBody().get(0), is("foo")); + assertThat(result.getBody().get(1), is("bar")); + } + + @Test + void givenGetLootsExpanded_whenCorrectRequest_thenResponseIsOkAndContainsOneLoot() { + doReturn(Collections.emptyList()).when(articleRepository).getPageNamesFromCategory(CATEGORY_LISTS); + doReturn(Collections.singletonList("Loot:Amazon")).when(articleRepository).getPageNamesFromCategory(InfoboxTemplate.LOOT.getCategoryName(), LOOT_NAMESPACE); + doReturn(Map.of("Loot:Amazon", LOOT_AMAZON_TEXT)).when(articleRepository).getArticlesFromCategory(Collections.singletonList("Loot:Amazon")); + + final ResponseEntity result = restTemplate.getForEntity("/api/v2/loot?expand=true", List.class); + + assertThat(result.getStatusCode(), is(HttpStatus.OK)); + assertThat(result.getBody().size(), is(1)); + + var loot2 = ((Map) ((Map) result.getBody().get(0)).get("loot2")); + + assertThat(loot2.get("kills"), is("22009")); + assertThat(loot2.get("name"), is("Amazon")); + assertThat(loot2.get("version"), is("8.6")); + assertThat(loot2.get("pageName"), is("Loot:Amazon")); + } + + @Test + void givenGetLootsByName_whenCorrectRequest_thenResponseIsOkAndContainsTheLoot() { + doReturn(LOOT_AMAZON_TEXT).when(articleRepository).getArticle("Loot_Statistics:Amazon"); + + final ResponseEntity result = restTemplate.getForEntity("/api/v2/loot/Amazon", String.class); + assertThat(result.getStatusCode(), is(HttpStatus.OK)); + + final JSONObject resultAsJSON = new JSONObject(result.getBody()).getJSONObject("loot2"); + assertThat(resultAsJSON.get("kills"), is("22009")); + assertThat(resultAsJSON.get("name"), is("Amazon")); + assertThat(resultAsJSON.get("version"), is("8.6")); + assertThat(resultAsJSON.get("pageName"), is("Loot_Statistics:Amazon")); + } + + @Test + void givenGetLootsByName_whenCorrectRequest_thenResponseIsOkAndContainsTwoLootEntities() { + doReturn(LOOT_FERUMBRAS_TEXT).when(articleRepository).getArticle("Loot_Statistics:Ferumbras"); + + final ResponseEntity result = restTemplate.getForEntity("/api/v2/loot/Ferumbras", String.class); + assertThat(result.getStatusCode(), is(HttpStatus.OK)); + + final JSONObject loot2Result = new JSONObject(result.getBody()).getJSONObject("loot2"); + assertThat(loot2Result.get("kills"), is("49")); + assertThat(loot2Result.get("name"), is("Ferumbras")); + assertThat(loot2Result.get("version"), is("8.6")); + assertThat(loot2Result.get("pageName"), is("Loot_Statistics:Ferumbras")); + + final JSONObject loot2RewardChestResult = new JSONObject(result.getBody()).getJSONObject("loot2_rc"); + assertThat(loot2RewardChestResult.get("kills"), is("1")); + assertThat(loot2RewardChestResult.get("name"), is("Ferumbras")); + assertThat(loot2RewardChestResult.get("version"), is("8.6")); + assertThat(loot2RewardChestResult.get("pageName"), is("Loot_Statistics:Ferumbras")); + } + + @Test + void givenGetLootsByName_whenWrongRequest_thenResponseIsNotFound() { + doReturn(null).when(articleRepository).getArticle("Loot:Foobar"); + + final ResponseEntity result = restTemplate.getForEntity("/api/v2/loot/Foobar", String.class); + assertThat(result.getStatusCode(), is(HttpStatus.NOT_FOUND)); + } +} diff --git a/src/main/java/com/tibiawiki/domain/factories/ArticleFactory.java b/src/main/java/com/tibiawiki/domain/factories/ArticleFactory.java index dec08e1..b358e11 100644 --- a/src/main/java/com/tibiawiki/domain/factories/ArticleFactory.java +++ b/src/main/java/com/tibiawiki/domain/factories/ArticleFactory.java @@ -1,12 +1,15 @@ package com.tibiawiki.domain.factories; import com.tibiawiki.domain.utils.TemplateUtils; -import io.vavr.Tuple2; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; +import java.util.Collections; +import java.util.HashMap; import java.util.Map; +import java.util.Optional; +import java.util.regex.Pattern; /** * Conversion from Article to infoboxPartOfArticle. @@ -17,6 +20,9 @@ public class ArticleFactory { private static final Logger log = LoggerFactory.getLogger(ArticleFactory.class); private static final String INFOBOX_HEADER = "{{Infobox"; private static final String LOOT2_HEADER = "{{Loot2"; + private static final Pattern LOOT2_HEADER_PATTERN = Pattern.compile("\\{\\{Loot2\\n"); + private static final String LOOT2_RC_HEADER = "{{Loot2_RC"; + private static final Pattern LOOT2_RC_HEADER_REGEX = Pattern.compile("\\{\\{Loot2_RC\\n"); public String extractInfoboxPartOfArticle(String articleContent) { return extractInfoboxPartOfArticle(Map.entry("Unknown", articleContent)); @@ -38,13 +44,18 @@ public String extractInfoboxPartOfArticle(Map.Entry pageNameAndA return ""; } - return TemplateUtils.getBetweenOuterBalancedBrackets(articleContent, INFOBOX_HEADER); + return TemplateUtils.getBetweenOuterBalancedBrackets(articleContent, INFOBOX_HEADER) + .orElse(""); } public String extractLootPartOfArticle(String pageName, String articleContent) { return extractLootPartOfArticle(Map.entry(pageName, articleContent)); } + public Map extractAllLootPartsOfArticle(String pageName, String articleContent) { + return extractAllLootPartsOfArticle(Map.entry(pageName, articleContent)); + } + /** * Given a certain Article, extract the part from it which is the first loot statistics template, or an empty String if it does not contain * an Loot2 template (which is perfectly valid in some cases). @@ -53,7 +64,7 @@ public String extractLootPartOfArticle(Map.Entry pageNameAndArti final String pageName = pageNameAndArticleContent.getKey(); final String articleContent = pageNameAndArticleContent.getValue(); - if (!articleContent.contains(LOOT2_HEADER)) { + if (!LOOT2_HEADER_PATTERN.matcher(articleContent).find()) { if (log.isWarnEnabled()) { log.warn("Cannot extract loot statistics template from article '{}'," + " since it contains no Loot2 template.", pageName); @@ -61,7 +72,37 @@ public String extractLootPartOfArticle(Map.Entry pageNameAndArti return ""; } - return TemplateUtils.getBetweenOuterBalancedBrackets(articleContent, LOOT2_HEADER); + return TemplateUtils.getBetweenOuterBalancedBrackets(articleContent, LOOT2_HEADER) + .orElse(""); + } + + /** + * Given a certain Article, extract the parts of all different supported loot statistics templates (Loot2 or Loot2_RC). + */ + public Map extractAllLootPartsOfArticle(Map.Entry pageNameAndArticleContent) { + final String pageName = pageNameAndArticleContent.getKey(); + final String articleContent = pageNameAndArticleContent.getValue(); + + if (!LOOT2_HEADER_PATTERN.matcher(articleContent).find() && !LOOT2_RC_HEADER_REGEX.matcher(articleContent).find()) { + if (log.isWarnEnabled()) { + log.warn("Cannot extract loot statistics template from article '{}'," + + " since it contains no Loot2 or Loot2_RC template.", pageName); + } + return Collections.emptyMap(); + } + + var result = new HashMap(2); + var loot2 = LOOT2_HEADER_PATTERN.matcher(articleContent).find() + ? TemplateUtils.getBetweenOuterBalancedBrackets(articleContent, LOOT2_HEADER) + : Optional.empty(); + var loot2Rc = LOOT2_RC_HEADER_REGEX.matcher(articleContent).find() + ? TemplateUtils.getBetweenOuterBalancedBrackets(articleContent, LOOT2_RC_HEADER) + : Optional.empty(); + + loot2.ifPresent(s -> result.put("loot2", (String) s)); + loot2Rc.ifPresent(s -> result.put("loot2_rc", (String) s)); + + return result; } /** @@ -69,10 +110,8 @@ public String extractLootPartOfArticle(Map.Entry pageNameAndArti * @param newContent the new infobox content * @return the full article content with the old infobox content replaced by the new infobox content */ - public String insertInfoboxPartOfArticle(String originalArticleContent, String newContent) { - final Tuple2 beforeAndAfterOuterBalancedBrackets = - TemplateUtils.getBeforeAndAfterOuterBalancedBrackets(originalArticleContent, INFOBOX_HEADER); - - return beforeAndAfterOuterBalancedBrackets._1() + newContent + beforeAndAfterOuterBalancedBrackets._2(); + public Optional insertInfoboxPartOfArticle(String originalArticleContent, String newContent) { + return TemplateUtils.getBeforeAndAfterOuterBalancedBrackets(originalArticleContent, INFOBOX_HEADER) + .map(beforeAndAfterOuterBalancedBrackets -> beforeAndAfterOuterBalancedBrackets._1() + newContent + beforeAndAfterOuterBalancedBrackets._2()); } } \ No newline at end of file diff --git a/src/main/java/com/tibiawiki/domain/factories/JsonFactory.java b/src/main/java/com/tibiawiki/domain/factories/JsonFactory.java index 41c0747..03093ea 100644 --- a/src/main/java/com/tibiawiki/domain/factories/JsonFactory.java +++ b/src/main/java/com/tibiawiki/domain/factories/JsonFactory.java @@ -11,14 +11,7 @@ import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; +import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -47,7 +40,7 @@ public class JsonFactory { private static final List ITEMS_WITH_NO_DROPPEDBY_LIST = Arrays.asList("Gold Coin", "Platinum Coin"); private static final String INFOBOX_HEADER_PATTERN = "\\{\\{Infobox[\\s|_](.*?)[\\||\\n]"; private static final String RARITY_PATTERN = "(always|common|uncommon|semi-rare|rare|very rare|extremely rare)(|\\?)"; - private static final String LOOT_LINE_NAME_PATTERN = "(\\w+:\\d+)"; + private static final String LOOT_LINE_NAME_PATTERN = "(\\w+:[\\d-]+)"; private static final String UNKNOWN = "Unknown"; private static final String RARITY = "rarity"; private static final String AMOUNT = "amount"; @@ -100,6 +93,23 @@ public JSONObject convertLootPartOfArticleToJson(String pageName, @Nullable fina return enhanceLootJsonObject(new JSONObject(parametersAndValues)); } + @NotNull + public JSONObject convertAllLootPartsOfArticleToJson(String pageName, Map lootPartsOfArticle) { + + if (lootPartsOfArticle.isEmpty()) { + return new JSONObject(); + } + + var result = new JSONObject(); + + lootPartsOfArticle.forEach((k, v) -> { + var value = convertLootPartOfArticleToJson(pageName, v); + result.put(k, value); + }); + + return result; + } + @NotNull public String convertJsonToInfoboxPartOfArticle(@Nullable JSONObject jsonObject, List fieldOrder) { if (jsonObject == null || jsonObject.isEmpty()) { @@ -349,14 +359,19 @@ private JSONArray makeLootTableArray(String lootValue) { return new JSONArray(); } - String lootItemsPartOfLootTable = TemplateUtils.getBetweenOuterBalancedBrackets(lootValue, "{{Loot Table"); - lootItemsPartOfLootTable = TemplateUtils.removeFirstAndLastLine(lootItemsPartOfLootTable); + var lootItemsPartOfLootTable = TemplateUtils.getBetweenOuterBalancedBrackets(lootValue, "{{Loot Table"); + + if (lootItemsPartOfLootTable.isEmpty()) { + return new JSONArray(); + } + + var lootItemsPartOfLootTableStripped = TemplateUtils.removeFirstAndLastLine(lootItemsPartOfLootTable.get()); - if (lootItemsPartOfLootTable.length() < 3) { + if (lootItemsPartOfLootTableStripped.length() < 3) { return new JSONArray(); } - List lootItemsList = Arrays.asList(Pattern.compile("(^|\n)(\\s|)\\|").split(lootItemsPartOfLootTable)); + List lootItemsList = Arrays.asList(Pattern.compile("(^|\n)(\\s|)\\|").split(lootItemsPartOfLootTableStripped)); for (String lootItemTemplate : lootItemsList) { if (lootItemTemplate.length() < 1) { diff --git a/src/main/java/com/tibiawiki/domain/utils/TemplateUtils.java b/src/main/java/com/tibiawiki/domain/utils/TemplateUtils.java index f7672c7..ecbad83 100644 --- a/src/main/java/com/tibiawiki/domain/utils/TemplateUtils.java +++ b/src/main/java/com/tibiawiki/domain/utils/TemplateUtils.java @@ -7,13 +7,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.Arrays; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; +import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -32,9 +26,9 @@ private TemplateUtils() { // no-args constructor, only static methods } - public static String getBetweenOuterBalancedBrackets(String text, String start) { - final Tuple2 startingAndEndingCurlyBrackets = getStartingAndEndingCurlyBrackets(text, start); - return text.substring(startingAndEndingCurlyBrackets._1(), startingAndEndingCurlyBrackets._2()); + public static Optional getBetweenOuterBalancedBrackets(String text, String start) { + return getStartingAndEndingCurlyBrackets(text, start) + .map(startingAndEndingCurlyBrackets -> text.substring(startingAndEndingCurlyBrackets._1(), startingAndEndingCurlyBrackets._2())); } /** @@ -43,9 +37,11 @@ public static String getBetweenOuterBalancedBrackets(String text, String start) * @return two strings, the first is the substring of the provided text before the start of the balanced brackets, * the second is the substring after the start of the balanced brackets. */ - public static Tuple2 getBeforeAndAfterOuterBalancedBrackets(String text, String start) { - final Tuple2 startingAndEndingCurlyBrackets = getStartingAndEndingCurlyBrackets(text, start); - return Tuple.of(text.substring(0, startingAndEndingCurlyBrackets._1()), text.substring(startingAndEndingCurlyBrackets._2())); + public static Optional> getBeforeAndAfterOuterBalancedBrackets(String text, String start) { + return getStartingAndEndingCurlyBrackets(text, start) + .map(startingAndEndingCurlyBrackets -> Tuple.of(text.substring(0, startingAndEndingCurlyBrackets._1()), + text.substring(startingAndEndingCurlyBrackets._2())) + ); } /** @@ -180,11 +176,11 @@ public static String removeLowerLevels(@Nullable String infoboxTemplatePartOfArt * @return a tuple of two integers: the index of the start of the curly brackets and an index of the end of * the curly brackets */ - private static Tuple2 getStartingAndEndingCurlyBrackets(String text, String start) { + private static Optional> getStartingAndEndingCurlyBrackets(String text, String start) { final int startingCurlyBrackets = text.indexOf(start); if (startingCurlyBrackets < 0) { - throw new IllegalArgumentException("Provided arguments text and start are not valid."); + return Optional.empty(); // could not find text in string } int endingCurlyBrackets = 0; @@ -206,6 +202,6 @@ private static Tuple2 getStartingAndEndingCurlyBrackets(String break; } } - return Tuple.of(startingCurlyBrackets, endingCurlyBrackets); + return Optional.of(Tuple.of(startingCurlyBrackets, endingCurlyBrackets)); } } \ No newline at end of file diff --git a/src/main/java/com/tibiawiki/process/ModifyAny.java b/src/main/java/com/tibiawiki/process/ModifyAny.java index 96c2e6a..2bdca3a 100644 --- a/src/main/java/com/tibiawiki/process/ModifyAny.java +++ b/src/main/java/com/tibiawiki/process/ModifyAny.java @@ -8,7 +8,7 @@ import com.tibiawiki.domain.objects.validation.ValidationResult; import com.tibiawiki.domain.repositories.ArticleRepository; import io.vavr.control.Try; -import org.springframework.beans.factory.annotation.Autowired; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; import java.util.List; @@ -20,21 +20,13 @@ * 4. Edit the wiki (via a repository) */ @Component +@RequiredArgsConstructor public class ModifyAny { - private WikiObjectFactory wikiObjectFactory; - private JsonFactory jsonFactory; - private ArticleFactory articleFactory; - private ArticleRepository articleRepository; - - @Autowired - protected ModifyAny(WikiObjectFactory wikiObjectFactory, JsonFactory jsonFactory, ArticleFactory articleFactory, - ArticleRepository articleRepository) { - this.wikiObjectFactory = wikiObjectFactory; - this.jsonFactory = jsonFactory; - this.articleFactory = articleFactory; - this.articleRepository = articleRepository; - } + private final WikiObjectFactory wikiObjectFactory; + private final JsonFactory jsonFactory; + private final ArticleFactory articleFactory; + private final ArticleRepository articleRepository; public Try modify(WikiObject wikiObject, String editSummary) { final String originalWikiObject = articleRepository.getArticle(wikiObject.getName()); @@ -43,6 +35,10 @@ public Try modify(WikiObject wikiObject, String editSummary) { .map(wikiObj -> wikiObjectFactory.createJSONObject(wikiObj, wikiObj.getTemplateType())) .map(json -> jsonFactory.convertJsonToInfoboxPartOfArticle(json, wikiObject.fieldOrder())) .map(s -> articleFactory.insertInfoboxPartOfArticle(originalWikiObject, s)) + .flatMap(s -> s.isEmpty() + ? Try.failure(new IllegalArgumentException("Could not find required text in article")) + : Try.success(s.get()) + ) .map(s -> articleRepository.modifyArticle(wikiObject.getName(), s, editSummary)) .flatMap(b -> b ? Try.success(wikiObject) diff --git a/src/main/java/com/tibiawiki/process/RetrieveLoot.java b/src/main/java/com/tibiawiki/process/RetrieveLoot.java index 83e8ace..67e13ef 100644 --- a/src/main/java/com/tibiawiki/process/RetrieveLoot.java +++ b/src/main/java/com/tibiawiki/process/RetrieveLoot.java @@ -29,15 +29,23 @@ public List getLootList() { return new ArrayList<>(lootStatisticsCategory); } - public Stream getLootJSON() { - return getArticlesFromLoot2TemplateAsJSON(getLootList()); + public Stream getLootJSONObject() { + return getArticlesFromLoot2TemplateAsJSONObject(getLootList()); } - public Optional getLootJSON(String pageName) { + public Optional getLootJSONObject(String pageName) { return getLootArticleAsJSON(pageName); } - private Stream getArticlesFromLoot2TemplateAsJSON(List pageNames) { + public Stream getAllLootPartsJSON() { + return getArticlesFromAllLootTemplatesAsJSON(getLootList()); + } + + public Optional getAllLootPartsJSON(String pageName) { + return getAllLootPartsAsJSON(pageName); + } + + private Stream getArticlesFromLoot2TemplateAsJSONObject(List pageNames) { return Stream.of(pageNames) .flatMap(lst -> articleRepository.getArticlesFromCategory(lst).entrySet().stream()) .map(e -> { @@ -46,9 +54,24 @@ private Stream getArticlesFromLoot2TemplateAsJSON(List pageN }); } + private Stream getArticlesFromAllLootTemplatesAsJSON(List pageNames) { + return Stream.of(pageNames) + .flatMap(lst -> articleRepository.getArticlesFromCategory(lst).entrySet().stream()) + .map(e -> { + var lootPartOfArticle = articleFactory.extractAllLootPartsOfArticle(e); + return jsonFactory.convertAllLootPartsOfArticleToJson(e.getKey(), lootPartOfArticle); + }); + } + private Optional getLootArticleAsJSON(String pageName) { return Optional.ofNullable(articleRepository.getArticle(pageName)) .map(articleContent -> articleFactory.extractLootPartOfArticle(pageName, articleContent)) .map(lootPartOfArticle -> jsonFactory.convertLootPartOfArticleToJson(pageName, lootPartOfArticle)); } + + private Optional getAllLootPartsAsJSON(String pageName) { + return Optional.ofNullable(articleRepository.getArticle(pageName)) + .map(articleContent -> articleFactory.extractAllLootPartsOfArticle(pageName, articleContent)) + .map(lootPartsOfArticle -> jsonFactory.convertAllLootPartsOfArticleToJson(pageName, lootPartsOfArticle)); + } } diff --git a/src/main/java/com/tibiawiki/serviceinterface/LootStatisticsResource.java b/src/main/java/com/tibiawiki/serviceinterface/LootStatisticsResource.java index ca5eab5..7b9e3be 100644 --- a/src/main/java/com/tibiawiki/serviceinterface/LootStatisticsResource.java +++ b/src/main/java/com/tibiawiki/serviceinterface/LootStatisticsResource.java @@ -1,34 +1,22 @@ package com.tibiawiki.serviceinterface; import com.tibiawiki.process.RetrieveLoot; -import io.swagger.annotations.Api; -import io.swagger.annotations.ApiOperation; -import io.swagger.annotations.ApiParam; -import io.swagger.annotations.ApiResponse; -import io.swagger.annotations.ApiResponses; +import io.swagger.annotations.*; +import lombok.RequiredArgsConstructor; import org.json.JSONObject; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.Produces; -import javax.ws.rs.QueryParam; +import javax.ws.rs.*; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; @Component @Api(value = "Loot Statistics") @Path("/loot") +@RequiredArgsConstructor public class LootStatisticsResource { - private RetrieveLoot retrieveLoot; - - @Autowired - private LootStatisticsResource(RetrieveLoot retrieveLoot) { - this.retrieveLoot = retrieveLoot; - } + private final RetrieveLoot retrieveLoot; @GET @ApiOperation(value = "Get a list of loot statistics") @@ -41,7 +29,7 @@ public Response getLoot(@ApiParam(value = "optionally expands the result to retr @QueryParam("expand") Boolean expand) { return Response.ok() .entity(expand != null && expand - ? retrieveLoot.getLootJSON().map(JSONObject::toMap) + ? retrieveLoot.getLootJSONObject().map(JSONObject::toMap) : retrieveLoot.getLootList() ) .build(); @@ -52,7 +40,7 @@ public Response getLoot(@ApiParam(value = "optionally expands the result to retr @ApiOperation(value = "Get a specific loot statistics page by creature name") @Produces(MediaType.APPLICATION_JSON) public Response getLootByName(@PathParam("name") String name) { - return retrieveLoot.getLootJSON("Loot_Statistics:" + name) + return retrieveLoot.getLootJSONObject("Loot_Statistics:" + name) .map(a -> Response.ok() .entity(a.toString(2)) .build()) diff --git a/src/main/java/com/tibiawiki/serviceinterface/LootStatisticsV2Resource.java b/src/main/java/com/tibiawiki/serviceinterface/LootStatisticsV2Resource.java new file mode 100644 index 0000000..6571e5c --- /dev/null +++ b/src/main/java/com/tibiawiki/serviceinterface/LootStatisticsV2Resource.java @@ -0,0 +1,50 @@ +package com.tibiawiki.serviceinterface; + +import com.tibiawiki.process.RetrieveLoot; +import io.swagger.annotations.*; +import lombok.RequiredArgsConstructor; +import org.json.JSONObject; +import org.springframework.stereotype.Component; + +import javax.ws.rs.*; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +@Component +@Api(value = "Loot Statistics") +@Path("/v2/loot") +@RequiredArgsConstructor +public class LootStatisticsV2Resource { + + private final RetrieveLoot retrieveLoot; + + @GET + @ApiOperation(value = "Get a list of loot statistics") + @ApiResponses(value = { + @ApiResponse(code = 200, message = "list of loot statistics retrieved") + }) + @Produces(MediaType.APPLICATION_JSON) + public Response getLoot(@ApiParam(value = "optionally expands the result to retrieve not only " + + "the loot statistics page names but the full loot statistics", required = false) + @QueryParam("expand") Boolean expand) { + return Response.ok() + .entity(expand != null && expand + ? retrieveLoot.getAllLootPartsJSON().map(JSONObject::toMap) + : retrieveLoot.getLootList() + ) + .build(); + } + + @GET + @Path("/{name}") + @ApiOperation(value = "Get a specific loot statistics page by creature name") + @Produces(MediaType.APPLICATION_JSON) + public Response getLootByName(@PathParam("name") String name) { + return retrieveLoot.getAllLootPartsJSON("Loot_Statistics:" + name) + .map(a -> Response.ok() + .entity(a.toString(2)) + .build()) + .orElseGet(() -> Response.status(Response.Status.NOT_FOUND) + .build()); + } +} diff --git a/src/main/java/com/tibiawiki/serviceinterface/config/JerseyConfig.java b/src/main/java/com/tibiawiki/serviceinterface/config/JerseyConfig.java index bf83a97..4674ab8 100644 --- a/src/main/java/com/tibiawiki/serviceinterface/config/JerseyConfig.java +++ b/src/main/java/com/tibiawiki/serviceinterface/config/JerseyConfig.java @@ -1,24 +1,6 @@ package com.tibiawiki.serviceinterface.config; -import com.tibiawiki.serviceinterface.AchievementsResource; -import com.tibiawiki.serviceinterface.BooksResource; -import com.tibiawiki.serviceinterface.BuildingsResource; -import com.tibiawiki.serviceinterface.CorpsesResource; -import com.tibiawiki.serviceinterface.CreaturesResource; -import com.tibiawiki.serviceinterface.EffectsResource; -import com.tibiawiki.serviceinterface.HuntingPlacesResource; -import com.tibiawiki.serviceinterface.ItemsResource; -import com.tibiawiki.serviceinterface.KeysResource; -import com.tibiawiki.serviceinterface.LocationsResource; -import com.tibiawiki.serviceinterface.LootStatisticsResource; -import com.tibiawiki.serviceinterface.MissilesResource; -import com.tibiawiki.serviceinterface.MountsResource; -import com.tibiawiki.serviceinterface.NPCsResource; -import com.tibiawiki.serviceinterface.ObjectsResource; -import com.tibiawiki.serviceinterface.OutfitsResource; -import com.tibiawiki.serviceinterface.QuestsResource; -import com.tibiawiki.serviceinterface.SpellsResource; -import com.tibiawiki.serviceinterface.StreetsResource; +import com.tibiawiki.serviceinterface.*; import io.swagger.jaxrs.config.BeanConfig; import io.swagger.jaxrs.listing.ApiListingResource; import io.swagger.jaxrs.listing.SwaggerSerializers; @@ -59,6 +41,7 @@ private void registerEndpoints() { register(EffectsResource.class); register(LocationsResource.class); register(LootStatisticsResource.class); + register(LootStatisticsV2Resource.class); register(HuntingPlacesResource.class); register(ItemsResource.class); register(KeysResource.class); @@ -78,7 +61,7 @@ private void configureSwagger() { BeanConfig beanConfig = new BeanConfig(); beanConfig.setConfigId("tibiawikiapi"); beanConfig.setTitle("TibiaWikiApi"); - beanConfig.setVersion("1.7.2"); + beanConfig.setVersion("1.8.0"); beanConfig.setContact("B. Komen"); beanConfig.setSchemes(new String[]{"https"}); beanConfig.setBasePath(this.apiPath); // location where dynamically created swagger.json is reachable diff --git a/src/test/java/com/tibiawiki/domain/factories/ArticleFactoryTest.java b/src/test/java/com/tibiawiki/domain/factories/ArticleFactoryTest.java index ac4b2e4..e8a47d7 100644 --- a/src/test/java/com/tibiawiki/domain/factories/ArticleFactoryTest.java +++ b/src/test/java/com/tibiawiki/domain/factories/ArticleFactoryTest.java @@ -2,11 +2,9 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.function.Executable; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; -import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.hamcrest.Matchers.*; public class ArticleFactoryTest { @@ -23,6 +21,39 @@ public class ArticleFactoryTest { "|Bear Paw, times:1043, amount:1, total:1043\n" + "|Honeycomb, times:250, amount:1, total:249\n" + "}}"; + private static final String SOME_TEXT_ONLY_LOOT2_RC_TEMPLATE = "{{Loot2_RC\n" + + "|version=8.6\n" + + "|kills=52807\n" + + "|name=Bear\n" + + "|Empty, times:24777\n" + + "|Meat, times:21065\n" + + "|Ham, times:10581\n" + + "|Bear Paw, times:1043, amount:1, total:1043\n" + + "|Honeycomb, times:250, amount:1, total:249\n" + + "}}"; + private static final String SOME_TEXT_BOTH_LOOT2_AND_LOOT2_RC_TEMPLATE = "__NOWYSIWYG__\n" + + "\n" + + "{{Loot2\n" + + "|version=8.6\n" + + "|kills=52807\n" + + "|name=Bear\n" + + "|Empty, times:24777\n" + + "|Meat, times:21065\n" + + "|Ham, times:10581\n" + + "|Bear Paw, times:1043, amount:1, total:1043\n" + + "|Honeycomb, times:250, amount:1, total:249\n" + + "}}\n" + + "\n" + + "{{Loot2_RC\n" + + "|version=8.6\n" + + "|kills=52807\n" + + "|name=Bear\n" + + "|Empty, times:24777\n" + + "|Meat, times:21065\n" + + "|Ham, times:10581\n" + + "|Bear Paw, times:1043, amount:1, total:1043\n" + + "|Honeycomb, times:250, amount:1, total:249\n" + + "}}"; private static final String SOME_TEXT_INFOBOX_WITH_BEFORE_AND_AFTER = "{{merge|blabla}}{{Infobox Achievement|List={{{1|}}}|GetValue={{{GetValue|}}}\n" + "| name = Goo Goo Dancer\n" + @@ -134,27 +165,64 @@ public void testExtractLootPartOfArticle_ALotOfStuffInArticleText() { assertThat(result, is(SOME_TEXT_ONLY_LOOT2_TEMPLATE)); } + @Test + public void testExtractAllLootPartsOfArticle_EmptyText() { + var result = target.extractAllLootPartsOfArticle("Unknown", SOME_TEXT_EMPTY); + + assertThat("Test: empty text results in no matches", result.isEmpty()); + } + + @Test + public void testExtractAllLootPartsOfArticle_NoLoot2OrLoot2RCTemplate() { + assertThat("Test: no Loot2 or Loot2_RC template results in no matches", + target.extractAllLootPartsOfArticle("Unknown", SOME_TEXT_NO_INFOBOX).isEmpty()); + } + + @Test + public void testExtractAllLootPartsOfArticle_OnlyLoot2TemplateInArticleText() { + var result = target.extractAllLootPartsOfArticle("Unknown", SOME_TEXT_ONLY_LOOT2_TEMPLATE); + + assertThat(result.get("loot2"), is(SOME_TEXT_ONLY_LOOT2_TEMPLATE)); + assertThat(result.get("loot2_rc"), nullValue()); + } + + @Test + public void testExtractAllLootPartsOfArticle_OnlyLoot2RCTemplateInArticleText() { + var result = target.extractAllLootPartsOfArticle("Unknown", SOME_TEXT_ONLY_LOOT2_RC_TEMPLATE); + + assertThat(result.get("loot2_rc"), is(SOME_TEXT_ONLY_LOOT2_RC_TEMPLATE)); + assertThat(result.get("loot2"), nullValue()); + } + + @Test + public void testExtractAllLootPartsOfArticle_BothLoot2AndLoot2RCTemplateInArticleText() { + var result = target.extractAllLootPartsOfArticle("Unknown", SOME_TEXT_BOTH_LOOT2_AND_LOOT2_RC_TEMPLATE); + + assertThat(result.get("loot2_rc"), notNullValue()); + assertThat(result.get("loot2"), notNullValue()); + } + @Test void testInsertInfoboxPartOfArticle_Empty() { - Executable closure = () -> target.insertInfoboxPartOfArticle(SOME_TEXT_EMPTY, "foobar"); - assertThrows(IllegalArgumentException.class, closure); + var result = target.insertInfoboxPartOfArticle(SOME_TEXT_EMPTY, "foobar"); + assertThat("Empty result when empty input", result.isEmpty()); } @Test void testInsertInfoboxPartOfArticle_NoInfobox() { - Executable closure = () -> target.insertInfoboxPartOfArticle(SOME_TEXT_NO_INFOBOX, "foobar"); - assertThrows(IllegalArgumentException.class, closure); + var result = target.insertInfoboxPartOfArticle(SOME_TEXT_NO_INFOBOX, "foobar"); + assertThat("Empty result when no infobox in input", result.isEmpty()); } @Test void testInsertInfoboxPartOfArticle_OnlyInfoboxInArticleText() { - String result = target.insertInfoboxPartOfArticle(SOME_TEXT_ONLY_INFOBOX, SOME_TEXT_ONLY_INFOBOX2); - assertThat(result, is(SOME_TEXT_ONLY_INFOBOX2)); + var result = target.insertInfoboxPartOfArticle(SOME_TEXT_ONLY_INFOBOX, SOME_TEXT_ONLY_INFOBOX2); + assertThat(result.get(), is(SOME_TEXT_ONLY_INFOBOX2)); } @Test void testInsertInfoboxPartOfArticle_WithTextBeforeAndAfter() { - String result = target.insertInfoboxPartOfArticle(SOME_TEXT_INFOBOX_WITH_BEFORE_AND_AFTER, SOME_TEXT_ONLY_INFOBOX2); - assertThat(result, is(SOME_TEXT_INFOBOX_WITH_BEFORE_AND_AFTER2)); + var result = target.insertInfoboxPartOfArticle(SOME_TEXT_INFOBOX_WITH_BEFORE_AND_AFTER, SOME_TEXT_ONLY_INFOBOX2); + assertThat(result.get(), is(SOME_TEXT_INFOBOX_WITH_BEFORE_AND_AFTER2)); } } \ No newline at end of file diff --git a/src/test/java/com/tibiawiki/process/ModifyAnyTest.java b/src/test/java/com/tibiawiki/process/ModifyAnyTest.java index 60a0536..8c9e73f 100644 --- a/src/test/java/com/tibiawiki/process/ModifyAnyTest.java +++ b/src/test/java/com/tibiawiki/process/ModifyAnyTest.java @@ -14,6 +14,7 @@ import org.mockito.Mock; import java.util.List; +import java.util.Optional; import static org.hamcrest.MatcherAssert.assertThat; import static org.mockito.ArgumentMatchers.*; @@ -49,7 +50,7 @@ public void testModify_Success() { doReturn("").when(articleRepository).getArticle(anyString()); doReturn(SOME_JSON_OBJECT).when(wikiObjectFactory).createJSONObject(eq(someAchievement), anyString()); doReturn("").when(jsonFactory).convertJsonToInfoboxPartOfArticle(any(JSONObject.class), any(List.class)); - doReturn("").when(articleFactory).insertInfoboxPartOfArticle(anyString(), anyString()); + doReturn(Optional.of("")).when(articleFactory).insertInfoboxPartOfArticle(anyString(), anyString()); doReturn(true).when(articleRepository).modifyArticle(anyString(), anyString(), anyString()); Try result = target.modify(someAchievement, "[test] editing the page"); @@ -63,7 +64,7 @@ public void testModify_Failure() { doReturn("").when(articleRepository).getArticle(anyString()); doReturn(SOME_JSON_OBJECT).when(wikiObjectFactory).createJSONObject(eq(someAchievement), anyString()); doReturn("").when(jsonFactory).convertJsonToInfoboxPartOfArticle(any(JSONObject.class), any(List.class)); - doReturn("").when(articleFactory).insertInfoboxPartOfArticle(anyString(), anyString()); + doReturn(Optional.empty()).when(articleFactory).insertInfoboxPartOfArticle(anyString(), anyString()); doReturn(false).when(articleRepository).modifyArticle(anyString(), anyString(), anyString()); Try result = target.modify(someAchievement, "[test] editing the page");