1 package org
.asamk
.signal
.manager
.storage
.keyValue
;
3 import org
.asamk
.signal
.manager
.storage
.Database
;
4 import org
.asamk
.signal
.manager
.storage
.Utils
;
5 import org
.slf4j
.Logger
;
6 import org
.slf4j
.LoggerFactory
;
8 import java
.sql
.Connection
;
9 import java
.sql
.PreparedStatement
;
10 import java
.sql
.ResultSet
;
11 import java
.sql
.SQLException
;
12 import java
.sql
.Types
;
13 import java
.util
.Objects
;
15 public class KeyValueStore
{
17 private static final String TABLE_KEY_VALUE
= "key_value";
18 private static final Logger logger
= LoggerFactory
.getLogger(KeyValueStore
.class);
20 private final Database database
;
22 public static void createSql(Connection connection
) throws SQLException
{
23 // When modifying the CREATE statement here, also add a migration in AccountDatabase.java
24 try (final var statement
= connection
.createStatement()) {
25 statement
.executeUpdate("""
26 CREATE TABLE key_value (
27 _id INTEGER PRIMARY KEY,
28 key TEXT UNIQUE NOT NULL,
35 public KeyValueStore(final Database database
) {
36 this.database
= database
;
39 public <T
> T
getEntry(KeyValueEntry
<T
> key
) {
40 try (final var connection
= database
.getConnection()) {
41 return getEntry(connection
, key
);
42 } catch (SQLException e
) {
43 throw new RuntimeException("Failed read from pre_key store", e
);
47 public <T
> boolean storeEntry(KeyValueEntry
<T
> key
, T value
) {
48 try (final var connection
= database
.getConnection()) {
49 return storeEntry(connection
, key
, value
);
50 } catch (SQLException e
) {
51 throw new RuntimeException("Failed update key_value store", e
);
55 private <T
> T
getEntry(final Connection connection
, final KeyValueEntry
<T
> key
) throws SQLException
{
62 ).formatted(TABLE_KEY_VALUE
);
63 try (final var statement
= connection
.prepareStatement(sql
)) {
64 statement
.setString(1, key
.key());
66 final var result
= Utils
.executeQueryForOptional(statement
,
67 resultSet
-> readValueFromResultSet(key
, resultSet
)).orElse(null);
70 return key
.defaultValue();
76 public <T
> boolean storeEntry(
77 final Connection connection
, final KeyValueEntry
<T
> key
, final T value
78 ) throws SQLException
{
79 final var entry
= getEntry(key
);
80 if (Objects
.equals(entry
, value
)) {
86 INSERT INTO %s (key, value)
88 ON CONFLICT (key) DO UPDATE SET value=excluded.value
90 ).formatted(TABLE_KEY_VALUE
);
91 try (final var statement
= connection
.prepareStatement(sql
)) {
92 statement
.setString(1, key
.key());
93 setParameterValue(statement
, 2, key
.clazz(), value
);
94 statement
.executeUpdate();
99 @SuppressWarnings("unchecked")
100 private static <T
> T
readValueFromResultSet(
101 final KeyValueEntry
<T
> key
, final ResultSet resultSet
102 ) throws SQLException
{
104 final var clazz
= key
.clazz();
105 if (clazz
== int.class || clazz
== Integer
.class) {
106 value
= resultSet
.getInt("value");
107 } else if (clazz
== long.class || clazz
== Long
.class) {
108 value
= resultSet
.getLong("value");
109 } else if (clazz
== boolean.class || clazz
== Boolean
.class) {
110 value
= resultSet
.getBoolean("value");
111 } else if (clazz
== byte[].class || clazz
== Byte
[].class) {
112 value
= resultSet
.getBytes("value");
113 } else if (clazz
== String
.class) {
114 value
= resultSet
.getString("value");
115 } else if (Enum
.class.isAssignableFrom(clazz
)) {
116 final var name
= resultSet
.getString("value");
121 value
= Enum
.valueOf((Class
<Enum
>) key
.clazz(), name
);
122 } catch (IllegalArgumentException e
) {
123 logger
.debug("Read invalid enum value from store, ignoring: {} for {}", name
, key
.clazz());
128 throw new AssertionError("Invalid key type " + clazz
.getSimpleName());
130 if (resultSet
.wasNull()) {
136 private static <T
> void setParameterValue(
137 final PreparedStatement statement
, final int parameterIndex
, final Class
<T
> clazz
, final T value
138 ) throws SQLException
{
139 if (clazz
== int.class || clazz
== Integer
.class) {
141 statement
.setNull(parameterIndex
, Types
.INTEGER
);
143 statement
.setInt(parameterIndex
, (int) value
);
145 } else if (clazz
== long.class || clazz
== Long
.class) {
147 statement
.setNull(parameterIndex
, Types
.INTEGER
);
149 statement
.setLong(parameterIndex
, (long) value
);
151 } else if (clazz
== boolean.class || clazz
== Boolean
.class) {
153 statement
.setNull(parameterIndex
, Types
.BOOLEAN
);
155 statement
.setBoolean(parameterIndex
, (boolean) value
);
157 } else if (clazz
== byte[].class || clazz
== Byte
[].class) {
159 statement
.setNull(parameterIndex
, Types
.BLOB
);
161 statement
.setBytes(parameterIndex
, (byte[]) value
);
163 } else if (clazz
== String
.class) {
165 statement
.setNull(parameterIndex
, Types
.VARCHAR
);
167 statement
.setString(parameterIndex
, (String
) value
);
169 } else if (Enum
.class.isAssignableFrom(clazz
)) {
171 statement
.setNull(parameterIndex
, Types
.VARCHAR
);
173 statement
.setString(parameterIndex
, ((Enum
<?
>) value
).name());
176 throw new AssertionError("Invalid key type " + clazz
.getSimpleName());