]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/storage/keyValue/KeyValueStore.java
9b239b727e1036707686d0634c0455a1da5c7340
[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
14 public class KeyValueStore {
15
16 private static final String TABLE_KEY_VALUE = "key_value";
17 private final static Logger logger = LoggerFactory.getLogger(KeyValueStore.class);
18
19 private final Database database;
20
21 public static void createSql(Connection connection) throws SQLException {
22 // When modifying the CREATE statement here, also add a migration in AccountDatabase.java
23 try (final var statement = connection.createStatement()) {
24 statement.executeUpdate("""
25 CREATE TABLE key_value (
26 _id INTEGER PRIMARY KEY,
27 key TEXT UNIQUE NOT NULL,
28 value ANY
29 ) STRICT;
30 """);
31 }
32 }
33
34 public KeyValueStore(final Database database) {
35 this.database = database;
36 }
37
38 public <T> T getEntry(KeyValueEntry<T> key) {
39 final var sql = (
40 """
41 SELECT key, value
42 FROM %s p
43 WHERE p.key = ?
44 """
45 ).formatted(TABLE_KEY_VALUE);
46 try (final var connection = database.getConnection()) {
47 try (final var statement = connection.prepareStatement(sql)) {
48 statement.setString(1, key.key());
49
50 final var result = Utils.executeQueryForOptional(statement,
51 resultSet -> readValueFromResultSet(key, resultSet)).orElse(null);
52
53 if (result == null) {
54 return key.defaultValue();
55 }
56 return result;
57 }
58 } catch (SQLException e) {
59 throw new RuntimeException("Failed read from pre_key store", e);
60 }
61 }
62
63 public <T> void storeEntry(KeyValueEntry<T> key, T value) {
64 final var sql = (
65 """
66 INSERT INTO %s (key, value)
67 VALUES (?1, ?2)
68 ON CONFLICT (key) DO UPDATE SET value=excluded.value
69 """
70 ).formatted(TABLE_KEY_VALUE);
71 try (final var connection = database.getConnection()) {
72 try (final var statement = connection.prepareStatement(sql)) {
73 statement.setString(1, key.key());
74 setParameterValue(statement, 2, key.clazz(), value);
75 statement.executeUpdate();
76 }
77 } catch (SQLException e) {
78 throw new RuntimeException("Failed update key_value store", e);
79 }
80 }
81
82 @SuppressWarnings("unchecked")
83 private static <T> T readValueFromResultSet(
84 final KeyValueEntry<T> key, final ResultSet resultSet
85 ) throws SQLException {
86 Object value;
87 final var clazz = key.clazz();
88 if (clazz == int.class || clazz == Integer.class) {
89 value = resultSet.getInt("value");
90 } else if (clazz == long.class || clazz == Long.class) {
91 value = resultSet.getLong("value");
92 } else if (clazz == boolean.class || clazz == Boolean.class) {
93 value = resultSet.getBoolean("value");
94 } else if (clazz == byte[].class || clazz == Byte[].class) {
95 value = resultSet.getBytes("value");
96 } else if (clazz == String.class) {
97 value = resultSet.getString("value");
98 } else if (Enum.class.isAssignableFrom(clazz)) {
99 final var name = resultSet.getString("value");
100 if (name == null) {
101 value = null;
102 } else {
103 try {
104 value = Enum.valueOf((Class<Enum>) key.clazz(), name);
105 } catch (IllegalArgumentException e) {
106 logger.debug("Read invalid enum value from store, ignoring: {} for {}", name, key.clazz());
107 value = null;
108 }
109 }
110 } else {
111 throw new AssertionError("Invalid key type " + clazz.getSimpleName());
112 }
113 if (resultSet.wasNull()) {
114 return null;
115 }
116 return (T) value;
117 }
118
119 private static <T> void setParameterValue(
120 final PreparedStatement statement, final int parameterIndex, final Class<T> clazz, final T value
121 ) throws SQLException {
122 if (clazz == int.class || clazz == Integer.class) {
123 if (value == null) {
124 statement.setNull(parameterIndex, Types.INTEGER);
125 } else {
126 statement.setInt(parameterIndex, (int) value);
127 }
128 } else if (clazz == long.class || clazz == Long.class) {
129 if (value == null) {
130 statement.setNull(parameterIndex, Types.INTEGER);
131 } else {
132 statement.setLong(parameterIndex, (long) value);
133 }
134 } else if (clazz == boolean.class || clazz == Boolean.class) {
135 if (value == null) {
136 statement.setNull(parameterIndex, Types.BOOLEAN);
137 } else {
138 statement.setBoolean(parameterIndex, (boolean) value);
139 }
140 } else if (clazz == byte[].class || clazz == Byte[].class) {
141 if (value == null) {
142 statement.setNull(parameterIndex, Types.BLOB);
143 } else {
144 statement.setBytes(parameterIndex, (byte[]) value);
145 }
146 } else if (clazz == String.class) {
147 if (value == null) {
148 statement.setNull(parameterIndex, Types.VARCHAR);
149 } else {
150 statement.setString(parameterIndex, (String) value);
151 }
152 } else if (Enum.class.isAssignableFrom(clazz)) {
153 if (value == null) {
154 statement.setNull(parameterIndex, Types.VARCHAR);
155 } else {
156 statement.setString(parameterIndex, ((Enum<?>) value).name());
157 }
158 } else {
159 throw new AssertionError("Invalid key type " + clazz.getSimpleName());
160 }
161 }
162 }