From 86c54bf04ce9b6263940b86f8f3b9ef59ed85ec6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20S=C3=B6derberg?= Date: Mon, 9 May 2022 19:05:24 +0200 Subject: [PATCH] feat: rewrite database interactions The goal of this PR is to completely re-write how PlotSquared interacts with databases. This will be achieved using Flyway, JDBI and new architectural principles. --- Core/build.gradle.kts | 4 ++ .../com/plotsquared/core/player/PlotRole.java | 8 +++ .../java/com/plotsquared/core/plot/Plot.java | 18 +++-- .../plotsquared/core/plot/PlotService.java | 56 +++++++++++++++ .../core/repository/PlotRepository.java | 21 ++++++ .../core/repository/PlotRoleRepository.java | 12 ++++ .../repository/PlotSettingsRepository.java | 8 +++ .../core/repository/Repository.java | 28 ++++++++ .../core/repository/dbo/PlotDBO.java | 65 ++++++++++++++++++ .../core/repository/dbo/PlotRoleDBO.java | 22 ++++++ .../core/repository/dbo/PlotSettingsDBO.java | 29 ++++++++ .../migrations/mysql/R__01_create_tables.sql | 68 +++++++++++++++++++ gradle/libs.versions.toml | 2 + 13 files changed, 331 insertions(+), 10 deletions(-) create mode 100644 Core/src/main/java/com/plotsquared/core/player/PlotRole.java create mode 100644 Core/src/main/java/com/plotsquared/core/plot/PlotService.java create mode 100644 Core/src/main/java/com/plotsquared/core/repository/PlotRepository.java create mode 100644 Core/src/main/java/com/plotsquared/core/repository/PlotRoleRepository.java create mode 100644 Core/src/main/java/com/plotsquared/core/repository/PlotSettingsRepository.java create mode 100644 Core/src/main/java/com/plotsquared/core/repository/Repository.java create mode 100644 Core/src/main/java/com/plotsquared/core/repository/dbo/PlotDBO.java create mode 100644 Core/src/main/java/com/plotsquared/core/repository/dbo/PlotRoleDBO.java create mode 100644 Core/src/main/java/com/plotsquared/core/repository/dbo/PlotSettingsDBO.java create mode 100644 Core/src/main/resources/migrations/mysql/R__01_create_tables.sql diff --git a/Core/build.gradle.kts b/Core/build.gradle.kts index 9bf697e9d..b4fb8b4ba 100644 --- a/Core/build.gradle.kts +++ b/Core/build.gradle.kts @@ -37,6 +37,10 @@ dependencies { // Logging compileOnlyApi(libs.log4j) + // Records are cool. Builders are cooler + compileOnly(libs.recordBuilderProcessor) + annotationProcessor(libs.recordBuilderProcessor) + // Other libraries api(libs.prtree) api(libs.aopalliance) diff --git a/Core/src/main/java/com/plotsquared/core/player/PlotRole.java b/Core/src/main/java/com/plotsquared/core/player/PlotRole.java new file mode 100644 index 000000000..cf3085f32 --- /dev/null +++ b/Core/src/main/java/com/plotsquared/core/player/PlotRole.java @@ -0,0 +1,8 @@ +package com.plotsquared.core.player; + +public enum PlotRole { + OWNER, + HELPER, + TRUSTED, + DENIED, +} diff --git a/Core/src/main/java/com/plotsquared/core/plot/Plot.java b/Core/src/main/java/com/plotsquared/core/plot/Plot.java index b709de857..e149bdf1c 100644 --- a/Core/src/main/java/com/plotsquared/core/plot/Plot.java +++ b/Core/src/main/java/com/plotsquared/core/plot/Plot.java @@ -290,14 +290,14 @@ public class Plot { * @param area the plot's PlotArea * @param merged an array giving merged plots * @param timestamp when the plot was created - * @param temp value representing whatever DBManager needs to to. Do not touch tbh. + * @param temp value representing whatever DBManager needs it to. Do not touch tbh. */ public Plot( @NonNull PlotId id, UUID owner, - HashSet trusted, - HashSet members, - HashSet denied, + Set trusted, + Set members, + Set denied, String alias, BlockLoc position, Collection> flags, @@ -310,9 +310,9 @@ public class Plot { this.area = area; this.owner = owner; this.settings = new PlotSettings(); - this.members = members; - this.trusted = trusted; - this.denied = denied; + this.members = new HashSet<>(members); + this.trusted = new HashSet<>(trusted); + this.denied = new HashSet<>(denied); this.settings.setAlias(alias); this.settings.setPosition(position); this.settings.setMerged(merged); @@ -321,9 +321,7 @@ public class Plot { if (area != null) { this.flagContainer.setParentContainer(area.getFlagContainer()); if (flags != null) { - for (PlotFlag flag : flags) { - this.flagContainer.addFlag(flag); - } + flags.forEach(this.flagContainer::addFlag); } } PlotSquared.platform().injector().injectMembers(this); diff --git a/Core/src/main/java/com/plotsquared/core/plot/PlotService.java b/Core/src/main/java/com/plotsquared/core/plot/PlotService.java new file mode 100644 index 000000000..826f48bc3 --- /dev/null +++ b/Core/src/main/java/com/plotsquared/core/plot/PlotService.java @@ -0,0 +1,56 @@ +package com.plotsquared.core.plot; + +import com.plotsquared.core.repository.PlotRepository; +import com.plotsquared.core.repository.PlotRoleRepository; +import com.plotsquared.core.repository.PlotSettingsRepository; +import com.plotsquared.core.repository.dbo.PlotSettingsDBOBuilder; +import org.checkerframework.checker.nullness.qual.NonNull; + +import javax.inject.Inject; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +public final class PlotService { + + private static final String PLOT_SETTINGS_DEFAULT_POSITION = "default"; + + private final PlotRepository plotRepository; + private final PlotSettingsRepository plotSettingsRepository; + private final PlotRoleRepository plotRoleRepository; + + @Inject + public PlotService( + final @NonNull PlotRepository plotRepository, + final @NonNull PlotSettingsRepository plotSettingsRepository, + final @NonNull PlotRoleRepository plotRoleRepository + ) { + this.plotRepository = plotRepository; + this.plotSettingsRepository = plotSettingsRepository; + this.plotRoleRepository = plotRoleRepository; + } + + /** + * Returns a list containing all the plots in the given {@code plotArea}. + * + * @param plotArea the area + * @return all plots in the area + */ + public @NonNull Collection getPlotsInArea(final @NonNull PlotArea plotArea) { + return this.plotRepository.getPlotsInArea(plotArea.getId()) + .map(plotDBO -> { + final var settings = this.plotSettingsRepository.findById(plotDBO) + .orElseGet(() -> PlotSettingsDBOBuilder.builder() + .plot(plotDBO) + .position(PLOT_SETTINGS_DEFAULT_POSITION) + .build() + ); + return plotDBO.toPlot( + plotArea, + settings, + this.plotRoleRepository.findAllFor(plotDBO), + List.of() + ); + }).collect(Collectors.toList()); + } +} diff --git a/Core/src/main/java/com/plotsquared/core/repository/PlotRepository.java b/Core/src/main/java/com/plotsquared/core/repository/PlotRepository.java new file mode 100644 index 000000000..1eb174fa4 --- /dev/null +++ b/Core/src/main/java/com/plotsquared/core/repository/PlotRepository.java @@ -0,0 +1,21 @@ +package com.plotsquared.core.repository; + +import com.plotsquared.core.plot.PlotId; +import com.plotsquared.core.repository.dbo.PlotDBO; +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.util.stream.Stream; + +/** + * {@link Repository} storing {@link PlotDBO plots} identified by their respective {@link PlotId}. + */ +public interface PlotRepository extends Repository { + + /** + * Returns all plots in the given {@code area}. + * + * @param area the plot area + * @return a stream with all plots in the given area. + */ + @NonNull Stream getPlotsInArea(@NonNull String area); +} diff --git a/Core/src/main/java/com/plotsquared/core/repository/PlotRoleRepository.java b/Core/src/main/java/com/plotsquared/core/repository/PlotRoleRepository.java new file mode 100644 index 000000000..42827beb8 --- /dev/null +++ b/Core/src/main/java/com/plotsquared/core/repository/PlotRoleRepository.java @@ -0,0 +1,12 @@ +package com.plotsquared.core.repository; + +import com.plotsquared.core.repository.dbo.PlotDBO; +import com.plotsquared.core.repository.dbo.PlotRoleDBO; +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.util.List; + +public interface PlotRoleRepository extends Repository { + + List findAllFor(@NonNull PlotDBO plotDBO); +} diff --git a/Core/src/main/java/com/plotsquared/core/repository/PlotSettingsRepository.java b/Core/src/main/java/com/plotsquared/core/repository/PlotSettingsRepository.java new file mode 100644 index 000000000..a3f1d7dab --- /dev/null +++ b/Core/src/main/java/com/plotsquared/core/repository/PlotSettingsRepository.java @@ -0,0 +1,8 @@ +package com.plotsquared.core.repository; + +import com.plotsquared.core.repository.dbo.PlotDBO; +import com.plotsquared.core.repository.dbo.PlotSettingsDBO; + +public interface PlotSettingsRepository extends Repository { + +} diff --git a/Core/src/main/java/com/plotsquared/core/repository/Repository.java b/Core/src/main/java/com/plotsquared/core/repository/Repository.java new file mode 100644 index 000000000..00be93d66 --- /dev/null +++ b/Core/src/main/java/com/plotsquared/core/repository/Repository.java @@ -0,0 +1,28 @@ +package com.plotsquared.core.repository; + +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.util.Optional; + +/** + * Generic object repository. + * + * @param the type of object stored in the repository. + * @param the type used to identify objects stored in the repository. + */ +public interface Repository<@NonNull T, @NonNull U> { + + /** + * Saves the given object. + * + * @param object {@code the object}. + */ + void save(T object); + + /** + * Finds the object by its {@code id}. + * + * @param id the id + */ + @NonNull Optional findById(U id); +} diff --git a/Core/src/main/java/com/plotsquared/core/repository/dbo/PlotDBO.java b/Core/src/main/java/com/plotsquared/core/repository/dbo/PlotDBO.java new file mode 100644 index 000000000..88a839c34 --- /dev/null +++ b/Core/src/main/java/com/plotsquared/core/repository/dbo/PlotDBO.java @@ -0,0 +1,65 @@ +package com.plotsquared.core.repository.dbo; + +import com.plotsquared.core.location.BlockLoc; +import com.plotsquared.core.plot.Plot; +import com.plotsquared.core.plot.PlotArea; +import com.plotsquared.core.plot.PlotId; +import com.plotsquared.core.plot.flag.PlotFlag; +import io.soabase.recordbuilder.core.RecordBuilder; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.time.Instant; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; + +@RecordBuilder +public record PlotDBO( + @Nullable Integer id, + int plotIdX, + int plotIdZ, + @NonNull String world, + @NonNull UUID owner, + @NonNull Instant timestamp +) { + + public @NonNull Plot toPlot( + final @NonNull PlotArea plotArea, + final @NonNull PlotSettingsDBO plotSettingsDBO, + final @NonNull Collection plotRoles, + final @NonNull Collection> flags + ) { + final PlotId plotId = PlotId.of(this.plotIdX(), this.plotIdZ()); + final int id = Objects.requireNonNull(id(), "id may not be null"); + + final Set trusted = new HashSet<>(); + final Set members = new HashSet<>(); + final Set denied = new HashSet<>(); + for (final PlotRoleDBO plotRole : plotRoles) { + switch (plotRole.plotRole()) { + case TRUSTED -> trusted.add(plotRole.userId()); + case HELPER -> members.add(plotRole.userId()); + case DENIED -> denied.add(plotRole.userId()); + } + } + + return new Plot( + plotId, + this.owner(), + Collections.unmodifiableSet(trusted), + Collections.unmodifiableSet(members), + Collections.unmodifiableSet(denied), + plotSettingsDBO.alias(), + BlockLoc.fromString(plotSettingsDBO.position()), + flags, + plotArea, + plotSettingsDBO.unwrapMerged(), + this.timestamp().toEpochMilli(), + id + ); + } +} diff --git a/Core/src/main/java/com/plotsquared/core/repository/dbo/PlotRoleDBO.java b/Core/src/main/java/com/plotsquared/core/repository/dbo/PlotRoleDBO.java new file mode 100644 index 000000000..52945dfee --- /dev/null +++ b/Core/src/main/java/com/plotsquared/core/repository/dbo/PlotRoleDBO.java @@ -0,0 +1,22 @@ +package com.plotsquared.core.repository.dbo; + +import com.plotsquared.core.player.PlotRole; +import io.soabase.recordbuilder.core.RecordBuilder; +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.util.UUID; + +@RecordBuilder +public record PlotRoleDBO( + @NonNull PlotDBO plot, + @NonNull UUID userId, + @NonNull PlotRole plotRole +) { + + public @NonNull Key key() { + return new Key(this.plot(), this.userId()); + } + + public record Key(@NonNull PlotDBO plot, @NonNull UUID userId) { + } +} diff --git a/Core/src/main/java/com/plotsquared/core/repository/dbo/PlotSettingsDBO.java b/Core/src/main/java/com/plotsquared/core/repository/dbo/PlotSettingsDBO.java new file mode 100644 index 000000000..31461eccd --- /dev/null +++ b/Core/src/main/java/com/plotsquared/core/repository/dbo/PlotSettingsDBO.java @@ -0,0 +1,29 @@ +package com.plotsquared.core.repository.dbo; + +import io.soabase.recordbuilder.core.RecordBuilder; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +@RecordBuilder +public record PlotSettingsDBO( + @NonNull PlotDBO plot, + @Nullable String alias, + Integer merged, + @NonNull String position +) { + + /** + * Unwraps {@link #merged()} into an array indicating whether the plot is merged in + * any given cardinal direction. The indices of the array correspond to the ordinals of + * {@link com.plotsquared.core.location.Direction}. + * + * @return unwrapped merged status + */ + public boolean@NonNull [] unwrapMerged() { + final boolean[] merged = new boolean[4]; + for (int i = 0; i < 4; i++) { + merged[3 - i] = (this.merged() & 1 << i) != 0; + } + return merged; + } +} diff --git a/Core/src/main/resources/migrations/mysql/R__01_create_tables.sql b/Core/src/main/resources/migrations/mysql/R__01_create_tables.sql new file mode 100644 index 000000000..3004e91e1 --- /dev/null +++ b/Core/src/main/resources/migrations/mysql/R__01_create_tables.sql @@ -0,0 +1,68 @@ +create table if not exists ${prefix}plot( + id int(11) not null auto_increment, + plot_id_x int(11) not null, + plot_id_z int(11) not null, + world varchar(45) not null, + owner varchar(40) not null, + timestamp timestamp not null default current_timestamp, + primary key (id) +) engine=InnoDB default charset=utf8 auto_increment=0; + +-- TODO: Migrating existing data to this table. +create table if not exists ${prefix}plot_role( + plot_id int(11) not null, + user_id varchar(45) not null, + role enum('helper', 'trusted', 'denied') not null, + foreign key (plot_id) references ${prefix}plot(id) on delete cascade, + primary key (plot_id, user_id) +) engine=InnoDB default charset=utf8 auto_increment=0; + +create table if not exists ${prefix}plot_comments( + world varchar(45) not null, + comment varchar(45) not null, + inbox varchar(45) not null, + timestamp int(11) not null, + sender varchar(45) not null +) engine=InnoDB default charset=utf8 auto_increment=0; + +-- TODO: Look into what to do with this one... +-- Most data is now found in flags. +create table if not exists ${prefix}plot_settings( + plot_plot_id int(11) not null, + biome varchar(45) default 'FOREST', -- Unused. Moved to flags. + rain int(1) default 0, -- Unused. Moved to flags. + custom_time tinyint(1) default 0, -- Unused. Moved to flags. + time int(11) default 8000, -- Unused. Moved to flags. + deny_entry tinyint(1) default 0, -- Unused. Moved to flags. + alias varchar(50) default null, + merged int(11) default null, + position varchar(50) not null default 'default', + foreign key (plot_plot_id) references ${prefix}plot(id) on delete cascade, + primary key (plot_plot_id) +) engine=InnoDB default charset=utf8; + +-- TODO: Look into adding foreign keys to this. +create table if not exists ${prefix}plot_rating( + plot_plot_id int(11) not null, + rating int(2) not null, + player varchar(45) not null +) engine=InnoDB default charset=utf8; + +-- TODO: Drop the key and make the player ID the key instead. +create table if not exists ${prefix}player_meta( + meta_id int(11) not null auto_increment, + uuid varchar(45) not null, + key varchar(32) not null, + value blob not null, + primary key (meta_id) +) engine=InnoDB default charset=utf8 auto_increment=0; + +-- TODO: Drop the ID and make (plot_id, flag) the new primary key. +create table if not exists ${prefix}plot_flags( + id int(11) not null auto_increment primary key, + plot_id int(11) not null, + flag varchar(64), + value varchar(512), + foreign key (plot_id) references ${prefix}plot(id) on delete cascade, + unique (plot_id, flag) +) engine=InnoDB default charset=utf8 auto_increment=0; diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d66b57d56..f0b613c42 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -36,6 +36,7 @@ paperlib = "1.0.7" squirrelid = "0.3.1" serverlib = "2.3.1" http4j = "1.3" +record-builder-processor = "33" # Gradle plugins shadow = "7.1.2" @@ -86,6 +87,7 @@ arkitektonika = { group = "com.intellectualsites.arkitektonika", name = "Arkitek http4j = { group = "com.intellectualsites.http", name = "HTTP4J", version.ref = "http4j" } paster = { group = "com.intellectualsites.paster", name = "Paster", version.ref = "paster" } guava = { group = "com.google.guava", name = "guava", version.ref = "guava" } +recordBuilderProcessor = { group = "io.soabase.record-builder", name = "record-builder-processor", version.ref = "record-builder-processor" } [plugins] shadow = { id = "com.github.johnrengelman.shadow", version.ref = "shadow" }