From e56709e4f101d855b1d420938c1467458ddbe41c Mon Sep 17 00:00:00 2001 From: Zefir Kirilov Date: Thu, 8 Jan 2026 12:02:08 +0200 Subject: [PATCH 01/10] created TypedIdentifier record --- .../smp/bankaccounts/api/TypedIdentifier.java | 120 ++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 src/main/java/pro/cloudnode/smp/bankaccounts/api/TypedIdentifier.java diff --git a/src/main/java/pro/cloudnode/smp/bankaccounts/api/TypedIdentifier.java b/src/main/java/pro/cloudnode/smp/bankaccounts/api/TypedIdentifier.java new file mode 100644 index 0000000..ce28083 --- /dev/null +++ b/src/main/java/pro/cloudnode/smp/bankaccounts/api/TypedIdentifier.java @@ -0,0 +1,120 @@ +/* + * BankAccounts is a Minecraft economy plugin that enables players to hold multiple bank accounts. + * Copyright © 2023–2026 Cloudnode OÜ. + * + * 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 pro.cloudnode.smp.bankaccounts.api; + +import org.bukkit.OfflinePlayer; +import org.jetbrains.annotations.NotNull; + +import java.util.Arrays; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * Represents a typed identifier of a resource. + * + * @param type the type of the resource + * @param id the identifier of the resource + */ +public record TypedIdentifier(@NotNull Type type, @NotNull String id) { + /** + * Parses a typed identifier from a string in the format {@code :}. + * + * @param identifier the identifier string to parse + * @return the corresponding typed identifier + * @throws IllegalArgumentException if the identifier format is invalid or the type is unknown + */ + @NotNull + public static TypedIdentifier fromString(final @NotNull String identifier) { + final int colonIndex = identifier.indexOf(':'); + if (colonIndex == -1) { + throw new IllegalArgumentException(String.format("Invalid identifier: %s", identifier)); + } + return new TypedIdentifier( + Type.fromName(identifier.substring(0, colonIndex)), + identifier.substring(colonIndex + 1) + ); + } + + /** + * Creates a typed identifier for a player. + * + * @param player the player to create the identifier for + * @return the typed identifier for the player + */ + @NotNull + public static TypedIdentifier player(final @NotNull OfflinePlayer player) { + return new TypedIdentifier(Type.PLAYER, player.getUniqueId().toString()); + } + + /** + * Returns a string representation of this typed identifier in the format {@code :}. + * + * @return the identifier string representation + */ + @Override + @NotNull + public String toString() { + return type.getName() + ':' + id; + } + + /** + * Represents the type of resource. + */ + public enum Type { + /** + * {@link pro.cloudnode.smp.bankaccounts.api.account.Account} type. + */ + ACCOUNT("account"), + + /** + * {@link org.bukkit.OfflinePlayer} type. + */ + PLAYER("player"); + + private static final @NotNull Map typeMap = Arrays.stream(Type.values()) + .collect(Collectors.toUnmodifiableMap(Type::getName, Function.identity())); + private final @NotNull String name; + + Type(final @NotNull String name) { + this.name = name; + } + + /** + * Returns the type corresponding to the given name. + * + * @param name the name to get the type for + * @return the corresponding type + * @throws IllegalArgumentException if the name is unknown + */ + public static @NotNull Type fromName(final @NotNull String name) { + final Type type = typeMap.get(name); + if (type == null) { + throw new IllegalArgumentException(String.format("Unknown type: %s", name)); + } + return type; + } + + /** + * Returns the name of this type. + * + * @return the type name + */ + public final @NotNull String getName() { + return name; + } + } +} From 77ca4505f70e4cc6d2d72eb1558f95fcf8c41bcf Mon Sep 17 00:00:00 2001 From: Zefir Kirilov Date: Thu, 8 Jan 2026 12:30:22 +0200 Subject: [PATCH 02/10] =?UTF-8?q?don=E2=80=99t=20re-arrange=20instance=20f?= =?UTF-8?q?inal=20fields=20above=20non-final=20fields?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/codeStyles/Project.xml | 51 ------------------------------------ 1 file changed, 51 deletions(-) diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 08a93c4..2569ffc 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -155,50 +155,6 @@ -
- - - - true - true - true - - - -
-
- - - - true - true - true - - - -
-
- - - - true - true - true - - - -
-
- - - - true - true - true - - - -
@@ -239,13 +195,6 @@
-
- - - true - - -
From 3d4e601563f3a2c5c6470a6ece2ac127a01eb44a Mon Sep 17 00:00:00 2001 From: Zefir Kirilov Date: Thu, 8 Jan 2026 12:30:47 +0200 Subject: [PATCH 03/10] add TypedIdentifier constructor using string type --- .../smp/bankaccounts/api/TypedIdentifier.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/main/java/pro/cloudnode/smp/bankaccounts/api/TypedIdentifier.java b/src/main/java/pro/cloudnode/smp/bankaccounts/api/TypedIdentifier.java index ce28083..4a189ba 100644 --- a/src/main/java/pro/cloudnode/smp/bankaccounts/api/TypedIdentifier.java +++ b/src/main/java/pro/cloudnode/smp/bankaccounts/api/TypedIdentifier.java @@ -30,6 +30,17 @@ * @param id the identifier of the resource */ public record TypedIdentifier(@NotNull Type type, @NotNull String id) { + /** + * Constructs a typed identifier from a string type. + * + * @param type the type of the resource + * @param id the identifier of the resource + * @throws IllegalArgumentException if the type is unknown + */ + public TypedIdentifier(final @NotNull String type, final @NotNull String id) { + this(Type.fromName(type), id); + } + /** * Parses a typed identifier from a string in the format {@code :}. * From cfe15f58ee7fb7c166004041d1fa92779a1253ad Mon Sep 17 00:00:00 2001 From: Zefir Kirilov Date: Sat, 10 Jan 2026 19:46:23 +0200 Subject: [PATCH 04/10] typed identifier improvements --- .../smp/bankaccounts/Serializable.java | 30 +++++ .../smp/bankaccounts/api/TypedIdentifier.java | 122 +++++++++--------- 2 files changed, 90 insertions(+), 62 deletions(-) create mode 100644 src/main/java/pro/cloudnode/smp/bankaccounts/Serializable.java diff --git a/src/main/java/pro/cloudnode/smp/bankaccounts/Serializable.java b/src/main/java/pro/cloudnode/smp/bankaccounts/Serializable.java new file mode 100644 index 0000000..41e46e4 --- /dev/null +++ b/src/main/java/pro/cloudnode/smp/bankaccounts/Serializable.java @@ -0,0 +1,30 @@ +/* + * BankAccounts is a Minecraft economy plugin that enables players to hold multiple bank accounts. + * Copyright © 2023–2026 Cloudnode OÜ. + * + * 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 pro.cloudnode.smp.bankaccounts; + +import org.jetbrains.annotations.NotNull; + +/** + * Represents an object that can be serialised to a string. + */ +public interface Serializable { + /** + * Serialises this object to a string. + * + * @return the serialised string representation + */ + @NotNull String serialize(); +} diff --git a/src/main/java/pro/cloudnode/smp/bankaccounts/api/TypedIdentifier.java b/src/main/java/pro/cloudnode/smp/bankaccounts/api/TypedIdentifier.java index 4a189ba..6f56c08 100644 --- a/src/main/java/pro/cloudnode/smp/bankaccounts/api/TypedIdentifier.java +++ b/src/main/java/pro/cloudnode/smp/bankaccounts/api/TypedIdentifier.java @@ -15,30 +15,27 @@ package pro.cloudnode.smp.bankaccounts.api; -import org.bukkit.OfflinePlayer; import org.jetbrains.annotations.NotNull; +import pro.cloudnode.smp.bankaccounts.Serializable; -import java.util.Arrays; -import java.util.Map; -import java.util.function.Function; -import java.util.stream.Collectors; +import java.util.Objects; /** - * Represents a typed identifier of a resource. - * - * @param type the type of the resource - * @param id the identifier of the resource + * Represents a typed identifier. */ -public record TypedIdentifier(@NotNull Type type, @NotNull String id) { +public class TypedIdentifier implements Serializable { + private final @NotNull Type type; + private final @NotNull String id; + /** - * Constructs a typed identifier from a string type. + * Constructs a typed identifier. * - * @param type the type of the resource - * @param id the identifier of the resource - * @throws IllegalArgumentException if the type is unknown + * @param type the type + * @param id the identifier */ - public TypedIdentifier(final @NotNull String type, final @NotNull String id) { - this(Type.fromName(type), id); + public TypedIdentifier(final @NotNull Type type, final @NotNull String id) { + this.type = type; + this.id = id; } /** @@ -49,83 +46,84 @@ public TypedIdentifier(final @NotNull String type, final @NotNull String id) { * @throws IllegalArgumentException if the identifier format is invalid or the type is unknown */ @NotNull - public static TypedIdentifier fromString(final @NotNull String identifier) { + public static TypedIdentifier deserialize(final @NotNull String identifier) { final int colonIndex = identifier.indexOf(':'); if (colonIndex == -1) { throw new IllegalArgumentException(String.format("Invalid identifier: %s", identifier)); } return new TypedIdentifier( - Type.fromName(identifier.substring(0, colonIndex)), + Type.deserialize(identifier.substring(0, colonIndex)), identifier.substring(colonIndex + 1) ); } /** - * Creates a typed identifier for a player. + * Returns a string representation of this typed identifier in the format {@code :}. * - * @param player the player to create the identifier for - * @return the typed identifier for the player + * @return the identifier string representation */ + @Override @NotNull - public static TypedIdentifier player(final @NotNull OfflinePlayer player) { - return new TypedIdentifier(Type.PLAYER, player.getUniqueId().toString()); + public String serialize() { + return type.toString() + ':' + id; } /** - * Returns a string representation of this typed identifier in the format {@code :}. + * Returns the identifier type. * - * @return the identifier string representation + * @return the type */ + public @NotNull Type type() { + return type; + } + + /** + * Returns the identifier value. + * + * @return the identifier + */ + public @NotNull String id() { + return id; + } + @Override - @NotNull - public String toString() { - return type.getName() + ':' + id; + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof final TypedIdentifier ti)) { + return false; + } + return this.type == ti.type && this.id.equals(ti.id); + } + + @Override + public int hashCode() { + return Objects.hash(type.serialize(), id); } /** - * Represents the type of resource. + * Represents an identifier type. */ - public enum Type { + public enum Type implements Serializable { /** - * {@link pro.cloudnode.smp.bankaccounts.api.account.Account} type. + * Represents an account holder. */ - ACCOUNT("account"), + HOLDER, /** - * {@link org.bukkit.OfflinePlayer} type. + * Represents an account. */ - PLAYER("player"); - - private static final @NotNull Map typeMap = Arrays.stream(Type.values()) - .collect(Collectors.toUnmodifiableMap(Type::getName, Function.identity())); - private final @NotNull String name; + ACCOUNT; - Type(final @NotNull String name) { - this.name = name; + @NotNull + public static Type deserialize(final @NotNull String value) { + return valueOf(value.toUpperCase()); } - /** - * Returns the type corresponding to the given name. - * - * @param name the name to get the type for - * @return the corresponding type - * @throws IllegalArgumentException if the name is unknown - */ - public static @NotNull Type fromName(final @NotNull String name) { - final Type type = typeMap.get(name); - if (type == null) { - throw new IllegalArgumentException(String.format("Unknown type: %s", name)); - } - return type; - } - - /** - * Returns the name of this type. - * - * @return the type name - */ - public final @NotNull String getName() { - return name; + @Override + public @NotNull String serialize() { + return name().toLowerCase(); } } } From e554096353321cc9693d632225cea5f98dad2734 Mon Sep 17 00:00:00 2001 From: Zefir Kirilov Date: Sat, 10 Jan 2026 19:49:15 +0200 Subject: [PATCH 05/10] created AccountId class --- .../smp/bankaccounts/api/account/Account.java | 6 ++-- .../bankaccounts/api/account/AccountId.java | 33 +++++++++++++++++++ .../api/account/AccountsRepository.java | 6 ++-- .../api/account/AccountsService.java | 24 +++++++------- .../bankaccounts/api/ledger/LedgerEntry.java | 13 ++++---- .../api/ledger/LedgerRepository.java | 10 +++--- .../api/ledger/LedgerService.java | 17 +++++----- 7 files changed, 73 insertions(+), 36 deletions(-) create mode 100644 src/main/java/pro/cloudnode/smp/bankaccounts/api/account/AccountId.java diff --git a/src/main/java/pro/cloudnode/smp/bankaccounts/api/account/Account.java b/src/main/java/pro/cloudnode/smp/bankaccounts/api/account/Account.java index 4ead52d..b74932e 100644 --- a/src/main/java/pro/cloudnode/smp/bankaccounts/api/account/Account.java +++ b/src/main/java/pro/cloudnode/smp/bankaccounts/api/account/Account.java @@ -25,7 +25,7 @@ * Represents a bank account. */ public final class Account { - private final @NotNull String id; + private final @NotNull AccountId id; private final @NotNull Type type; private final @NotNull Instant created; private boolean allowNegative; @@ -33,7 +33,7 @@ public final class Account { private @Nullable String name; Account( - final @NotNull String id, + final @NotNull AccountId id, final @NotNull Type type, final boolean allowNegative, final @NotNull Status status, @@ -54,7 +54,7 @@ public final class Account { * @return the account ID */ @NotNull - public String id() { + public AccountId id() { return id; } diff --git a/src/main/java/pro/cloudnode/smp/bankaccounts/api/account/AccountId.java b/src/main/java/pro/cloudnode/smp/bankaccounts/api/account/AccountId.java new file mode 100644 index 0000000..dce4c42 --- /dev/null +++ b/src/main/java/pro/cloudnode/smp/bankaccounts/api/account/AccountId.java @@ -0,0 +1,33 @@ +/* + * BankAccounts is a Minecraft economy plugin that enables players to hold multiple bank accounts. + * Copyright © 2023–2026 Cloudnode OÜ. + * + * 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 pro.cloudnode.smp.bankaccounts.api.account; + +import org.jetbrains.annotations.NotNull; +import pro.cloudnode.smp.bankaccounts.api.TypedIdentifier; + +/** + * Represents an account identifier. + */ +public final class AccountId extends TypedIdentifier { + /** + * Constructs an account identifier. + * + * @param id the account ID + */ + public AccountId(@NotNull String id) { + super(Type.ACCOUNT, id); + } +} diff --git a/src/main/java/pro/cloudnode/smp/bankaccounts/api/account/AccountsRepository.java b/src/main/java/pro/cloudnode/smp/bankaccounts/api/account/AccountsRepository.java index b4e4663..1e43dce 100644 --- a/src/main/java/pro/cloudnode/smp/bankaccounts/api/account/AccountsRepository.java +++ b/src/main/java/pro/cloudnode/smp/bankaccounts/api/account/AccountsRepository.java @@ -85,7 +85,7 @@ INSERT INTO bank_accounts ( created ) VALUES (?, ?, ?, ?, ?, ?) """, stmt -> { - stmt.setString(1, account.id()); + stmt.setString(1, account.id().id()); stmt.setInt(2, account.type().ordinal()); stmt.setBoolean(3, account.allowNegative()); stmt.setInt(4, account.status().ordinal()); @@ -109,7 +109,7 @@ public void update(final @NotNull Account account) throws RepositoryException { stmt.setBoolean(1, account.allowNegative()); stmt.setInt(2, account.status().ordinal()); stmt.setString(3, account.name().orElse(null)); - stmt.setString(4, account.id()); + stmt.setString(4, account.id().id()); } ) > 0; @@ -123,7 +123,7 @@ public void update(final @NotNull Account account) throws RepositoryException { @NotNull protected Account map(final @NotNull ResultSet resultSet) throws SQLException { return new Account( - resultSet.getString("id"), + new AccountId(resultSet.getString("id")), Account.Type.values()[resultSet.getInt("type")], resultSet.getBoolean("allow_negative"), Account.Status.values()[resultSet.getInt("status")], diff --git a/src/main/java/pro/cloudnode/smp/bankaccounts/api/account/AccountsService.java b/src/main/java/pro/cloudnode/smp/bankaccounts/api/account/AccountsService.java index 5c8a3ff..47b3d12 100644 --- a/src/main/java/pro/cloudnode/smp/bankaccounts/api/account/AccountsService.java +++ b/src/main/java/pro/cloudnode/smp/bankaccounts/api/account/AccountsService.java @@ -74,13 +74,13 @@ public AccountsService( * @throws InternalException if the account cannot be persisted due to an internal error */ @NotNull - public Account create(final @NotNull String id, final @NotNull Account.Type type) throws InternalException { - if (id.length() > MAX_ID_LENGTH) { + public Account create(final @NotNull AccountId id, final @NotNull Account.Type type) throws InternalException { + if (id.id().length() > MAX_ID_LENGTH) { throw new IllegalArgumentException(String.format("Account ID cannot exceed %d characters", MAX_ID_LENGTH)); } try { - if (repository.exists(id)) { + if (repository.exists(id.id())) { throw new IllegalArgumentException(String.format("Account with ID %s already exists", id)); } } catch (final Repository.RepositoryException e) { @@ -113,7 +113,7 @@ public Account create(final @NotNull Account.Type type) throws IllegalStateExcep String id = IdGenerator.BASE58.random(idLength); try { if (!repository.exists(id)) { - return create(id, type); + return create(new AccountId(id), type); } } catch (final Repository.RepositoryException e) { logger.log( @@ -142,9 +142,9 @@ public Account create(final @NotNull Account.Type type) throws IllegalStateExcep * @throws InternalException if the account cannot be retrieved due to an internal error */ @NotNull - public Optional get(final @NotNull String id) throws InternalException { + public Optional get(final @NotNull AccountId id) throws InternalException { try { - return repository.getById(id); + return repository.getById(id.id()); } catch (final Repository.RepositoryException e) { logger.log(Level.SEVERE, String.format("Failed to retrieve account with ID: %s", id), e); throw new InternalException("Failed to retrieve account"); @@ -155,18 +155,18 @@ public Optional get(final @NotNull String id) throws InternalException /** * Retrieves the account with the specified ID if it is not frozen. * - * @param accountId the ID of the account to retrieve + * @param id the ID of the account to retrieve * @return the account if found and not frozen * @throws AccountNotFoundException if no account exists with the given ID * @throws AccountFrozenException if the account is frozen * @throws InternalException if the account cannot be retrieved due to an internal error */ @NotNull - public Account getNonFrozenAccountOrThrow(String accountId) + public Account getNonFrozenAccountOrThrow(final @NotNull AccountId id) throws AccountNotFoundException, AccountFrozenException, InternalException { - final Account account = get(accountId).orElseThrow(() -> new AccountNotFoundException(accountId)); + final Account account = get(id).orElseThrow(() -> new AccountNotFoundException(id)); if (account.frozen()) { - throw new AccountFrozenException(accountId); + throw new AccountFrozenException(id); } return account; } @@ -181,7 +181,7 @@ public static final class AccountNotFoundException extends ServiceException { * @param id the account ID */ @ApiStatus.Internal - public AccountNotFoundException(final @NotNull String id) { + public AccountNotFoundException(final @NotNull AccountId id) { super(String.format("Account %s does not exist", id)); } } @@ -196,7 +196,7 @@ public static final class AccountFrozenException extends ServiceException { * @param id the account ID */ @ApiStatus.Internal - public AccountFrozenException(final @NotNull String id) { + public AccountFrozenException(final @NotNull AccountId id) { super(String.format("Account %s is frozen", id)); } } diff --git a/src/main/java/pro/cloudnode/smp/bankaccounts/api/ledger/LedgerEntry.java b/src/main/java/pro/cloudnode/smp/bankaccounts/api/ledger/LedgerEntry.java index 5e22445..b422a64 100644 --- a/src/main/java/pro/cloudnode/smp/bankaccounts/api/ledger/LedgerEntry.java +++ b/src/main/java/pro/cloudnode/smp/bankaccounts/api/ledger/LedgerEntry.java @@ -17,6 +17,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import pro.cloudnode.smp.bankaccounts.api.account.AccountId; import java.math.BigDecimal; import java.time.Instant; @@ -27,26 +28,26 @@ */ public final class LedgerEntry { private final @NotNull String id; - private final @NotNull String account; + private final @NotNull AccountId account; private final @NotNull BigDecimal amount; private final @NotNull BigDecimal balance; private final @NotNull Instant created; private final @NotNull LedgerEntry.Initiator initiator; private final @NotNull String channel; private final @Nullable String description; - private final @Nullable String relatedAccount; + private final @Nullable AccountId relatedAccount; private final @Nullable String previousId; LedgerEntry( final @NotNull String id, - final @NotNull String account, + final @NotNull AccountId account, final @NotNull BigDecimal amount, final @NotNull BigDecimal balance, final @NotNull Instant created, final @NotNull LedgerEntry.Initiator initiator, final @NotNull String channel, final @Nullable String description, - final @Nullable String relatedAccount, + final @Nullable AccountId relatedAccount, final @Nullable String previousId ) { this.id = id; @@ -77,7 +78,7 @@ public String id() { * @return the account ID */ @NotNull - public String account() { + public AccountId account() { return account; } @@ -149,7 +150,7 @@ public Optional description() { * @return the related account ID if the account exists, empty otherwise */ @NotNull - public Optional relatedAccount() { + public Optional relatedAccount() { return Optional.ofNullable(relatedAccount); } diff --git a/src/main/java/pro/cloudnode/smp/bankaccounts/api/ledger/LedgerRepository.java b/src/main/java/pro/cloudnode/smp/bankaccounts/api/ledger/LedgerRepository.java index 45c65ea..c4d4f81 100644 --- a/src/main/java/pro/cloudnode/smp/bankaccounts/api/ledger/LedgerRepository.java +++ b/src/main/java/pro/cloudnode/smp/bankaccounts/api/ledger/LedgerRepository.java @@ -18,6 +18,8 @@ import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import pro.cloudnode.smp.bankaccounts.api.Repository; +import pro.cloudnode.smp.bankaccounts.api.TypedIdentifier; +import pro.cloudnode.smp.bankaccounts.api.account.AccountId; import javax.sql.DataSource; import java.sql.ResultSet; @@ -147,14 +149,14 @@ INSERT INTO bank_ledger_entries ( """, Stream.of(transaction.debit(), transaction.credit()).map((ledgerEntry) -> (stmt) -> { stmt.setString(1, ledgerEntry.id()); - stmt.setString(2, ledgerEntry.account()); + stmt.setString(2, ledgerEntry.account().id()); stmt.setBigDecimal(3, ledgerEntry.amount()); stmt.setBigDecimal(4, ledgerEntry.balance()); stmt.setTimestamp(5, Timestamp.from(ledgerEntry.created())); stmt.setInt(6, ledgerEntry.initiator().ordinal()); stmt.setString(7, ledgerEntry.channel()); stmt.setString(8, ledgerEntry.description().orElse(null)); - stmt.setString(9, ledgerEntry.relatedAccount().orElse(null)); + stmt.setString(9, ledgerEntry.relatedAccount().map(TypedIdentifier::id).orElse(null)); stmt.setString(10, ledgerEntry.previousId().orElse(null)); }).toList() ); @@ -172,14 +174,14 @@ INSERT INTO bank_ledger_entries ( protected LedgerEntry map(final @NotNull ResultSet resultSet) throws SQLException { return new LedgerEntry( resultSet.getString("id"), - resultSet.getString("account"), + new AccountId(resultSet.getString("account")), resultSet.getBigDecimal("amount"), resultSet.getBigDecimal("balance"), resultSet.getTimestamp("created").toInstant(), LedgerEntry.Initiator.values()[resultSet.getInt("initiator")], resultSet.getString("channel"), resultSet.getString("description"), - resultSet.getString("related_account"), + new AccountId(resultSet.getString("related_account")), resultSet.getString("previous_id") ); } diff --git a/src/main/java/pro/cloudnode/smp/bankaccounts/api/ledger/LedgerService.java b/src/main/java/pro/cloudnode/smp/bankaccounts/api/ledger/LedgerService.java index 603175f..7ae2b80 100644 --- a/src/main/java/pro/cloudnode/smp/bankaccounts/api/ledger/LedgerService.java +++ b/src/main/java/pro/cloudnode/smp/bankaccounts/api/ledger/LedgerService.java @@ -22,6 +22,7 @@ import pro.cloudnode.smp.bankaccounts.api.Repository; import pro.cloudnode.smp.bankaccounts.api.Service; import pro.cloudnode.smp.bankaccounts.api.account.Account; +import pro.cloudnode.smp.bankaccounts.api.account.AccountId; import pro.cloudnode.smp.bankaccounts.api.account.AccountsService; import javax.sql.DataSource; @@ -79,10 +80,10 @@ public LedgerService( * @throws InternalException if the ledger could not be queried due to an internal error */ @NotNull - public Optional getLast(final @NotNull String account) throws InternalException { + public Optional getLast(final @NotNull AccountId account) throws InternalException { final List latest; try { - latest = repository.getLatest(account, 1); + latest = repository.getLatest(account.id(), 1); } catch (final Repository.RepositoryException e) { logger.log(Level.SEVERE, String.format("Failed to get last ledger entry for account %s", account), e); throw new InternalException("Failed to get ledger entry"); @@ -103,10 +104,10 @@ public Optional getLast(final @NotNull String account) throws Inter * entries * @throws InternalException if the balance could not be queried due to an internal error * @see LedgerEntry#balance() - * @see #getLast(String) + * @see #getLast(AccountId) */ @NotNull - public BigDecimal getBalance(final @NotNull String account) throws InternalException { + public BigDecimal getBalance(final @NotNull AccountId account) throws InternalException { return getLast(account).map(LedgerEntry::balance).orElse(BigDecimal.ZERO); } @@ -129,8 +130,8 @@ public BigDecimal getBalance(final @NotNull String account) throws InternalExcep */ @NotNull public Transaction createTransfer( - final @NotNull String sender, - final @NotNull String recipient, + final @NotNull AccountId sender, + final @NotNull AccountId recipient, final @NotNull BigDecimal amount, final @NotNull LedgerEntry.Initiator initiator, final @NotNull String channel, @@ -285,7 +286,7 @@ public static final class InsufficientBalanceException extends ServiceException /** * ID of the account which has insufficient funds. */ - public final @NotNull String account; + public final @NotNull AccountId account; /** * Balance of the account. @@ -298,7 +299,7 @@ public static final class InsufficientBalanceException extends ServiceException public final @NotNull BigDecimal amount; private InsufficientBalanceException( - final @NotNull String account, + final @NotNull AccountId account, final @NotNull BigDecimal balance, final @NotNull BigDecimal amount ) { From 6aaea5c90a00ad78c30ff49a52847d9354e8b115 Mon Sep 17 00:00:00 2001 From: Zefir Kirilov Date: Sat, 10 Jan 2026 19:50:42 +0200 Subject: [PATCH 06/10] created basic ACL service and role-based account ACL --- .../pro/cloudnode/smp/bankaccounts/API.java | 7 + .../api/acl/AccessControlListEntry.java | 101 +++++++++ .../bankaccounts/api/acl/AccountAclEntry.java | 95 +++++++++ .../api/acl/AccountAclRepository.java | 191 ++++++++++++++++++ .../smp/bankaccounts/api/acl/AclService.java | 93 +++++++++ src/main/resources/db/init.sql | 13 ++ 6 files changed, 500 insertions(+) create mode 100644 src/main/java/pro/cloudnode/smp/bankaccounts/api/acl/AccessControlListEntry.java create mode 100644 src/main/java/pro/cloudnode/smp/bankaccounts/api/acl/AccountAclEntry.java create mode 100644 src/main/java/pro/cloudnode/smp/bankaccounts/api/acl/AccountAclRepository.java create mode 100644 src/main/java/pro/cloudnode/smp/bankaccounts/api/acl/AclService.java diff --git a/src/main/java/pro/cloudnode/smp/bankaccounts/API.java b/src/main/java/pro/cloudnode/smp/bankaccounts/API.java index 37391e6..16523bf 100644 --- a/src/main/java/pro/cloudnode/smp/bankaccounts/API.java +++ b/src/main/java/pro/cloudnode/smp/bankaccounts/API.java @@ -17,6 +17,7 @@ import org.jetbrains.annotations.NotNull; import pro.cloudnode.smp.bankaccounts.api.account.AccountsService; +import pro.cloudnode.smp.bankaccounts.api.acl.AclService; import pro.cloudnode.smp.bankaccounts.api.ledger.LedgerService; import javax.sql.DataSource; @@ -36,6 +37,11 @@ public final class API { */ public final @NotNull LedgerService ledger; + /** + * Service for managing access control lists (ACLs) and relations. + */ + public final @NotNull AclService acl; + API( final @NotNull Logger parentLogger, final @NotNull DataSource dataSource, @@ -44,5 +50,6 @@ public final class API { ) { this.accounts = new AccountsService(parentLogger, dataSource, idLengthAccount); this.ledger = new LedgerService(parentLogger, dataSource, accounts, idLengthTransaction); + this.acl = new AclService(parentLogger, dataSource); } } diff --git a/src/main/java/pro/cloudnode/smp/bankaccounts/api/acl/AccessControlListEntry.java b/src/main/java/pro/cloudnode/smp/bankaccounts/api/acl/AccessControlListEntry.java new file mode 100644 index 0000000..8862422 --- /dev/null +++ b/src/main/java/pro/cloudnode/smp/bankaccounts/api/acl/AccessControlListEntry.java @@ -0,0 +1,101 @@ +/* + * BankAccounts is a Minecraft economy plugin that enables players to hold multiple bank accounts. + * Copyright © 2023–2026 Cloudnode OÜ. + * + * 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 pro.cloudnode.smp.bankaccounts.api.acl; + +import org.jetbrains.annotations.NotNull; +import pro.cloudnode.smp.bankaccounts.Serializable; +import pro.cloudnode.smp.bankaccounts.api.TypedIdentifier; + +import java.time.Instant; + +/** + * Represents an access control list entry. + * + * @param the relation type + */ +public abstract class AccessControlListEntry { + private final @NotNull TypedIdentifier subject; + private @NotNull T relation; + private final @NotNull TypedIdentifier resource; + private final @NotNull Instant created; + + AccessControlListEntry( + final @NotNull TypedIdentifier subject, + final @NotNull T relation, + final @NotNull TypedIdentifier resource, + final @NotNull Instant created + ) { + this.subject = subject; + this.relation = relation; + this.resource = resource; + this.created = created; + } + + /** + * Returns the identifier of the subject. + * + * @return the subject ID + */ + @NotNull + public TypedIdentifier subject() { + return subject; + } + + /** + * Returns the relation of the subject to the resource. + * + * @return the relation + */ + @NotNull + public T relation() { + return relation; + } + + /** + * Sets the relation of the subject to the resource. + * + * @param relation the new relation + */ + public void relation(final @NotNull T relation) { + this.relation = relation; + } + + /** + * Returns the identifier of the resource. + * + * @return the resource ID + */ + @NotNull + public TypedIdentifier resource() { + return resource; + } + + /** + * Returns the timestamp when the ACL entry was created. + * + * @return the creation timestamp + */ + @NotNull + public Instant created() { + return created; + } + + /** + * Represents the relation of a subject to a resource. + */ + public interface Relation extends Serializable { + } +} diff --git a/src/main/java/pro/cloudnode/smp/bankaccounts/api/acl/AccountAclEntry.java b/src/main/java/pro/cloudnode/smp/bankaccounts/api/acl/AccountAclEntry.java new file mode 100644 index 0000000..3787fd6 --- /dev/null +++ b/src/main/java/pro/cloudnode/smp/bankaccounts/api/acl/AccountAclEntry.java @@ -0,0 +1,95 @@ +/* + * BankAccounts is a Minecraft economy plugin that enables players to hold multiple bank accounts. + * Copyright © 2023–2026 Cloudnode OÜ. + * + * 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 pro.cloudnode.smp.bankaccounts.api.acl; + +import org.jetbrains.annotations.NotNull; +import pro.cloudnode.smp.bankaccounts.api.TypedIdentifier; + +import java.time.Instant; + +/** + * Represents an access control list entry for an account. + */ +public final class AccountAclEntry extends AccessControlListEntry { + AccountAclEntry( + final @NotNull TypedIdentifier subject, + final @NotNull AccountAclEntry.Role role, + final @NotNull String account, + final @NotNull Instant created + ) { + super(subject, role, new TypedIdentifier(TypedIdentifier.Type.ACCOUNT, account), created); + } + + /** + * Constructs a new ACL entry. + * + * @param subject the subject identifier + * @param role the role of the subject + * @param account the account ID + */ + public AccountAclEntry( + final @NotNull TypedIdentifier subject, + final @NotNull Role role, + final @NotNull String account + ) { + this(subject, role, account, Instant.now()); + } + + /** + * Returns the ID of the account to which this ACL entry applies. + * + * @return the account ID + */ + @NotNull + public String account() { + return resource().id(); + } + + /** + * Represents the relation of a subject to an account. + */ + public enum Role implements AccessControlListEntry.Relation { + /** + * Read-only access to the account. + */ + VIEWER, + + /** + * Read-only access to most of the account, but can request payments. + */ + COLLECTOR, + + /** + * Access to initiate money transfers and request payments. + */ + SIGNATORY, + + /** + * Full access to the account, except ACL management or account closure. + */ + MANAGER, + + /** + * Full access to the account. + */ + OWNER; + + @Override + public final @NotNull String serialize() { + return name().toLowerCase(); + } + } +} diff --git a/src/main/java/pro/cloudnode/smp/bankaccounts/api/acl/AccountAclRepository.java b/src/main/java/pro/cloudnode/smp/bankaccounts/api/acl/AccountAclRepository.java new file mode 100644 index 0000000..25bd591 --- /dev/null +++ b/src/main/java/pro/cloudnode/smp/bankaccounts/api/acl/AccountAclRepository.java @@ -0,0 +1,191 @@ +/* + * BankAccounts is a Minecraft economy plugin that enables players to hold multiple bank accounts. + * Copyright © 2023–2026 Cloudnode OÜ. + * + * 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 pro.cloudnode.smp.bankaccounts.api.acl; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import pro.cloudnode.smp.bankaccounts.api.Repository; +import pro.cloudnode.smp.bankaccounts.api.TypedIdentifier; + +import javax.sql.DataSource; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.util.List; +import java.util.Optional; + +/** + * Represents the account access control list repository. + */ +@ApiStatus.Internal +public final class AccountAclRepository extends Repository { + /** + * Constructs an account ACL repository with the specified data source. + * + * @param dataSource the database source + */ + @ApiStatus.Internal + public AccountAclRepository(final @NotNull DataSource dataSource) { + super(dataSource); + } + + /** + * Retrieves the ACL entry for the specified subject and account. + * + * @param subject the subject identifier + * @param account the account ID + * @return the ACL entry if found, otherwise empty + * @throws RepositoryException if an error occurs during query execution + */ + @ApiStatus.Internal + @NotNull + public Optional get(final @NotNull TypedIdentifier subject, final @NotNull String account) { + return queryOne( + "SELECT * FROM bank_account_acl_entries WHERE subject_type = ? AND subject_id = ? AND account = ?", + stmt -> { + stmt.setString(1, subject.type().name()); + stmt.setString(2, subject.id()); + stmt.setString(3, account); + } + ); + } + + /** + * Retrieves all ACL entries for a given account. + * + * @param account the account ID + * @return the list of ACL entries + * @throws RepositoryException if an error occurs during query execution + */ + @ApiStatus.Internal + @NotNull + public List getAllForAccount(final @NotNull String account) { + return queryMany( + "SELECT * FROM bank_account_acl_entries WHERE account = ?", + stmt -> stmt.setString(1, account) + ); + } + + /** + * Retrieves all ACL entries for a given subject. + * + * @param subject the subject identifier + * @return the list of ACL entries + * @throws RepositoryException if an error occurs during query execution + */ + @ApiStatus.Internal + @NotNull + public List getAllForSubject(final @NotNull TypedIdentifier subject) { + return queryMany( + "SELECT * FROM bank_account_acl_entries WHERE subject_type = ? AND subject_id = ?", stmt -> { + stmt.setString(1, subject.type().name()); + stmt.setString(2, subject.id()); + } + ); + } + + /** + * Inserts a new ACL entry into the database. + * + * @param entry the ACL entry to insert + * @throws RepositoryException if an error occurs during query execution + */ + @ApiStatus.Internal + public void insert(final @NotNull AccountAclEntry entry) { + queryUpdate( + """ + INSERT INTO bank_account_acl_entries ( + subject_type, + subject_id, + relation, + account, + created + ) VALUES (?, ?, ?, ?, ?) + """, stmt -> { + stmt.setString(1, entry.subject().type().name()); + stmt.setString(2, entry.subject().id()); + stmt.setString(3, entry.relation().serialize()); + stmt.setString(4, entry.account()); + stmt.setTimestamp(5, Timestamp.from(entry.created())); + } + ); + } + + /** + * Updates an existing ACL entry in the database. + * + * @param entry the ACL entry to update + * @throws IllegalStateException if no rows were modified + * @throws Repository.RepositoryException if an error occurs during query execution + */ + @ApiStatus.Internal + public void update(final @NotNull AccountAclEntry entry) { + final boolean updated = queryUpdate( + """ + UPDATE bank_account_acl_entries + SET relation = ? + WHERE subject_type = ? AND subject_id = ? AND account = ? + """, stmt -> { + stmt.setString(1, entry.relation().serialize()); + stmt.setString(2, entry.subject().type().name()); + stmt.setString(3, entry.subject().id()); + stmt.setString(4, entry.account()); + } + ) > 0; + + if (!updated) { + throw new IllegalStateException(String.format( + "No rows were modified for subject %s on account %s", + entry.subject().id(), + entry.account() + )); + } + } + + /** + * Removes an ACL entry from the database. + * + * @param subject the subject identifier + * @param accountId the account ID + * @throws RepositoryException if an error occurs during query execution + */ + @ApiStatus.Internal + public void remove(final @NotNull TypedIdentifier subject, final @NotNull String accountId) { + queryUpdate( + "DELETE FROM bank_account_acl_entries WHERE subject_type = ? AND subject_id = ? AND account = ?", + stmt -> { + stmt.setString(1, subject.type().name()); + stmt.setString(2, subject.id()); + stmt.setString(3, accountId); + } + ); + } + + @ApiStatus.Internal + @Override + @NotNull + protected AccountAclEntry map(final @NotNull ResultSet resultSet) throws SQLException { + return new AccountAclEntry( + new TypedIdentifier( + TypedIdentifier.Type.valueOf(resultSet.getString("subject_type")), + resultSet.getString("subject_id") + ), + AccountAclEntry.Role.valueOf(resultSet.getString("relation").toUpperCase()), + resultSet.getString("account"), + resultSet.getTimestamp("created").toInstant() + ); + } +} diff --git a/src/main/java/pro/cloudnode/smp/bankaccounts/api/acl/AclService.java b/src/main/java/pro/cloudnode/smp/bankaccounts/api/acl/AclService.java new file mode 100644 index 0000000..18dbfe3 --- /dev/null +++ b/src/main/java/pro/cloudnode/smp/bankaccounts/api/acl/AclService.java @@ -0,0 +1,93 @@ +/* + * BankAccounts is a Minecraft economy plugin that enables players to hold multiple bank accounts. + * Copyright © 2023–2026 Cloudnode OÜ. + * + * 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 pro.cloudnode.smp.bankaccounts.api.acl; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import pro.cloudnode.smp.bankaccounts.api.Repository; +import pro.cloudnode.smp.bankaccounts.api.Service; +import pro.cloudnode.smp.bankaccounts.api.TypedIdentifier; +import pro.cloudnode.smp.bankaccounts.api.account.AccountId; + +import javax.sql.DataSource; +import java.util.Optional; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Represents the ACL service. + */ +public final class AclService extends Service { + private final @NotNull AccountAclRepository account; + + /** + * Constructs a new ACL service. + * + * @param parentLogger the parent logger, or null if none + * @param dataSource the database source + */ + @ApiStatus.Internal + public AclService(final @Nullable Logger parentLogger, final @NotNull DataSource dataSource) { + super(parentLogger); + this.account = new AccountAclRepository(dataSource); + } + + /** + * Retrieves the ACL account entry for the specified subject and account. + * + * @param subject the subject identifier + * @param account the account identifier + * @return the ACL entry if present, otherwise empty + * @throws ServiceException if the subject type is not allowed for accounts + */ + @NotNull + public Optional get(final @NotNull TypedIdentifier subject, final @NotNull AccountId account) + throws ServiceException { + if (subject.type() != TypedIdentifier.Type.HOLDER) { + throw new InternalException(String.format("Invalid subject type %s for accounts", subject.type().name())); + } + try { + return this.account.get(subject, account.id()); + } catch (final Repository.RepositoryException e) { + logger.log( + Level.SEVERE, + String.format("Failed to get ACL for: %s, %s", subject.serialize(), account.serialize()), + e + ); + throw new InternalException("Failed to check for ID collision"); + } + } + + /** + * Retrieves the ACL entry for the specified subject and resource. + * + * @param subject the subject identifier + * @param resource the resource identifier + * @return the ACL entry if present, otherwise empty + * @throws ServiceException if the subject type is invalid for the resource or the resource type is unsupported + */ + @NotNull + public Optional> get( + final @NotNull TypedIdentifier subject, + final @NotNull TypedIdentifier resource + ) throws ServiceException { + return switch (resource.type()) { + case ACCOUNT -> get(subject, new AccountId(resource.id())); + default -> throw new InternalException(String.format("%s is not a resource ID", resource.serialize())); + }; + } +} diff --git a/src/main/resources/db/init.sql b/src/main/resources/db/init.sql index a2e28a7..861b5bf 100644 --- a/src/main/resources/db/init.sql +++ b/src/main/resources/db/init.sql @@ -38,3 +38,16 @@ CREATE TABLE bank_ledger_entries ); CREATE INDEX idx_bank_ledger_entries_account_created_desc ON bank_ledger_entries (account, created DESC); + +CREATE TABLE bank_account_acl_entries +( + subject_type VARCHAR(64) NOT NULL, + subject_id VARCHAR(64) NOT NULL, + relation VARCHAR(64) NOT NULL, + account VARCHAR(32) NOT NULL, + created NOT NULL, + PRIMARY KEY (subject_type, subject_id, account), + FOREIGN KEY (account) REFERENCES bank_accounts (id) ON DELETE CASCADE +); + +CREATE INDEX idx_bank_account_acl_entries_subject ON bank_account_acl_entries (subject_type, subject_id); From dea17b60455a0f1cf274f93f8c70b1490a5b63b1 Mon Sep 17 00:00:00 2001 From: Zefir Kirilov Date: Sun, 11 Jan 2026 09:19:22 +0200 Subject: [PATCH 07/10] use AccountId instead of String in account ACL entry --- .../smp/bankaccounts/api/acl/AccountAclEntry.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/main/java/pro/cloudnode/smp/bankaccounts/api/acl/AccountAclEntry.java b/src/main/java/pro/cloudnode/smp/bankaccounts/api/acl/AccountAclEntry.java index 3787fd6..af7fb1e 100644 --- a/src/main/java/pro/cloudnode/smp/bankaccounts/api/acl/AccountAclEntry.java +++ b/src/main/java/pro/cloudnode/smp/bankaccounts/api/acl/AccountAclEntry.java @@ -17,6 +17,7 @@ import org.jetbrains.annotations.NotNull; import pro.cloudnode.smp.bankaccounts.api.TypedIdentifier; +import pro.cloudnode.smp.bankaccounts.api.account.AccountId; import java.time.Instant; @@ -27,10 +28,10 @@ public final class AccountAclEntry extends AccessControlListEntry Date: Sun, 11 Jan 2026 09:20:25 +0200 Subject: [PATCH 08/10] account ACL role deserialisation --- .../smp/bankaccounts/api/acl/AccountAclEntry.java | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/main/java/pro/cloudnode/smp/bankaccounts/api/acl/AccountAclEntry.java b/src/main/java/pro/cloudnode/smp/bankaccounts/api/acl/AccountAclEntry.java index af7fb1e..e1cb214 100644 --- a/src/main/java/pro/cloudnode/smp/bankaccounts/api/acl/AccountAclEntry.java +++ b/src/main/java/pro/cloudnode/smp/bankaccounts/api/acl/AccountAclEntry.java @@ -89,8 +89,20 @@ public enum Role implements AccessControlListEntry.Relation { OWNER; @Override - public final @NotNull String serialize() { + @NotNull + public final String serialize() { return name().toLowerCase(); } + + /** + * Returns a role from string. + * + * @param value the string representation of the role + * @return the role + */ + @NotNull + public static Role deserialize(final @NotNull String value) { + return valueOf(value.toUpperCase()); + } } } From a01e13c54fecee80c2d12d109341f6d91eb1fe6b Mon Sep 17 00:00:00 2001 From: Zefir Kirilov Date: Sun, 11 Jan 2026 09:40:01 +0200 Subject: [PATCH 09/10] correctly use Account ID in Account ACL entry repository --- .../smp/bankaccounts/api/acl/AccountAclRepository.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/pro/cloudnode/smp/bankaccounts/api/acl/AccountAclRepository.java b/src/main/java/pro/cloudnode/smp/bankaccounts/api/acl/AccountAclRepository.java index 25bd591..ae2a5b5 100644 --- a/src/main/java/pro/cloudnode/smp/bankaccounts/api/acl/AccountAclRepository.java +++ b/src/main/java/pro/cloudnode/smp/bankaccounts/api/acl/AccountAclRepository.java @@ -19,6 +19,7 @@ import org.jetbrains.annotations.NotNull; import pro.cloudnode.smp.bankaccounts.api.Repository; import pro.cloudnode.smp.bankaccounts.api.TypedIdentifier; +import pro.cloudnode.smp.bankaccounts.api.account.AccountId; import javax.sql.DataSource; import java.sql.ResultSet; @@ -118,7 +119,7 @@ INSERT INTO bank_account_acl_entries ( stmt.setString(1, entry.subject().type().name()); stmt.setString(2, entry.subject().id()); stmt.setString(3, entry.relation().serialize()); - stmt.setString(4, entry.account()); + stmt.setString(4, entry.account().id()); stmt.setTimestamp(5, Timestamp.from(entry.created())); } ); @@ -142,7 +143,7 @@ public void update(final @NotNull AccountAclEntry entry) { stmt.setString(1, entry.relation().serialize()); stmt.setString(2, entry.subject().type().name()); stmt.setString(3, entry.subject().id()); - stmt.setString(4, entry.account()); + stmt.setString(4, entry.account().id()); } ) > 0; @@ -184,7 +185,7 @@ protected AccountAclEntry map(final @NotNull ResultSet resultSet) throws SQLExce resultSet.getString("subject_id") ), AccountAclEntry.Role.valueOf(resultSet.getString("relation").toUpperCase()), - resultSet.getString("account"), + new AccountId(resultSet.getString("account")), resultSet.getTimestamp("created").toInstant() ); } From 95a763e85a210c779da53450f5136e493c4754af Mon Sep 17 00:00:00 2001 From: Zefir Kirilov Date: Sun, 11 Jan 2026 09:48:39 +0200 Subject: [PATCH 10/10] added missing javadoc --- .../pro/cloudnode/smp/bankaccounts/api/TypedIdentifier.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/pro/cloudnode/smp/bankaccounts/api/TypedIdentifier.java b/src/main/java/pro/cloudnode/smp/bankaccounts/api/TypedIdentifier.java index 6f56c08..6f0d9ef 100644 --- a/src/main/java/pro/cloudnode/smp/bankaccounts/api/TypedIdentifier.java +++ b/src/main/java/pro/cloudnode/smp/bankaccounts/api/TypedIdentifier.java @@ -116,6 +116,12 @@ public enum Type implements Serializable { */ ACCOUNT; + /** + * Returns a type from a string. + * + * @param value the string representation of the type + * @return the type + */ @NotNull public static Type deserialize(final @NotNull String value) { return valueOf(value.toUpperCase());