From 3ac4bd4935e598da305b77e7dea8af20413ba668 Mon Sep 17 00:00:00 2001 From: Wojtek Rzepecki Date: Mon, 19 Jul 2021 17:40:26 +0200 Subject: [PATCH] [EGD-7127] Store imported contacts in DB Added API for storing imported contacts list in phonebook database --- module-db/CMakeLists.txt | 2 + module-db/Interface/ContactRecord.cpp | 77 ++++++++ module-db/Interface/ContactRecord.hpp | 18 ++ module-db/doc/contacts_import.md | 3 + module-db/doc/contacts_import.puml | 30 +++ module-db/doc/contacts_import.svg | 40 ++++ .../QueryCheckContactsListDuplicates.cpp | 35 ++++ .../QueryCheckContactsListDuplicates.hpp | 40 ++++ .../phonebook/QueryMergeContactsList.cpp | 29 +++ .../phonebook/QueryMergeContactsList.hpp | 42 ++++ module-db/tests/ContactsRecord_tests.cpp | 186 ++++++++++++++++++ .../doc/contacts_import/contacts_import.svg | 40 ++++ 12 files changed, 542 insertions(+) create mode 100644 module-db/doc/contacts_import.md create mode 100644 module-db/doc/contacts_import.puml create mode 100644 module-db/doc/contacts_import.svg create mode 100644 module-db/queries/phonebook/QueryCheckContactsListDuplicates.cpp create mode 100644 module-db/queries/phonebook/QueryCheckContactsListDuplicates.hpp create mode 100644 module-db/queries/phonebook/QueryMergeContactsList.cpp create mode 100644 module-db/queries/phonebook/QueryMergeContactsList.hpp create mode 100644 out/module-db/doc/contacts_import/contacts_import.svg diff --git a/module-db/CMakeLists.txt b/module-db/CMakeLists.txt index ddba9b599e21aae6f3f93396416d53732f3f2b77..da6b76119171bd35f7f4388cef81ebb335508f01 100644 --- a/module-db/CMakeLists.txt +++ b/module-db/CMakeLists.txt @@ -102,6 +102,8 @@ set(SOURCES queries/phonebook/QueryContactUpdate.cpp queries/phonebook/QueryContactRemove.cpp queries/phonebook/QueryNumberGetByID.cpp + queries/phonebook/QueryMergeContactsList.cpp + queries/phonebook/QueryCheckContactsListDuplicates.cpp queries/alarms/QueryAlarmsAdd.cpp queries/alarms/QueryAlarmsRemove.cpp queries/alarms/QueryAlarmsEdit.cpp diff --git a/module-db/Interface/ContactRecord.cpp b/module-db/Interface/ContactRecord.cpp index 73c43efa06511a047aff879918a8aff0be44a368..7bdc35a5d34cb3ca6367ef6840cad563d5b3dae6 100644 --- a/module-db/Interface/ContactRecord.cpp +++ b/module-db/Interface/ContactRecord.cpp @@ -6,6 +6,8 @@ #include "queries/phonebook/QueryContactGetByID.hpp" #include "queries/phonebook/QueryContactUpdate.hpp" #include "queries/phonebook/QueryContactRemove.hpp" +#include "queries/phonebook/QueryMergeContactsList.hpp" +#include "queries/phonebook/QueryCheckContactsListDuplicates.hpp" #include #include @@ -159,6 +161,12 @@ auto ContactRecordInterface::runQuery(std::shared_ptr query) -> std:: else if (typeid(*query) == typeid(db::query::NumberGetByID)) { return numberGetByIdQuery(query); } + else if (typeid(*query) == typeid(db::query::MergeContactsList)) { + return mergeContactsListQuery(query); + } + else if (typeid(*query) == typeid(db::query::CheckContactsListDuplicates)) { + return checkContactsListDuplicatesQuery(query); + } else { error_db_data("Unexpected query type."); return nullptr; @@ -357,6 +365,26 @@ auto ContactRecordInterface::numberGetByIdQuery(std::shared_ptr query return response; } +auto ContactRecordInterface::mergeContactsListQuery(std::shared_ptr query) + -> std::unique_ptr +{ + auto mergeQuery = static_cast(query.get()); + auto ret = ContactRecordInterface::MergeContactsList(mergeQuery->getContactsList()); + auto response = std::make_unique(ret); + response->setRequestQuery(query); + return response; +} + +auto ContactRecordInterface::checkContactsListDuplicatesQuery(std::shared_ptr query) + -> std::unique_ptr +{ + auto mergeQuery = static_cast(query.get()); + auto response = std::make_unique( + std::move(ContactRecordInterface::CheckContactsListDuplicates(mergeQuery->getContactsList()))); + response->setRequestQuery(query); + return response; +} + auto ContactRecordInterface::splitNumberIDs(const std::string &numberIDs) -> std::vector { std::stringstream source(numberIDs); @@ -1172,3 +1200,52 @@ auto ContactRecordInterface::GetNumbersIdsByContact(std::uint32_t contactId) -> } return numbersIds; } + +auto ContactRecordInterface::MergeContactsList(std::vector &contacts) -> bool +{ + std::vector contactNumberHolders; + auto numberMatcher = buildNumberMatcher(contactNumberHolders); + + for (auto &contact : contacts) { + // Important: Comparing only single number contacts + if (contact.numbers.size() > 1) { + LOG_WARN("Contact with multiple numbers detected - ignoring all numbers except first"); + } + auto matchedNumber = numberMatcher.bestMatch(contact.numbers[0].number, utils::PhoneNumber::Match::POSSIBLE); + + if (matchedNumber == numberMatcher.END) { + if (!Add(contact)) { + LOG_ERROR("Contacts list merge fail when adding the contact."); + return false; + } + } + else { + // Complete override of the contact data + contact.ID = matchedNumber->getContactID(); + Update(contact); + // Rebuild number matcher + numberMatcher = buildNumberMatcher(contactNumberHolders); + } + } + return true; +} + +auto ContactRecordInterface::CheckContactsListDuplicates(std::vector &contacts) + -> std::vector +{ + std::vector duplicates; + std::vector contactNumberHolders; + auto numberMatcher = buildNumberMatcher(contactNumberHolders); + + for (auto &contact : contacts) { + // Important: Comparing only single number contacts + if (contact.numbers.size() > 1) { + LOG_WARN("Contact with multiple numbers detected - ignoring all numbers except first"); + } + auto matchedNumber = numberMatcher.bestMatch(contact.numbers[0].number, utils::PhoneNumber::Match::POSSIBLE); + if (matchedNumber != numberMatcher.END) { + duplicates.push_back(contact); + } + } + return duplicates; +} diff --git a/module-db/Interface/ContactRecord.hpp b/module-db/Interface/ContactRecord.hpp index 1a44c48bdf5258d946039be41573cb0f5ac0594a..c6eb06a921f95b506b4e647d608bc3ba2b8af870 100644 --- a/module-db/Interface/ContactRecord.hpp +++ b/module-db/Interface/ContactRecord.hpp @@ -220,6 +220,22 @@ class ContactRecordInterface : public RecordInterface std::vector; + /** + * @brief Merge contacts list with overriding the duplicates in contacts DB + * + * @param contacts vector of contacts with single number + * @return boolean status + */ + auto MergeContactsList(std::vector &contacts) -> bool; + + /** + * @brief Check which contacts in vector are duplicating contacts in DB + * + * @param contacts vector of contacts with single number + * @return vector of contacts with numbers appearing in contacts DB + */ + auto CheckContactsListDuplicates(std::vector &contacts) -> std::vector; + private: ContactsDB *contactDB; @@ -248,4 +264,6 @@ class ContactRecordInterface : public RecordInterface query) -> std::unique_ptr; auto removeQuery(std::shared_ptr query) -> std::unique_ptr; auto numberGetByIdQuery(std::shared_ptr query) -> std::unique_ptr; + auto mergeContactsListQuery(std::shared_ptr query) -> std::unique_ptr; + auto checkContactsListDuplicatesQuery(std::shared_ptr query) -> std::unique_ptr; }; diff --git a/module-db/doc/contacts_import.md b/module-db/doc/contacts_import.md new file mode 100644 index 0000000000000000000000000000000000000000..c7bc6b42dc07e9d96a80e8b2be8e924caff22007 --- /dev/null +++ b/module-db/doc/contacts_import.md @@ -0,0 +1,3 @@ +## Sequence of contacts import to contacts DB + +![](contacts_import.svg) \ No newline at end of file diff --git a/module-db/doc/contacts_import.puml b/module-db/doc/contacts_import.puml new file mode 100644 index 0000000000000000000000000000000000000000..8c50d0a69d2410706d89b9186bfc05c12d349224 --- /dev/null +++ b/module-db/doc/contacts_import.puml @@ -0,0 +1,30 @@ +@startuml + +participant "Application" as app +participant "ContactRecord" as rec +participant "contacts DB" as db + +== Checking for duplicates == + +app -> rec : contacts to check\n(query::CheckContactsListDuplicates) +rec -> db : get numbers +db -> rec +rec -> rec : numbers comparison +rec -> app : duplicated contacts + +== Merging contacts list to DB == + +app -> rec : contacts to merge\n(query::MergeContactsList) +rec -> db : get numbers +db -> rec +rec -> rec : numbers comparison + +group number not found + rec -> db : Add contact +end +group number found in db + rec -> db : Update contact by overriding old data +end +rec -> app : status response + +@enduml \ No newline at end of file diff --git a/module-db/doc/contacts_import.svg b/module-db/doc/contacts_import.svg new file mode 100644 index 0000000000000000000000000000000000000000..819efaf570dee29140055980c9b14ee74a4e5190 --- /dev/null +++ b/module-db/doc/contacts_import.svg @@ -0,0 +1,40 @@ +ApplicationApplicationContactRecordContactRecordcontacts DBcontacts DBChecking for duplicatescontacts to check(query::CheckContactsListDuplicates)get numbersnumbers comparisonduplicated contactsMerging contacts list to DBcontacts to merge(query::MergeContactsList)get numbersnumbers comparisonnumber not foundAdd contactnumber found in dbUpdate contact by overriding old datastatus response \ No newline at end of file diff --git a/module-db/queries/phonebook/QueryCheckContactsListDuplicates.cpp b/module-db/queries/phonebook/QueryCheckContactsListDuplicates.cpp new file mode 100644 index 0000000000000000000000000000000000000000..24546b4a3c426a29161c8a35303c743444909558 --- /dev/null +++ b/module-db/queries/phonebook/QueryCheckContactsListDuplicates.cpp @@ -0,0 +1,35 @@ +// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved. +// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md + +#include "QueryCheckContactsListDuplicates.hpp" +#include + +using namespace db::query; + +CheckContactsListDuplicates::CheckContactsListDuplicates(std::vector contacts) + : Query(Query::Type::Read), contacts(std::move(contacts)) +{} + +std::vector &CheckContactsListDuplicates::getContactsList() +{ + return contacts; +} + +[[nodiscard]] auto CheckContactsListDuplicates::debugInfo() const -> std::string +{ + return "CheckContactsListDuplicates"; +} + +CheckContactsListDuplicatesResult::CheckContactsListDuplicatesResult(std::vector duplicates) + : duplicates(std::move(duplicates)) +{} + +std::vector &CheckContactsListDuplicatesResult::getDuplicates() +{ + return duplicates; +} + +[[nodiscard]] auto CheckContactsListDuplicatesResult::debugInfo() const -> std::string +{ + return "CheckContactsListDuplicatesResult"; +} diff --git a/module-db/queries/phonebook/QueryCheckContactsListDuplicates.hpp b/module-db/queries/phonebook/QueryCheckContactsListDuplicates.hpp new file mode 100644 index 0000000000000000000000000000000000000000..f8dad372c47a2d7463aa46e7c7deae5bebbec4de --- /dev/null +++ b/module-db/queries/phonebook/QueryCheckContactsListDuplicates.hpp @@ -0,0 +1,40 @@ +// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved. +// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md + +#pragma once + +#include "Common/Query.hpp" +#include +#include +#include + +#include + +namespace db::query +{ + + class CheckContactsListDuplicates : public Query + { + public: + CheckContactsListDuplicates(std::vector contacts); + [[nodiscard]] auto debugInfo() const -> std::string override; + + std::vector &getContactsList(); + + private: + std::vector contacts; + }; + + class CheckContactsListDuplicatesResult : public QueryResult + { + public: + CheckContactsListDuplicatesResult(std::vector duplicates); + std::vector &getDuplicates(); + + [[nodiscard]] auto debugInfo() const -> std::string override; + + private: + std::vector duplicates; + }; + +}; // namespace db::query diff --git a/module-db/queries/phonebook/QueryMergeContactsList.cpp b/module-db/queries/phonebook/QueryMergeContactsList.cpp new file mode 100644 index 0000000000000000000000000000000000000000..e7397fa1607218f85e35d62506ea8e334145b28e --- /dev/null +++ b/module-db/queries/phonebook/QueryMergeContactsList.cpp @@ -0,0 +1,29 @@ +// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved. +// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md + +#include "QueryMergeContactsList.hpp" +#include + +using namespace db::query; + +MergeContactsList::MergeContactsList(std::vector contacts) + : Query(Query::Type::Read), contacts(std::move(contacts)) +{} + +std::vector &MergeContactsList::getContactsList() +{ + return contacts; +} + +MergeContactsListResult::MergeContactsListResult(bool result) : result(result) +{} + +[[nodiscard]] auto MergeContactsList::debugInfo() const -> std::string +{ + return "MergeContactsList"; +} + +[[nodiscard]] auto MergeContactsListResult::debugInfo() const -> std::string +{ + return "MergeContactsListResult"; +} diff --git a/module-db/queries/phonebook/QueryMergeContactsList.hpp b/module-db/queries/phonebook/QueryMergeContactsList.hpp new file mode 100644 index 0000000000000000000000000000000000000000..e3c25d5f714a804665f000e169cdf57c87ef617a --- /dev/null +++ b/module-db/queries/phonebook/QueryMergeContactsList.hpp @@ -0,0 +1,42 @@ +// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved. +// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md + +#pragma once + +#include "Common/Query.hpp" +#include +#include +#include + +#include + +namespace db::query +{ + + class MergeContactsList : public Query + { + public: + MergeContactsList(std::vector contacts); + [[nodiscard]] auto debugInfo() const -> std::string override; + + std::vector &getContactsList(); + + private: + std::vector contacts; + }; + + class MergeContactsListResult : public QueryResult + { + public: + MergeContactsListResult(bool result); + [[nodiscard]] auto getResult() const noexcept -> bool + { + return result; + } + [[nodiscard]] auto debugInfo() const -> std::string override; + + private: + bool result = false; + }; + +}; // namespace db::query diff --git a/module-db/tests/ContactsRecord_tests.cpp b/module-db/tests/ContactsRecord_tests.cpp index 5daf8cea0f18fdd333020c2c94bf64c45959360d..8f1101a4d2fc7b04918fd334fdd3f9122ef94770 100644 --- a/module-db/tests/ContactsRecord_tests.cpp +++ b/module-db/tests/ContactsRecord_tests.cpp @@ -396,3 +396,189 @@ TEST_CASE("Contact record numbers update") Database::deinitialize(); } + +TEST_CASE("Contacts list merge") +{ + Database::initialize(); + const auto contactsPath = (std::filesystem::path{"sys/user"} / "contacts.db"); + RemoveDbFiles(contactsPath.stem()); + + ContactsDB contactDB(contactsPath.c_str()); + REQUIRE(contactDB.isInitialized()); + + // Preparation of DB initial state + auto records = ContactRecordInterface(&contactDB); + std::array, 3> rawContactsInitial = { + {{"600100100", "test1"}, {"600100200", "test2"}, {"600100300", "test3"}}}; + for (auto &rawContact : rawContactsInitial) { + ContactRecord record; + record.primaryName = rawContact.second; + record.numbers = std::vector({ContactRecord::Number(rawContact.first, std::string(""))}); + REQUIRE(records.Add(record)); + } + + SECTION("Merge contacts without overlaps") + { + std::array, 3> rawContactsToAdd = { + {{"600100400", "test4"}, {"600100500", "test5"}, {"600100600", "test6"}}}; + + // Prepare contacts list to merge + std::vector contacts; + for (auto &rawContact : rawContactsToAdd) { + ContactRecord record; + record.primaryName = rawContact.second; + record.numbers = + std::vector({ContactRecord::Number(rawContact.first, std::string(""))}); + contacts.push_back(record); + } + REQUIRE(records.MergeContactsList(contacts)); + + // Validate if non-overlapping were appended to DB + REQUIRE(records.GetCount() == (rawContactsInitial.size() + rawContactsToAdd.size())); + + auto validatationRecord = records.GetByID(1); + REQUIRE(validatationRecord.numbers[0].number.getEntered() == rawContactsInitial[0].first); + REQUIRE(validatationRecord.primaryName == rawContactsInitial[0].second); + validatationRecord = records.GetByID(2); + REQUIRE(validatationRecord.numbers[0].number.getEntered() == rawContactsInitial[1].first); + REQUIRE(validatationRecord.primaryName == rawContactsInitial[1].second); + validatationRecord = records.GetByID(3); + REQUIRE(validatationRecord.numbers[0].number.getEntered() == rawContactsInitial[2].first); + REQUIRE(validatationRecord.primaryName == rawContactsInitial[2].second); + validatationRecord = records.GetByID(4); + REQUIRE(validatationRecord.numbers[0].number.getEntered() == rawContactsToAdd[0].first); + REQUIRE(validatationRecord.primaryName == rawContactsToAdd[0].second); + validatationRecord = records.GetByID(5); + REQUIRE(validatationRecord.numbers[0].number.getEntered() == rawContactsToAdd[1].first); + REQUIRE(validatationRecord.primaryName == rawContactsToAdd[1].second); + validatationRecord = records.GetByID(6); + REQUIRE(validatationRecord.numbers[0].number.getEntered() == rawContactsToAdd[2].first); + REQUIRE(validatationRecord.primaryName == rawContactsToAdd[2].second); + } + + SECTION("Merge contacts with numbers overlaps") + { + REQUIRE(records.GetCount() == rawContactsInitial.size()); + + std::array, 3> rawContactsOverlapping = { + {{rawContactsInitial[1].first, "test7"}, {"600100800", "test8"}, {rawContactsInitial[0].first, "test9"}}}; + constexpr auto numberOfNewContacts = 1; + + // Prepare contacts list to merge + std::vector contacts; + for (auto &rawContact : rawContactsOverlapping) { + ContactRecord record; + record.primaryName = rawContact.second; + record.numbers = + std::vector({ContactRecord::Number(rawContact.first, std::string(""))}); + contacts.push_back(record); + } + REQUIRE(records.MergeContactsList(contacts)); + + REQUIRE(records.GetCount() == (rawContactsInitial.size() + numberOfNewContacts)); + + // Overlapping contacts replaced with same ID + auto validatationRecord = records.GetByID(1); + REQUIRE(validatationRecord.numbers[0].number.getEntered() == rawContactsInitial[0].first); + REQUIRE(validatationRecord.primaryName == rawContactsOverlapping[2].second); + validatationRecord = records.GetByID(2); + REQUIRE(validatationRecord.numbers[0].number.getEntered() == rawContactsInitial[1].first); + REQUIRE(validatationRecord.primaryName == rawContactsOverlapping[0].second); + // Non-overlapping contact left untouched + validatationRecord = records.GetByID(3); + REQUIRE(validatationRecord.numbers[0].number.getEntered() == rawContactsInitial[2].first); + REQUIRE(validatationRecord.primaryName == rawContactsInitial[2].second); + // Non-overlapping new contact added + validatationRecord = records.GetByID(4); + REQUIRE(validatationRecord.numbers[0].number.getEntered() == rawContactsOverlapping[1].first); + REQUIRE(validatationRecord.primaryName == rawContactsOverlapping[1].second); + } + + Database::deinitialize(); +} + +TEST_CASE("Contacts list merge - advanced cases") +{ + Database::initialize(); + const auto contactsPath = (std::filesystem::path{"sys/user"} / "contacts.db"); + RemoveDbFiles(contactsPath.stem()); + + ContactsDB contactDB(contactsPath.c_str()); + REQUIRE(contactDB.isInitialized()); + + // Preparation of DB initial state + auto records = ContactRecordInterface(&contactDB); + // 3 numbers in single contact + std::array numbers = {"600100100", "600100200", "600100300"}; + ContactRecord record; + record.primaryName = "test"; + for (auto &number : numbers) { + record.numbers.push_back({ContactRecord::Number(number, std::string(""))}); + } + REQUIRE(records.Add(record)); + + SECTION("Compared number is secondary number") + { + std::pair rawContact = {"600100200", "test2"}; + std::vector contacts; + + // Prepare contacts list to merge + ContactRecord record; + record.primaryName = rawContact.second; + record.numbers = std::vector({ContactRecord::Number(rawContact.first, std::string(""))}); + contacts.push_back(record); + REQUIRE(records.MergeContactsList(contacts)); + + REQUIRE(records.GetCount() == 1); + + // First contact replaced + auto validatationRecord = records.GetByID(1); + REQUIRE(validatationRecord.numbers[0].number.getEntered() == rawContact.first); + REQUIRE(validatationRecord.primaryName == rawContact.second); + } +} + +TEST_CASE("Contacts list duplicates search") +{ + Database::initialize(); + const auto contactsPath = (std::filesystem::path{"sys/user"} / "contacts.db"); + RemoveDbFiles(contactsPath.stem()); + + ContactsDB contactDB(contactsPath.c_str()); + REQUIRE(contactDB.isInitialized()); + + // Preparation of DB initial state + auto records = ContactRecordInterface(&contactDB); + std::array, 3> rawContactsInitial = { + {{"600100100", "test1"}, {"600100200", "test2"}, {"600100300", "test3"}}}; + for (auto &rawContact : rawContactsInitial) { + ContactRecord record; + record.primaryName = rawContact.second; + record.numbers = std::vector({ContactRecord::Number(rawContact.first, std::string(""))}); + REQUIRE(records.Add(record)); + } + + // Prepare contacts list to compare with DB + std::array, 3> rawContactsToCheck = { + {rawContactsInitial[2], {"600100500", "test5"}, rawContactsInitial[0]}}; + constexpr auto numOfDuplicatedContacts = 2; + + std::vector contacts; + for (auto &rawContact : rawContactsToCheck) { + ContactRecord record; + record.primaryName = rawContact.second; + record.numbers = std::vector({ContactRecord::Number(rawContact.first, std::string(""))}); + contacts.push_back(record); + } + auto duplicates = records.CheckContactsListDuplicates(contacts); + + REQUIRE(duplicates.size() == numOfDuplicatedContacts); + + REQUIRE(duplicates[0].numbers[0].number.getEntered() == rawContactsToCheck[0].first); + REQUIRE(duplicates[0].primaryName == rawContactsToCheck[0].second); + + REQUIRE(duplicates[1].numbers[0].number.getEntered() == rawContactsToCheck[2].first); + REQUIRE(duplicates[1].primaryName == rawContactsToCheck[2].second); + + Database::deinitialize(); +} diff --git a/out/module-db/doc/contacts_import/contacts_import.svg b/out/module-db/doc/contacts_import/contacts_import.svg new file mode 100644 index 0000000000000000000000000000000000000000..819efaf570dee29140055980c9b14ee74a4e5190 --- /dev/null +++ b/out/module-db/doc/contacts_import/contacts_import.svg @@ -0,0 +1,40 @@ +ApplicationApplicationContactRecordContactRecordcontacts DBcontacts DBChecking for duplicatescontacts to check(query::CheckContactsListDuplicates)get numbersnumbers comparisonduplicated contactsMerging contacts list to DBcontacts to merge(query::MergeContactsList)get numbersnumbers comparisonnumber not foundAdd contactnumber found in dbUpdate contact by overriding old datastatus response \ No newline at end of file