From dea36e09e00326ab25c290ea98cd2cf950d3549d Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Wed, 1 Apr 2026 08:06:27 -0300 Subject: [PATCH 1/3] Rename "others" folder to "feeds" --- .../cc/wordview/api/controller/HomeController.java | 9 ++++----- .../cc/wordview/api/runtime/ResourceResolver.java | 12 ++++++------ src/main/resources/application-dev.properties | 2 +- src/main/resources/application-prod.properties | 3 ++- src/main/resources/{others => feeds}/home.json | 0 5 files changed, 13 insertions(+), 13 deletions(-) rename src/main/resources/{others => feeds}/home.json (100%) diff --git a/src/main/java/cc/wordview/api/controller/HomeController.java b/src/main/java/cc/wordview/api/controller/HomeController.java index 1c6a8de..fa9719e 100644 --- a/src/main/java/cc/wordview/api/controller/HomeController.java +++ b/src/main/java/cc/wordview/api/controller/HomeController.java @@ -19,12 +19,11 @@ import cc.wordview.api.Application; import cc.wordview.api.runtime.ResourceResolver; +import cc.wordview.gengolex.Language; +import cc.wordview.gengolex.LanguageNotFoundException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.CrossOrigin; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import java.io.FileInputStream; import java.io.IOException; @@ -42,7 +41,7 @@ public class HomeController { @GetMapping(produces = "application/json;charset=utf-8") public ResponseEntity getHome() throws IOException { - try (InputStream homeFile = new FileInputStream(resourceResolver.getOthersPath() + "/home.json")) { + try (InputStream homeFile = new FileInputStream(resourceResolver.getFeedsPath() + "/home.json")) { String text = new String(homeFile.readAllBytes(), StandardCharsets.UTF_8) .replace("\n", ""); diff --git a/src/main/java/cc/wordview/api/runtime/ResourceResolver.java b/src/main/java/cc/wordview/api/runtime/ResourceResolver.java index 5bb4de2..bcbfe56 100644 --- a/src/main/java/cc/wordview/api/runtime/ResourceResolver.java +++ b/src/main/java/cc/wordview/api/runtime/ResourceResolver.java @@ -49,8 +49,8 @@ public class ResourceResolver { @Value("${wordview.translations_path}") private String translationsPath; - @Value("${wordview.others_path}") - private String othersPath; + @Value("${wordview.feeds_path}") + private String feedsPath; @PostConstruct public void debugShowPaths() { @@ -61,9 +61,9 @@ public void debugShowPaths() { Lyrics Path: {} Phrases Path: {} Translations Path: {} - Others Path: {} + Feeds Path: {} """, - dictionariesPath, imagesPath, lyricsPath, phrasesPath, translationsPath, othersPath); + dictionariesPath, imagesPath, lyricsPath, phrasesPath, translationsPath, feedsPath); } public String getDictionariesPath() throws IOException { @@ -86,8 +86,8 @@ public String getTranslationsPath() throws IOException { return resolvePathOrClasspath(translationsPath); } - public String getOthersPath() throws IOException { - return resolvePathOrClasspath(othersPath); + public String getFeedsPath() throws IOException { + return resolvePathOrClasspath(feedsPath); } /** diff --git a/src/main/resources/application-dev.properties b/src/main/resources/application-dev.properties index 67c3226..b7415a5 100644 --- a/src/main/resources/application-dev.properties +++ b/src/main/resources/application-dev.properties @@ -20,8 +20,8 @@ wordview.images_path=classpath:/images/ wordview.lyrics_path=classpath:/lyrics/ wordview.phrases_path=classpath:/phrases/ wordview.translations_path=classpath:/translations/ +wordview.feeds_path=classpath:/feeds/ spring.jackson.default-property-inclusion=non_null -wordview.others_path=classpath:/others/ server.compression.enabled=true server.compression.mime-types=text/html,text/xml,text/plain,text/css,text/javascript,application/javascript,application/json diff --git a/src/main/resources/application-prod.properties b/src/main/resources/application-prod.properties index 6c118e8..ee92e76 100644 --- a/src/main/resources/application-prod.properties +++ b/src/main/resources/application-prod.properties @@ -17,4 +17,5 @@ spring.jpa.open-in-view=false wordview.dictionaries_path= wordview.images_path= wordview.lyrics_path= -wordview.phrases_path= \ No newline at end of file +wordview.phrases_path= +wordview.feeds_path= \ No newline at end of file diff --git a/src/main/resources/others/home.json b/src/main/resources/feeds/home.json similarity index 100% rename from src/main/resources/others/home.json rename to src/main/resources/feeds/home.json From 5d0fbb6a240c7adbcff3d138151241c24f5993cc Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Wed, 1 Apr 2026 09:00:30 -0300 Subject: [PATCH 2/3] Make MissingServletRequestParameterException return a 400 --- .../api/controller/response/ApiExceptionHandler.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/cc/wordview/api/controller/response/ApiExceptionHandler.java b/src/main/java/cc/wordview/api/controller/response/ApiExceptionHandler.java index 08fe5a2..7078d44 100644 --- a/src/main/java/cc/wordview/api/controller/response/ApiExceptionHandler.java +++ b/src/main/java/cc/wordview/api/controller/response/ApiExceptionHandler.java @@ -22,6 +22,7 @@ import cc.wordview.wordfind.exception.LyricsNotFoundException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @@ -41,7 +42,10 @@ public ResponseEntity unauthorized(Exception e) { return error(HttpStatus.UNAUTHORIZED, e); } - @ExceptionHandler(RequestValidationException.class) + @ExceptionHandler({ + RequestValidationException.class, + MissingServletRequestParameterException.class + }) public ResponseEntity badRequest(Exception e) { return error(HttpStatus.BAD_REQUEST, e); } From e25aad61ea950684aa190fed50a37ac371293ec3 Mon Sep 17 00:00:00 2001 From: Arthur Araujo Date: Wed, 1 Apr 2026 09:01:05 -0300 Subject: [PATCH 3/3] Implement language specific feeds --- .../api/controller/HomeController.java | 23 +++++-- .../wordview/api/runtime/cache/FeedCache.java | 67 +++++++++++++++++++ .../resources/feeds/{home.json => en.json} | 21 ------ src/main/resources/feeds/ja.json | 23 +++++++ src/main/resources/feeds/pt.json | 16 +++++ .../api/controller/HomeControllerTest.java | 53 +++++++++++++++ 6 files changed, 176 insertions(+), 27 deletions(-) create mode 100644 src/main/java/cc/wordview/api/runtime/cache/FeedCache.java rename src/main/resources/feeds/{home.json => en.json} (53%) create mode 100644 src/main/resources/feeds/ja.json create mode 100644 src/main/resources/feeds/pt.json create mode 100644 src/test/java/cc/wordview/api/test/api/controller/HomeControllerTest.java diff --git a/src/main/java/cc/wordview/api/controller/HomeController.java b/src/main/java/cc/wordview/api/controller/HomeController.java index fa9719e..ae368e0 100644 --- a/src/main/java/cc/wordview/api/controller/HomeController.java +++ b/src/main/java/cc/wordview/api/controller/HomeController.java @@ -18,9 +18,12 @@ package cc.wordview.api.controller; import cc.wordview.api.Application; +import cc.wordview.api.exception.NoSuchEntryException; import cc.wordview.api.runtime.ResourceResolver; +import cc.wordview.api.runtime.cache.FeedCache; import cc.wordview.gengolex.Language; import cc.wordview.gengolex.LanguageNotFoundException; +import jakarta.annotation.PostConstruct; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -37,15 +40,23 @@ @RequestMapping(path = Application.API_PATH + "/home") public class HomeController { @Autowired - private ResourceResolver resourceResolver; + private FeedCache cache; @GetMapping(produces = "application/json;charset=utf-8") - public ResponseEntity getHome() throws IOException { - try (InputStream homeFile = new FileInputStream(resourceResolver.getFeedsPath() + "/home.json")) { - String text = new String(homeFile.readAllBytes(), StandardCharsets.UTF_8) - .replace("\n", ""); + public ResponseEntity getHome(@RequestParam String learnLang) throws LanguageNotFoundException, NoSuchEntryException { + // Make gengolex itself check if the tag is valid + Language.Companion.byTag(learnLang); + var home = cache.get(learnLang); - return ok(text.trim()); + if (home == null) { + throw new NoSuchEntryException("Unable to find a feed for this language"); } + + return ok(home); + } + + @PostConstruct + private void initializeFeeds() throws IOException { + cache.init(); } } \ No newline at end of file diff --git a/src/main/java/cc/wordview/api/runtime/cache/FeedCache.java b/src/main/java/cc/wordview/api/runtime/cache/FeedCache.java new file mode 100644 index 0000000..77d256c --- /dev/null +++ b/src/main/java/cc/wordview/api/runtime/cache/FeedCache.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2025 Arthur Araujo + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package cc.wordview.api.runtime.cache; + +import cc.wordview.api.runtime.ResourceResolver; +import cc.wordview.gengolex.Language; +import cc.wordview.gengolex.LanguageNotFoundException; +import lombok.SneakyThrows; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +@Component +public class FeedCache extends HashMapCacheManager { + private static final Logger logger = LoggerFactory.getLogger(FeedCache.class); + + @Autowired + private ResourceResolver resourceResolver; + + // This won't throw language not found, just quiet the compiler here + @SneakyThrows(LanguageNotFoundException.class) + @Override + public void init() throws IOException { + String feedsPath = resourceResolver.getFeedsPath(); + + String[] langTag = { + Language.ENGLISH.getTag(), + Language.PORTUGUESE.getTag(), + Language.JAPANESE.getTag() + }; + + for (var tag : langTag) { + try (InputStream feedFile = new FileInputStream(feedsPath + "/" + tag + ".json")) { + String feedJson = new String(feedFile.readAllBytes(), StandardCharsets.UTF_8) + .replace("\n", "").trim(); + + map.put(tag, feedJson); + } catch (FileNotFoundException e) { + logger.warn("Feed is missing for language {}", Language.Companion.byTag(tag).name()); + } + } + + logger.info("Preloaded {} feeds", map.size()); + } +} diff --git a/src/main/resources/feeds/home.json b/src/main/resources/feeds/en.json similarity index 53% rename from src/main/resources/feeds/home.json rename to src/main/resources/feeds/en.json index e44481c..6585be5 100644 --- a/src/main/resources/feeds/home.json +++ b/src/main/resources/feeds/en.json @@ -17,33 +17,12 @@ "cover": "https://i.ytimg.com/vi_webp/6gluNoLVKiQ/maxresdefault.webp", "duration": 132 }, - { - "id": "ZnUEeXpxBJ0", - "title": "Aquarela", - "artist": "Toquinho", - "cover": "https://i.ytimg.com/vi_webp/ZnUEeXpxBJ0/maxresdefault.webp", - "duration": 252 - }, - { - "id": "ZpT9VCUS54s", - "title": "彗星のパレード", - "artist": "まじ娘", - "cover": "https://i.ytimg.com/vi_webp/ZpT9VCUS54s/maxresdefault.webp", - "duration": 261 - }, { "id": "HCTunqv1Xt4", "title": "When I'm Sixty Four", "artist": "The Beatles", "cover": "https://i.ytimg.com/vi_webp/HCTunqv1Xt4/maxresdefault.webp", "duration": 158 - }, - { - "id": "9NPv4q57on8", - "title": "あの夢をなぞって", - "artist": "YOASOBI", - "cover": "https://i.ytimg.com/vi_webp/9NPv4q57on8/maxresdefault.webp", - "duration": 240 } ] } diff --git a/src/main/resources/feeds/ja.json b/src/main/resources/feeds/ja.json new file mode 100644 index 0000000..f5676ba --- /dev/null +++ b/src/main/resources/feeds/ja.json @@ -0,0 +1,23 @@ +{ + "categories": [ + { + "id": "editors-pick", + "entries": [ + { + "id": "ZpT9VCUS54s", + "title": "彗星のパレード", + "artist": "まじ娘", + "cover": "https://i.ytimg.com/vi_webp/ZpT9VCUS54s/maxresdefault.webp", + "duration": 261 + }, + { + "id": "9NPv4q57on8", + "title": "あの夢をなぞって", + "artist": "YOASOBI", + "cover": "https://i.ytimg.com/vi_webp/9NPv4q57on8/maxresdefault.webp", + "duration": 240 + } + ] + } + ] +} \ No newline at end of file diff --git a/src/main/resources/feeds/pt.json b/src/main/resources/feeds/pt.json new file mode 100644 index 0000000..3a76964 --- /dev/null +++ b/src/main/resources/feeds/pt.json @@ -0,0 +1,16 @@ +{ + "categories": [ + { + "id": "editors-pick", + "entries": [ + { + "id": "ZnUEeXpxBJ0", + "title": "Aquarela", + "artist": "Toquinho", + "cover": "https://i.ytimg.com/vi_webp/ZnUEeXpxBJ0/maxresdefault.webp", + "duration": 252 + } + ] + } + ] +} \ No newline at end of file diff --git a/src/test/java/cc/wordview/api/test/api/controller/HomeControllerTest.java b/src/test/java/cc/wordview/api/test/api/controller/HomeControllerTest.java new file mode 100644 index 0000000..f76a2b8 --- /dev/null +++ b/src/test/java/cc/wordview/api/test/api/controller/HomeControllerTest.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2025 Arthur Araujo + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package cc.wordview.api.test.api.controller; + +import org.junit.jupiter.api.Test; + +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +public class HomeControllerTest extends ControllerTest { + @Test + void noLearnLangSpecified() throws Exception { + req.get("/home").andExpect(status().isBadRequest()); + } + + @Test + void english() throws Exception { + req.get("/home?learnLang=en") + .andExpect(status().isOk()); + } + + @Test + void portuguese() throws Exception { + req.get("/home?learnLang=pt") + .andExpect(status().isOk()); + } + + @Test + void japanese() throws Exception { + req.get("/home?learnLang=ja") + .andExpect(status().isOk()); + } + + @Test + void inexistentLang() throws Exception { + req.get("/home?learnLang=sql") + .andExpect(status().isNotFound()); + } +}