diff --git a/build.gradle b/build.gradle index be5573d..d35b83d 100644 --- a/build.gradle +++ b/build.gradle @@ -30,6 +30,11 @@ archivesBaseName = 'janitor_bot' version = getVersion() println("Version: ${version}") +java { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} + dependencies { implementation group: 'net.dv8tion', name: 'JDA', version: jda_version implementation group: 'com.electronwill.night-config', name: 'toml', version: nightconfig_version diff --git a/src/main/java/sciwhiz12/janitor/JanitorBot.java b/src/main/java/sciwhiz12/janitor/JanitorBot.java index 4f59553..ecd8756 100644 --- a/src/main/java/sciwhiz12/janitor/JanitorBot.java +++ b/src/main/java/sciwhiz12/janitor/JanitorBot.java @@ -10,6 +10,7 @@ import sciwhiz12.janitor.commands.CommandRegistry; import sciwhiz12.janitor.config.BotConfig; import sciwhiz12.janitor.msg.Messages; import sciwhiz12.janitor.msg.Translations; +import sciwhiz12.janitor.storage.GuildStorage; import sciwhiz12.janitor.utils.Util; import java.nio.file.Path; diff --git a/src/main/java/sciwhiz12/janitor/moderation/notes/NoteStorage.java b/src/main/java/sciwhiz12/janitor/moderation/notes/NoteStorage.java index c82d205..5b8f384 100644 --- a/src/main/java/sciwhiz12/janitor/moderation/notes/NoteStorage.java +++ b/src/main/java/sciwhiz12/janitor/moderation/notes/NoteStorage.java @@ -9,9 +9,10 @@ import com.google.gson.reflect.TypeToken; import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.User; import org.checkerframework.checker.nullness.qual.Nullable; -import sciwhiz12.janitor.GuildStorage; import sciwhiz12.janitor.JanitorBot; +import sciwhiz12.janitor.storage.GuildStorage; import sciwhiz12.janitor.storage.JsonStorage; +import sciwhiz12.janitor.storage.StorageKey; import java.lang.reflect.Type; import java.util.HashMap; @@ -19,10 +20,10 @@ import java.util.Map; public class NoteStorage extends JsonStorage { private static final Type NOTE_MAP_TYPE = new TypeToken>() {}.getType(); - public static final String STORAGE_KEY = "notes"; + public static final StorageKey KEY = new StorageKey<>("notes", NoteStorage.class); public static NoteStorage get(GuildStorage storage, Guild guild) { - return storage.getOrCreate(guild, STORAGE_KEY, () -> new NoteStorage(storage.getBot())); + return storage.getOrCreate(guild, KEY, () -> new NoteStorage(storage.getBot())); } private final Gson gson; @@ -33,8 +34,8 @@ public class NoteStorage extends JsonStorage { public NoteStorage(JanitorBot bot) { this.bot = bot; this.gson = new GsonBuilder() - .registerTypeAdapter(NoteEntry.class, new NoteEntry.Serializer(bot)) - .create(); + .registerTypeAdapter(NoteEntry.class, new NoteEntry.Serializer(bot)) + .create(); } public JanitorBot getBot() { @@ -58,8 +59,8 @@ public class NoteStorage extends JsonStorage { public int getAmountOfNotes(User target) { return (int) notes.values().stream() - .filter(entry -> entry.getTarget() == target) - .count(); + .filter(entry -> entry.getTarget() == target) + .count(); } public Map getNotes() { diff --git a/src/main/java/sciwhiz12/janitor/moderation/warns/WarningStorage.java b/src/main/java/sciwhiz12/janitor/moderation/warns/WarningStorage.java index b540c34..219d843 100644 --- a/src/main/java/sciwhiz12/janitor/moderation/warns/WarningStorage.java +++ b/src/main/java/sciwhiz12/janitor/moderation/warns/WarningStorage.java @@ -8,9 +8,10 @@ import com.google.gson.JsonObject; import com.google.gson.reflect.TypeToken; import net.dv8tion.jda.api.entities.Guild; import org.checkerframework.checker.nullness.qual.Nullable; -import sciwhiz12.janitor.GuildStorage; import sciwhiz12.janitor.JanitorBot; +import sciwhiz12.janitor.storage.GuildStorage; import sciwhiz12.janitor.storage.JsonStorage; +import sciwhiz12.janitor.storage.StorageKey; import java.lang.reflect.Type; import java.util.HashMap; @@ -18,10 +19,10 @@ import java.util.Map; public class WarningStorage extends JsonStorage { private static final Type WARNING_MAP_TYPE = new TypeToken>() {}.getType(); - public static final String STORAGE_KEY = "warnings"; + public static final StorageKey KEY = new StorageKey<>("warnings", WarningStorage.class); public static WarningStorage get(GuildStorage storage, Guild guild) { - return storage.getOrCreate(guild, STORAGE_KEY, () -> new WarningStorage(storage.getBot())); + return storage.getOrCreate(guild, KEY, () -> new WarningStorage(storage.getBot())); } private final Gson gson; @@ -32,8 +33,8 @@ public class WarningStorage extends JsonStorage { public WarningStorage(JanitorBot bot) { this.bot = bot; this.gson = new GsonBuilder() - .registerTypeAdapter(WarningEntry.class, new WarningEntry.Serializer(bot)) - .create(); + .registerTypeAdapter(WarningEntry.class, new WarningEntry.Serializer(bot)) + .create(); } public JanitorBot getBot() { diff --git a/src/main/java/sciwhiz12/janitor/GuildStorage.java b/src/main/java/sciwhiz12/janitor/storage/GuildStorage.java similarity index 52% rename from src/main/java/sciwhiz12/janitor/GuildStorage.java rename to src/main/java/sciwhiz12/janitor/storage/GuildStorage.java index 9c98f7b..c28cf91 100644 --- a/src/main/java/sciwhiz12/janitor/GuildStorage.java +++ b/src/main/java/sciwhiz12/janitor/storage/GuildStorage.java @@ -1,10 +1,11 @@ -package sciwhiz12.janitor; +package sciwhiz12.janitor.storage; import com.google.common.base.Preconditions; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import net.dv8tion.jda.api.entities.Guild; -import sciwhiz12.janitor.storage.IStorage; +import sciwhiz12.janitor.JanitorBot; +import sciwhiz12.janitor.Logging; import java.io.IOException; import java.io.Reader; @@ -12,18 +13,20 @@ import java.io.Writer; import java.nio.file.Files; import java.nio.file.Path; import java.util.HashMap; -import java.util.IdentityHashMap; import java.util.Map; import java.util.function.Supplier; import static java.nio.file.StandardOpenOption.*; +/** + * A storage system for guild-specific data. + */ public class GuildStorage { private static final Gson GSON = new GsonBuilder().setPrettyPrinting().serializeNulls().create(); private final JanitorBot bot; private final Path mainFolder; - private final Map> guildStorage = new IdentityHashMap<>(); + private final Map>> guildStorages = new HashMap<>(); public GuildStorage(JanitorBot bot, Path mainFolder) { Preconditions.checkArgument(Files.isDirectory(mainFolder) || Files.notExists(mainFolder)); @@ -35,28 +38,31 @@ public class GuildStorage { return bot; } - public T getOrCreate(Guild guild, String key, Supplier defaultSupplier) { - final Map storageMap = guildStorage.computeIfAbsent(guild, g -> new HashMap<>()); - //noinspection unchecked - return (T) storageMap.computeIfAbsent(key, k -> load(guild, key, defaultSupplier.get())); + public S getOrCreate(Guild guild, StorageKey key, Supplier defaultSupplier) { + return getOrCreate(guild.getIdLong(), key, defaultSupplier); } - private Path getFile(Guild guild, String key) { - final Path guildFolder = makeFolder(guild); + public S getOrCreate(long guildID, StorageKey key, Supplier defaultSupplier) { + final Map> storageMappy = guildStorages.computeIfAbsent(guildID, id -> new HashMap<>()); + return key.getType().cast(storageMappy.computeIfAbsent(key.getStorageID(), + k -> new InnerStorage<>(key, load(guildID, key.getStorageID(), defaultSupplier.get()))).getStorage()); + } + + private Path getFile(long guildID, String key) { + final Path guildFolder = Path.of(Long.toHexString(guildID)); final Path file = Path.of(key + ".json"); return mainFolder.resolve(guildFolder).resolve(file); } - public T load(Guild guild, String key, T storage) { - final Path file = getFile(guild, key); + public T load(long guildID, String key, T storage) { + final Path file = getFile(guildID, key); if (Files.notExists(file)) return storage; - Logging.JANITOR.debug("Loading storage {} for guild {}", key, guild); + Logging.JANITOR.debug("Loading storage {} for guild {}", key, guildID); try (Reader reader = Files.newBufferedReader(file)) { storage.read(reader); - } - catch (IOException e) { - Logging.JANITOR.error("Error while loading storage {} for guild {}", key, guild, e); + } catch (IOException e) { + Logging.JANITOR.error("Error while loading storage {} for guild {}", key, guildID, e); } return storage; } @@ -69,23 +75,22 @@ public class GuildStorage { if (!isAutosave) Logging.JANITOR.debug("Saving guild storage to files under {}...", mainFolder); boolean anySaved = false; - for (Guild guild : guildStorage.keySet()) { - final Map storageMap = guildStorage.get(guild); + for (long guildID : guildStorages.keySet()) { + final Map> storageMap = guildStorages.get(guildID); for (String key : storageMap.keySet()) { - final IStorage storage = storageMap.get(key); - if (storage.dirty()) { - final Path file = getFile(guild, key); + final InnerStorage inner = storageMap.get(key); + if (inner.dirty()) { + final Path file = getFile(guildID, key); try { if (Files.notExists(file.getParent())) Files.createDirectories(file.getParent()); if (Files.notExists(file)) Files.createFile(file); try (Writer writer = Files - .newBufferedWriter(file, CREATE, WRITE, TRUNCATE_EXISTING)) { - storage.write(writer); + .newBufferedWriter(file, CREATE, WRITE, TRUNCATE_EXISTING)) { + inner.getStorage().write(writer); anySaved = true; } - } - catch (IOException e) { - Logging.JANITOR.error("Error while writing storage {} for guild {}", key, guild, e); + } catch (IOException e) { + Logging.JANITOR.error("Error while writing storage {} for guild {}", key, guildID, e); } } } @@ -94,10 +99,9 @@ public class GuildStorage { Logging.JANITOR.info("Saved guild storage to files under {}", mainFolder); } - private Path makeFolder(Guild guild) { - return Path.of(Long.toHexString(guild.getIdLong())); - } - + /** + * A thread that calls {@link GuildStorage#save(boolean)} between specified delays. + */ public static class SavingThread extends Thread { private final GuildStorage storage; private volatile boolean running = true; @@ -117,9 +121,35 @@ public class GuildStorage { public void run() { while (running) { storage.save(true); - try { Thread.sleep(storage.getBot().getConfig().AUTOSAVE_INTERVAL.get() * 1000); } - catch (InterruptedException ignored) {} + try { + Thread.sleep(storage.getBot().getConfig().AUTOSAVE_INTERVAL.get() * 1000); + } catch (InterruptedException ignored) {} } } } + + /** + * For internal use only. + */ + static class InnerStorage { + private final StorageKey key; + private final S storage; + + InnerStorage(StorageKey key, S storage) { + this.key = key; + this.storage = storage; + } + + public StorageKey getKey() { + return key; + } + + public S getStorage() { + return storage; + } + + public boolean dirty() { + return storage.dirty(); + } + } } diff --git a/src/main/java/sciwhiz12/janitor/storage/StorageKey.java b/src/main/java/sciwhiz12/janitor/storage/StorageKey.java new file mode 100644 index 0000000..5e5cddb --- /dev/null +++ b/src/main/java/sciwhiz12/janitor/storage/StorageKey.java @@ -0,0 +1,67 @@ +package sciwhiz12.janitor.storage; + +import com.google.common.base.Preconditions; + +import java.util.Objects; + +/** + * A storage key, used to retrieve an instance of an {@link IStorage} from a {@link GuildStorage}. + * + * @param the type of the {@link IStorage} + */ +public class StorageKey { + private final String storageID; + private final Class type; + + /** + * Creates a {@link StorageKey} with the given storage ID and type. + * + * @param storageID the storage ID + * @param type the class type of the generic type + * + * @throws NullPointerException if {@code storageID} or {@code type} is {@code null} + * @throws IllegalArgumentException if {@code storageID} is empty or blank + */ + public StorageKey(String storageID, Class type) { + Preconditions.checkNotNull(storageID, "Storage ID must not be null"); + Preconditions.checkArgument(!storageID.isBlank(), "Storage ID must not be empty or blank"); + Preconditions.checkNotNull(type, "Class type must not be null"); + this.storageID = storageID; + this.type = type; + } + + /** + * Returns the storage ID, used by {@link GuildStorage} to uniquely identify this storage's data. + * + *

This is currently used by {@code GuildStorage} as the folder name of the storage. + * + * @return the storage ID + */ + public String getStorageID() { + return storageID; + } + + /** + * Returns the class of the {@link IStorage} subtype that this storage key represents, which + * is also used in the key's generics. + * + * @return the class of the generic type + */ + public Class getType() { + return type; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + StorageKey that = (StorageKey) o; + return storageID.equals(that.storageID) && + type.equals(that.type); + } + + @Override + public int hashCode() { + return Objects.hash(storageID, type); + } +}