- saveLocked();
- }
-
- private Optional<Recipient> findByNumberLocked(final String number) {
- return recipients.entrySet()
- .stream()
- .filter(entry -> entry.getValue().getAddress().number().isPresent() && number.equals(entry.getValue()
- .getAddress()
- .number()
- .get()))
- .findFirst()
- .map(Map.Entry::getValue);
- }
-
- private Optional<Recipient> findByUuidLocked(final UUID uuid) {
- return recipients.entrySet()
- .stream()
- .filter(entry -> entry.getValue().getAddress().uuid().isPresent() && uuid.equals(entry.getValue()
- .getAddress()
- .uuid()
- .get()))
- .findFirst()
- .map(Map.Entry::getValue);
- }
-
- private RecipientId nextIdLocked() {
- return new RecipientId(++this.lastId, this);
- }
-
- private void saveLocked() {
- if (isBulkUpdating) {
- return;
- }
- final var base64 = Base64.getEncoder();
- var storage = new Storage(recipients.entrySet().stream().map(pair -> {
- final var recipient = pair.getValue();
- final var recipientContact = recipient.getContact();
- final var contact = recipientContact == null
- ? null
- : new Storage.Recipient.Contact(recipientContact.getGivenName(),
- recipientContact.getFamilyName(),
- recipientContact.getColor(),
- recipientContact.getMessageExpirationTime(),
- recipientContact.isBlocked(),
- recipientContact.isArchived(),
- recipientContact.isProfileSharingEnabled());
- final var recipientProfile = recipient.getProfile();
- final var profile = recipientProfile == null
- ? null
- : new Storage.Recipient.Profile(recipientProfile.getLastUpdateTimestamp(),
- recipientProfile.getGivenName(),
- recipientProfile.getFamilyName(),
- recipientProfile.getAbout(),
- recipientProfile.getAboutEmoji(),
- recipientProfile.getAvatarUrlPath(),
- recipientProfile.getMobileCoinAddress() == null
- ? null
- : base64.encodeToString(recipientProfile.getMobileCoinAddress()),
- recipientProfile.getUnidentifiedAccessMode().name(),
- recipientProfile.getCapabilities().stream().map(Enum::name).collect(Collectors.toSet()));
- return new Storage.Recipient(pair.getKey().id(),
- recipient.getAddress().number().orElse(null),
- recipient.getAddress().uuid().map(UUID::toString).orElse(null),
- recipient.getProfileKey() == null
- ? null
- : base64.encodeToString(recipient.getProfileKey().serialize()),
- recipient.getExpiringProfileKeyCredential() == null
- ? null
- : base64.encodeToString(recipient.getExpiringProfileKeyCredential().serialize()),
- contact,
- profile);
- }).toList(), lastId);
-
- // Write to memory first to prevent corrupting the file in case of serialization errors
- try (var inMemoryOutput = new ByteArrayOutputStream()) {
- objectMapper.writeValue(inMemoryOutput, storage);
-
- var input = new ByteArrayInputStream(inMemoryOutput.toByteArray());
- try (var outputStream = new FileOutputStream(file)) {
- input.transferTo(outputStream);
- }
- } catch (Exception e) {
- logger.error("Error saving recipient store file: {}", e.getMessage());
- }
- }
-
- private record Storage(List<Recipient> recipients, long lastId) {
-
- private record Recipient(
- long id,
- String number,
- String uuid,
- String profileKey,
- String expiringProfileKeyCredential,
- Storage.Recipient.Contact contact,
- Storage.Recipient.Profile profile
- ) {
-
- private record Contact(
- String name,
- String familyName,
- String color,
- int messageExpirationTime,
- boolean blocked,
- boolean archived,
- boolean profileSharingEnabled
- ) {}
-
- private record Profile(
- long lastUpdateTimestamp,
- String givenName,
- String familyName,
- String about,
- String aboutEmoji,
- String avatarUrlPath,
- String mobileCoinAddress,
- String unidentifiedAccessMode,
- Set<String> capabilities
- ) {}
+ }
+
+ private Optional<RecipientWithAddress> findByNumber(
+ final Connection connection, final String number
+ ) throws SQLException {
+ final var sql = """
+ SELECT r._id, r.number, r.uuid
+ FROM %s r
+ WHERE r.number = ?
+ """.formatted(TABLE_RECIPIENT);
+ try (final var statement = connection.prepareStatement(sql)) {
+ statement.setString(1, number);
+ return Utils.executeQueryForOptional(statement, this::getRecipientWithAddressFromResultSet);
+ }
+ }
+
+ private Optional<RecipientWithAddress> findByUuid(
+ final Connection connection, final UUID uuid
+ ) throws SQLException {
+ final var sql = """
+ SELECT r._id, r.number, r.uuid
+ FROM %s r
+ WHERE r.uuid = ?
+ """.formatted(TABLE_RECIPIENT);
+ try (final var statement = connection.prepareStatement(sql)) {
+ statement.setBytes(1, UuidUtil.toByteArray(uuid));
+ return Utils.executeQueryForOptional(statement, this::getRecipientWithAddressFromResultSet);
+ }
+ }
+
+ private Contact getContact(final Connection connection, final RecipientId recipientId) throws SQLException {
+ final var sql = (
+ """
+ SELECT r.given_name, r.family_name, r.expiration_time, r.profile_sharing, r.color, r.blocked, r.archived
+ FROM %s r
+ WHERE r._id = ? AND (%s)
+ """
+ ).formatted(TABLE_RECIPIENT, SQL_IS_CONTACT);
+ try (final var statement = connection.prepareStatement(sql)) {
+ statement.setLong(1, recipientId.id());
+ return Utils.executeQueryForOptional(statement, this::getContactFromResultSet).orElse(null);
+ }
+ }
+
+ private ProfileKey getProfileKey(final Connection connection, final RecipientId recipientId) throws SQLException {
+ final var sql = (
+ """
+ SELECT r.profile_key
+ FROM %s r
+ WHERE r._id = ?
+ """
+ ).formatted(TABLE_RECIPIENT);
+ try (final var statement = connection.prepareStatement(sql)) {
+ statement.setLong(1, recipientId.id());
+ return Utils.executeQueryForOptional(statement, this::getProfileKeyFromResultSet).orElse(null);
+ }
+ }
+
+ private ExpiringProfileKeyCredential getExpiringProfileKeyCredential(
+ final Connection connection, final RecipientId recipientId
+ ) throws SQLException {
+ final var sql = (
+ """
+ SELECT r.profile_key_credential
+ FROM %s r
+ WHERE r._id = ?
+ """
+ ).formatted(TABLE_RECIPIENT);
+ try (final var statement = connection.prepareStatement(sql)) {
+ statement.setLong(1, recipientId.id());
+ return Utils.executeQueryForOptional(statement, this::getExpiringProfileKeyCredentialFromResultSet)
+ .orElse(null);
+ }
+ }
+
+ private Profile getProfile(final Connection connection, final RecipientId recipientId) throws SQLException {
+ final var sql = (
+ """
+ SELECT r.profile_last_update_timestamp, r.profile_given_name, r.profile_family_name, r.profile_about, r.profile_about_emoji, r.profile_avatar_url_path, r.profile_mobile_coin_address, r.profile_unidentified_access_mode, r.profile_capabilities
+ FROM %s r
+ WHERE r._id = ? AND r.profile_capabilities IS NOT NULL
+ """
+ ).formatted(TABLE_RECIPIENT);
+ try (final var statement = connection.prepareStatement(sql)) {
+ statement.setLong(1, recipientId.id());
+ return Utils.executeQueryForOptional(statement, this::getProfileFromResultSet).orElse(null);
+ }
+ }
+
+ private RecipientAddress getRecipientAddressFromResultSet(ResultSet resultSet) throws SQLException {
+ final var uuid = Optional.ofNullable(resultSet.getBytes("uuid")).map(UuidUtil::parseOrNull);
+ final var number = Optional.ofNullable(resultSet.getString("number"));
+ return new RecipientAddress(uuid, number);
+ }
+
+ private RecipientId getRecipientIdFromResultSet(ResultSet resultSet) throws SQLException {
+ return new RecipientId(resultSet.getLong("_id"), this);
+ }
+
+ private RecipientWithAddress getRecipientWithAddressFromResultSet(final ResultSet resultSet) throws SQLException {
+ return new RecipientWithAddress(getRecipientIdFromResultSet(resultSet),
+ getRecipientAddressFromResultSet(resultSet));
+ }
+
+ private Recipient getRecipientFromResultSet(final ResultSet resultSet) throws SQLException {
+ return new Recipient(getRecipientIdFromResultSet(resultSet),
+ getRecipientAddressFromResultSet(resultSet),
+ getContactFromResultSet(resultSet),
+ getProfileKeyFromResultSet(resultSet),
+ getExpiringProfileKeyCredentialFromResultSet(resultSet),
+ getProfileFromResultSet(resultSet));
+ }
+
+ private Contact getContactFromResultSet(ResultSet resultSet) throws SQLException {
+ return new Contact(resultSet.getString("given_name"),
+ resultSet.getString("family_name"),
+ resultSet.getString("color"),
+ resultSet.getInt("expiration_time"),
+ resultSet.getBoolean("blocked"),
+ resultSet.getBoolean("archived"),
+ resultSet.getBoolean("profile_sharing"));
+ }
+
+ private Profile getProfileFromResultSet(ResultSet resultSet) throws SQLException {
+ final var profileCapabilities = resultSet.getString("profile_capabilities");
+ final var profileUnidentifiedAccessMode = resultSet.getString("profile_unidentified_access_mode");
+ return new Profile(resultSet.getLong("profile_last_update_timestamp"),
+ resultSet.getString("profile_given_name"),
+ resultSet.getString("profile_family_name"),
+ resultSet.getString("profile_about"),
+ resultSet.getString("profile_about_emoji"),
+ resultSet.getString("profile_avatar_url_path"),
+ resultSet.getBytes("profile_mobile_coin_address"),
+ profileUnidentifiedAccessMode == null
+ ? Profile.UnidentifiedAccessMode.UNKNOWN
+ : Profile.UnidentifiedAccessMode.valueOfOrUnknown(profileUnidentifiedAccessMode),
+ profileCapabilities == null
+ ? Set.of()
+ : Arrays.stream(profileCapabilities.split(","))
+ .map(Profile.Capability::valueOfOrNull)
+ .filter(Objects::nonNull)
+ .collect(Collectors.toSet()));
+ }
+
+ private ProfileKey getProfileKeyFromResultSet(ResultSet resultSet) throws SQLException {
+ final var profileKey = resultSet.getBytes("profile_key");
+
+ if (profileKey == null) {
+ return null;
+ }
+ try {
+ return new ProfileKey(profileKey);
+ } catch (InvalidInputException ignored) {
+ return null;
+ }
+ }
+
+ private ExpiringProfileKeyCredential getExpiringProfileKeyCredentialFromResultSet(ResultSet resultSet) throws SQLException {
+ final var profileKeyCredential = resultSet.getBytes("profile_key_credential");
+
+ if (profileKeyCredential == null) {
+ return null;
+ }
+ try {
+ return new ExpiringProfileKeyCredential(profileKeyCredential);
+ } catch (Throwable ignored) {
+ return null;