mirror of
https://github.com/AsamK/signal-cli
synced 2025-08-29 02:20:39 +00:00
Move configuration store to db
This commit is contained in:
parent
90ec01bfbf
commit
c0f771684d
7 changed files with 264 additions and 86 deletions
|
@ -1111,6 +1111,13 @@
|
||||||
"queryAllDeclaredConstructors":true,
|
"queryAllDeclaredConstructors":true,
|
||||||
"methods":[{"name":"<init>","parameterTypes":["java.lang.Boolean","java.lang.Boolean","java.lang.Boolean","java.lang.Boolean","java.lang.Boolean","org.asamk.signal.manager.api.PhoneNumberSharingMode"] }, {"name":"linkPreviews","parameterTypes":[] }, {"name":"phoneNumberSharingMode","parameterTypes":[] }, {"name":"phoneNumberUnlisted","parameterTypes":[] }, {"name":"readReceipts","parameterTypes":[] }, {"name":"typingIndicators","parameterTypes":[] }, {"name":"unidentifiedDeliveryIndicators","parameterTypes":[] }]
|
"methods":[{"name":"<init>","parameterTypes":["java.lang.Boolean","java.lang.Boolean","java.lang.Boolean","java.lang.Boolean","java.lang.Boolean","org.asamk.signal.manager.api.PhoneNumberSharingMode"] }, {"name":"linkPreviews","parameterTypes":[] }, {"name":"phoneNumberSharingMode","parameterTypes":[] }, {"name":"phoneNumberUnlisted","parameterTypes":[] }, {"name":"readReceipts","parameterTypes":[] }, {"name":"typingIndicators","parameterTypes":[] }, {"name":"unidentifiedDeliveryIndicators","parameterTypes":[] }]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name":"org.asamk.signal.manager.storage.configuration.LegacyConfigurationStore$Storage",
|
||||||
|
"allDeclaredFields":true,
|
||||||
|
"queryAllDeclaredMethods":true,
|
||||||
|
"queryAllDeclaredConstructors":true,
|
||||||
|
"methods":[{"name":"<init>","parameterTypes":["java.lang.Boolean","java.lang.Boolean","java.lang.Boolean","java.lang.Boolean","java.lang.Boolean","org.asamk.signal.manager.api.PhoneNumberSharingMode"] }]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name":"org.asamk.signal.manager.storage.contacts.LegacyContactInfo",
|
"name":"org.asamk.signal.manager.storage.contacts.LegacyContactInfo",
|
||||||
"allDeclaredFields":true,
|
"allDeclaredFields":true,
|
||||||
|
|
|
@ -30,7 +30,7 @@ import java.util.UUID;
|
||||||
public class AccountDatabase extends Database {
|
public class AccountDatabase extends Database {
|
||||||
|
|
||||||
private final static Logger logger = LoggerFactory.getLogger(AccountDatabase.class);
|
private final static Logger logger = LoggerFactory.getLogger(AccountDatabase.class);
|
||||||
private static final long DATABASE_VERSION = 16;
|
private static final long DATABASE_VERSION = 17;
|
||||||
|
|
||||||
private AccountDatabase(final HikariDataSource dataSource) {
|
private AccountDatabase(final HikariDataSource dataSource) {
|
||||||
super(logger, DATABASE_VERSION, dataSource);
|
super(logger, DATABASE_VERSION, dataSource);
|
||||||
|
@ -503,5 +503,17 @@ public class AccountDatabase extends Database {
|
||||||
""");
|
""");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (oldVersion < 17) {
|
||||||
|
logger.debug("Updating database: Adding key_value table");
|
||||||
|
try (final var statement = connection.createStatement()) {
|
||||||
|
statement.executeUpdate("""
|
||||||
|
CREATE TABLE key_value (
|
||||||
|
_id INTEGER PRIMARY KEY,
|
||||||
|
key TEXT UNIQUE NOT NULL,
|
||||||
|
value ANY
|
||||||
|
) STRICT;
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ import org.asamk.signal.manager.api.ServiceEnvironment;
|
||||||
import org.asamk.signal.manager.api.TrustLevel;
|
import org.asamk.signal.manager.api.TrustLevel;
|
||||||
import org.asamk.signal.manager.helper.RecipientAddressResolver;
|
import org.asamk.signal.manager.helper.RecipientAddressResolver;
|
||||||
import org.asamk.signal.manager.storage.configuration.ConfigurationStore;
|
import org.asamk.signal.manager.storage.configuration.ConfigurationStore;
|
||||||
|
import org.asamk.signal.manager.storage.configuration.LegacyConfigurationStore;
|
||||||
import org.asamk.signal.manager.storage.contacts.ContactsStore;
|
import org.asamk.signal.manager.storage.contacts.ContactsStore;
|
||||||
import org.asamk.signal.manager.storage.contacts.LegacyJsonContactsStore;
|
import org.asamk.signal.manager.storage.contacts.LegacyJsonContactsStore;
|
||||||
import org.asamk.signal.manager.storage.groups.GroupInfoV1;
|
import org.asamk.signal.manager.storage.groups.GroupInfoV1;
|
||||||
|
@ -20,6 +21,7 @@ import org.asamk.signal.manager.storage.groups.LegacyGroupStore;
|
||||||
import org.asamk.signal.manager.storage.identities.IdentityKeyStore;
|
import org.asamk.signal.manager.storage.identities.IdentityKeyStore;
|
||||||
import org.asamk.signal.manager.storage.identities.LegacyIdentityKeyStore;
|
import org.asamk.signal.manager.storage.identities.LegacyIdentityKeyStore;
|
||||||
import org.asamk.signal.manager.storage.identities.SignalIdentityKeyStore;
|
import org.asamk.signal.manager.storage.identities.SignalIdentityKeyStore;
|
||||||
|
import org.asamk.signal.manager.storage.keyValue.KeyValueStore;
|
||||||
import org.asamk.signal.manager.storage.messageCache.MessageCache;
|
import org.asamk.signal.manager.storage.messageCache.MessageCache;
|
||||||
import org.asamk.signal.manager.storage.prekeys.KyberPreKeyStore;
|
import org.asamk.signal.manager.storage.prekeys.KyberPreKeyStore;
|
||||||
import org.asamk.signal.manager.storage.prekeys.LegacyPreKeyStore;
|
import org.asamk.signal.manager.storage.prekeys.LegacyPreKeyStore;
|
||||||
|
@ -151,7 +153,7 @@ public class SignalAccount implements Closeable {
|
||||||
private RecipientStore recipientStore;
|
private RecipientStore recipientStore;
|
||||||
private StickerStore stickerStore;
|
private StickerStore stickerStore;
|
||||||
private ConfigurationStore configurationStore;
|
private ConfigurationStore configurationStore;
|
||||||
private ConfigurationStore.Storage configurationStoreStorage;
|
private KeyValueStore keyValueStore;
|
||||||
|
|
||||||
private MessageCache messageCache;
|
private MessageCache messageCache;
|
||||||
private MessageSendLogStore messageSendLogStore;
|
private MessageSendLogStore messageSendLogStore;
|
||||||
|
@ -216,7 +218,6 @@ public class SignalAccount implements Closeable {
|
||||||
signalAccount.aciAccountData.setLocalRegistrationId(registrationId);
|
signalAccount.aciAccountData.setLocalRegistrationId(registrationId);
|
||||||
signalAccount.pniAccountData.setLocalRegistrationId(pniRegistrationId);
|
signalAccount.pniAccountData.setLocalRegistrationId(pniRegistrationId);
|
||||||
signalAccount.settings = settings;
|
signalAccount.settings = settings;
|
||||||
signalAccount.configurationStore = new ConfigurationStore(signalAccount::saveConfigurationStore);
|
|
||||||
|
|
||||||
signalAccount.registered = false;
|
signalAccount.registered = false;
|
||||||
|
|
||||||
|
@ -266,8 +267,6 @@ public class SignalAccount implements Closeable {
|
||||||
pniIdentityKey,
|
pniIdentityKey,
|
||||||
profileKey);
|
profileKey);
|
||||||
|
|
||||||
signalAccount.configurationStore = new ConfigurationStore(signalAccount::saveConfigurationStore);
|
|
||||||
|
|
||||||
signalAccount.getRecipientTrustedResolver()
|
signalAccount.getRecipientTrustedResolver()
|
||||||
.resolveSelfRecipientTrusted(signalAccount.getSelfRecipientAddress());
|
.resolveSelfRecipientTrusted(signalAccount.getSelfRecipientAddress());
|
||||||
signalAccount.previousStorageVersion = CURRENT_STORAGE_VERSION;
|
signalAccount.previousStorageVersion = CURRENT_STORAGE_VERSION;
|
||||||
|
@ -774,12 +773,10 @@ public class SignalAccount implements Closeable {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rootNode.hasNonNull("configurationStore")) {
|
if (rootNode.hasNonNull("configurationStore")) {
|
||||||
configurationStoreStorage = jsonProcessor.convertValue(rootNode.get("configurationStore"),
|
final var configurationStoreStorage = jsonProcessor.convertValue(rootNode.get("configurationStore"),
|
||||||
ConfigurationStore.Storage.class);
|
LegacyConfigurationStore.Storage.class);
|
||||||
configurationStore = ConfigurationStore.fromStorage(configurationStoreStorage,
|
LegacyConfigurationStore.migrate(configurationStoreStorage, getConfigurationStore());
|
||||||
this::saveConfigurationStore);
|
migratedLegacyConfig = true;
|
||||||
} else {
|
|
||||||
configurationStore = new ConfigurationStore(this::saveConfigurationStore);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
migratedLegacyConfig = loadLegacyThreadStore(rootNode) || migratedLegacyConfig;
|
migratedLegacyConfig = loadLegacyThreadStore(rootNode) || migratedLegacyConfig;
|
||||||
|
@ -965,11 +962,6 @@ public class SignalAccount implements Closeable {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void saveConfigurationStore(ConfigurationStore.Storage storage) {
|
|
||||||
this.configurationStoreStorage = storage;
|
|
||||||
save();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void save() {
|
private void save() {
|
||||||
synchronized (fileChannel) {
|
synchronized (fileChannel) {
|
||||||
var rootNode = jsonProcessor.createObjectNode();
|
var rootNode = jsonProcessor.createObjectNode();
|
||||||
|
@ -1028,8 +1020,7 @@ public class SignalAccount implements Closeable {
|
||||||
pniAccountData.getPreKeyMetadata().activeLastResortKyberPreKeyId)
|
pniAccountData.getPreKeyMetadata().activeLastResortKyberPreKeyId)
|
||||||
.put("profileKey",
|
.put("profileKey",
|
||||||
profileKey == null ? null : Base64.getEncoder().encodeToString(profileKey.serialize()))
|
profileKey == null ? null : Base64.getEncoder().encodeToString(profileKey.serialize()))
|
||||||
.put("registered", registered)
|
.put("registered", registered);
|
||||||
.putPOJO("configurationStore", configurationStoreStorage);
|
|
||||||
try {
|
try {
|
||||||
try (var output = new ByteArrayOutputStream()) {
|
try (var output = new ByteArrayOutputStream()) {
|
||||||
// Write to memory first to prevent corrupting the file in case of serialization errors
|
// Write to memory first to prevent corrupting the file in case of serialization errors
|
||||||
|
@ -1295,8 +1286,13 @@ public class SignalAccount implements Closeable {
|
||||||
return getOrCreate(() -> senderKeyStore, () -> senderKeyStore = new SenderKeyStore(getAccountDatabase()));
|
return getOrCreate(() -> senderKeyStore, () -> senderKeyStore = new SenderKeyStore(getAccountDatabase()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private KeyValueStore getKeyValueStore() {
|
||||||
|
return getOrCreate(() -> keyValueStore, () -> keyValueStore = new KeyValueStore(getAccountDatabase()));
|
||||||
|
}
|
||||||
|
|
||||||
public ConfigurationStore getConfigurationStore() {
|
public ConfigurationStore getConfigurationStore() {
|
||||||
return configurationStore;
|
return getOrCreate(() -> configurationStore,
|
||||||
|
() -> configurationStore = new ConfigurationStore(getKeyValueStore()));
|
||||||
}
|
}
|
||||||
|
|
||||||
public MessageCache getMessageCache() {
|
public MessageCache getMessageCache() {
|
||||||
|
@ -1662,7 +1658,7 @@ public class SignalAccount implements Closeable {
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isDiscoverableByPhoneNumber() {
|
public boolean isDiscoverableByPhoneNumber() {
|
||||||
final var phoneNumberUnlisted = configurationStore.getPhoneNumberUnlisted();
|
final var phoneNumberUnlisted = getConfigurationStore().getPhoneNumberUnlisted();
|
||||||
return phoneNumberUnlisted == null || !phoneNumberUnlisted;
|
return phoneNumberUnlisted == null || !phoneNumberUnlisted;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,106 +1,75 @@
|
||||||
package org.asamk.signal.manager.storage.configuration;
|
package org.asamk.signal.manager.storage.configuration;
|
||||||
|
|
||||||
import org.asamk.signal.manager.api.PhoneNumberSharingMode;
|
import org.asamk.signal.manager.api.PhoneNumberSharingMode;
|
||||||
|
import org.asamk.signal.manager.storage.keyValue.KeyValueEntry;
|
||||||
|
import org.asamk.signal.manager.storage.keyValue.KeyValueStore;
|
||||||
|
|
||||||
public class ConfigurationStore {
|
public class ConfigurationStore {
|
||||||
|
|
||||||
private final Saver saver;
|
private final KeyValueStore keyValueStore;
|
||||||
|
|
||||||
private Boolean readReceipts;
|
private final KeyValueEntry<Boolean> readReceipts = new KeyValueEntry<>("config-read-receipts", Boolean.class);
|
||||||
private Boolean unidentifiedDeliveryIndicators;
|
private final KeyValueEntry<Boolean> unidentifiedDeliveryIndicators = new KeyValueEntry<>(
|
||||||
private Boolean typingIndicators;
|
"config-unidentified-delivery-indicators",
|
||||||
private Boolean linkPreviews;
|
Boolean.class);
|
||||||
private Boolean phoneNumberUnlisted;
|
private final KeyValueEntry<Boolean> typingIndicators = new KeyValueEntry<>("config-typing-indicators",
|
||||||
private PhoneNumberSharingMode phoneNumberSharingMode;
|
Boolean.class);
|
||||||
|
private final KeyValueEntry<Boolean> linkPreviews = new KeyValueEntry<>("config-link-previews", Boolean.class);
|
||||||
|
private final KeyValueEntry<Boolean> phoneNumberUnlisted = new KeyValueEntry<>("config-phone-number-unlisted",
|
||||||
|
Boolean.class);
|
||||||
|
private final KeyValueEntry<PhoneNumberSharingMode> phoneNumberSharingMode = new KeyValueEntry<>(
|
||||||
|
"config-phone-number-sharing-mode",
|
||||||
|
PhoneNumberSharingMode.class);
|
||||||
|
|
||||||
public ConfigurationStore(final Saver saver) {
|
public ConfigurationStore(final KeyValueStore keyValueStore) {
|
||||||
this.saver = saver;
|
this.keyValueStore = keyValueStore;
|
||||||
}
|
|
||||||
|
|
||||||
public static ConfigurationStore fromStorage(Storage storage, Saver saver) {
|
|
||||||
final var store = new ConfigurationStore(saver);
|
|
||||||
store.readReceipts = storage.readReceipts;
|
|
||||||
store.unidentifiedDeliveryIndicators = storage.unidentifiedDeliveryIndicators;
|
|
||||||
store.typingIndicators = storage.typingIndicators;
|
|
||||||
store.linkPreviews = storage.linkPreviews;
|
|
||||||
store.phoneNumberSharingMode = storage.phoneNumberSharingMode;
|
|
||||||
return store;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Boolean getReadReceipts() {
|
public Boolean getReadReceipts() {
|
||||||
return readReceipts;
|
return keyValueStore.getEntry(readReceipts);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setReadReceipts(final boolean readReceipts) {
|
public void setReadReceipts(final boolean value) {
|
||||||
this.readReceipts = readReceipts;
|
keyValueStore.storeEntry(readReceipts, value);
|
||||||
saver.save(toStorage());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Boolean getUnidentifiedDeliveryIndicators() {
|
public Boolean getUnidentifiedDeliveryIndicators() {
|
||||||
return unidentifiedDeliveryIndicators;
|
return keyValueStore.getEntry(unidentifiedDeliveryIndicators);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setUnidentifiedDeliveryIndicators(final boolean unidentifiedDeliveryIndicators) {
|
public void setUnidentifiedDeliveryIndicators(final boolean value) {
|
||||||
this.unidentifiedDeliveryIndicators = unidentifiedDeliveryIndicators;
|
keyValueStore.storeEntry(unidentifiedDeliveryIndicators, value);
|
||||||
saver.save(toStorage());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Boolean getTypingIndicators() {
|
public Boolean getTypingIndicators() {
|
||||||
return typingIndicators;
|
return keyValueStore.getEntry(typingIndicators);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setTypingIndicators(final boolean typingIndicators) {
|
public void setTypingIndicators(final boolean value) {
|
||||||
this.typingIndicators = typingIndicators;
|
keyValueStore.storeEntry(typingIndicators, value);
|
||||||
saver.save(toStorage());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Boolean getLinkPreviews() {
|
public Boolean getLinkPreviews() {
|
||||||
return linkPreviews;
|
return keyValueStore.getEntry(linkPreviews);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setLinkPreviews(final boolean linkPreviews) {
|
public void setLinkPreviews(final boolean value) {
|
||||||
this.linkPreviews = linkPreviews;
|
keyValueStore.storeEntry(linkPreviews, value);
|
||||||
saver.save(toStorage());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Boolean getPhoneNumberUnlisted() {
|
public Boolean getPhoneNumberUnlisted() {
|
||||||
return phoneNumberUnlisted;
|
return keyValueStore.getEntry(phoneNumberUnlisted);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setPhoneNumberUnlisted(final boolean phoneNumberUnlisted) {
|
public void setPhoneNumberUnlisted(final boolean value) {
|
||||||
this.phoneNumberUnlisted = phoneNumberUnlisted;
|
keyValueStore.storeEntry(phoneNumberUnlisted, value);
|
||||||
saver.save(toStorage());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public PhoneNumberSharingMode getPhoneNumberSharingMode() {
|
public PhoneNumberSharingMode getPhoneNumberSharingMode() {
|
||||||
return phoneNumberSharingMode;
|
return keyValueStore.getEntry(phoneNumberSharingMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setPhoneNumberSharingMode(final PhoneNumberSharingMode phoneNumberSharingMode) {
|
public void setPhoneNumberSharingMode(final PhoneNumberSharingMode value) {
|
||||||
this.phoneNumberSharingMode = phoneNumberSharingMode;
|
keyValueStore.storeEntry(phoneNumberSharingMode, value);
|
||||||
saver.save(toStorage());
|
|
||||||
}
|
|
||||||
|
|
||||||
private Storage toStorage() {
|
|
||||||
return new Storage(readReceipts,
|
|
||||||
unidentifiedDeliveryIndicators,
|
|
||||||
typingIndicators,
|
|
||||||
linkPreviews,
|
|
||||||
phoneNumberUnlisted,
|
|
||||||
phoneNumberSharingMode);
|
|
||||||
}
|
|
||||||
|
|
||||||
public record Storage(
|
|
||||||
Boolean readReceipts,
|
|
||||||
Boolean unidentifiedDeliveryIndicators,
|
|
||||||
Boolean typingIndicators,
|
|
||||||
Boolean linkPreviews,
|
|
||||||
Boolean phoneNumberUnlisted,
|
|
||||||
PhoneNumberSharingMode phoneNumberSharingMode
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public interface Saver {
|
|
||||||
|
|
||||||
void save(Storage storage);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
package org.asamk.signal.manager.storage.configuration;
|
||||||
|
|
||||||
|
import org.asamk.signal.manager.api.PhoneNumberSharingMode;
|
||||||
|
|
||||||
|
public class LegacyConfigurationStore {
|
||||||
|
|
||||||
|
public static void migrate(Storage storage, ConfigurationStore configurationStore) {
|
||||||
|
if (storage.readReceipts != null) {
|
||||||
|
configurationStore.setReadReceipts(storage.readReceipts);
|
||||||
|
}
|
||||||
|
if (storage.unidentifiedDeliveryIndicators != null) {
|
||||||
|
configurationStore.setUnidentifiedDeliveryIndicators(storage.unidentifiedDeliveryIndicators);
|
||||||
|
}
|
||||||
|
if (storage.typingIndicators != null) {
|
||||||
|
configurationStore.setTypingIndicators(storage.typingIndicators);
|
||||||
|
}
|
||||||
|
if (storage.linkPreviews != null) {
|
||||||
|
configurationStore.setLinkPreviews(storage.linkPreviews);
|
||||||
|
}
|
||||||
|
if (storage.phoneNumberSharingMode != null) {
|
||||||
|
configurationStore.setPhoneNumberSharingMode(storage.phoneNumberSharingMode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record Storage(
|
||||||
|
Boolean readReceipts,
|
||||||
|
Boolean unidentifiedDeliveryIndicators,
|
||||||
|
Boolean typingIndicators,
|
||||||
|
Boolean linkPreviews,
|
||||||
|
Boolean phoneNumberUnlisted,
|
||||||
|
PhoneNumberSharingMode phoneNumberSharingMode
|
||||||
|
) {}
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
package org.asamk.signal.manager.storage.keyValue;
|
||||||
|
|
||||||
|
public record KeyValueEntry<T>(String key, Class<T> clazz, T defaultValue) {
|
||||||
|
|
||||||
|
public KeyValueEntry(String key, Class<T> clazz) {
|
||||||
|
this(key, clazz, null);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,153 @@
|
||||||
|
package org.asamk.signal.manager.storage.keyValue;
|
||||||
|
|
||||||
|
import org.asamk.signal.manager.storage.Database;
|
||||||
|
import org.asamk.signal.manager.storage.Utils;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.sql.Types;
|
||||||
|
|
||||||
|
public class KeyValueStore {
|
||||||
|
|
||||||
|
private static final String TABLE_KEY_VALUE = "key_value";
|
||||||
|
private final static Logger logger = LoggerFactory.getLogger(KeyValueStore.class);
|
||||||
|
|
||||||
|
private final Database database;
|
||||||
|
|
||||||
|
public static void createSql(Connection connection) throws SQLException {
|
||||||
|
// When modifying the CREATE statement here, also add a migration in AccountDatabase.java
|
||||||
|
try (final var statement = connection.createStatement()) {
|
||||||
|
statement.executeUpdate("""
|
||||||
|
CREATE TABLE key_value (
|
||||||
|
_id INTEGER PRIMARY KEY,
|
||||||
|
key TEXT UNIQUE NOT NULL,
|
||||||
|
value ANY
|
||||||
|
) STRICT;
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public KeyValueStore(final Database database) {
|
||||||
|
this.database = database;
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> T getEntry(KeyValueEntry<T> key) {
|
||||||
|
final var sql = (
|
||||||
|
"""
|
||||||
|
SELECT key, value
|
||||||
|
FROM %s p
|
||||||
|
WHERE p.key = ?
|
||||||
|
"""
|
||||||
|
).formatted(TABLE_KEY_VALUE);
|
||||||
|
try (final var connection = database.getConnection()) {
|
||||||
|
try (final var statement = connection.prepareStatement(sql)) {
|
||||||
|
statement.setString(1, key.key());
|
||||||
|
|
||||||
|
final var result = Utils.executeQueryForOptional(statement,
|
||||||
|
resultSet -> readValueFromResultSet(key, resultSet)).orElse(null);
|
||||||
|
|
||||||
|
if (result == null) {
|
||||||
|
return key.defaultValue();
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new RuntimeException("Failed read from pre_key store", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> void storeEntry(KeyValueEntry<T> key, T value) {
|
||||||
|
final var sql = (
|
||||||
|
"""
|
||||||
|
INSERT INTO %s (key, value)
|
||||||
|
VALUES (?1, ?2)
|
||||||
|
ON CONFLICT (key) DO UPDATE SET value=excluded.value
|
||||||
|
"""
|
||||||
|
).formatted(TABLE_KEY_VALUE);
|
||||||
|
try (final var connection = database.getConnection()) {
|
||||||
|
try (final var statement = connection.prepareStatement(sql)) {
|
||||||
|
statement.setString(1, key.key());
|
||||||
|
setParameterValue(statement, 2, key.clazz(), value);
|
||||||
|
statement.executeUpdate();
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new RuntimeException("Failed update key_value store", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <T> T readValueFromResultSet(
|
||||||
|
final KeyValueEntry<T> key, final ResultSet resultSet
|
||||||
|
) throws SQLException {
|
||||||
|
Object value;
|
||||||
|
final var clazz = key.clazz();
|
||||||
|
if (clazz == int.class || clazz == Integer.class) {
|
||||||
|
value = resultSet.getInt("value");
|
||||||
|
} else if (clazz == long.class || clazz == Long.class) {
|
||||||
|
value = resultSet.getLong("value");
|
||||||
|
} else if (clazz == boolean.class || clazz == Boolean.class) {
|
||||||
|
value = resultSet.getBoolean("value");
|
||||||
|
} else if (clazz == String.class) {
|
||||||
|
value = resultSet.getString("value");
|
||||||
|
} else if (Enum.class.isAssignableFrom(clazz)) {
|
||||||
|
final var name = resultSet.getString("value");
|
||||||
|
if (name == null) {
|
||||||
|
value = null;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
value = Enum.valueOf((Class<Enum>) key.clazz(), name);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
logger.debug("Read invalid enum value from store, ignoring: {} for {}", name, key.clazz());
|
||||||
|
value = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new AssertionError("Invalid key type " + clazz.getSimpleName());
|
||||||
|
}
|
||||||
|
if (resultSet.wasNull()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (T) value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <T> void setParameterValue(
|
||||||
|
final PreparedStatement statement, final int parameterIndex, final Class<T> clazz, final T value
|
||||||
|
) throws SQLException {
|
||||||
|
if (clazz == int.class || clazz == Integer.class) {
|
||||||
|
if (value == null) {
|
||||||
|
statement.setNull(parameterIndex, Types.INTEGER);
|
||||||
|
} else {
|
||||||
|
statement.setInt(parameterIndex, (int) value);
|
||||||
|
}
|
||||||
|
} else if (clazz == long.class || clazz == Long.class) {
|
||||||
|
if (value == null) {
|
||||||
|
statement.setNull(parameterIndex, Types.INTEGER);
|
||||||
|
} else {
|
||||||
|
statement.setLong(parameterIndex, (long) value);
|
||||||
|
}
|
||||||
|
} else if (clazz == boolean.class || clazz == Boolean.class) {
|
||||||
|
if (value == null) {
|
||||||
|
statement.setNull(parameterIndex, Types.BOOLEAN);
|
||||||
|
} else {
|
||||||
|
statement.setBoolean(parameterIndex, (boolean) value);
|
||||||
|
}
|
||||||
|
} else if (clazz == String.class) {
|
||||||
|
if (value == null) {
|
||||||
|
statement.setNull(parameterIndex, Types.VARCHAR);
|
||||||
|
} else {
|
||||||
|
statement.setString(parameterIndex, (String) value);
|
||||||
|
}
|
||||||
|
} else if (Enum.class.isAssignableFrom(clazz)) {
|
||||||
|
if (value == null) {
|
||||||
|
statement.setNull(parameterIndex, Types.VARCHAR);
|
||||||
|
} else {
|
||||||
|
statement.setString(parameterIndex, ((Enum<?>) value).name());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new AssertionError("Invalid key type " + clazz.getSimpleName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue