diff --git a/Sofa/framework/Core/src/sofa/core/objectmodel/BaseLink.cpp b/Sofa/framework/Core/src/sofa/core/objectmodel/BaseLink.cpp index f9697caf6ee..38067eacab0 100644 --- a/Sofa/framework/Core/src/sofa/core/objectmodel/BaseLink.cpp +++ b/Sofa/framework/Core/src/sofa/core/objectmodel/BaseLink.cpp @@ -105,7 +105,7 @@ bool BaseLink::ParseString(const std::string& text, std::string* path, std::stri { if (owner) { - msg_error(owner) << "ERROR parsing Link \""< > entries; - /// Cut the path using space as a separator. This has several - /// questionnable consequence among which space are not allowed in part of a path (so no name containing space) - /// tokenizing the path using '@' as a separator would solve the issue. - while (istr >> path) + auto paths = sofa::helper::split(std::string(cleanedPath), '@'); + std::erase_if(paths, [](const std::string& path) { return path.empty(); }); + std::transform(paths.begin(), paths.end(), paths.begin(), [](const std::string& path) { return "@" + path; }); + + for (const auto& path : paths) { - if (path[0] != '@') - { - msg_error(owner) << "Parsing Link \"" << path <<"\": first character should be '@'."; - ok = false; - } - else - { - /// Check if the path is pointing to any object of Base type. - Base* ptr = PathResolver::FindBaseFromClassAndPath(owner, getDestClass(), path); + /// Check if the path is pointing to any object of Base type. + Base* ptr = PathResolver::FindBaseFromClassAndPath(owner, getDestClass(), path); - /// We found either a valid Base object or none. - /// ptr can be nullptr, as the destination can be added later in the graph - /// instead, we will check for failed links after init is completed - entries.emplace_back(ptr, path); - } + /// We found either a valid Base object or none. + /// ptr can be nullptr, as the destination can be added later in the graph + /// instead, we will check for failed links after init is completed + entries.emplace_back(ptr, path); } /// Check for the case where multiple link has been read while we are a single multilink. @@ -316,9 +326,9 @@ bool BaseLink::read( const std::string& str ) /// Add the detected objects that are not already present to the container of this Link clear(); - for (const auto& [base, path] : entries) + for (const auto& [base2, path2] : entries) { - ok = add(base, path) && ok; + ok = add(base2, path2) && ok; } return ok; } diff --git a/Sofa/framework/Core/test/objectmodel/SingleLink_test.cpp b/Sofa/framework/Core/test/objectmodel/SingleLink_test.cpp index 7fe373945b3..1fee486a805 100644 --- a/Sofa/framework/Core/test/objectmodel/SingleLink_test.cpp +++ b/Sofa/framework/Core/test/objectmodel/SingleLink_test.cpp @@ -20,8 +20,11 @@ * Contact information: contact@sofa-framework.org * ******************************************************************************/ #include +#include +#include +#include using sofa::core::objectmodel::BaseObject ; -#include +#include using sofa::core::objectmodel::BaseNode ; #include @@ -45,6 +48,7 @@ class SingleLink_test: public BaseTest SingleLink m_link ; sofa::core::objectmodel::BaseObject::SPtr m_dst ; sofa::core::objectmodel::BaseObject::SPtr m_src ; + sofa::simulation::Node::SPtr m_root { nullptr }; /// Create a link to an object. void doSetUp() override @@ -57,6 +61,20 @@ class SingleLink_test: public BaseTest m_src->addLink(&m_link); m_link.add(m_dst.get()); } + + void setupContext() + { + m_root = sofa::simulation::getSimulation()->createNewGraph("root"); + + m_root->addObject(this->m_src); + m_root->addObject(this->m_dst); + + m_link.set(nullptr); + ASSERT_EQ(m_link.get(), nullptr); + + m_src->addLink(&m_link); + m_link.setOwner(m_src.get()); + } }; TEST_F(SingleLink_test, checkAccess ) @@ -142,3 +160,205 @@ TEST_F(SingleLink_test, checkEmptyness ) m_link.clear(); ASSERT_EQ( m_link.size(), 0 ); } + +TEST_F(SingleLink_test, readWithoutContext) +{ + m_link.set(nullptr); + ASSERT_EQ(m_link.get(), nullptr); + + const bool success = m_link.read("@/destination"); + ASSERT_FALSE(success); + ASSERT_EQ(m_link.get(), nullptr); +} + + + +TEST_F(SingleLink_test, readWithContextEmptyString) +{ + setupContext(); + + const bool success = m_link.read(""); + ASSERT_TRUE(success); + ASSERT_EQ(m_link.get(), nullptr); +} + +TEST_F(SingleLink_test, readWithContextJustAArobase) +{ + setupContext(); + + // the path points to an invalid object, but it's ok + const bool success = m_link.read("@"); + ASSERT_TRUE(success); + ASSERT_EQ(m_link.get(), nullptr); +} + +TEST_F(SingleLink_test, readWithContextValidLink) +{ + setupContext(); + + const bool success = m_link.read("@/destination"); + ASSERT_TRUE(success); + ASSERT_EQ(m_link.get(), m_dst.get()); +} + +TEST_F(SingleLink_test, readWithContextInvalidLink) +{ + setupContext(); + + // the path points to an invalid object, but it's ok + const bool success = m_link.read("@/foo"); + ASSERT_TRUE(success); + ASSERT_EQ(m_link.get(), nullptr); +} + +TEST_F(SingleLink_test, readWithContextValidLinkDoubleSlash) +{ + setupContext(); + + const bool success = m_link.read("@//destination"); + ASSERT_TRUE(success); + ASSERT_EQ(m_link.get(), m_dst.get()); +} + +TEST_F(SingleLink_test, readWithContextValidLinkNoSlash) +{ + setupContext(); + + const bool success = m_link.read("@destination"); + ASSERT_TRUE(success); + ASSERT_EQ(m_link.get(), m_dst.get()); +} + +TEST_F(SingleLink_test, readWithContextValidLinkLeadingSpaces) +{ + setupContext(); + + const bool success = m_link.read(" @/destination"); + ASSERT_TRUE(success); + ASSERT_EQ(m_link.get(), m_dst.get()); + ASSERT_EQ(m_link.getSize(), 1); +} + +TEST_F(SingleLink_test, readWithContextValidLinkTrailingSpaces) +{ + setupContext(); + + const bool success = m_link.read("@/destination "); + ASSERT_TRUE(success); + ASSERT_EQ(m_link.get(), m_dst.get()); + ASSERT_EQ(m_link.getSize(), 1); +} + +TEST_F(SingleLink_test, readWithContextValidLinkTrailingSpacesButTheyArePartOfThePath) +{ + setupContext(); + + m_dst->setName("destination "); + + const bool success = m_link.read("@/destination "); + ASSERT_TRUE(success); + ASSERT_EQ(m_link.get(), m_dst.get()); + ASSERT_EQ(m_link.getSize(), 1); +} + +TEST_F(SingleLink_test, readWithContextWithDot) +{ + setupContext(); + m_dst->setName("Component.With.Dots"); + + const bool success = m_link.read("@Component.With.Dots"); + ASSERT_TRUE(success); +} + +TEST_F(SingleLink_test, readWithContextWithSpace) +{ + setupContext(); + m_dst->setName("Backward Euler ODE Solver"); + + const bool success = m_link.read("@Backward Euler ODE Solver"); + ASSERT_TRUE(success); +} + +TEST_F(SingleLink_test, readComplex) +{ + m_root = sofa::simulation::getSimulation()->createNewGraph("root"); + m_root->addObject(this->m_src); + + auto child1 = m_root->createChild("child 1"); + auto child2 = child1->createChild("child 2"); + child2->addObject(this->m_dst); + m_dst->setName("My Object"); + + m_link.set(nullptr); + ASSERT_EQ(m_link.get(), nullptr); + + m_src->addLink(&m_link); + m_link.setOwner(m_src.get()); + + { + const bool success = m_link.read("@/child 1/child 2/My Object"); + ASSERT_TRUE(success); + ASSERT_EQ(m_link.get(), m_dst.get()); + ASSERT_EQ(m_link.getSize(), 1); + } + + { + const bool success = m_link.read("@/child 2/My Object"); + ASSERT_TRUE(success); + ASSERT_EQ(m_link.get(), nullptr); + ASSERT_EQ(m_link.getSize(), 1); + } + + { + const bool success = m_link.read("@/child 1/child 2/../child 2/My Object"); + ASSERT_TRUE(success); + ASSERT_EQ(m_link.get(), m_dst.get()); + ASSERT_EQ(m_link.getSize(), 1); + } +} + +TEST_F(SingleLink_test, readComplex2) +{ + m_root = sofa::simulation::getSimulation()->createNewGraph("root"); + + auto child1 = m_root->createChild("child 1"); + child1->addObject(this->m_src); + + auto child2 = child1->createChild("child 2"); + child2->addObject(this->m_dst); + m_dst->setName("My Object"); + + m_link.set(nullptr); + ASSERT_EQ(m_link.get(), nullptr); + + m_src->addLink(&m_link); + m_link.setOwner(m_src.get()); + + { + const bool success = m_link.read("@/child 1/child 2/My Object"); + ASSERT_TRUE(success); + ASSERT_EQ(m_link.get(), m_dst.get()); + ASSERT_EQ(m_link.getSize(), 1); + } + + { + const bool success = m_link.read("@child 2/My Object"); + ASSERT_TRUE(success); + ASSERT_EQ(m_link.get(), m_dst.get()); + ASSERT_EQ(m_link.getSize(), 1); + } + + { + const bool success = m_link.read("@child 2/My Object "); //trailing spaces + ASSERT_TRUE(success); + ASSERT_EQ(m_link.get(), m_dst.get()); + ASSERT_EQ(m_link.getSize(), 1); + } + + { + const bool success = m_link.read("@/../child 1/child 2/My Object"); //ill-formed + ASSERT_TRUE(success); + ASSERT_EQ(m_link.get(), nullptr); + ASSERT_EQ(m_link.getSize(), 1); + } +} diff --git a/Sofa/framework/Helper/CMakeLists.txt b/Sofa/framework/Helper/CMakeLists.txt index 5de5b85a719..a6b74531705 100644 --- a/Sofa/framework/Helper/CMakeLists.txt +++ b/Sofa/framework/Helper/CMakeLists.txt @@ -45,6 +45,7 @@ set(HEADER_FILES ${SRC_ROOT}/GenerateRigid.h ${SRC_ROOT}/IndexOpenMP.h ${SRC_ROOT}/LCPcalc.h + ${SRC_ROOT}/LinkParser.h ${SRC_ROOT}/MarchingCubeUtility.h ${SRC_ROOT}/MatEigen.h ${SRC_ROOT}/MemoryManager.h @@ -149,6 +150,7 @@ set(SOURCE_FILES ${SRC_ROOT}/Factory.cpp ${SRC_ROOT}/GenerateRigid.cpp ${SRC_ROOT}/LCPcalc.cpp + ${SRC_ROOT}/LinkParser.cpp ${SRC_ROOT}/MarchingCubeUtility.cpp ${SRC_ROOT}/NameDecoder.cpp ${SRC_ROOT}/OptionsGroup.cpp diff --git a/Sofa/framework/Helper/src/sofa/helper/LinkParser.cpp b/Sofa/framework/Helper/src/sofa/helper/LinkParser.cpp new file mode 100644 index 00000000000..fc9708fb007 --- /dev/null +++ b/Sofa/framework/Helper/src/sofa/helper/LinkParser.cpp @@ -0,0 +1,119 @@ +#include +#include + +namespace sofa::helper +{ +sofa::helper::LinkParser::LinkParser(std::string linkString) + : m_initialLinkString(std::move(linkString)), m_linkString(m_initialLinkString) +{ +} +bool LinkParser::hasPrefix() const { return !m_linkString.empty() && m_linkString[0] == prefix; } + +bool LinkParser::isAbsolute() const +{ + if (m_linkString.size() > 1) + { + return m_linkString[1] == separator; + } + return false; +} + +LinkParser& LinkParser::cleanLink() +{ + m_linkString = sofa::helper::removeLeadingCharacter(m_linkString, ' '); + + //replace backslash by slash + sofa::helper::replaceAll(m_linkString, "\\", std::string(1, separator)); + + //replace double slash by single slash + while (m_linkString.find("//") != std::string::npos) + { + sofa::helper::replaceAll(m_linkString, "//", std::string(1, separator)); + } + + auto decomposition = this->split(); + + if (decomposition.empty()) + return *this; + + // a "." references itself, so it can be removed + auto it = std::find(decomposition.begin(), decomposition.end(), "."); + while (it != decomposition.end()) + { + decomposition.erase(it); + it = std::find(decomposition.begin(), decomposition.end(), "."); + } + + m_linkString = this->join(decomposition.begin(), decomposition.end(), isAbsolute(), hasPrefix()); + + return *this; +} +void LinkParser::validate() +{ + cleanLink(); + + if (!hasPrefix()) + { + addError("Parsing link '" + m_linkString + "' failed: missing prefix '@'"); + } + + const auto decomposition = this->split(); + + for (const auto& element : decomposition) + { + if (!element.empty()) + { + if (element.front() == '[') + { + if (element.back() != ']') + { + addError("The element '" + element + "' from link '" + m_initialLinkString + + "' is not valid: it starts with '[' but does not end with ']'"); + } + else + { + addError("The element '" + element + "' from link '" + m_initialLinkString + + "' is not valid: index references are not supported"); + } + } + } + } +} + +std::string LinkParser::getLink() const +{ + return m_linkString; +} + +sofa::type::vector LinkParser::split() const +{ + if (m_linkString.empty()) + return {}; + + std::string withoutPrefix = m_linkString; + if (m_linkString[0] == prefix) + { + withoutPrefix = m_linkString.substr(1); + } + + auto decomposition = sofa::helper::split(withoutPrefix, separator); + std::erase_if(decomposition, [](const std::string& path) { return path.empty(); }); + + return decomposition; +} +std::vector LinkParser::getErrors() const +{ + std::vector errors; + for (const auto& error : m_errors) + { + errors.push_back(error); + } + return errors; +} + +void LinkParser::addError(std::string s) +{ + m_errors.push_back(s); +} + +} // namespace sofa::helper diff --git a/Sofa/framework/Helper/src/sofa/helper/LinkParser.h b/Sofa/framework/Helper/src/sofa/helper/LinkParser.h new file mode 100644 index 00000000000..b2b6b609e28 --- /dev/null +++ b/Sofa/framework/Helper/src/sofa/helper/LinkParser.h @@ -0,0 +1,96 @@ +/****************************************************************************** +* SOFA, Simulation Open-Framework Architecture * +* (c) 2006 INRIA, USTL, UJF, CNRS, MGH * +* * +* This program is free software; you can redistribute it and/or modify it * +* under the terms of the GNU Lesser General Public License as published by * +* the Free Software Foundation; either version 2.1 of the License, or (at * +* your option) any later version. * +* * +* This program is distributed in the hope that it will be useful, but WITHOUT * +* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * +* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License * +* for more details. * +* * +* You should have received a copy of the GNU Lesser General Public License * +* along with this program. If not, see . * +******************************************************************************* +* Authors: The SOFA Team and external contributors (see Authors.txt) * +* * +* Contact information: contact@sofa-framework.org * +******************************************************************************/ +#pragma once + +#include +#include + +#include + +#include + +namespace sofa::helper +{ + +struct SOFA_HELPER_API LinkParser +{ + static constexpr char prefix { '@' }; + static constexpr char separator { '/' }; + + + explicit LinkParser(std::string linkString); + + //pre-condition: cleaned + [[nodiscard]] bool hasPrefix() const; + + //pre-condition: cleaned + [[nodiscard]] bool isAbsolute() const; + + LinkParser& cleanLink(); + + void validate(); + + [[nodiscard]] std::string getLink() const; + + template + bool submitErrors(T* t) + { + for (const auto& error : m_errors) + { + msg_error(t) << error; + } + return m_errors.empty(); + } + + //pre-condition: cleaned + [[nodiscard]] sofa::type::vector split() const; + + [[nodiscard]] std::vector getErrors() const; + + template + std::string join(InputIt first, InputIt last, bool isAbsolute, bool withPrefix = true) + { + std::string result = sofa::helper::join(first, last, separator); + if (isAbsolute) + { + result = separator + result; + } + if (withPrefix) + { + result = prefix + result; + } + return result; + } + + +private: + + void addError(std::string s); + + std::string m_initialLinkString; + std::string m_linkString; + + + sofa::type::vector< std::string > m_errors; +}; + +} diff --git a/Sofa/framework/Helper/src/sofa/helper/StringUtils.cpp b/Sofa/framework/Helper/src/sofa/helper/StringUtils.cpp index 3d8c95304d6..d752b4c39df 100644 --- a/Sofa/framework/Helper/src/sofa/helper/StringUtils.cpp +++ b/Sofa/framework/Helper/src/sofa/helper/StringUtils.cpp @@ -184,6 +184,16 @@ std::string_view removeTrailingCharacters(std::string_view sv, const std::initia return sv.substr(0, end - sv.begin()); } +std::string_view removeLeadingCharacter(std::string_view sv, char character) +{ + auto begin = sv.begin(); + while (begin != sv.end() && *begin == character) + { + ++begin; + } + return sv.substr(begin - sv.begin()); +} + } // namespace sofa::helper diff --git a/Sofa/framework/Helper/src/sofa/helper/StringUtils.h b/Sofa/framework/Helper/src/sofa/helper/StringUtils.h index 8ee732f6bb5..366c65e6c07 100644 --- a/Sofa/framework/Helper/src/sofa/helper/StringUtils.h +++ b/Sofa/framework/Helper/src/sofa/helper/StringUtils.h @@ -109,4 +109,7 @@ SOFA_HELPER_API std::string_view removeTrailingCharacter(std::string_view sv, ch ///@brief Removes specified trailing characters from a string view. SOFA_HELPER_API std::string_view removeTrailingCharacters(std::string_view sv, std::initializer_list characters); +///@brief Removes specified leading character from a string view +SOFA_HELPER_API std::string_view removeLeadingCharacter(std::string_view sv, char character); + } // namespace sofa::helper diff --git a/Sofa/framework/Helper/test/CMakeLists.txt b/Sofa/framework/Helper/test/CMakeLists.txt index 67aa4c3fe8b..a7a2ddb1ef0 100644 --- a/Sofa/framework/Helper/test/CMakeLists.txt +++ b/Sofa/framework/Helper/test/CMakeLists.txt @@ -7,6 +7,7 @@ set(SOURCE_FILES Factory_test.cpp IotaView_test.cpp KdTree_test.cpp + LinkParser_test.cpp NameDecoder_test.cpp OptionsGroup_test.cpp SelectableItem_test.cpp diff --git a/Sofa/framework/Helper/test/LinkParser_test.cpp b/Sofa/framework/Helper/test/LinkParser_test.cpp new file mode 100644 index 00000000000..df5fd936fca --- /dev/null +++ b/Sofa/framework/Helper/test/LinkParser_test.cpp @@ -0,0 +1,97 @@ +#include +#include + +namespace sofa +{ + +TEST(LinkParser, empty) +{ + sofa::helper::LinkParser parser(""); + EXPECT_EQ(parser.getLink(), ""); +} + +TEST(LinkParser, hasPrefixTrue) +{ + sofa::helper::LinkParser parser("@dsaf"); + EXPECT_TRUE(parser.hasPrefix()); +} + +TEST(LinkParser, hasPrefixFalse) +{ + sofa::helper::LinkParser parser("dsaf"); + EXPECT_FALSE(parser.hasPrefix()); +} + +TEST(LinkParser, leadingSpace) +{ + sofa::helper::LinkParser parser(" example"); + EXPECT_EQ(parser.cleanLink().getLink(), "example"); +} + +TEST(LinkParser, separator) +{ + sofa::helper::LinkParser parser("example\\separator"); + EXPECT_EQ(parser.cleanLink().getLink(), "example/separator"); +} + +TEST(LinkParser, doubleSeparator) +{ + sofa::helper::LinkParser parser("example\\\\separator"); + EXPECT_EQ(parser.cleanLink().getLink(), "example/separator"); +} + +TEST(LinkParser, split) +{ + sofa::helper::LinkParser parser("@/root/node1/node2/component"); + const auto decomposition = parser.split(); + ASSERT_EQ(decomposition.size(), 4); + EXPECT_EQ(decomposition[0], "root"); + EXPECT_EQ(decomposition[1], "node1"); + EXPECT_EQ(decomposition[2], "node2"); + EXPECT_EQ(decomposition[3], "component"); +} + +TEST(LinkParser, splitWithSpace) +{ + sofa::helper::LinkParser parser("@/root/node 1/node 2/component "); + const auto decomposition = parser.split(); + ASSERT_EQ(decomposition.size(), 4); + EXPECT_EQ(decomposition[0], "root"); + EXPECT_EQ(decomposition[1], "node 1"); + EXPECT_EQ(decomposition[2], "node 2"); + EXPECT_EQ(decomposition[3], "component "); +} + +TEST(LinkParser, validateInvalidBrackets) +{ + sofa::helper::LinkParser parser("@/root/[invalid/component"); + parser.validate(); + const auto errors = parser.getErrors(); + ASSERT_FALSE(errors.empty()); + EXPECT_NE(errors[0].find("it starts with '['"), std::string::npos); + EXPECT_NE(errors[0].find("does not end with ']'"), std::string::npos); +} + +TEST(LinkParser, validBrackets) +{ + sofa::helper::LinkParser parser("@/root/[valid]/component"); + const auto decomposition = parser.split(); + ASSERT_EQ(decomposition.size(), 3); + EXPECT_EQ(decomposition[1], "[valid]"); +} + +TEST(LinkParser, cleanLinkMore) +{ + sofa::helper::LinkParser parser(" @root\\\\node//component"); + parser.cleanLink(); + EXPECT_EQ(parser.getLink(), "@root/node/component"); +} + +TEST(LinkParser, multipleBackslashes) +{ + sofa::helper::LinkParser parser("@root\\\\\\\\node"); + parser.cleanLink(); + EXPECT_EQ(parser.getLink(), "@root/node"); +} + +} diff --git a/Sofa/framework/Helper/test/StringUtils_test.cpp b/Sofa/framework/Helper/test/StringUtils_test.cpp index d08ed341faa..72ccd9830bc 100644 --- a/Sofa/framework/Helper/test/StringUtils_test.cpp +++ b/Sofa/framework/Helper/test/StringUtils_test.cpp @@ -91,6 +91,47 @@ TEST(removeTrailingCharactersTest, mixOfCharacters) EXPECT_EQ(result, "Hello"); } +// Test cases for removeLeadingCharacter +TEST(removeLeadingCharacterTest, emptyString) +{ + std::string_view input = ""; + constexpr char character = ' '; + std::string_view result = sofa::helper::removeLeadingCharacter(input, character); + EXPECT_EQ(result, ""); +} + +TEST(removeLeadingCharacterTest, singleLeadingCharacter) +{ + std::string_view input = " Hello"; + constexpr char character = ' '; + std::string_view result = sofa::helper::removeLeadingCharacter(input, character); + EXPECT_EQ(result, "Hello"); +} + +TEST(removeLeadingCharacterTest, multipleLeadingCharacters) +{ + std::string_view input = " Hello"; + constexpr char character = ' '; + std::string_view result = sofa::helper::removeLeadingCharacter(input, character); + EXPECT_EQ(result, "Hello"); +} + +TEST(removeLeadingCharacterTest, noLeadingCharacters) +{ + std::string_view input = "Hello"; + constexpr char character = ' '; + std::string_view result = sofa::helper::removeLeadingCharacter(input, character); + EXPECT_EQ(result, "Hello"); +} + +TEST(removeLeadingCharacterTest, leadingCharactersNotMatching) +{ + std::string_view input = "World"; + constexpr char character = '!'; + std::string_view result = sofa::helper::removeLeadingCharacter(input, character); + EXPECT_EQ(result, "World"); +} + TEST(StringUtilsTest, string_to_widestring_to_string) { std::string ascii_chars; @@ -134,4 +175,14 @@ TEST(StringUtilsTest, upcaseString) EXPECT_EQ("ABCDEF", helper::upcaseString("AbCdEf")); EXPECT_EQ("ABCDEF", helper::upcaseString("ABCDEF")); } + +TEST(StringUtilsTest, split) +{ + std::string toSplit { "@dfasfasd"}; + const auto v = sofa::helper::split(toSplit, '@'); + ASSERT_EQ(v.size(), 2); + EXPECT_EQ(v[0], ""); + EXPECT_EQ(v[1], "dfasfasd"); +} + } diff --git a/Sofa/framework/Simulation/Core/src/sofa/simulation/Node.cpp b/Sofa/framework/Simulation/Core/src/sofa/simulation/Node.cpp index b3f963731b1..ea3f95996ae 100644 --- a/Sofa/framework/Simulation/Core/src/sofa/simulation/Node.cpp +++ b/Sofa/framework/Simulation/Core/src/sofa/simulation/Node.cpp @@ -19,8 +19,10 @@ * * * Contact information: contact@sofa-framework.org * ******************************************************************************/ -#include - +#include +#include +#include +#include #include #include #include @@ -61,6 +63,7 @@ #include #include #include +#include #include #include @@ -412,216 +415,130 @@ core::objectmodel::BaseObject* Node::getObject(const std::string& objectName) co sofa::core::objectmodel::Base* Node::findLinkDestClass(const core::objectmodel::BaseClass* destType, const std::string& path, const core::objectmodel::BaseLink* link) { - std::string pathStr; - if (link) + sofa::helper::LinkParser parser(path); + parser.validate(); + + //early return if the parser has errors + if (!parser.submitErrors(this)) { - if (!link->parseString(path,&pathStr)) - return nullptr; + return nullptr; } - else + + const auto decomposition = parser.split(); + + // if the link is absolute, we need to start from the root Node + if (parser.isAbsolute()) { - if (!BaseLink::ParseString(path,&pathStr,nullptr,this)) - return nullptr; + if (auto* root = this->getRoot(); root && root != this) + { + const auto pathFromRoot = parser.join(decomposition.begin(), decomposition.end(), false, true); + return root->findLinkDestClass(destType, pathFromRoot, link); + } } - if(DEBUG_LINK) - dmsg_info() << "LINK: Looking for " << destType->className << "<" << destType->templateName << "> " << pathStr << " from Node " << getName() ; + // not an absolute link: relative to the current Node. We are searching in the children Node, + // and the components stored in the current Node - std::size_t ppos = 0; - const std::size_t psize = pathStr.size(); - if (ppos == psize || (ppos == psize-2 && pathStr[ppos] == '[' && pathStr[ppos+1] == ']')) // self-reference + // paths such as '@/', '@' or '@.': implicit searching based on the destination type + if (decomposition.empty() || decomposition.size() == 1 && decomposition[0] == ".") { - if(DEBUG_LINK) - dmsg_info() << " self-reference link." ; - - if (!link || !link->getOwnerBase()) return destType->dynamicCast(this); - return destType->dynamicCast(link->getOwnerBase()); + if (destType->hasParent(sofa::core::BaseState::GetClass())) + return destType->dynamicCast(this->getState()); + if (destType->hasParent(core::topology::BaseMeshTopology::GetClass())) + return destType->dynamicCast(this->getMeshTopologyLink()); + if (destType->hasParent(core::topology::Topology::GetClass())) + return destType->dynamicCast(this->getTopology()); + if (destType->hasParent(core::visual::Shader::GetClass())) + return destType->dynamicCast(this->getShader()); + if (destType->hasParent(core::behavior::BaseAnimationLoop::GetClass())) + return destType->dynamicCast(this->getAnimationLoop()); + if (destType->hasParent(core::behavior::OdeSolver::GetClass())) + return destType->dynamicCast(this->getOdeSolver()); + if (destType->hasParent(core::collision::Pipeline::GetClass())) + return destType->dynamicCast(this->getCollisionPipeline()); + if (destType->hasParent(core::visual::VisualLoop::GetClass())) + return destType->dynamicCast(this->getVisualLoop()); } - Node* node = this; - sofa::core::objectmodel::BaseObject* master = nullptr; - bool based = false; - if (ppos < psize && pathStr[ppos] == '[') // relative index in the list of objects - { - if (pathStr[psize-1] != ']') - { - msg_error() << "Invalid index-based path \"" << path << "\""; - return nullptr; - } - char* endptr = nullptr; - errno = 0; - long parsedIndex = std::strtol(pathStr.c_str()+ppos+1, &endptr, 10); - if (errno != 0 || endptr == pathStr.c_str()+ppos+1 || parsedIndex < INT_MIN || parsedIndex > INT_MAX) - { - msg_error() << "Invalid index in path \"" << path << "\""; - return nullptr; - } - int index = static_cast(parsedIndex); - if(DEBUG_LINK) - dmsg_info() << " index-based path to " << index ; + if (decomposition.empty()) + { + return destType->dynamicCast(this); + } + + const auto& firstElement = decomposition.front(); - ObjectReverseIterator it = object.rbegin(); - const ObjectReverseIterator itend = object.rend(); - if (link && link->getOwnerBase()) - { - // index from last - Base* b = link->getOwnerBase(); - while (it != itend && *it != b) - ++it; - } - while (it != itend && index < 0) + if (firstElement == "..") + { + //we don't know which parent of this Node it refers to. We are searching among all parents. + sofa::core::objectmodel::Base* parentSearching { nullptr }; + const auto pathFromFirstParent = parser.join(decomposition.begin() + 1, decomposition.end(), false, true); + for (auto* parent : this->getParents()) { - ++it; - ++index; + parentSearching = parent->findLinkDestClass(destType, pathFromFirstParent, link); + if (parentSearching) + { + return parentSearching; + } } - if (it == itend) - return nullptr; + return nullptr; + } - if(DEBUG_LINK) - dmsg_info() << " found " << it->get()->getTypeName() << " " << it->get()->getName() << "." ; + const auto firstElementWithoutTrailingSpaces = sofa::helper::removeTrailingCharacter(firstElement, ' '); - return destType->dynamicCast(it->get()); - } - else if (ppos < psize && pathStr[ppos] == '/') // absolute path + //searching for a component + auto* component = this->getObject(firstElement); + if (!component) { - if(DEBUG_LINK) - dmsg_info() << " absolute path" ; - BaseNode* basenode = this->getRoot(); - if (!basenode) return nullptr; - node = down_cast(basenode); - ++ppos; - based = true; - } - while(ppos < psize) + //searching for a component without trailing spaces + component = this->getObject(std::string(firstElementWithoutTrailingSpaces)); + } + + //searching for a child + auto* childFromFirstElement = this->getChild(firstElement); + if (!childFromFirstElement) { - if ((ppos+1 < psize && pathStr.substr(ppos,2) == "./") - || pathStr.substr(ppos) == ".") - { - // this must be this node - if(DEBUG_LINK) - dmsg_info() << " to current node" ; + //searching for a child without trailing spaces + childFromFirstElement = this->getChild(std::string(firstElementWithoutTrailingSpaces)); + } - ppos += 2; - based = true; - } - else if ((ppos+2 < psize && pathStr.substr(ppos,3) == "../") // relative - || pathStr.substr(ppos) == "..") - { - ppos += 3; - if (master) - { - master = master->getMaster(); - if(DEBUG_LINK) - dmsg_info() << " to master object " << master->getName() ; - } - else - { - core::objectmodel::BaseNode* firstParent = node->getFirstParent(); - if (!firstParent) return nullptr; - node = static_cast(firstParent); // TODO: explore other parents - if(DEBUG_LINK) - dmsg_info() << " to parent node " << node->getName() ; - } - based = true; - } - else if (pathStr[ppos] == '/') + if (decomposition.size() == 1) + { + if (component) { - // extra / - if(DEBUG_LINK) - dmsg_info() << " extra '/'" ; - ppos += 1; + return destType->dynamicCast(component); } - else + + if (childFromFirstElement) { - std::size_t p2pos = pathStr.find('/',ppos); - if (p2pos == std::string::npos) p2pos = psize; - std::string nameStr = pathStr.substr(ppos,p2pos-ppos); - ppos = p2pos+1; - if (master) + if (auto* destNode = destType->dynamicCast(childFromFirstElement)) { - if(DEBUG_LINK) - dmsg_info() << " to slave object " << nameStr ; - master = master->getSlave(nameStr); - if (!master) return nullptr; + return destNode; } - else - { - for (;;) - { - sofa::core::objectmodel::BaseObject* obj = node->getObject(nameStr); - Node* childPtr = node->getChild(nameStr); - if (childPtr) - { - node = childPtr; - if(DEBUG_LINK) - dmsg_info() << " to child node " << nameStr ; - break; - } - else if (obj) - { - master = obj; - if(DEBUG_LINK) - dmsg_info() << " to object " << nameStr ; - break; - } - if (based) return nullptr; - // this can still be found from an ancestor node - core::objectmodel::BaseNode* firstParent = node->getFirstParent(); - if (!firstParent) return nullptr; - node = static_cast(firstParent); // TODO: explore other parents - if(DEBUG_LINK) - dmsg_info() << " looking in ancestor node " << node->getName() ; - } - } - based = true; } } - if (master) - { - if(DEBUG_LINK) - dmsg_info() << " found " << master->getTypeName() << " " << master->getName() << "." ; - return destType->dynamicCast(master); - } - else + + // checking if the link points to a slave + if (decomposition.size() == 2 && component) { - Base* r = destType->dynamicCast(node); - if (r) + if (auto* slave = component->getSlave(decomposition[1])) { - if(DEBUG_LINK) - dmsg_info() << " found node " << node->getName() << "." ; - return r; + return destType->dynamicCast(slave); } - for (ObjectIterator it = node->object.begin(), itend = node->object.end(); it != itend; ++it) + if (auto* slaveWithoutTrailingSpaces = component->getSlave( + std::string(sofa::helper::removeTrailingCharacter(decomposition[1], ' ')))) { - sofa::core::objectmodel::BaseObject* obj = it->get(); - Base *o = destType->dynamicCast(obj); - if (!o) continue; - if(DEBUG_LINK) - dmsg_info() << " found " << obj->getTypeName() << " " << obj->getName() << "." ; - if (!r) r = o; - else return nullptr; // several objects are possible, this is an ambiguous path + return destType->dynamicCast(slaveWithoutTrailingSpaces); } - if (r) return r; - // no object found, we look in parent nodes if the searched class is one of the known standard single components (state, topology, ...) - if (destType->hasParent(sofa::core::BaseState::GetClass())) - return destType->dynamicCast(node->getState()); - else if (destType->hasParent(core::topology::BaseMeshTopology::GetClass())) - return destType->dynamicCast(node->getMeshTopologyLink()); - else if (destType->hasParent(core::topology::Topology::GetClass())) - return destType->dynamicCast(node->getTopology()); - else if (destType->hasParent(core::visual::Shader::GetClass())) - return destType->dynamicCast(node->getShader()); - else if (destType->hasParent(core::behavior::BaseAnimationLoop::GetClass())) - return destType->dynamicCast(node->getAnimationLoop()); - else if (destType->hasParent(core::behavior::OdeSolver::GetClass())) - return destType->dynamicCast(node->getOdeSolver()); - else if (destType->hasParent(core::collision::Pipeline::GetClass())) - return destType->dynamicCast(node->getCollisionPipeline()); - else if (destType->hasParent(core::visual::VisualLoop::GetClass())) - return destType->dynamicCast(node->getVisualLoop()); + } - return nullptr; + //recursive call + if (childFromFirstElement) + { + const auto pathFromChild = parser.join(decomposition.begin() + 1, decomposition.end(), false, true); + return childFromFirstElement->findLinkDestClass(destType, pathFromChild, link); } + + return nullptr; } /// Add an object. Detect the implemented interfaces and add the object to the corresponding lists. diff --git a/Sofa/framework/Simulation/simutest/parallel_scenes_test.cpp b/Sofa/framework/Simulation/simutest/parallel_scenes_test.cpp index ce7dd52a81d..fd63ced849d 100644 --- a/Sofa/framework/Simulation/simutest/parallel_scenes_test.cpp +++ b/Sofa/framework/Simulation/simutest/parallel_scenes_test.cpp @@ -153,9 +153,9 @@ class ParallelScenesTest : public virtual sofa::testing::BaseTest - - - + + +