]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/storage/keyValue/KeyValueStore.java
17135d1093daa0cebeca7e37bad32e4712113c10
[signal-cli] / lib / src / main / java / org / asamk / signal / manager / storage / keyValue / KeyValueStore.java
1 package org.asamk.signal.manager.storage.keyValue;
2
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;
7
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;
14
15 public class KeyValueStore {
16
17 private static final String TABLE_KEY_VALUE = "key_value";
18 private static final Logger logger = LoggerFactory.getLogger(KeyValueStore.class);
19
20 private final Database database;
21
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,
29 value ANY
30 ) STRICT;
31 """);
32 }
33 }
34
35 public KeyValueStore(final Database database) {
36 this.database = database;
37 }
38
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);
44 }
45 }
46
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);
52 }
53 }
54
55 public <T> T getEntry(final Connection connection, final KeyValueEntry<T> key) throws SQLException {
56 final var sql = (
57 """
58 SELECT key, value
59 FROM %s p
60 WHERE p.key = ?
61 """
62 ).formatted(TABLE_KEY_VALUE);
63 try (final var statement = connection.prepareStatement(sql)) {
64 statement.setString(1, key.key());
65
66 final var result = Utils.executeQueryForOptional(statement,
67 resultSet -> readValueFromResultSet(key, resultSet)).orElse(null);
68
69 if (result == null) {
70 return key.defaultValue();
71 }
72 return result;
73 }
74 }
75
76 public <T> boolean storeEntry(
77 final Connection connection,
78 final KeyValueEntry<T> key,
79 final T value
80 ) throws SQLException {
81 final var entry = getEntry(connection, key);
82 if (Objects.equals(entry, value)) {
83 return false;
84 }
85
86 final var sql = (
87 """
88 INSERT INTO %s (key, value)
89 VALUES (?1, ?2)
90 ON CONFLICT (key) DO UPDATE SET value=excluded.value
91 """
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();
97 }
98 return true;
99 }
100
101 @SuppressWarnings("unchecked")
102 private static <T> T readValueFromResultSet(
103 final KeyValueEntry<T> key,
104 final ResultSet resultSet
105 ) throws SQLException {
106 Object value;
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");
120 if (name == null) {
121 value = null;
122 } else {
123 try {
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());
127 value = null;
128 }
129 }
130 } else {
131 throw new AssertionError("Invalid key type " + clazz.getSimpleName());
132 }
133 if (resultSet.wasNull()) {
134 return null;
135 }
136 return (T) value;
137 }
138
139 private static <T> void setParameterValue(
140 final PreparedStatement statement,
141 final int parameterIndex,
142 final Class<T> clazz,
143 final T value
144 ) throws SQLException {
145 if (clazz == int.class || clazz == Integer.class) {
146 if (value == null) {
147 statement.setNull(parameterIndex, Types.INTEGER);
148 } else {
149 statement.setInt(parameterIndex, (int) value);
150 }
151 } else if (clazz == long.class || clazz == Long.class) {
152 if (value == null) {
153 statement.setNull(parameterIndex, Types.INTEGER);
154 } else {
155 statement.setLong(parameterIndex, (long) value);
156 }
157 } else if (clazz == boolean.class || clazz == Boolean.class) {
158 if (value == null) {
159 statement.setNull(parameterIndex, Types.BOOLEAN);
160 } else {
161 statement.setBoolean(parameterIndex, (boolean) value);
162 }
163 } else if (clazz == byte[].class || clazz == Byte[].class) {
164 if (value == null) {
165 statement.setNull(parameterIndex, Types.BLOB);
166 } else {
167 statement.setBytes(parameterIndex, (byte[]) value);
168 }
169 } else if (clazz == String.class) {
170 if (value == null) {
171 statement.setNull(parameterIndex, Types.VARCHAR);
172 } else {
173 statement.setString(parameterIndex, (String) value);
174 }
175 } else if (Enum.class.isAssignableFrom(clazz)) {
176 if (value == null) {
177 statement.setNull(parameterIndex, Types.VARCHAR);
178 } else {
179 statement.setString(parameterIndex, ((Enum<?>) value).name());
180 }
181 } else {
182 throw new AssertionError("Invalid key type " + clazz.getSimpleName());
183 }
184 }
185 }