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 key_value 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 public <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
,
78 final KeyValueEntry
<T
> key
,
80 ) throws SQLException
{
81 final var entry
= getEntry(connection
, key
);
82 if (Objects
.equals(entry
, value
)) {
88 INSERT INTO %s (key, value)
90 ON CONFLICT (key) DO UPDATE SET value=excluded.value
92 ).formatted(TABLE_KEY_VALUE
);
93 try (final var statement
= connection
.prepareStatement(sql
)) {
94 statement
.setString(1, key
.key());
95 setParameterValue(statement
, 2, key
.clazz(), value
);
96 statement
.executeUpdate();
101 @SuppressWarnings("unchecked")
102 private static <T
> T
readValueFromResultSet(
103 final KeyValueEntry
<T
> key
,
104 final ResultSet resultSet
105 ) throws SQLException
{
107 final var clazz
= key
.clazz();
108 if (clazz
== int.class || clazz
== Integer
.class) {
109 value
= resultSet
.getInt("value");
110 } else if (clazz
== long.class || clazz
== Long
.class) {
111 value
= resultSet
.getLong("value");
112 } else if (clazz
== boolean.class || clazz
== Boolean
.class) {
113 value
= resultSet
.getBoolean("value");
114 } else if (clazz
== byte[].class || clazz
== Byte
[].class) {
115 value
= resultSet
.getBytes("value");
116 } else if (clazz
== String
.class) {
117 value
= resultSet
.getString("value");
118 } else if (Enum
.class.isAssignableFrom(clazz
)) {
119 final var name
= resultSet
.getString("value");
124 value
= Enum
.valueOf((Class
<Enum
>) key
.clazz(), name
);
125 } catch (IllegalArgumentException e
) {
126 logger
.debug("Read invalid enum value from store, ignoring: {} for {}", name
, key
.clazz());
131 throw new AssertionError("Invalid key type " + clazz
.getSimpleName());
133 if (resultSet
.wasNull()) {
139 private static <T
> void setParameterValue(
140 final PreparedStatement statement
,
141 final int parameterIndex
,
142 final Class
<T
> clazz
,
144 ) throws SQLException
{
145 if (clazz
== int.class || clazz
== Integer
.class) {
147 statement
.setNull(parameterIndex
, Types
.INTEGER
);
149 statement
.setInt(parameterIndex
, (int) value
);
151 } else if (clazz
== long.class || clazz
== Long
.class) {
153 statement
.setNull(parameterIndex
, Types
.INTEGER
);
155 statement
.setLong(parameterIndex
, (long) value
);
157 } else if (clazz
== boolean.class || clazz
== Boolean
.class) {
159 statement
.setNull(parameterIndex
, Types
.BOOLEAN
);
161 statement
.setBoolean(parameterIndex
, (boolean) value
);
163 } else if (clazz
== byte[].class || clazz
== Byte
[].class) {
165 statement
.setNull(parameterIndex
, Types
.BLOB
);
167 statement
.setBytes(parameterIndex
, (byte[]) value
);
169 } else if (clazz
== String
.class) {
171 statement
.setNull(parameterIndex
, Types
.VARCHAR
);
173 statement
.setString(parameterIndex
, (String
) value
);
175 } else if (Enum
.class.isAssignableFrom(clazz
)) {
177 statement
.setNull(parameterIndex
, Types
.VARCHAR
);
179 statement
.setString(parameterIndex
, ((Enum
<?
>) value
).name());
182 throw new AssertionError("Invalid key type " + clazz
.getSimpleName());