diff --git a/core/src/main/java/dev/felnull/itts/core/savedata/legacy/LegacyMigrator.java b/core/src/main/java/dev/felnull/itts/core/savedata/legacy/LegacyMigrator.java index b98b39c..0170fe4 100644 --- a/core/src/main/java/dev/felnull/itts/core/savedata/legacy/LegacyMigrator.java +++ b/core/src/main/java/dev/felnull/itts/core/savedata/legacy/LegacyMigrator.java @@ -3,6 +3,7 @@ import com.google.common.io.Files; import com.google.gson.Gson; import com.google.gson.JsonObject; +import com.google.gson.JsonSyntaxException; import dev.felnull.fnjl.util.FNDataUtil; import dev.felnull.fnjl.util.FNStringUtil; import dev.felnull.itts.core.dict.CustomDictionaryEntry; @@ -47,19 +48,56 @@ public class LegacyMigrator { */ private final Gson gson = new Gson(); + + /** + * Jsonの保存先ディレクトリ + */ + private final File jsonSaveDir; + + /** + * グローバル辞書ファイル + */ + private final File globalDictFile; + private LegacyMigrator() { + this(JSON_SAVE_DIR, GLOBAL_DICT_DIR); } - private void execute(DataRepository repo) { - migrateServerData(repo); - migrateServerUserData(repo); - migrateServerDictUseData(repo); - migrateServerDictData(repo); - migrateGlobalDictData(repo); + LegacyMigrator(File jsonSaveDir, File globalDictFile) { + this.jsonSaveDir = jsonSaveDir; + this.globalDictFile = globalDictFile; + } + + void moveOldData(File moveDir) throws IOException { + FNDataUtil.wishMkdir(moveDir); + + if (jsonSaveDir.exists()) { + Files.move(jsonSaveDir, new File(moveDir, jsonSaveDir.getName())); + } + + if (globalDictFile.exists()) { + Files.move(globalDictFile, new File(moveDir, globalDictFile.getName())); + } + } + + void execute(DataRepository repo) { + runMigration("ServerData", () -> migrateServerData(repo)); + runMigration("ServerUserData", () -> migrateServerUserData(repo)); + runMigration("ServerDictUseData", () -> migrateServerDictUseData(repo)); + runMigration("ServerDictData", () -> migrateServerDictData(repo)); + runMigration("GlobalDictData", () -> migrateGlobalDictData(repo)); + } + + private void runMigration(String name, Runnable migration) { + try { + migration.run(); + } catch (Exception e) { + LOGGER.error("Migration failed for {}", name, e); + } } private void migrateServerData(DataRepository repo) { - File dir = new File(JSON_SAVE_DIR, "server"); + File dir = new File(jsonSaveDir, "server"); if (!dir.exists()) { return; } @@ -70,41 +108,45 @@ private void migrateServerData(DataRepository repo) { } Arrays.stream(files).forEach((file) -> { - String name = FNStringUtil.removeExtension(file.getName()); - long serverId; try { - serverId = Long.parseLong(name); - } catch (NumberFormatException e) { - LOGGER.error("Invalid file name {}", file.getName()); - return; - } - - JsonObject jo = loadJson(file); - if (jo == null) { - return; + String name = FNStringUtil.removeExtension(file.getName()); + long serverId; + try { + serverId = Long.parseLong(name); + } catch (NumberFormatException e) { + LOGGER.error("Invalid file name {}", file.getName()); + return; + } + + JsonObject jo = loadJson(file); + if (jo == null) { + return; + } + + String defaultVoiceType = JsonUtils.getString(jo, "default_voice_type", null); + String ignoreRegex = JsonUtils.getString(jo, "ignore_regex", "(!|/|\\\\$|`).*"); + boolean needJoin = JsonUtils.getBoolean(jo, "need_join", false); + boolean overwriteAloud = JsonUtils.getBoolean(jo, "overwrite_aloud", false); + boolean notifyMove = JsonUtils.getBoolean(jo, "notify_move", true); + int readLimit = JsonUtils.getInt(jo, "read_limit", 200); + int nameReadLimit = JsonUtils.getInt(jo, "name_read_limit", 20); + + ServerData serverData = repo.getServerData(serverId); + serverData.setDefaultVoiceType(defaultVoiceType); + serverData.setIgnoreRegex(ignoreRegex); + serverData.setNeedJoin(needJoin); + serverData.setOverwriteAloud(overwriteAloud); + serverData.setNotifyMove(notifyMove); + serverData.setReadLimit(readLimit); + serverData.setNameReadLimit(nameReadLimit); + } catch (Exception e) { + LOGGER.error("Migration failed for file {}", file.getName(), e); } - - String defaultVoiceType = JsonUtils.getString(jo, "default_voice_type", null); - String ignoreRegex = JsonUtils.getString(jo, "ignore_regex", "(!|/|\\\\$|`).*"); - boolean needJoin = JsonUtils.getBoolean(jo, "need_join", false); - boolean overwriteAloud = JsonUtils.getBoolean(jo, "overwrite_aloud", false); - boolean notifyMove = JsonUtils.getBoolean(jo, "notify_move", true); - int readLimit = JsonUtils.getInt(jo, "read_limit", 200); - int nameReadLimit = JsonUtils.getInt(jo, "name_read_limit", 20); - - ServerData serverData = repo.getServerData(serverId); - serverData.setDefaultVoiceType(defaultVoiceType); - serverData.setIgnoreRegex(ignoreRegex); - serverData.setNeedJoin(needJoin); - serverData.setOverwriteAloud(overwriteAloud); - serverData.setNotifyMove(notifyMove); - serverData.setReadLimit(readLimit); - serverData.setNameReadLimit(nameReadLimit); }); } private void migrateServerUserData(DataRepository repo) { - File dir = new File(JSON_SAVE_DIR, "server_users"); + File dir = new File(jsonSaveDir, "server_users"); if (!dir.exists()) { return; } @@ -115,51 +157,55 @@ private void migrateServerUserData(DataRepository repo) { } Arrays.stream(files).forEach((file) -> { - String name = FNStringUtil.removeExtension(file.getName()); - long serverId; try { - serverId = Long.parseLong(name); - } catch (NumberFormatException e) { - LOGGER.error("Invalid file name {}", file.getName()); - return; - } - - JsonObject jo = loadJson(file); - if (jo == null) { - return; + String name = FNStringUtil.removeExtension(file.getName()); + long serverId; + try { + serverId = Long.parseLong(name); + } catch (NumberFormatException e) { + LOGGER.error("Invalid file name {}", file.getName()); + return; + } + + JsonObject jo = loadJson(file); + if (jo == null) { + return; + } + + JsonObject dataJo = jo.getAsJsonObject("data"); + if (dataJo == null) { + return; + } + + dataJo.entrySet().stream() + .filter(it -> it.getValue().isJsonObject()) + .forEach((entry) -> { + long userId; + try { + userId = Long.parseLong(entry.getKey()); + } catch (NumberFormatException e) { + LOGGER.error("Invalid data name {}", entry.getKey()); + return; + } + JsonObject entryJo = entry.getValue().getAsJsonObject(); + + String voiceType = JsonUtils.getString(entryJo, "voice_type", null); + boolean deny = JsonUtils.getBoolean(entryJo, "deny", false); + String nickName = JsonUtils.getString(entryJo, "nick_name", null); + + ServerUserData serverUserData = repo.getServerUserData(serverId, userId); + serverUserData.setVoiceType(voiceType); + serverUserData.setDeny(deny); + serverUserData.setNickName(nickName); + }); + } catch (Exception e) { + LOGGER.error("Migration failed for file {}", file.getName(), e); } - - JsonObject dataJo = jo.getAsJsonObject("data"); - if (dataJo == null) { - return; - } - - dataJo.entrySet().stream() - .filter(it -> it.getValue().isJsonObject()) - .forEach((entry) -> { - long userId; - try { - userId = Long.parseLong(name); - } catch (NumberFormatException e) { - LOGGER.error("Invalid data name {}", file.getName()); - return; - } - JsonObject entryJo = entry.getValue().getAsJsonObject(); - - String voiceType = JsonUtils.getString(entryJo, "voice_type", null); - boolean deny = JsonUtils.getBoolean(entryJo, "deny", false); - String nickName = JsonUtils.getString(entryJo, "nick_name", null); - - ServerUserData serverUserData = repo.getServerUserData(serverId, userId); - serverUserData.setVoiceType(voiceType); - serverUserData.setDeny(deny); - serverUserData.setNickName(nickName); - }); }); } private void migrateServerDictUseData(DataRepository repo) { - File dir = new File(JSON_SAVE_DIR, "dict_use"); + File dir = new File(jsonSaveDir, "dict_use"); if (!dir.exists()) { return; } @@ -170,47 +216,51 @@ private void migrateServerDictUseData(DataRepository repo) { } Arrays.stream(files).forEach((file) -> { - String name = FNStringUtil.removeExtension(file.getName()); - long serverId; try { - serverId = Long.parseLong(name); - } catch (NumberFormatException e) { - LOGGER.error("Invalid file name {}", file.getName()); - return; - } - - JsonObject jo = loadJson(file); - if (jo == null) { - return; + String name = FNStringUtil.removeExtension(file.getName()); + long serverId; + try { + serverId = Long.parseLong(name); + } catch (NumberFormatException e) { + LOGGER.error("Invalid file name {}", file.getName()); + return; + } + + JsonObject jo = loadJson(file); + if (jo == null) { + return; + } + + JsonObject dataJo = jo.getAsJsonObject("data"); + if (dataJo == null) { + return; + } + + dataJo.entrySet().stream() + .filter(it -> it.getValue().isJsonPrimitive()) + .filter(it -> it.getValue().getAsJsonPrimitive().isNumber()) + .forEach((entry) -> { + String dictName = entry.getKey(); + int priority = entry.getValue().getAsInt(); + + DictionaryUseData dictionaryUseData = repo.getDictionaryUseData(serverId, dictName); + + if (priority >= 0) { + dictionaryUseData.setEnable(true); + dictionaryUseData.setPriority(priority); + } else { + dictionaryUseData.setEnable(false); + dictionaryUseData.setPriority(null); + } + }); + } catch (Exception e) { + LOGGER.error("Migration failed for file {}", file.getName(), e); } - - JsonObject dataJo = jo.getAsJsonObject("data"); - if (dataJo == null) { - return; - } - - dataJo.entrySet().stream() - .filter(it -> it.getValue().isJsonPrimitive()) - .filter(it -> it.getValue().getAsJsonPrimitive().isNumber()) - .forEach((entry) -> { - String dictName = entry.getKey(); - int priority = entry.getValue().getAsInt(); - - DictionaryUseData dictionaryUseData = repo.getDictionaryUseData(serverId, dictName); - - if (priority >= 0) { - dictionaryUseData.setEnable(true); - dictionaryUseData.setPriority(priority); - } else { - dictionaryUseData.setEnable(false); - dictionaryUseData.setPriority(null); - } - }); }); } private void migrateServerDictData(DataRepository repo) { - File dir = new File(JSON_SAVE_DIR, "server_dict"); + File dir = new File(jsonSaveDir, "server_dict"); if (!dir.exists()) { return; } @@ -221,41 +271,51 @@ private void migrateServerDictData(DataRepository repo) { } Arrays.stream(files).forEach((file) -> { - String name = FNStringUtil.removeExtension(file.getName()); - long serverId; try { - serverId = Long.parseLong(name); - } catch (NumberFormatException e) { - LOGGER.error("Invalid file name {}", file.getName()); - return; + String name = FNStringUtil.removeExtension(file.getName()); + long serverId; + try { + serverId = Long.parseLong(name); + } catch (NumberFormatException e) { + LOGGER.error("Invalid file name {}", file.getName()); + return; + } + + JsonObject jo = loadJson(file); + if (jo == null) { + return; + } + + Map dictEntry = loadDict(jo); + CustomDictionaryData dictionaryData = repo.getServerCustomDictionaryData(serverId); + dictEntry.forEach((target, read) -> { + if (dictionaryData.getByTarget(target).isEmpty()) { + dictionaryData.add(new CustomDictionaryEntry(target, read, ReplaceType.WORD)); + } + }); + } catch (Exception e) { + LOGGER.error("Migration failed for file {}", file.getName(), e); } - - JsonObject jo = loadJson(file); - if (jo == null) { - return; - } - - Map dictEntry = loadDict(jo); - CustomDictionaryData dictionaryData = repo.getServerCustomDictionaryData(serverId); - dictEntry.forEach((target, read) -> { - dictionaryData.add(new CustomDictionaryEntry(target, read, ReplaceType.WORD)); - }); }); } private void migrateGlobalDictData(DataRepository repo) { - if (!GLOBAL_DICT_DIR.exists()) { + if (!globalDictFile.exists()) { return; } - JsonObject jo = loadJson(GLOBAL_DICT_DIR); + JsonObject jo = loadJson(globalDictFile); if (jo == null) { return; } Map dictEntry = loadDict(jo); CustomDictionaryData dictionaryData = repo.getGlobalCustomDictionaryData(); - dictEntry.forEach((target, read) -> dictionaryData.add(new CustomDictionaryEntry(target, read, ReplaceType.WORD))); + dictEntry.forEach((target, read) -> { + if (dictionaryData.getByTarget(target).isEmpty()) { + dictionaryData.add(new CustomDictionaryEntry(target, read, ReplaceType.WORD)); + } + }); } private Map loadDict(JsonObject jo) { @@ -280,11 +340,16 @@ private JsonObject loadJson(File file) { try (Reader reader = new FileReader(file); Reader bufReader = new BufferedReader(reader)) { jo = gson.fromJson(bufReader, JsonObject.class); - } catch (IOException e) { + } catch (IOException | JsonSyntaxException e) { LOGGER.error("Loading failed {}", file.getName(), e); return null; } + if (jo == null) { + LOGGER.error("Empty json file {}", file.getName()); + return null; + } + int version = JsonUtils.getInt(jo, "version", -1); if (version != 0) { LOGGER.error("Unsupported config version {}", file.getName()); @@ -314,9 +379,12 @@ public static void checkAndExecution(Supplier daoProvider) { // Jsonを読み取ってDBに書き込む LegacyMigrator migrator = new LegacyMigrator(); - migrator.execute(repo); - repo.dispose(); + try { + migrator.execute(repo); + } finally { + repo.dispose(); + } // 旧データを退避 DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH.mm.ss"); @@ -324,14 +392,7 @@ public static void checkAndExecution(Supplier daoProvider) { File moveDir = new File("old_save_data-" + timeText); try { - if (JSON_SAVE_DIR.exists()) { - Files.move(JSON_SAVE_DIR, moveDir); - } - - if (GLOBAL_DICT_DIR.exists()) { - FNDataUtil.wishMkdir(moveDir); - Files.move(GLOBAL_DICT_DIR, new File(moveDir, "global_dict.json")); - } + migrator.moveOldData(moveDir); } catch (IOException e) { throw new IllegalStateException("Failed to move Old SaveData", e); } diff --git a/core/src/test/java/dev/felnull/itts/core/savedata/legacy/LegacyMigratorTest.java b/core/src/test/java/dev/felnull/itts/core/savedata/legacy/LegacyMigratorTest.java new file mode 100644 index 0000000..95da5b5 --- /dev/null +++ b/core/src/test/java/dev/felnull/itts/core/savedata/legacy/LegacyMigratorTest.java @@ -0,0 +1,546 @@ +package dev.felnull.itts.core.savedata.legacy; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import dev.felnull.itts.core.dict.ReplaceType; +import dev.felnull.itts.core.savedata.dao.DAO; +import dev.felnull.itts.core.savedata.dao.DAOFactory; +import dev.felnull.itts.core.savedata.repository.*; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.Mockito; + +import java.io.*; +import java.nio.file.Path; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.*; + +class LegacyMigratorTest { + + private static final long SERVER_ID = 1219628077353668688L; + private static final long USER_ID_1 = 1346500316383805532L; + private static final long USER_ID_2 = 328520268274204673L; + + private final Gson gson = new Gson(); + + private DataRepository createRepository(Path tempDir) { + File dbFile = new File(tempDir.toFile(), "test.db"); + DataRepository repo = DataRepository.create( + DAOFactory.getInstance().createSQLiteDAO(dbFile)); + repo.init(); + return repo; + } + + private void writeJson(File file, JsonObject jo) throws IOException { + file.getParentFile().mkdirs(); + try (Writer writer = new FileWriter(file)) { + gson.toJson(jo, writer); + } + } + + private void writeRawFile(File file, String content) throws IOException { + file.getParentFile().mkdirs(); + try (Writer writer = new FileWriter(file)) { + writer.write(content); + } + } + + private JsonObject createVersionedJson() { + JsonObject jo = new JsonObject(); + jo.addProperty("version", 0); + return jo; + } + + @Test + void testMigrateServerData(@TempDir Path tempDir) throws IOException { + File saveDir = new File(tempDir.toFile(), "save_data"); + File globalDict = new File(tempDir.toFile(), "global_dict.json"); + + JsonObject serverJson = createVersionedJson(); + serverJson.addProperty("ignore_regex", "(!|/|\\\\$|`).*"); + serverJson.addProperty("need_join", true); + serverJson.addProperty("overwrite_aloud", true); + serverJson.addProperty("notify_move", false); + serverJson.addProperty("read_limit", 100); + serverJson.addProperty("name_read_limit", 10); + + writeJson(new File(saveDir, "server/" + SERVER_ID + ".json"), serverJson); + + DataRepository repo = createRepository(tempDir); + LegacyMigrator migrator = new LegacyMigrator(saveDir, globalDict); + migrator.execute(repo); + + ServerData serverData = repo.getServerData(SERVER_ID); + assertNull(serverData.getDefaultVoiceType()); + assertEquals("(!|/|\\\\$|`).*", serverData.getIgnoreRegex()); + assertTrue(serverData.isNeedJoin()); + assertTrue(serverData.isOverwriteAloud()); + assertFalse(serverData.isNotifyMove()); + assertEquals(100, serverData.getReadLimit()); + assertEquals(10, serverData.getNameReadLimit()); + + repo.dispose(); + } + + @Test + void testMigrateServerUserData_userIdNotServerId(@TempDir Path tempDir) throws IOException { + File saveDir = new File(tempDir.toFile(), "save_data"); + File globalDict = new File(tempDir.toFile(), "global_dict.json"); + + JsonObject user1 = new JsonObject(); + user1.addProperty("deny", true); + user1.addProperty("nick_name", "テストユーザー1"); + + JsonObject user2 = new JsonObject(); + user2.addProperty("deny", false); + user2.addProperty("voice_type", "katyou"); + + JsonObject data = new JsonObject(); + data.add(String.valueOf(USER_ID_1), user1); + data.add(String.valueOf(USER_ID_2), user2); + + JsonObject usersJson = createVersionedJson(); + usersJson.add("data", data); + + writeJson(new File(saveDir, "server_users/" + SERVER_ID + ".json"), usersJson); + + DataRepository repo = createRepository(tempDir); + LegacyMigrator migrator = new LegacyMigrator(saveDir, globalDict); + migrator.execute(repo); + + ServerUserData userData1 = repo.getServerUserData(SERVER_ID, USER_ID_1); + assertTrue(userData1.isDeny()); + assertEquals("テストユーザー1", userData1.getNickName()); + assertNull(userData1.getVoiceType()); + + ServerUserData userData2 = repo.getServerUserData(SERVER_ID, USER_ID_2); + assertFalse(userData2.isDeny()); + assertEquals("katyou", userData2.getVoiceType()); + assertNull(userData2.getNickName()); + + repo.dispose(); + } + + @Test + void testMigrateServerDictData(@TempDir Path tempDir) throws IOException { + File saveDir = new File(tempDir.toFile(), "save_data"); + File globalDict = new File(tempDir.toFile(), "global_dict.json"); + + JsonObject data = new JsonObject(); + data.addProperty("test", "テスト"); + data.addProperty("hello", "こんにちは"); + + JsonObject dictJson = createVersionedJson(); + dictJson.add("data", data); + + writeJson(new File(saveDir, "server_dict/" + SERVER_ID + ".json"), dictJson); + + DataRepository repo = createRepository(tempDir); + LegacyMigrator migrator = new LegacyMigrator(saveDir, globalDict); + migrator.execute(repo); + + CustomDictionaryData dictData = repo.getServerCustomDictionaryData(SERVER_ID); + List entries = dictData.getAll(); + assertEquals(2, entries.size()); + + Set targets = entries.stream() + .map(e -> e.entry().target()) + .collect(Collectors.toSet()); + assertTrue(targets.contains("test")); + assertTrue(targets.contains("hello")); + + repo.dispose(); + } + + @Test + void testMigrateGlobalDictData(@TempDir Path tempDir) throws IOException { + File saveDir = new File(tempDir.toFile(), "save_data"); + File globalDict = new File(tempDir.toFile(), "global_dict.json"); + + JsonObject data = new JsonObject(); + data.addProperty("world", "せかい"); + data.addProperty("abc", "えーびーしー"); + + JsonObject dictJson = createVersionedJson(); + dictJson.add("data", data); + + writeJson(globalDict, dictJson); + + DataRepository repo = createRepository(tempDir); + LegacyMigrator migrator = new LegacyMigrator(saveDir, globalDict); + migrator.execute(repo); + + CustomDictionaryData dictData = repo.getGlobalCustomDictionaryData(); + List entries = dictData.getAll(); + assertEquals(2, entries.size()); + + Set targets = entries.stream() + .map(e -> e.entry().target()) + .collect(Collectors.toSet()); + assertTrue(targets.contains("world")); + assertTrue(targets.contains("abc")); + + entries.forEach(e -> assertEquals(ReplaceType.WORD, e.entry().replaceType())); + + repo.dispose(); + } + + @Test + void testMigrateServerDictUseData(@TempDir Path tempDir) throws IOException { + File saveDir = new File(tempDir.toFile(), "save_data"); + File globalDict = new File(tempDir.toFile(), "global_dict.json"); + + JsonObject data = new JsonObject(); + data.addProperty("server", 2); + data.addProperty("global", 3); + data.addProperty("unit", -1); + + JsonObject dictUseJson = createVersionedJson(); + dictUseJson.add("data", data); + + writeJson(new File(saveDir, "dict_use/" + SERVER_ID + ".json"), dictUseJson); + + DataRepository repo = createRepository(tempDir); + LegacyMigrator migrator = new LegacyMigrator(saveDir, globalDict); + migrator.execute(repo); + + DictionaryUseData serverDict = repo.getDictionaryUseData(SERVER_ID, "server"); + assertEquals(true, serverDict.isEnable()); + assertEquals(2, serverDict.getPriority()); + + DictionaryUseData globalDictUse = repo.getDictionaryUseData(SERVER_ID, "global"); + assertEquals(true, globalDictUse.isEnable()); + assertEquals(3, globalDictUse.getPriority()); + + DictionaryUseData unitDict = repo.getDictionaryUseData(SERVER_ID, "unit"); + assertEquals(false, unitDict.isEnable()); + assertNull(unitDict.getPriority()); + + repo.dispose(); + } + + @Test + void testMalformedJsonSkipped(@TempDir Path tempDir) throws IOException { + File saveDir = new File(tempDir.toFile(), "save_data"); + File globalDict = new File(tempDir.toFile(), "global_dict.json"); + + writeRawFile(new File(saveDir, "server/" + SERVER_ID + ".json"), "{invalid json!!!"); + + JsonObject validUsersJson = createVersionedJson(); + JsonObject userData = new JsonObject(); + JsonObject user = new JsonObject(); + user.addProperty("deny", true); + userData.add(String.valueOf(USER_ID_1), user); + validUsersJson.add("data", userData); + writeJson(new File(saveDir, "server_users/" + SERVER_ID + ".json"), validUsersJson); + + DataRepository repo = createRepository(tempDir); + LegacyMigrator migrator = new LegacyMigrator(saveDir, globalDict); + + assertDoesNotThrow(() -> migrator.execute(repo)); + + ServerUserData serverUserData = repo.getServerUserData(SERVER_ID, USER_ID_1); + assertTrue(serverUserData.isDeny()); + + repo.dispose(); + } + + @Test + void testEmptyJsonFileSkipped(@TempDir Path tempDir) throws IOException { + File saveDir = new File(tempDir.toFile(), "save_data"); + File globalDict = new File(tempDir.toFile(), "global_dict.json"); + + writeRawFile(new File(saveDir, "server/" + SERVER_ID + ".json"), ""); + + DataRepository repo = createRepository(tempDir); + LegacyMigrator migrator = new LegacyMigrator(saveDir, globalDict); + + assertDoesNotThrow(() -> migrator.execute(repo)); + + repo.dispose(); + } + + @Test + void testDictMigrationIdempotent(@TempDir Path tempDir) throws IOException { + File saveDir = new File(tempDir.toFile(), "save_data"); + File globalDict = new File(tempDir.toFile(), "global_dict.json"); + + JsonObject serverDictData = new JsonObject(); + serverDictData.addProperty("aaa", "えーえーえー"); + serverDictData.addProperty("bbb", "びーびーびー"); + + JsonObject serverDictJson = createVersionedJson(); + serverDictJson.add("data", serverDictData); + writeJson(new File(saveDir, "server_dict/" + SERVER_ID + ".json"), serverDictJson); + + JsonObject globalDictData = new JsonObject(); + globalDictData.addProperty("xxx", "えっくすえっくすえっくす"); + + JsonObject globalDictJson = createVersionedJson(); + globalDictJson.add("data", globalDictData); + writeJson(globalDict, globalDictJson); + + DataRepository repo = createRepository(tempDir); + LegacyMigrator migrator = new LegacyMigrator(saveDir, globalDict); + + migrator.execute(repo); + migrator.execute(repo); + + CustomDictionaryData serverDict = repo.getServerCustomDictionaryData(SERVER_ID); + assertEquals(2, serverDict.getAll().size()); + + CustomDictionaryData globalDictionary = repo.getGlobalCustomDictionaryData(); + assertEquals(1, globalDictionary.getAll().size()); + + repo.dispose(); + } + + @Test + void testNonExistentDirectoriesSkipped(@TempDir Path tempDir) { + File saveDir = new File(tempDir.toFile(), "save_data"); + File globalDict = new File(tempDir.toFile(), "global_dict.json"); + + DataRepository repo = createRepository(tempDir); + LegacyMigrator migrator = new LegacyMigrator(saveDir, globalDict); + + assertDoesNotThrow(() -> migrator.execute(repo)); + + repo.dispose(); + } + + @Test + void testInvalidFileNameSkipped(@TempDir Path tempDir) throws IOException { + File saveDir = new File(tempDir.toFile(), "save_data"); + File globalDict = new File(tempDir.toFile(), "global_dict.json"); + + JsonObject validJson = createVersionedJson(); + validJson.add("data", new JsonObject()); + + writeJson(new File(saveDir, "server/not_a_number.json"), validJson); + writeJson(new File(saveDir, "server_users/invalid.json"), validJson); + writeJson(new File(saveDir, "dict_use/abc.json"), validJson); + writeJson(new File(saveDir, "server_dict/xyz.json"), validJson); + + DataRepository repo = createRepository(tempDir); + LegacyMigrator migrator = new LegacyMigrator(saveDir, globalDict); + + assertDoesNotThrow(() -> migrator.execute(repo)); + + repo.dispose(); + } + + @Test + void testInvalidUserIdSkipped(@TempDir Path tempDir) throws IOException { + File saveDir = new File(tempDir.toFile(), "save_data"); + File globalDict = new File(tempDir.toFile(), "global_dict.json"); + + JsonObject invalidUser = new JsonObject(); + invalidUser.addProperty("deny", true); + + JsonObject validUser = new JsonObject(); + validUser.addProperty("deny", true); + + JsonObject data = new JsonObject(); + data.add("not_a_number", invalidUser); + data.add(String.valueOf(USER_ID_1), validUser); + + JsonObject usersJson = createVersionedJson(); + usersJson.add("data", data); + + writeJson(new File(saveDir, "server_users/" + SERVER_ID + ".json"), usersJson); + + DataRepository repo = createRepository(tempDir); + LegacyMigrator migrator = new LegacyMigrator(saveDir, globalDict); + + assertDoesNotThrow(() -> migrator.execute(repo)); + + ServerUserData userData = repo.getServerUserData(SERVER_ID, USER_ID_1); + assertTrue(userData.isDeny()); + + repo.dispose(); + } + + @Test + void testUnsupportedVersionSkipped(@TempDir Path tempDir) throws IOException { + File saveDir = new File(tempDir.toFile(), "save_data"); + File globalDict = new File(tempDir.toFile(), "global_dict.json"); + + JsonObject serverJson = new JsonObject(); + serverJson.addProperty("version", 1); + serverJson.addProperty("need_join", true); + + writeJson(new File(saveDir, "server/" + SERVER_ID + ".json"), serverJson); + + DataRepository repo = createRepository(tempDir); + LegacyMigrator migrator = new LegacyMigrator(saveDir, globalDict); + + assertDoesNotThrow(() -> migrator.execute(repo)); + + ServerData serverData = repo.getServerData(SERVER_ID); + assertFalse(serverData.isNeedJoin()); + + repo.dispose(); + } + + @Test + void testMissingDataKeySkipped(@TempDir Path tempDir) throws IOException { + File saveDir = new File(tempDir.toFile(), "save_data"); + File globalDict = new File(tempDir.toFile(), "global_dict.json"); + + JsonObject noDataJson = createVersionedJson(); + + writeJson(new File(saveDir, "server_users/" + SERVER_ID + ".json"), noDataJson); + writeJson(new File(saveDir, "dict_use/" + SERVER_ID + ".json"), noDataJson); + writeJson(new File(saveDir, "server_dict/" + SERVER_ID + ".json"), noDataJson); + writeJson(globalDict, noDataJson); + + DataRepository repo = createRepository(tempDir); + LegacyMigrator migrator = new LegacyMigrator(saveDir, globalDict); + + assertDoesNotThrow(() -> migrator.execute(repo)); + + CustomDictionaryData serverDict = repo.getServerCustomDictionaryData(SERVER_ID); + assertTrue(serverDict.getAll().isEmpty()); + + CustomDictionaryData globalDictData = repo.getGlobalCustomDictionaryData(); + assertTrue(globalDictData.getAll().isEmpty()); + + repo.dispose(); + } + + @Test + void testMoveOldData_bothExist(@TempDir Path tempDir) throws IOException { + File saveDir = new File(tempDir.toFile(), "save_data"); + File globalDict = new File(tempDir.toFile(), "global_dict.json"); + File moveDir = new File(tempDir.toFile(), "old_save_data"); + + new File(saveDir, "server").mkdirs(); + writeRawFile(new File(saveDir, "server/123.json"), "{}"); + writeRawFile(globalDict, "{}"); + + LegacyMigrator migrator = new LegacyMigrator(saveDir, globalDict); + migrator.moveOldData(moveDir); + + assertTrue(moveDir.exists()); + assertTrue(new File(moveDir, "save_data").exists()); + assertTrue(new File(moveDir, "save_data/server/123.json").exists()); + assertTrue(new File(moveDir, "global_dict.json").exists()); + + assertFalse(saveDir.exists()); + assertFalse(globalDict.exists()); + } + + @Test + void testMoveOldData_onlySaveDir(@TempDir Path tempDir) throws IOException { + File saveDir = new File(tempDir.toFile(), "save_data"); + File globalDict = new File(tempDir.toFile(), "global_dict.json"); + File moveDir = new File(tempDir.toFile(), "old_save_data"); + + saveDir.mkdirs(); + writeRawFile(new File(saveDir, "server/123.json"), "{}"); + + LegacyMigrator migrator = new LegacyMigrator(saveDir, globalDict); + migrator.moveOldData(moveDir); + + assertTrue(new File(moveDir, "save_data").exists()); + assertFalse(new File(moveDir, "global_dict.json").exists()); + assertFalse(saveDir.exists()); + } + + @Test + void testMoveOldData_onlyGlobalDict(@TempDir Path tempDir) throws IOException { + File saveDir = new File(tempDir.toFile(), "save_data"); + File globalDict = new File(tempDir.toFile(), "global_dict.json"); + File moveDir = new File(tempDir.toFile(), "old_save_data"); + + writeRawFile(globalDict, "{}"); + + LegacyMigrator migrator = new LegacyMigrator(saveDir, globalDict); + migrator.moveOldData(moveDir); + + assertTrue(moveDir.exists()); + assertFalse(new File(moveDir, "save_data").exists()); + assertTrue(new File(moveDir, "global_dict.json").exists()); + assertFalse(globalDict.exists()); + } + + @Test + void testFileErrorIsolation(@TempDir Path tempDir) throws Exception { + File saveDir = new File(tempDir.toFile(), "save_data"); + File globalDict = new File(tempDir.toFile(), "global_dict.json"); + + long serverId1 = 111111111111111111L; + long serverId2 = 222222222222222222L; + + JsonObject server1 = createVersionedJson(); + server1.addProperty("need_join", true); + writeJson(new File(saveDir, "server/" + serverId1 + ".json"), server1); + + JsonObject server2 = createVersionedJson(); + server2.addProperty("need_join", true); + writeJson(new File(saveDir, "server/" + serverId2 + ".json"), server2); + + File dbFile = new File(tempDir.toFile(), "test.db"); + DAO dao = DAOFactory.getInstance().createSQLiteDAO(dbFile); + DAO spyDao = Mockito.spy(dao); + DAO.ServerDataTable spyTable = Mockito.spy(spyDao.serverDataTable()); + Mockito.doThrow(new IllegalStateException("test")) + .doCallRealMethod() + .when(spyTable) + .insertRecordIfNotExists(Mockito.any(), Mockito.any(), Mockito.any()); + Mockito.when(spyDao.serverDataTable()).thenReturn(spyTable); + + DataRepository repo = DataRepository.create(spyDao); + repo.init(); + + LegacyMigrator migrator = new LegacyMigrator(saveDir, globalDict); + assertDoesNotThrow(() -> migrator.execute(repo)); + + boolean s1 = repo.getServerData(serverId1).isNeedJoin(); + boolean s2 = repo.getServerData(serverId2).isNeedJoin(); + assertTrue(s1 || s2, "At least one server should have been migrated"); + + repo.dispose(); + } + + @Test + void testPhaseErrorIsolation(@TempDir Path tempDir) throws Exception { + File saveDir = new File(tempDir.toFile(), "save_data"); + File globalDict = new File(tempDir.toFile(), "global_dict.json"); + + JsonObject serverJson = createVersionedJson(); + serverJson.addProperty("need_join", true); + writeJson(new File(saveDir, "server/" + SERVER_ID + ".json"), serverJson); + + JsonObject user = new JsonObject(); + user.addProperty("deny", true); + JsonObject data = new JsonObject(); + data.add(String.valueOf(USER_ID_1), user); + JsonObject usersJson = createVersionedJson(); + usersJson.add("data", data); + writeJson(new File(saveDir, "server_users/" + SERVER_ID + ".json"), usersJson); + + File dbFile = new File(tempDir.toFile(), "test.db"); + DAO dao = DAOFactory.getInstance().createSQLiteDAO(dbFile); + DAO spyDao = Mockito.spy(dao); + DAO.ServerDataTable spyTable = Mockito.spy(spyDao.serverDataTable()); + Mockito.doThrow(new IllegalStateException("test")) + .when(spyTable) + .insertRecordIfNotExists(Mockito.any(), Mockito.any(), Mockito.any()); + Mockito.when(spyDao.serverDataTable()).thenReturn(spyTable); + + DataRepository repo = DataRepository.create(spyDao); + repo.init(); + + LegacyMigrator migrator = new LegacyMigrator(saveDir, globalDict); + assertDoesNotThrow(() -> migrator.execute(repo)); + + ServerUserData userData = repo.getServerUserData(SERVER_ID, USER_ID_1); + assertTrue(userData.isDeny()); + + repo.dispose(); + } +}