diff --git a/source/RobotAPI/libraries/armem/core/MemoryID.cpp b/source/RobotAPI/libraries/armem/core/MemoryID.cpp index 664319410949b11c7563512a6ccc22dc0c2dd3e1..61dd4836ca9f16ffd6b36342facbd2f610c75ff3 100644 --- a/source/RobotAPI/libraries/armem/core/MemoryID.cpp +++ b/source/RobotAPI/libraries/armem/core/MemoryID.cpp @@ -1,20 +1,45 @@ #include "MemoryID.h" -#include <SimoxUtility/algorithm/string.h> - #include "error/ArMemError.h" +#include <SimoxUtility/algorithm/advanced.h> +#include <SimoxUtility/algorithm/string/string_tools.h> + +#include <boost/algorithm/string.hpp> + +#include <forward_list> + namespace armarx::armem { + const std::string MemoryID::delimiter = "/"; + + MemoryID::MemoryID() { } MemoryID::MemoryID(const std::string& string) { - std::vector<std::string> items = simox::alg::split(string, "/", false, false); + std::forward_list<std::string> items; + boost::split(items, string, boost::is_any_of(delimiter)); + + // Handle escaped /'s + for (auto it = items.begin(); it != items.end(); ++it) + { + while (it->size() > 0 && it->back() == '\\') + { + // The / causing the split was escaped. Merge the items together. + auto next = simox::alg::advanced(it, 1); + if (next != items.end()) + { + // "it\" + "next" => "it/next" + *it = it->substr(0, it->size() - 1) + delimiter + *next; + items.erase_after(it); // Invalidates `next`, but not `it`. + } + } + } auto it = items.begin(); if (it == items.end()) @@ -55,19 +80,19 @@ namespace armarx::armem } - std::string MemoryID::str() const + std::string MemoryID::str(bool escapeDelimiters) const { - return str("/"); + return str(delimiter, escapeDelimiters); } - std::string MemoryID::str(const std::string& delimeter) const + std::string MemoryID::str(const std::string& delimiter, bool escapeDelimiter) const { - std::vector<std::string> items = getAllItems(); + std::vector<std::string> items = getAllItems(escapeDelimiter); while (items.size() > 0 && items.back().empty()) { items.pop_back(); } - return simox::alg::join(items, delimeter, false, false); + return simox::alg::join(items, delimiter, false, false); } std::string MemoryID::getLeafItem() const @@ -86,52 +111,55 @@ namespace armarx::armem bool MemoryID::hasGap() const { bool emptyFound = false; - for (const std::string& name : getAllItems()) + for (const std::string& item : getAllItems()) { - if (name.empty()) + if (item.empty()) { emptyFound = true; } - else + else if (emptyFound) { - if (emptyFound) - { - return true; - } + // Found a non-empty item after an empty item. + return true; } } return false; } + bool MemoryID::isWellDefined() const + { + return !hasGap(); + } + MemoryID MemoryID::fromString(const std::string& string) { return MemoryID(string); } - std::vector<std::string> MemoryID::getItems() const + std::vector<std::string> MemoryID::getItems(bool escapeDelimiters) const { std::vector<std::string> items; - items.push_back(memoryName); + items.push_back(escape(memoryName, escapeDelimiters)); if (!hasCoreSegmentName()) { return items; } - items.push_back(coreSegmentName); + items.push_back(escape(coreSegmentName, escapeDelimiters)); if (!hasProviderSegmentName()) { return items; } - items.push_back(providerSegmentName); + items.push_back(escape(providerSegmentName, escapeDelimiters)); if (!hasEntityName()) { return items; } - items.push_back(entityName); + items.push_back(escape(entityName, escapeDelimiters)); if (!hasTimestamp()) { @@ -148,11 +176,12 @@ namespace armarx::armem return items; } - std::vector<std::string> MemoryID::getAllItems() const + std::vector<std::string> MemoryID::getAllItems(bool escapeDelimiters) const { return { - memoryName, coreSegmentName, providerSegmentName, entityName, + escape(memoryName, escapeDelimiters), escape(coreSegmentName, escapeDelimiters), + escape(providerSegmentName, escapeDelimiters), escape(entityName, escapeDelimiters), timestampStr(), instanceIndexStr() }; } @@ -276,6 +305,7 @@ namespace armarx::armem return id; } + std::string MemoryID::timestampStr() const { return hasTimestamp() ? std::to_string(timestamp.toMicroSeconds()) : ""; @@ -306,8 +336,46 @@ namespace armarx::armem and instanceIndex == other.instanceIndex; } + bool MemoryID::operator<(const MemoryID& rhs) const + { + int c = memoryName.compare(rhs.memoryName); + if (c != 0) + { + return c < 0; + } + // Equal memory name + c = coreSegmentName.compare(rhs.coreSegmentName); + if (c != 0) + { + return c < 0; + } + // Equal core segment ID + c = providerSegmentName.compare(rhs.providerSegmentName); + if (c != 0) + { + return c < 0; + } + // Equal provider segment ID + c = entityName.compare(rhs.entityName); + if (c != 0) + { + return c < 0; + } + // Equal entity ID + if (timestamp != rhs.timestamp) + { + return timestamp < rhs.timestamp; + } + // Equal entity snapshot ID + return instanceIndex < rhs.instanceIndex; + } + long long MemoryID::parseInteger(const std::string& string, const std::string& semanticName) { + if (string.empty()) + { + return -1; + } try { return std::stoll(string); @@ -322,15 +390,44 @@ namespace armarx::armem } } + std::string MemoryID::escapeDelimiter(const std::string& name) + { + return simox::alg::replace_all(name, delimiter, "\\" + delimiter); + } + + std::string MemoryID::escape(const std::string& name, bool escapeDelimiters) + { + if (escapeDelimiters) + { + return escapeDelimiter(name); + } + else + { + return name; + } + } + std::ostream& operator<<(std::ostream& os, const MemoryID id) { return os << "'" << id.str() << "'"; } - bool - contains(const MemoryID& general, const MemoryID& specific) + bool contains(const MemoryID& general, const MemoryID& specific) { + if (!general.isWellDefined()) + { + std::stringstream ss; + ss << "ID `general` is not well-defined, which is required for `" << __FUNCTION__ << "()`."; + throw error::InvalidMemoryID(general, ss.str()); + } + if (!specific.isWellDefined()) + { + std::stringstream ss; + ss << "ID `specific` is not well-defined, which is required for `" << __FUNCTION__ << "()`."; + throw error::InvalidMemoryID(specific, ss.str()); + } + if (general.memoryName.empty()) { return true; diff --git a/source/RobotAPI/libraries/armem/core/MemoryID.h b/source/RobotAPI/libraries/armem/core/MemoryID.h index 1fc22f7288b51970f298a9309cbe33b087d48f5f..09415ec4abd73185f77050c89683b326f3c3e9d0 100644 --- a/source/RobotAPI/libraries/armem/core/MemoryID.h +++ b/source/RobotAPI/libraries/armem/core/MemoryID.h @@ -12,11 +12,37 @@ namespace armarx::armem /** * @brief A memory ID. * - * Structure: - * `MemoryName/CoreSegmentName/ProviderSegmentName/EntityName/Timestamp/InstanceIndex` + * A memory ID is an index into the hierarchical memory structure. + * It specifies the keys for the different levels, starting from the + * memory name and ending at the instance index. * - * Example: - * `VisionMemory/RGBImages/PrimesenseRGB/image/1245321323/0` + * A memory ID need not be complete, e.g. it may specify only the memory + * and core segment names (thus representing a core segment ID). + * A memory ID that fully identifies a level starting from the memory is + * called well-defined. + * @see `isWellDefined()` + * + * Memory IDs can be encoded in strings using a delimiter: + * - Structure: "MemoryName/CoreSegmentName/ProviderSegmentName/EntityName/Timestamp/InstanceIndex" + * - Example: "Vision/RGBImages/Primesense/image/1245321323/0" + * @see `str()` + * + * If an ID does not specify the lower levels, these parts can be omitted. + * Thus, an entity ID could look like: + * - Structure: "MemoryName/CoreSegmentName/ProviderSegmentName/EntityName" + * - Example: "Vision/RGBImages/Primesense/image" + * + * If a name contains a "/", it will be escaped: + * - Example: "Vision/RGBImages/Primesense/my\/entity\/with\/slashes" + * + * Memory IDs may be not well-defined. This can occur e.g. when preparing + * an entity instance ID which is still pending the timestamp. + * It could look like (note the missing timestamp): + * - Example: "Vision/RGBImages/Primesense/image//0" + * + * These IDs are still valid and can be handled (encoded as string etc.). + * However, some operations may not be well-defined for non-well-defined IDs + * (such as `contains()`). */ class MemoryID { @@ -30,26 +56,37 @@ namespace armarx::armem int instanceIndex = -1; - public: + /// Construct a default (empty) memory ID. MemoryID(); + /// (Re-)Construct a memory ID from a string representation as returned by `str()`. explicit MemoryID(const std::string& string); - std::string str() const; - std::string str(const std::string& delimeter) const; - static MemoryID fromString(const std::string& string); - - std::string timestampStr() const; - std::string instanceIndexStr() const; - - - /// Get all levels as string. - std::vector<std::string> getAllItems() const; - /// Get the levels from root to first not defined level (excluding). - std::vector<std::string> getItems() const; - + /** + * @brief Indicate whether this ID is well-defined. + * + * A well-defined ID has no specified level after a non-specified level (i.e., no gaps). + * + * Well-defined examples: + * - "" (empty, but well-defined) + * - "Memory" (a memory ID) + * - "Memory/Core" (a core segment ID) + * - "Memory/Core/Provider" (a provider segment ID) + * + * Non-well-defined examples: + * - "Memory//Provider" (no core segment name) + * - "/Core" (no memory name) + * - "Mem/Core/Prov/entity//0" (no timestamp) + * - "///entity//0" (no memory, core segment and provider segment names) + * + * @return True if `*this` is a well-defined memory ID. + */ + bool isWellDefined() const; + + + // Checks whether a specific level is specified. bool hasMemoryName() const { @@ -113,15 +150,49 @@ namespace armarx::armem MemoryID withInstanceIndex(int index) const; + // String conversion + + /** + * @brief Get a string representation of this memory ID. + * + * Items are separated by a delimiter. If `escapeDelimiter` is true, + * delimiters occuring inside names are escaped with backward slashes. + * This allows to reconstruct the memory ID from the result of `str()` + * in these cases. + * + * @param escapeDelimiter If true, escape delimiters inside names + * @return A string representation of this MemoryID. + */ + std::string str(bool escapeDelimiters = true) const; + + /// Get the timestamp as string. + std::string timestampStr() const; + /// Get the instance index as string. + std::string instanceIndexStr() const; + + /// Alias for constructor from string. + static MemoryID fromString(const std::string& string); + /// Reconstruct a timestamp from a string as returned by `timestampStr()`. + static Time timestampFromStr(const std::string& timestamp); + /// Reconstruct an instance index from a string as returned by `instanceIndexStr()`. + static int instanceIndexFromStr(const std::string& index); + + + /// Get all levels as strings. + std::vector<std::string> getAllItems(bool escapeDelimiters = false) const; + /// Get the levels from root to first not defined level (excluding). + std::vector<std::string> getItems(bool escapeDelimiters = false) const; + + + // Other utility. + /// Indicate whether this ID has a gap such as in 'Memory//MyProvider' (no core segment name). bool hasGap() const; /// Get the lowest defined level (or empty string if there is none). std::string getLeafItem() const; - static Time timestampFromStr(const std::string& timestamp); - static int instanceIndexFromStr(const std::string& index); - + // Operators bool operator ==(const MemoryID& other) const; inline bool operator !=(const MemoryID& other) const @@ -129,18 +200,59 @@ namespace armarx::armem return !(*this == other); } + bool operator< (const MemoryID& rhs) const; + inline bool operator> (const MemoryID& rhs) const + { + return rhs < (*this); + } + inline bool operator<=(const MemoryID& rhs) const + { + return !operator> (rhs); + } + inline bool operator>=(const MemoryID& rhs) const + { + return !operator< (rhs); + } + friend std::ostream& operator<<(std::ostream& os, const MemoryID id); private: static long long parseInteger(const std::string& string, const std::string& semanticName); + static std::string escapeDelimiter(const std::string& name); + static std::string escape(const std::string& name, bool escapeDelimiters); + + static const std::string delimiter; + + + // Do not allow specifying the delimiter from outside. + std::string str(const std::string& delimiter, bool escapeDelimiter) const; }; - bool - contains(const MemoryID& general, const MemoryID& specific); + /** + * @brief Indicates whether `general` is "less specific" than, or equal to, `specific`, + * i.e. `general` "contains" `specific`. + * + * A memory ID A is said to be less specific than B, if B the same values as A + * for all levels specified in A, but potentially also specifies the lower levels. + * + * Examples: + * - "" contains "" + * - "m" contains "m" and "m/c", but not "n" and "n/c" + * - "m/c" contains "m/c" and "m/c/p", but not "m/d" and "m/c/q" + * + * If a memory ID has a gap (`@see MemoryID::hasGap()`), such as "m//p", + * the levels after the gap are ignored. + * - "m//p" contains "m", "m/c" and "m/c/p". + * + * @param general The less specific memory ID + * @param specific The more specific memory ID + * @return True if `general` is less specific than `specific`. + */ + bool contains(const MemoryID& general, const MemoryID& specific); } diff --git a/source/RobotAPI/libraries/armem/test/ArMemMemoryIDTest.cpp b/source/RobotAPI/libraries/armem/test/ArMemMemoryIDTest.cpp index d811c053ba4c57864030eaa8b0d5a7485a6df58c..1f94d9adb9a2d023a3813c0e09edf8b41367ace1 100644 --- a/source/RobotAPI/libraries/armem/test/ArMemMemoryIDTest.cpp +++ b/source/RobotAPI/libraries/armem/test/ArMemMemoryIDTest.cpp @@ -26,6 +26,7 @@ #include <RobotAPI/Test.h> #include "../core/MemoryID.h" +#include "../core/error.h" #include <iostream> @@ -33,22 +34,106 @@ namespace armem = armarx::armem; -BOOST_AUTO_TEST_CASE(test_memoryid_contains) +BOOST_AUTO_TEST_CASE(test_MemoryID_contains) { armem::MemoryID general, specific; - BOOST_CHECK(armem::contains(general, specific)); + // Both empty. + BOOST_TEST_CONTEXT(VAROUT(general) << " | " << VAROUT(specific)) + { + BOOST_CHECK(armem::contains(general, specific)); + } + // Diverging. general.memoryName = "a"; specific.memoryName = "b"; - BOOST_CHECK(not armem::contains(general, specific)); + BOOST_TEST_CONTEXT(VAROUT(general) << " | " << VAROUT(specific)) + { + BOOST_CHECK(not armem::contains(general, specific)); + BOOST_CHECK(not armem::contains(specific, general)); + } + // Identical. specific.memoryName = "a"; - BOOST_CHECK(armem::contains(general, specific)); + BOOST_TEST_CONTEXT(VAROUT(general) << " | " << VAROUT(specific)) + { + BOOST_CHECK(armem::contains(general, specific)); + BOOST_CHECK(armem::contains(specific, general)); + } - specific.providerSegmentName = "aa"; + // general contains specific + specific.coreSegmentName = "c"; + + BOOST_TEST_CONTEXT(VAROUT(general) << " | " << VAROUT(specific)) + { + BOOST_CHECK(armem::contains(general, specific)); + BOOST_CHECK(not armem::contains(specific, general)); + } + + // general contains specific + specific.providerSegmentName = "d"; + + BOOST_TEST_CONTEXT(VAROUT(general) << " | " << VAROUT(specific)) + { + BOOST_CHECK(armem::contains(general, specific)); + BOOST_CHECK(not armem::contains(specific, general)); + } + + // Not well-defined ID - throw an exception. + specific.coreSegmentName.clear(); + + BOOST_TEST_CONTEXT(VAROUT(general) << " | " << VAROUT(specific)) + { + BOOST_CHECK_THROW(armem::contains(general, specific), armem::error::InvalidMemoryID); + BOOST_CHECK_THROW(armem::contains(specific, general), armem::error::InvalidMemoryID); + } +} + + + +BOOST_AUTO_TEST_CASE(test_MemoryID_from_to_string) +{ + armem::MemoryID idIn {"Memory/Core/Prov/entity/2810381/2"}; + + BOOST_CHECK_EQUAL(idIn.memoryName, "Memory"); + BOOST_CHECK_EQUAL(idIn.coreSegmentName, "Core"); + BOOST_CHECK_EQUAL(idIn.providerSegmentName, "Prov"); + BOOST_CHECK_EQUAL(idIn.entityName, "entity"); + BOOST_CHECK_EQUAL(idIn.timestamp, IceUtil::Time::microSeconds(2810381)); + BOOST_CHECK_EQUAL(idIn.instanceIndex, 2); + + + BOOST_TEST_CONTEXT(VAROUT(idIn.str())) + { + armem::MemoryID idOut(idIn.str()); + BOOST_CHECK_EQUAL(idOut, idIn); + } + + idIn.entityName = "KIT/Amicelli/0"; // Like an ObjectID + BOOST_CHECK_EQUAL(idIn.entityName, "KIT/Amicelli/0"); + + BOOST_TEST_CONTEXT(VAROUT(idIn.str())) + { + armem::MemoryID idOut(idIn.str()); + BOOST_CHECK_EQUAL(idOut.entityName, "KIT/Amicelli/0"); + BOOST_CHECK_EQUAL(idOut, idIn); + } + + idIn = armem::MemoryID {"InThe\\/Mid/AtTheEnd\\//\\/AtTheStart/YCB\\/sugar\\/-1//2"}; + BOOST_CHECK_EQUAL(idIn.memoryName, "InThe/Mid"); + BOOST_CHECK_EQUAL(idIn.coreSegmentName, "AtTheEnd/"); + BOOST_CHECK_EQUAL(idIn.providerSegmentName, "/AtTheStart"); + BOOST_CHECK_EQUAL(idIn.entityName, "YCB/sugar/-1"); + BOOST_CHECK_EQUAL(idIn.timestamp, IceUtil::Time::microSeconds(-1)); + BOOST_CHECK_EQUAL(idIn.instanceIndex, 2); + + BOOST_TEST_CONTEXT(VAROUT(idIn.str())) + { + armem::MemoryID idOut(idIn.str()); + BOOST_CHECK_EQUAL(idOut, idIn); + } - BOOST_CHECK(armem::contains(general, specific)); } +