AutoAPMS
Streamlining behaviors in ROS 2
Loading...
Searching...
No Matches
tree_document.cpp
1// Copyright 2024 Robin Müller
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15#include "auto_apms_behavior_tree_core/tree/tree_document.hpp"
16
17#include <algorithm>
18#include <filesystem>
19
20#include "ament_index_cpp/get_package_share_directory.hpp"
21#include "auto_apms_behavior_tree_core/builder.hpp"
22#include "auto_apms_behavior_tree_core/exceptions.hpp"
23#include "auto_apms_behavior_tree_core/node/node_model_type.hpp"
24#include "auto_apms_behavior_tree_core/tree/tree_resource.hpp"
25#include "auto_apms_util/container.hpp"
26#include "auto_apms_util/logging.hpp"
27#include "auto_apms_util/string.hpp"
28#include "behaviortree_cpp/xml_parsing.h"
29
31{
32
33std::vector<std::string> getAllTreeNamesImpl(const tinyxml2::XMLDocument & doc)
34{
35 std::vector<std::string> names;
36 if (const tinyxml2::XMLElement * root = doc.RootElement()) {
37 if (strcmp(root->Name(), TreeDocument::ROOT_ELEMENT_NAME) == 0) {
38 // Found root element
39 for (const tinyxml2::XMLElement * child = root->FirstChildElement(); child != nullptr;
40 child = child->NextSiblingElement()) {
41 if (strcmp(TreeDocument::TREE_ELEMENT_NAME, child->Name()) == 0) {
42 if (const char * name = child->Attribute(TreeDocument::TREE_NAME_ATTRIBUTE_NAME)) {
43 names.push_back(name);
44 } else {
45 throw exceptions::TreeDocumentError(
46 "Cannot get tree name, because required attribute '" +
47 std::string(TreeDocument::TREE_NAME_ATTRIBUTE_NAME) + "' is missing.");
48 }
49 }
50 }
51 } else if (strcmp(root->Name(), TreeDocument::TREE_ELEMENT_NAME) == 0) {
52 // Found behavior tree element as root
53 if (const char * name = root->Attribute(TreeDocument::TREE_NAME_ATTRIBUTE_NAME)) names.push_back(name);
54 }
55 }
56 return names;
57}
58
60: doc_ptr_(doc_ptr), ele_ptr_(ele_ptr)
61{
62 if (!doc_ptr) {
63 throw exceptions::TreeDocumentError("Cannot create an instance of NodeElement with doc_ptr=nullptr.");
64 }
65 if (!ele_ptr) {
66 throw exceptions::TreeDocumentError("Cannot create an instance of NodeElement with ele_ptr=nullptr.");
67 }
68 const NodeModelMap model = doc_ptr_->getNodeModel(true);
69 NodeModelMap::const_iterator it = model.find(ele_ptr_->Name());
70 std::vector<NodePortInfo> port_infos_str_vec;
71 if (it != model.end()) port_infos_str_vec = it->second.port_infos;
72
73 // There should always be a corresponding element in the nodes model. However, if the constructor is called e.g.
74 // before the document registered the respective node, that's not the case and setPorts() won't work as expected.
75 // Therefore, we must ensure that all factory methods for a NodeElement verify that the node is known to the document
76 // before creating an instance. NOTE: We must not throw, since the constructor must also succeed for the derived
77 // TreeElement class for which there is no node model and setPorts() is deleted anyways.
78 if (!port_infos_str_vec.empty()) {
79 for (const NodePortInfo & port_info : port_infos_str_vec) {
80 port_names_.push_back(port_info.port_name);
81 if (!port_info.port_default.empty()) port_default_values_[port_info.port_name] = port_info.port_default;
82 }
83
84 // Set the node's attributes according to the default values for all the ports that have a default value and have
85 // not been set previously.
86 PortValues existing_port_values = getPorts();
87 for (const auto & [name, val] : port_default_values_) {
88 if (existing_port_values.find(name) == existing_port_values.end()) {
89 ele_ptr_->SetAttribute(name.c_str(), val.c_str());
90 }
91 }
92 }
93}
94
96{
97 XMLElement * other_ele = other.ele_ptr_;
98 if (ele_ptr_ != other_ele) {
99 // Copy the other element and all its children
100 other_ele = other_ele->DeepClone(doc_ptr_)->ToElement();
101 // Insert new element in place of this
102 XMLElement * prev = ele_ptr_->PreviousSiblingElement();
103 if (prev) {
104 ele_ptr_->Parent()->InsertAfterChild(ele_ptr_->PreviousSibling(), other_ele);
105 } else {
106 ele_ptr_->Parent()->InsertFirstChild(other_ele);
107 }
108 // Delete this element
109 doc_ptr_->DeleteNode(ele_ptr_);
110 // Update the internal pointer
111 ele_ptr_ = other_ele;
112 }
113 port_names_ = other.port_names_;
114 port_default_values_ = other.port_default_values_;
115 return *this;
116}
117
118bool TreeDocument::NodeElement::operator==(const NodeElement & other) const { return ele_ptr_ == other.ele_ptr_; }
119
120bool TreeDocument::NodeElement::operator!=(const NodeElement & other) const { return !this->operator==(other); }
121
123 const std::string & name, const NodeElement * before_this)
124{
125 if (const std::set<std::string> names = doc_ptr_->getRegisteredNodeNames(true); names.find(name) == names.end()) {
126 throw exceptions::TreeDocumentError(
127 "Cannot insert unknown node <" + name +
128 ">. Before inserting a new node, the associated document must register the corresponding behavior tree "
129 "node. Consider using a signature of insertNode() that does this automatically.");
130 }
131 XMLElement * ele = doc_ptr_->NewElement(name.c_str());
132 return insertBeforeImpl(before_this, ele);
133}
134
136 const std::string & name, const NodeRegistrationOptions & registration_options, const NodeElement * before_this)
137{
138 // Check if the node name is known. If the user tries to register a node with the same name as one of the native
139 // nodes, we want registerNodes to throw since those names are reserved (Therefore we set include_native to false).
140 if (const std::set<std::string> names = doc_ptr_->getRegisteredNodeNames(false); names.find(name) == names.end()) {
141 doc_ptr_->registerNodes(NodeManifest({{name, registration_options}}));
142 }
143 return insertNode(name, before_this);
144}
145
147 const std::string & tree_name, const NodeElement * before_this)
148{
149 if (!doc_ptr_->hasTreeName(tree_name)) {
150 throw exceptions::TreeDocumentError(
151 "Cannot insert subtree node for tree '" + tree_name + "' because no tree with that name exists.");
152 }
153 NodeElement ele = insertNode(SUBTREE_ELEMENT_NAME, before_this);
154 ele.ele_ptr_->SetAttribute(TREE_NAME_ATTRIBUTE_NAME, tree_name.c_str());
155 return model::SubTree(ele.doc_ptr_, ele.ele_ptr_);
156}
157
159{
160 const std::string tree_name = tree.getName();
161 if (doc_ptr_->hasTreeName(tree_name)) {
162 // A tree with this name already exists in the document.
163 // If the provided tree element is from THIS document, we can compare directly using pointer equality.
164 // If it's from an external document, we compare the XML content to detect conflicting trees.
165 const TreeElement existing_tree = doc_ptr_->getTree(tree_name);
166 if (tree.doc_ptr_ == doc_ptr_) {
167 // Tree element is from this document - they must be the same tree element
168 if (existing_tree != tree) {
169 throw exceptions::TreeDocumentError(
170 "Cannot insert subtree node using the provided tree element, because another tree with name '" + tree_name +
171 "' already exists.");
172 }
173 } else {
174 // Tree element is from an external document - compare XML content
175 const std::string existing_xml = existing_tree.writeToString();
176 const std::string incoming_xml = tree.writeToString();
177 if (existing_xml != incoming_xml) {
178 throw exceptions::TreeDocumentError(
179 "Cannot insert subtree node using the provided tree element, because another tree with name '" + tree_name +
180 "' already exists with different content.");
181 }
182 }
183 // If we reach here, the trees are equivalent - proceed to insert the subtree node
184 } else {
185 // Add the tree provided as argument to the document
186 doc_ptr_->mergeTree(tree, false);
187 }
188 return insertSubTreeNode(tree_name, before_this);
189}
190
192 const TreeElement & tree, const NodeElement * before_this)
193{
194 const XMLElement * root_child = tree.ele_ptr_->FirstChildElement();
195 if (!root_child) {
196 throw exceptions::TreeDocumentError(
197 "Cannot insert tree element '" + tree.getFullyQualifiedName() + "' because it has no child nodes.");
198 }
199 if (root_child->NextSibling()) {
200 throw exceptions::TreeDocumentError(
201 "Cannot insert tree element '" + tree.getFullyQualifiedName() + "' because it has more than one child node.");
202 }
203 doc_ptr_->registerNodes(tree.getRequiredNodeManifest());
204 return insertTreeFromDocument(*tree.doc_ptr_, tree.getName(), before_this);
205}
206
208 const TreeDocument & doc, const std::string & tree_name, const NodeElement * before_this)
209{
210 const std::vector<std::string> other_tree_names = doc.getAllTreeNames();
211 if (other_tree_names.empty()) {
212 throw exceptions::TreeDocumentError(
213 "Cannot insert tree '" + tree_name + "' because document has no <" + TREE_ELEMENT_NAME + "> elements.");
214 }
215 if (!auto_apms_util::contains(other_tree_names, tree_name)) {
216 throw exceptions::TreeDocumentError(
217 "Cannot insert tree '" + tree_name + "' because document doesn't specify a tree with that name.");
218 }
219
220 // List including the target tree name and the names of its dependencies (Trees required by SubTree nodes)
221 std::set<std::string> required_tree_names = {tree_name};
222
223 // Find all subtree nodes and push back the associated tree to required_tree_names.
224 // NOTE: All tree elements within a TreeDocument instance always have exactly one child, so we don't have to verify
225 // that here again
226 TreeDocument temp_doc(doc_ptr_->format_version_, doc_ptr_->tree_node_loader_ptr_); // Temporary working document
227 doc.DeepCopy(&temp_doc);
228 const TreeElement other_tree(&temp_doc, temp_doc.getXMLElementForTreeWithName(tree_name));
229 ConstDeepApplyCallback collect_dependency_tree_names;
230 collect_dependency_tree_names = [&required_tree_names, &collect_dependency_tree_names](const NodeElement & ele) {
231 if (ele.getRegistrationName() == SUBTREE_ELEMENT_NAME) {
232 if (const char * name = ele.ele_ptr_->Attribute(TREE_NAME_ATTRIBUTE_NAME)) {
233 // Add the tree name to the list
234 required_tree_names.insert(name);
235
236 // Search for more tree dependencies under the tree pointed by this subtree node
237 ele.doc_ptr_->getTree(name).deepApplyConst(collect_dependency_tree_names);
238 } else {
239 throw exceptions::TreeDocumentError("Subtree element has no name attribute.");
240 };
241 }
242 // The return value doesn't matter here
243 return false;
244 };
245 other_tree.deepApplyConst(collect_dependency_tree_names);
246
247 // Verify that all child nodes of the trees listed in required_tree_names are known to the builder
248 ConstDeepApplyCallback apply = [available_names = doc_ptr_->getRegisteredNodeNames(true)](const NodeElement & ele) {
249 return available_names.find(ele.getRegistrationName()) == available_names.end();
250 };
251 for (const std::string & name : required_tree_names) {
252 const NodeElement ele(&temp_doc, temp_doc.getXMLElementForTreeWithName(name)->FirstChildElement());
253 if (const std::vector<NodeElement> found = ele.deepApplyConst(apply); !found.empty()) {
254 std::vector<std::string> names;
255 for (const NodeElement & ele : found) names.push_back(ele.getFullyQualifiedName());
256 throw exceptions::TreeDocumentError(
257 "Cannot insert tree '" + tree_name + "' because the following nodes found in tree '" + name +
258 "' are unknown to the builder:\n\t- " + auto_apms_util::join(names, "\n\t- "));
259 }
260 }
261
262 // Remove the data from each tree that the target tree doesn't directly depend on and merge the working document. This
263 // approach ensures, that the verification logic inside merge() is applied to the working document.
264 required_tree_names.erase(tree_name); // Don't add the target tree twice
265 for (const std::string & name : other_tree_names) {
266 if (required_tree_names.find(name) == required_tree_names.end()) {
267 // Remove if not required
268 temp_doc.removeTree(name);
269 }
270 }
271 doc_ptr_->mergeTreeDocument(static_cast<const XMLDocument &>(temp_doc), false);
272
273 // Clone and insert the target tree
274 return insertBeforeImpl(
275 before_this, doc.getXMLElementForTreeWithName(tree_name)->FirstChildElement()->DeepClone(doc_ptr_)->ToElement());
276}
277
279 const TreeDocument & doc, const NodeElement * before_this)
280{
281 return insertTreeFromDocument(doc, doc.getRootTreeName(), before_this);
282}
283
285 const std::string & tree_str, const std::string & tree_name, const NodeElement * before_this)
286{
287 TreeDocument insert_doc(doc_ptr_->format_version_, doc_ptr_->tree_node_loader_ptr_);
288 insert_doc.mergeString(tree_str);
289 return insertTreeFromDocument(insert_doc, tree_name, before_this);
290}
291
293 const std::string & tree_str, const NodeElement * before_this)
294{
295 TreeDocument insert_doc(doc_ptr_->format_version_, doc_ptr_->tree_node_loader_ptr_);
296 insert_doc.mergeString(tree_str);
297 return insertTreeFromDocument(insert_doc, before_this);
298}
299
301 const std::string & path, const std::string & tree_name, const NodeElement * before_this)
302{
303 TreeDocument insert_doc(doc_ptr_->format_version_, doc_ptr_->tree_node_loader_ptr_);
304 insert_doc.mergeFile(path);
305 return insertTreeFromDocument(insert_doc, tree_name, before_this);
306}
307
309 const std::string & path, const NodeElement * before_this)
310{
311 TreeDocument insert_doc(doc_ptr_->format_version_, doc_ptr_->tree_node_loader_ptr_);
312 insert_doc.mergeFile(path);
313 return insertTreeFromDocument(insert_doc, before_this);
314}
315
317 const TreeResource & resource, const std::string & tree_name, const NodeElement * before_this)
318{
319 TreeDocument insert_doc(doc_ptr_->format_version_, doc_ptr_->tree_node_loader_ptr_);
320 insert_doc.mergeFile(resource.build_request_file_path_);
321
322 // We register all associated node plugins beforehand, so that the user doesn't have to do that manually. This means,
323 // that also potentially unused nodes are available and registered with the factory. This seems unnecessary, but it's
324 // very convenient and the performance probably doesn't suffer too much.
325 doc_ptr_->registerNodes(resource.getNodeManifest(), false);
326
327 return insertTreeFromDocument(insert_doc, tree_name, before_this);
328}
329
331 const TreeResource & resource, const NodeElement * before_this)
332{
333 return insertTreeFromResource(resource, resource.getRootTreeName(), before_this);
334}
335
336bool TreeDocument::NodeElement::hasChildren() const { return ele_ptr_->FirstChild() == nullptr ? false : true; }
337
339 const std::string & registration_name, const std::string & instance_name, bool deep_search) const
340{
341 if (registration_name.empty() && instance_name.empty()) return NodeElement(doc_ptr_, ele_ptr_->FirstChildElement());
342
343 ConstDeepApplyCallback apply = [&registration_name, &instance_name](const NodeElement & ele) {
344 if (registration_name.empty()) return ele.getName() == instance_name;
345 if (instance_name.empty()) return ele.getRegistrationName() == registration_name;
346 return ele.getRegistrationName() == registration_name && ele.getName() == instance_name;
347 };
348
349 if (deep_search) {
350 if (const std::vector<NodeElement> found = deepApplyConst(apply); !found.empty()) return found[0];
351 } else {
352 for (auto child : *this) {
353 if (apply(child)) return child;
354 }
355 }
356
357 // Cannot find node in children of this
358 throw exceptions::TreeDocumentError(
359 "Cannot find a child node that matches the search arguments (registration_name: '" + registration_name +
360 "' - instance_name: '" + instance_name + "') in parent element '" + getFullyQualifiedName() + "'.");
361}
362
364 const std::string & registration_name, const std::string & instance_name, bool deep_search)
365{
366 XMLElement * found = getFirstNode(registration_name, instance_name, deep_search).ele_ptr_;
367 found->Parent()->DeleteChild(found);
368 return *this;
369}
370
372{
373 ele_ptr_->DeleteChildren();
374 return *this;
375}
376
377const std::vector<std::string> & TreeDocument::NodeElement::getPortNames() const { return port_names_; }
378
380{
381 PortValues values;
382 for (const tinyxml2::XMLAttribute * attr = ele_ptr_->FirstAttribute(); attr != nullptr; attr = attr->Next()) {
383 if (const std::string attr_name = attr->Name(); auto_apms_util::contains(port_names_, attr_name)) {
384 values[attr_name] = attr->Value();
385 }
386 }
387 return values;
388}
389
391{
392 // Verify port_values
393 std::vector<std::string> unknown_keys;
394 for (const auto & [key, _] : port_values) {
395 if (!auto_apms_util::contains(port_names_, key)) unknown_keys.push_back(key);
396 }
397 if (!unknown_keys.empty()) {
398 throw exceptions::TreeDocumentError(
399 "Cannot set ports. According to the node model, the following ports are not implemented by '" +
400 std::string(ele_ptr_->Name()) + "': [ " + auto_apms_util::join(unknown_keys, ", ") + " ].");
401 }
402
403 // Populate attributes according to the content of port_values
404 for (const auto & [key, val] : port_values) {
405 ele_ptr_->SetAttribute(key.c_str(), val.c_str());
406 }
407 return *this;
408}
409
411{
412 for (const std::string & name : port_names_) {
413 PortValues::const_iterator it = port_default_values_.find(name);
414 if (it != port_default_values_.end()) {
415 ele_ptr_->SetAttribute(it->first.c_str(), it->second.c_str());
416 } else {
417 ele_ptr_->DeleteAttribute(name.c_str());
418 }
419 }
420 return *this;
421}
422
424{
425 ele_ptr_->SetAttribute(BT::toStr(type).c_str(), script.str().c_str());
426 return *this;
427}
428
430{
431 ele_ptr_->SetAttribute(BT::toStr(type).c_str(), script.str().c_str());
432 return *this;
433}
434
436{
437 ele_ptr_->SetAttribute(NODE_INSTANCE_NAME_ATTRIBUTE_NAME, instance_name.c_str());
438 return *this;
439}
440
441std::string TreeDocument::NodeElement::getRegistrationName() const { return ele_ptr_->Name(); }
442
444{
445 if (const char * name = ele_ptr_->Attribute(NODE_INSTANCE_NAME_ATTRIBUTE_NAME)) return name;
446 return getRegistrationName();
447}
448
450{
451 std::string registration_name = getRegistrationName();
452 std::string instance_name = getName();
453 if (registration_name == instance_name) return registration_name;
454 return instance_name + " (" + registration_name + ")";
455}
456
458
459TreeDocument::XMLElement * TreeDocument::NodeElement::getXMLElement() { return ele_ptr_; }
460
461const std::vector<TreeDocument::NodeElement> TreeDocument::NodeElement::deepApplyConst(
462 ConstDeepApplyCallback apply_callback) const
463{
464 std::vector<NodeElement> found;
465 deepApplyImpl(*this, apply_callback, found);
466 return found;
467}
468
469std::vector<TreeDocument::NodeElement> TreeDocument::NodeElement::deepApply(DeepApplyCallback apply_callback)
470{
471 std::vector<NodeElement> found;
472 deepApplyImpl(*this, apply_callback, found);
473 return found;
474}
475
476TreeDocument::NodeElement TreeDocument::NodeElement::insertBeforeImpl(
477 const NodeElement * before_this, XMLElement * add_this)
478{
479 if (before_this) {
480 XMLElement * prev = nullptr;
481 XMLElement * curr = ele_ptr_->FirstChildElement();
482
483 // Traverse through siblings of first child to find the before_this node.
484 // If there are no siblings, add the first child to this;
485 bool found = false;
486 while (curr) {
487 if (curr == before_this->ele_ptr_) {
488 found = true;
489 break;
490 }
491 prev = curr;
492 curr = curr->NextSiblingElement();
493 }
494 if (prev) {
495 if (found) {
496 ele_ptr_->InsertAfterChild(prev, add_this);
497 } else {
498 throw exceptions::TreeDocumentError(
499 "NodeElement before_this (" + before_this->getFullyQualifiedName() + ") is not a child of " +
500 getFullyQualifiedName() + ".");
501 }
502 } else {
503 ele_ptr_->InsertFirstChild(add_this);
504 }
505 } else {
506 ele_ptr_->InsertEndChild(add_this);
507 }
508 return NodeElement(doc_ptr_, add_this);
509}
510
511void TreeDocument::NodeElement::deepApplyImpl(
512 const NodeElement & parent, ConstDeepApplyCallback apply_callback, std::vector<NodeElement> & vec)
513{
514 for (XMLElement * child = parent.ele_ptr_->FirstChildElement(); child != nullptr;
515 child = child->NextSiblingElement()) {
516 const NodeElement child_ele(parent.doc_ptr_, child);
517
518 // Apply on current child element
519 if (apply_callback(child_ele)) vec.push_back(child_ele);
520
521 // Search children of child before evaluating the siblings
522 deepApplyImpl(child_ele, apply_callback, vec);
523 }
524}
525
526void TreeDocument::NodeElement::deepApplyImpl(
527 NodeElement & parent, DeepApplyCallback apply_callback, std::vector<NodeElement> & vec)
528{
529 for (XMLElement * child = parent.ele_ptr_->FirstChildElement(); child != nullptr;
530 child = child->NextSiblingElement()) {
531 NodeElement child_ele(parent.doc_ptr_, child);
532
533 // Apply on current child element
534 if (apply_callback(child_ele)) vec.push_back(child_ele);
535
536 // Search children of child before evaluating the siblings
537 deepApplyImpl(child_ele, apply_callback, vec);
538 }
539}
540
541TreeDocument::NodeElement::ChildIterator::ChildIterator() : doc_ptr_(nullptr), current_(nullptr) {}
542
543TreeDocument::NodeElement::ChildIterator::ChildIterator(TreeDocument * doc_ptr, tinyxml2::XMLElement * current)
544: doc_ptr_(doc_ptr), current_(current)
545{
546}
547
548TreeDocument::NodeElement::ChildIterator::value_type TreeDocument::NodeElement::ChildIterator::operator*() const
549{
550 return NodeElement(doc_ptr_, current_);
551}
552
554{
555 if (current_) current_ = current_->NextSiblingElement();
556 return *this;
557}
558
565
566bool TreeDocument::NodeElement::ChildIterator::operator==(const ChildIterator & other) const
567{
568 return current_ == other.current_;
569}
570
571bool TreeDocument::NodeElement::ChildIterator::operator!=(const ChildIterator & other) const
572{
573 return current_ != other.current_;
574}
575
580
582
583TreeDocument::TreeElement::TreeElement(TreeDocument * doc_ptr, XMLElement * ele_ptr) : NodeElement(doc_ptr, ele_ptr)
584{
585 if (!ele_ptr->Attribute(TREE_NAME_ATTRIBUTE_NAME)) {
586 throw exceptions::TreeDocumentError(
587 "Cannot create tree element without a '" + std::string(TREE_NAME_ATTRIBUTE_NAME) + "' attribute.");
588 }
589}
590
592{
593 const std::string other_tree_name = other.getName();
594 // For a tree replacement to be allowed, the other tree name must be the same or at least not already taken by another
595 // tree inside this document.
596 if (getName() != other_tree_name && doc_ptr_->hasTreeName(other.getName())) {
597 throw exceptions::TreeDocumentError(
598 "Cannot copy tree '" + other.getName() + "' because another tree with this name already exists.");
599 }
601 return *this;
602}
603
605{
606 ele_ptr_->SetAttribute(TREE_NAME_ATTRIBUTE_NAME, tree_name.c_str());
607 return *this;
608}
609
611{
612 if (const char * name = ele_ptr_->Attribute(TREE_NAME_ATTRIBUTE_NAME)) return name;
613 return "unknown";
614}
615
617{
618 doc_ptr_->setRootTreeName(getName());
619 return *this;
620}
621
623{
624 NodeManifest m;
625 deepApplyConst([this, &m](const NodeElement & node) {
626 const std::string name = node.getRegistrationName();
627 const bool is_native_node = doc_ptr_->native_node_names_.find(name) != doc_ptr_->native_node_names_.end();
628 if (!is_native_node && !m.contains(name)) {
629 if (!doc_ptr_->registered_nodes_manifest_.contains(name)) {
630 throw exceptions::NodeManifestError(
631 "Cannot assemble the required node manifest for tree '" + getName() +
632 "' since there are no registration options for node '" + name + "'.");
633 }
634 m.add(name, doc_ptr_->registered_nodes_manifest_[name]);
635 }
636 return false;
637 });
638 return m;
639}
640
642{
643 TreeDocument doc(doc_ptr_->format_version_, doc_ptr_->tree_node_loader_ptr_);
644 return doc.mergeTree(*this, true).verify();
645}
646
648{
649 XMLDocument tree_doc;
650 tree_doc.InsertEndChild(ele_ptr_->DeepClone(&tree_doc));
651 tinyxml2::XMLPrinter printer;
652 tree_doc.Print(&printer);
653 return printer.CStr();
654}
655
657 const std::string & registration_name, const std::string & instance_name, bool deep_search)
658{
659 NodeElement::removeFirstChild(registration_name, instance_name, deep_search);
660 return *this;
661}
662
668
669TreeDocument::TreeDocument(const std::string & format_version, NodeRegistrationLoader::SharedPtr tree_node_loader)
670// It's important to initialize XMLDocument using PRESERVE_WHITESPACE, since encoded port data (like
671// NodeRegistrationOptions) may be sensitive to changes in the whitespaces (think of the YAML format).
672: XMLDocument(true, tinyxml2::PRESERVE_WHITESPACE),
673 all_node_classes_package_map_(auto_apms_behavior_tree::core::NodeRegistrationLoader().getClassPackageMap()),
674 native_node_names_(BT::BehaviorTreeFactory().builtinNodes()),
675 format_version_(format_version),
676 tree_node_loader_ptr_(tree_node_loader),
677 registered_nodes_manifest_(),
678 factory_(),
679 logger_(rclcpp::get_logger(LOGGER_NAME))
680{
681 reset();
682}
683
684TreeDocument & TreeDocument::mergeTreeDocument(const XMLDocument & other, bool adopt_root_tree)
685{
686 std::set<std::string> include_stack;
687 return mergeTreeDocumentImpl(other, adopt_root_tree, include_stack);
688}
689
690TreeDocument & TreeDocument::mergeTreeDocumentImpl(
691 const XMLDocument & other, bool adopt_root_tree, std::set<std::string> & include_stack)
692{
693 const XMLElement * other_root = other.RootElement();
694 if (!other_root) {
695 throw exceptions::TreeDocumentError("Cannot merge tree documents: other_root is nullptr.");
696 };
697
698 auto verify_tree_structure = [](const XMLElement * tree_ele) {
699 const char * tree_id = tree_ele->Attribute(TREE_NAME_ATTRIBUTE_NAME);
700 if (!tree_id) {
701 throw exceptions::TreeDocumentError(
702 "Cannot merge tree document: Found a <" + std::string(TREE_ELEMENT_NAME) +
703 "> element that doesn't specify the required attribute '" + TREE_NAME_ATTRIBUTE_NAME + "'.");
704 }
705 const XMLElement * tree_root_child = tree_ele->FirstChildElement();
706 if (!tree_root_child) {
707 throw exceptions::TreeDocumentError(
708 "Cannot merge tree document: Tree '" + std::string(tree_id) + "' has no child nodes.");
709 }
710 if (tree_root_child->NextSibling()) {
711 throw exceptions::TreeDocumentError(
712 "Cannot merge tree document: Tree '" + std::string(tree_id) + "' has more than one child node.");
713 }
714 };
715
716 // Helper lambda to check for duplicate tree names and throw with a descriptive message
717 auto check_duplicates = [this](const std::vector<std::string> & new_tree_names, const std::string & source) {
718 const auto common = auto_apms_util::getCommonElements(getAllTreeNames(), new_tree_names);
719 if (!common.empty()) {
720 throw exceptions::TreeDocumentError(
721 "Cannot merge tree document: The following trees from " + source + " are already defined: [ " +
722 auto_apms_util::join(common, ", ") + " ].");
723 }
724 };
725
726 // Get tree names from the other document (used for duplicate check and adopt_root_tree logic)
727 const std::vector<std::string> other_tree_names = getAllTreeNamesImpl(other);
728
729 if (strcmp(other_root->Name(), ROOT_ELEMENT_NAME) == 0) {
730 // Verify format
731 if (const char * ver = other_root->Attribute(BTCPP_FORMAT_ATTRIBUTE_NAME)) {
732 if (std::string(ver) != format_version_) {
733 throw exceptions::TreeDocumentError(
734 "Cannot merge tree document: Format of other document (" + std::string(BTCPP_FORMAT_ATTRIBUTE_NAME) + ": " +
735 ver + ") is not compatible with this document (" + std::string(BTCPP_FORMAT_ATTRIBUTE_NAME) + ": " +
736 format_version_ + ").");
737 }
738 } else {
739 throw exceptions::TreeDocumentError(
740 "Cannot merge tree document: Root element of other document doesn't have required attribute '" +
741 std::string(BTCPP_FORMAT_ATTRIBUTE_NAME) + "'.");
742 }
743
744 // Iterate over all include elements and store the associated content to a temporary buffer document
745 TreeDocument include_doc(format_version_, tree_node_loader_ptr_);
746 for (const XMLElement * include_ele = other_root->FirstChildElement(INCLUDE_ELEMENT_NAME); include_ele != nullptr;
747 include_ele = include_ele->NextSiblingElement(INCLUDE_ELEMENT_NAME)) {
748 if (const char * path = include_ele->Attribute(INCLUDE_PATH_ATTRIBUTE_NAME)) {
749 if (std::string(path).empty()) {
750 throw exceptions::TreeDocumentError(
751 "Cannot merge tree document: Found an <" + std::string(INCLUDE_ELEMENT_NAME) +
752 "> element that specifies an empty path in attribute '" + INCLUDE_PATH_ATTRIBUTE_NAME + "'.");
753 }
754 std::string absolute_path = path;
755 if (const char * ros_pkg = include_ele->Attribute(INCLUDE_ROS_PKG_ATTRIBUTE_NAME)) {
756 if (std::string(ros_pkg).empty()) {
757 throw exceptions::TreeDocumentError(
758 "Cannot merge tree document: Found an <" + std::string(INCLUDE_ELEMENT_NAME) +
759 "> element that specifies an empty ROS package name in attribute '" + INCLUDE_ROS_PKG_ATTRIBUTE_NAME +
760 "'.");
761 }
762 if (std::filesystem::path(path).is_absolute()) {
763 throw exceptions::TreeDocumentError(
764 "Cannot merge tree document: Found an <" + std::string(INCLUDE_ELEMENT_NAME) +
765 "> element that specifies an absolute path '" + std::string(path) + "' in attribute '" +
766 INCLUDE_PATH_ATTRIBUTE_NAME + "' together with a ROS package name in attribute '" +
767 INCLUDE_ROS_PKG_ATTRIBUTE_NAME + "'. Please remove the attribute " + INCLUDE_ROS_PKG_ATTRIBUTE_NAME +
768 " or use a relative path if you want to refer to a location "
769 "relative to the package's share directory.");
770 }
771 try {
772 absolute_path =
773 (std::filesystem::path(ament_index_cpp::get_package_share_directory(ros_pkg)) / path).string();
774 } catch (const ament_index_cpp::PackageNotFoundError & e) {
775 throw exceptions::TreeDocumentError(
776 "Cannot merge tree document: Found an <" + std::string(INCLUDE_ELEMENT_NAME) +
777 "> element that specifies a non-existing ROS package '" + std::string(ros_pkg) + "' in attribute '" +
778 INCLUDE_ROS_PKG_ATTRIBUTE_NAME + "'.");
779 }
780 }
781
782 // Recursively merge the included file to the buffer document, passing the include stack for circular detection
783 try {
784 include_doc.mergeFileImpl(absolute_path, false, include_stack);
785 } catch (const std::exception & e) {
786 throw exceptions::TreeDocumentError(
787 "Cannot merge tree document: Failed to include file '" + absolute_path + "': " + e.what());
788 }
789 } else {
790 throw exceptions::TreeDocumentError(
791 "Cannot merge tree document: Found an <" + std::string(INCLUDE_ELEMENT_NAME) +
792 "> element that doesn't specify the required attribute '" + INCLUDE_PATH_ATTRIBUTE_NAME + "'.");
793 }
794 }
795
796 // Check for duplicates from included files before inserting
797 check_duplicates(include_doc.getAllTreeNames(), "included files");
798
799 // Iterate over all the tree elements of the include buffer and include them in this document
800 for (const XMLElement * child = include_doc.RootElement()->FirstChildElement(TREE_ELEMENT_NAME); child != nullptr;
801 child = child->NextSiblingElement(TREE_ELEMENT_NAME)) {
802 // We do not need to verify the tree structure here, since it has already been verified during the recursive
803 // mergeFile() calls by the for loop below
804
805 // If tree element is valid, append to this document
806 RootElement()->InsertEndChild(child->DeepClone(this));
807 }
808
809 // Check for duplicates from other document's direct trees before inserting
810 check_duplicates(other_tree_names, "the merged document");
811
812 // Iterate over all the tree elements of other root and verify them
813 for (const XMLElement * child = other_root->FirstChildElement(TREE_ELEMENT_NAME); child != nullptr;
814 child = child->NextSiblingElement(TREE_ELEMENT_NAME)) {
815 verify_tree_structure(child);
816
817 // If tree element is valid, append to this document
818 RootElement()->InsertEndChild(child->DeepClone(this));
819 }
820
821 if (adopt_root_tree) {
822 // After (!) all tree elements have been inserted, adopt the name specified in the corresponding attribute of the
823 // root element
824 if (const char * name = other_root->Attribute(ROOT_TREE_ATTRIBUTE_NAME)) setRootTreeName(name);
825 }
826 } else if (strcmp(other_root->Name(), TREE_ELEMENT_NAME) == 0) {
827 // Allow a single behavior tree without <root> element.
828 // We assume that the the format complies with the version configured at construction time
829 verify_tree_structure(other_root);
830
831 // Check for duplicates before inserting
832 check_duplicates(other_tree_names, "the merged document");
833
834 // If tree element is valid, append to this document
835 RootElement()->InsertEndChild(other_root->DeepClone(this));
836 } else {
837 throw exceptions::TreeDocumentError(
838 "Cannot merge tree document: Root element of other document must either be <" + std::string(ROOT_ELEMENT_NAME) +
839 "> or <" + TREE_ELEMENT_NAME + ">.");
840 }
841
842 if (adopt_root_tree && other_tree_names.size() == 1) {
843 // If there is only one tree element, we change the root tree to the respective tree after (!) all tree elements
844 // have been inserted
845 setRootTreeName(other_tree_names[0]);
846 }
847
848 return *this;
849}
850
851TreeDocument & TreeDocument::mergeTreeDocument(const TreeDocument & other, bool adopt_root_tree)
852{
854 std::set<std::string> include_stack;
855 return mergeTreeDocumentImpl(static_cast<const XMLDocument &>(other), adopt_root_tree, include_stack);
856}
857
858TreeDocument & TreeDocument::mergeString(const std::string & tree_str, bool adopt_root_tree)
859{
860 XMLDocument other_doc;
861 if (other_doc.Parse(tree_str.c_str()) != tinyxml2::XMLError::XML_SUCCESS) {
862 throw exceptions::TreeDocumentError("Cannot merge tree document from string: " + std::string(other_doc.ErrorStr()));
863 }
864 std::set<std::string> include_stack;
865 return mergeTreeDocumentImpl(other_doc, adopt_root_tree, include_stack);
866}
867
868TreeDocument & TreeDocument::mergeFile(const std::string & path, bool adopt_root_tree)
869{
870 std::set<std::string> include_stack;
871 return mergeFileImpl(path, adopt_root_tree, include_stack);
872}
873
874TreeDocument & TreeDocument::mergeFileImpl(
875 const std::string & path, bool adopt_root_tree, std::set<std::string> & include_stack)
876{
877 // Get canonical path for consistent circular include detection
878 std::string canonical_path;
879 try {
880 canonical_path = std::filesystem::canonical(path).string();
881 } catch (const std::filesystem::filesystem_error &) {
882 // If canonical fails (e.g., file doesn't exist), use the original path
883 // The error will be caught later when loading the file
884 canonical_path = path;
885 }
886
887 // Check for circular includes
888 if (include_stack.count(canonical_path) > 0) {
889 throw exceptions::TreeDocumentError(
890 "Cannot merge tree document: Circular include detected for file '" + path + "'.");
891 }
892
893 // Add to include stack
894 include_stack.insert(canonical_path);
895
896 XMLDocument other_doc;
897 if (other_doc.LoadFile(path.c_str()) != tinyxml2::XMLError::XML_SUCCESS) {
898 throw exceptions::TreeDocumentError("Cannot create tree document from file " + path + ": " + other_doc.ErrorStr());
899 }
900
901 mergeTreeDocumentImpl(other_doc, adopt_root_tree, include_stack);
902
903 // Remove from include stack (for recursive calls that continue after this one)
904 include_stack.erase(canonical_path);
905 return *this;
906}
907
908TreeDocument & TreeDocument::mergeResource(const TreeResource & resource, bool adopt_root_tree)
909{
910 registerNodes(resource.getNodeManifest(), false);
911 return mergeFile(resource.build_request_file_path_, adopt_root_tree);
912}
913
914TreeDocument & TreeDocument::mergeTree(const TreeElement & tree, bool make_root_tree)
915{
916 XMLDocument tree_doc;
917 tree_doc.InsertEndChild(tree.ele_ptr_->DeepClone(&tree_doc));
919 mergeTreeDocument(tree_doc, make_root_tree);
920 return *this;
921}
922
924{
925 if (tree_name.empty()) {
926 throw exceptions::TreeDocumentError("Cannot create a new tree with an empty name");
927 }
928 if (hasTreeName(tree_name)) {
929 throw exceptions::TreeDocumentError(
930 "Cannot create a new tree with name '" + tree_name + "' because it already exists.");
931 }
932 TreeDocument::XMLElement * new_ele = RootElement()->InsertNewChildElement(TreeDocument::TREE_ELEMENT_NAME);
933 new_ele->SetAttribute(TreeDocument::TREE_NAME_ATTRIBUTE_NAME, tree_name.c_str());
934 return TreeElement(this, new_ele);
935}
936
938{
939 return mergeTree(other_tree).getTree(other_tree.getName());
940}
941
943{
944 std::string name(tree_name);
945 if (name.empty()) {
946 if (other.hasRootTreeName()) {
947 name = other.getRootTreeName();
948 } else if (const std::vector<std::string> names = other.getAllTreeNames(); names.size() == 1) {
949 name = names[0];
950 } else {
951 throw exceptions::TreeDocumentError(
952 "Failed to create new tree element from another document because argument tree_name was omitted and it was not "
953 "possible to determine the root tree automatically.");
954 }
955 }
956 TreeElement tree_ele = newTree(name);
957 tree_ele.insertTreeFromDocument(other, name);
958 return tree_ele;
959}
960
961TreeDocument::TreeElement TreeDocument::newTreeFromString(const std::string & tree_str, const std::string & tree_name)
962{
963 TreeDocument new_doc(format_version_, tree_node_loader_ptr_);
964 new_doc.mergeString(tree_str, true);
965 return newTreeFromDocument(new_doc, tree_name);
966}
967
968TreeDocument::TreeElement TreeDocument::newTreeFromFile(const std::string & path, const std::string & tree_name)
969{
970 TreeDocument new_doc(format_version_, tree_node_loader_ptr_);
971 new_doc.mergeFile(path, true);
972 return newTreeFromDocument(new_doc, tree_name);
973}
974
976 const TreeResource & resource, const std::string & tree_name)
977{
978 TreeDocument new_doc(format_version_, tree_node_loader_ptr_);
979 new_doc.mergeFile(resource.build_request_file_path_);
980 if (resource.hasRootTreeName()) new_doc.setRootTreeName(resource.getRootTreeName());
981 registerNodes(resource.getNodeManifest(), false);
982 return newTreeFromDocument(new_doc, tree_name);
983}
984
985bool TreeDocument::hasTreeName(const std::string & tree_name) const
986{
987 if (auto_apms_util::contains(getAllTreeNames(), tree_name)) return true;
988 return false;
989}
990
992{
993 return TreeElement(this, getXMLElementForTreeWithName(tree_name));
994}
995
996TreeDocument & TreeDocument::setRootTreeName(const std::string & tree_name)
997{
998 if (tree_name.empty()) {
999 throw exceptions::TreeDocumentError("Cannot set root tree name with empty string.");
1000 }
1001 if (!hasTreeName(tree_name)) {
1002 throw exceptions::TreeDocumentError(
1003 "Cannot make tree with name '" + tree_name + "' the root tree because it doesn't exist.");
1004 }
1005 RootElement()->SetAttribute(ROOT_TREE_ATTRIBUTE_NAME, tree_name.c_str());
1006 return *this;
1007}
1008
1010{
1011 if (RootElement()->Attribute(ROOT_TREE_ATTRIBUTE_NAME)) return true;
1012 return false;
1013}
1014
1016{
1017 if (const auto tree_name = RootElement()->Attribute(ROOT_TREE_ATTRIBUTE_NAME)) return tree_name;
1018 throw exceptions::TreeDocumentError(
1019 "Cannot get root tree name because the document's root element has no attribute '" +
1020 std::string(ROOT_TREE_ATTRIBUTE_NAME) + "'.");
1021}
1022
1024
1025TreeDocument & TreeDocument::removeTree(const std::string & tree_name)
1026{
1027 RootElement()->DeleteChild(getXMLElementForTreeWithName(tree_name));
1028 return *this;
1029}
1030
1032
1033std::vector<std::string> TreeDocument::getAllTreeNames() const { return getAllTreeNamesImpl(*this); }
1034
1035TreeDocument & TreeDocument::applyNodeNamespace(const std::string & node_namespace, const std::string & sep)
1036{
1037 // Get the old node names before applying namespace
1038 std::set<std::string> old_node_names;
1039 for (const auto & [name, _] : registered_nodes_manifest_.map()) {
1040 old_node_names.insert(name);
1041 }
1042
1043 // Apply namespace to the registered nodes manifest
1044 registered_nodes_manifest_.applyNodeNamespace(node_namespace, sep);
1045
1046 // Helper to recursively rename nodes in the XML
1047 auto rename_recursive = [&old_node_names, &node_namespace, &sep](XMLElement * parent, auto & self) -> void {
1048 for (XMLElement * child = parent->FirstChildElement(); child != nullptr; child = child->NextSiblingElement()) {
1049 const char * name = child->Name();
1050 // Rename if the node name is in the old registered nodes - we don't want to rename native nodes.
1051 if (name && old_node_names.count(name) > 0) {
1052 child->SetName((node_namespace + sep + name).c_str());
1053 }
1054 self(child, self);
1055 }
1056 };
1057
1058 // Process all trees
1059 for (XMLElement * tree_ele = RootElement()->FirstChildElement(TREE_ELEMENT_NAME); tree_ele != nullptr;
1060 tree_ele = tree_ele->NextSiblingElement(TREE_ELEMENT_NAME)) {
1061 rename_recursive(tree_ele, rename_recursive);
1062 }
1063
1064 // Also update the BehaviorTreeFactory registrations so that the factory
1065 // recognizes the new (namespaced) node IDs used in the XML.
1066 // We must copy the manifest and builder data BEFORE unregistering, as unregisterBuilder
1067 // invalidates any references to the internal maps.
1068 std::vector<std::pair<std::string, std::pair<BT::TreeNodeManifest, BT::NodeBuilder>>> registrations_to_update;
1069 const auto & factory_manifests = factory_.manifests();
1070 const auto & factory_builders = factory_.builders();
1071 for (const auto & old_name : old_node_names) {
1072 const auto manifest_it = factory_manifests.find(old_name);
1073 const auto builder_it = factory_builders.find(old_name);
1074 if (manifest_it == factory_manifests.end() || builder_it == factory_builders.end()) {
1075 // This should never happen since we derived old_node_names from registered_nodes_manifest_
1076 throw exceptions::TreeDocumentError(
1077 "Internal error while applying node namespace: Node '" + old_name +
1078 "' not found in BehaviorTreeFactory registrations.");
1079 }
1080 // Copy the data before we modify the factory
1081 registrations_to_update.emplace_back(old_name, std::make_pair(manifest_it->second, builder_it->second));
1082 }
1083
1084 // Now perform the unregister/register operations using the copied data
1085 for (const auto & [old_name, manifest_builder_pair] : registrations_to_update) {
1086 factory_.unregisterBuilder(old_name);
1087 BT::TreeNodeManifest new_manifest = manifest_builder_pair.first;
1088 new_manifest.registration_ID = node_namespace + sep + old_name;
1089 factory_.registerBuilder(new_manifest, manifest_builder_pair.second);
1090 }
1091
1092 return *this;
1093}
1094
1095TreeDocument & TreeDocument::registerNodes(const NodeManifest & tree_node_manifest, bool override)
1096{
1097 // Make sure that there are no node names that are reserved for native nodes
1098 std::set<std::string> all_registration_names;
1099 for (const auto & [name, _] : tree_node_manifest.map()) all_registration_names.insert(name);
1100 if (const std::set<std::string> common =
1101 auto_apms_util::getCommonElements(all_registration_names, native_node_names_);
1102 !common.empty()) {
1103 throw exceptions::TreeDocumentError(
1104 "Found reserved node registration names in the node manifest. The following names are not allowed, because "
1105 "they refer to native behavior tree nodes: [ " +
1106 auto_apms_util::join(std::vector<std::string>(common.begin(), common.end()), ", ") + " ].");
1107 }
1108
1109 for (const auto & [node_name, params] : tree_node_manifest.map()) {
1110 // By design, we require the user to maintain unique registration names for nodes. Theoretically, we could tolerate
1111 // multiple registrations of the same node name if both registration options are identical, but this would
1112 // complicate the implementation (comparing every option one by one) and indicates bad structuring of node manifests
1113 // anyways. So we discourage this practice by throwing an error.
1114 if (registered_nodes_manifest_.contains(node_name)) {
1115 if (override) {
1116 // If override is true, register the new node plugin instead of the current one
1117 factory_.unregisterBuilder(node_name);
1118 registered_nodes_manifest_.remove(node_name);
1119 } else {
1120 // If overriding is not explicitly wanted, we must throw
1121 throw exceptions::TreeDocumentError(
1122 "Tried to register node '" + node_name + "' (Class: " + params.class_name +
1123 ") which is already known. You must make sure that the registration names are unique or explicitly allow "
1124 "overriding previously registered nodes with the same name by setting override=true.");
1125 }
1126 }
1127
1128 // Check if the class we search for is actually available with the loader.
1129 if (!tree_node_loader_ptr_->isClassAvailable(params.class_name)) {
1130 if (all_node_classes_package_map_.find(params.class_name) == all_node_classes_package_map_.end()) {
1131 throw exceptions::TreeDocumentError(
1132 "Node '" + node_name + " (" + params.class_name +
1133 ")' cannot be registered, because the class name is not known to the class loader. "
1134 "Make sure that it's spelled correctly and registered by calling "
1135 "auto_apms_behavior_tree_register_nodes() in the CMakeLists.txt of the "
1136 "corresponding package.");
1137 }
1138 throw exceptions::TreeDocumentError(
1139 "Node '" + node_name + " (" + params.class_name +
1140 ")' cannot be registered, because the corresponding resource belongs to excluded package '" +
1141 all_node_classes_package_map_.at(params.class_name) + "'.");
1142 }
1143
1144 pluginlib::UniquePtr<NodeRegistrationInterface> plugin_instance;
1145 try {
1146 plugin_instance = tree_node_loader_ptr_->createUniqueInstance(params.class_name);
1147 } catch (const pluginlib::CreateClassException & e) {
1148 throw pluginlib::CreateClassException(
1149 "Failed to create an instance of node '" + node_name + " (" + params.class_name +
1150 ")'. Remember that the AUTO_APMS_BEHAVIOR_TREE_REGISTER_NODE "
1151 "macro must be called in the source file for the node class to be discoverable. "
1152 "Error message: " +
1153 e.what() + ".");
1154 }
1155
1156 try {
1157 if (plugin_instance->requiresRosNodeContext()) {
1158 if (only_non_ros_nodes_) {
1159 throw exceptions::NodeRegistrationError(
1160 "Node '" + node_name +
1161 "' relies on ROS 2 functionality but this instance only allows to use non-ROS nodes.");
1162 }
1163 RosNodeContext ros_node_context(
1164 ros_node_wptr_.lock(), tree_node_waitables_callback_group_wptr_.lock(),
1165 tree_node_waitables_executor_wptr_.lock(), params);
1166 plugin_instance->registerWithBehaviorTreeFactory(factory_, node_name, &ros_node_context);
1167 } else {
1168 // Create a dummy object of RosNodeContext to allow for parsing the registration params nevertheless
1169 RosNodeContext ros_node_context(nullptr, nullptr, nullptr, params);
1170 plugin_instance->registerWithBehaviorTreeFactory(factory_, node_name, &ros_node_context);
1171 }
1172 } catch (const std::exception & e) {
1173 throw exceptions::NodeRegistrationError(
1174 "Cannot register node '" + node_name + " (" + params.class_name + ")': " + e.what() + ".");
1175 }
1176 registered_nodes_manifest_.add(node_name, params);
1177 }
1178 return *this;
1179}
1180
1181std::set<std::string> TreeDocument::getRegisteredNodeNames(bool include_native) const
1182{
1183 std::set<std::string> names;
1184 if (include_native) names = native_node_names_;
1185 for (const auto & [name, _] : registered_nodes_manifest_.map()) names.insert(name);
1186 return names;
1187}
1188
1190{
1191 NodeManifest m;
1192 TreeDocument * doc = const_cast<TreeDocument *>(this);
1193 for (const std::string & tree_name : getAllTreeNames()) {
1194 XMLElement * ptr = const_cast<XMLElement *>(getXMLElementForTreeWithName(tree_name));
1195 const TreeElement ele(doc, ptr);
1196 m.merge(ele.getRequiredNodeManifest(), true);
1197 }
1198 return m;
1199}
1200
1202{
1203 // Create or get the TreeNodesModel element
1204 tinyxml2::XMLElement * model_root = RootElement()->FirstChildElement(TREE_NODE_MODEL_ELEMENT_NAME);
1205
1206 // If no TreeNodesModel element exists, create one
1207 if (!model_root) {
1208 model_root = NewElement(TREE_NODE_MODEL_ELEMENT_NAME);
1209 RootElement()->InsertEndChild(model_root);
1210 }
1211
1212 // Iterate through the model_map and create XML elements for each node
1213 for (const auto & [node_name, model] : model_map) {
1214 // Create the element with the node type as the tag name
1215 tinyxml2::XMLElement * node_element = NewElement(BT::toStr(model.type).c_str());
1216 node_element->SetAttribute("ID", node_name.c_str());
1217
1218 // Add port information
1219 for (const auto & port_info : model.port_infos) {
1220 tinyxml2::XMLElement * port_element = nullptr;
1221
1222 // Create the appropriate port element based on direction
1223 switch (port_info.port_direction) {
1224 case BT::PortDirection::INPUT:
1225 port_element = NewElement("input_port");
1226 break;
1227 case BT::PortDirection::OUTPUT:
1228 port_element = NewElement("output_port");
1229 break;
1230 case BT::PortDirection::INOUT:
1231 port_element = NewElement("inout_port");
1232 break;
1233 }
1234
1235 // Set port attributes
1236 port_element->SetAttribute("name", port_info.port_name.c_str());
1237
1238 if (!port_info.port_type.empty()) {
1239 port_element->SetAttribute("type", port_info.port_type.c_str());
1240 }
1241
1242 if (port_info.port_has_default) {
1243 port_element->SetAttribute("default", port_info.port_default.c_str());
1244 }
1245
1246 // Set port description as text content
1247 if (!port_info.port_description.empty()) {
1248 port_element->SetText(port_info.port_description.c_str());
1249 }
1250
1251 node_element->InsertEndChild(port_element);
1252 }
1253
1254 model_root->InsertEndChild(node_element);
1255 }
1256
1257 return *this;
1258}
1259
1260NodeModelMap TreeDocument::getNodeModel(tinyxml2::XMLDocument & doc, const NodeManifest & manifest)
1261{
1262 const tinyxml2::XMLElement * root = doc.RootElement();
1263 if (!root) {
1264 throw exceptions::TreeDocumentError("Node model document has no root element.");
1265 }
1266 if (const char * ver = root->Attribute(TreeDocument::BTCPP_FORMAT_ATTRIBUTE_NAME)) {
1267 const std::string expected_format = TreeDocument::BTCPP_FORMAT_DEFAULT_VERSION;
1268 if (std::string(ver) != expected_format) {
1269 throw exceptions::TreeDocumentError(
1270 "Cannot parse node model document: Format of model document (" +
1271 std::string(TreeDocument::BTCPP_FORMAT_ATTRIBUTE_NAME) + ": " + ver +
1272 ") doesn't comply with the expected format (" + std::string(TreeDocument::BTCPP_FORMAT_ATTRIBUTE_NAME) + ": " +
1273 expected_format + ").");
1274 }
1275 } else {
1276 throw exceptions::TreeDocumentError(
1277 "Cannot parse node model document: Root element of model document doesn't have required attribute '" +
1278 std::string(TreeDocument::BTCPP_FORMAT_ATTRIBUTE_NAME) + "'.");
1279 }
1280 tinyxml2::XMLElement * model_ele = doc.RootElement()->FirstChildElement(TreeDocument::TREE_NODE_MODEL_ELEMENT_NAME);
1281 if (!model_ele) {
1282 throw exceptions::TreeDocumentError(
1283 "Element <" + std::string(TreeDocument::TREE_NODE_MODEL_ELEMENT_NAME) +
1284 "> doesn't exist in node model document.");
1285 }
1286
1287 NodeModelMap model_map;
1288 for (tinyxml2::XMLElement * ele = model_ele->FirstChildElement(); ele != nullptr; ele = ele->NextSiblingElement()) {
1289 const char * node_name = ele->Attribute("ID");
1290 if (!node_name) {
1291 throw exceptions::TreeDocumentError(
1292 "Element '" + std::string(ele->Name()) + "' in node model document is missing the required attribute 'ID'");
1293 }
1294 NodeModel & model = model_map[node_name];
1295 model.type = BT::convertFromString<BT::NodeType>(ele->Name());
1296 for (const tinyxml2::XMLElement * port_ele = ele->FirstChildElement(); port_ele != nullptr;
1297 port_ele = port_ele->NextSiblingElement()) {
1298 const std::string direction = port_ele->Name();
1299 NodePortInfo port_info;
1300 if (direction == "input_port") {
1301 port_info.port_direction = BT::PortDirection::INPUT;
1302 } else if (direction == "output_port") {
1303 port_info.port_direction = BT::PortDirection::OUTPUT;
1304 } else if (direction == "inout_port") {
1305 port_info.port_direction = BT::PortDirection::INOUT;
1306 } else {
1307 throw exceptions::TreeDocumentError(
1308 "Unknown port direction in node model for '" + std::string(node_name) + "': " + direction);
1309 }
1310 if (const char * c = port_ele->Attribute("name")) {
1311 port_info.port_name = c;
1312 }
1313 if (const char * c = port_ele->Attribute("type")) {
1314 port_info.port_type = c;
1315 }
1316 if (const char * c = port_ele->Attribute("default")) {
1317 port_info.port_has_default = true;
1318 port_info.port_default = c;
1319 } else {
1320 port_info.port_has_default = false;
1321 }
1322 if (const char * c = port_ele->GetText()) {
1323 port_info.port_description = c;
1324 }
1325 model.port_infos.push_back(std::move(port_info));
1326 }
1327 // Port infos may be empty if there are no ports
1328 }
1329
1330 // Collect hidden ports from the node manifest
1331 std::map<std::string, std::vector<std::string>> hidden_ports;
1332 for (const auto & [node_name, registration_options] : manifest.map()) {
1333 // Extract hidden_ports from registration options
1334 for (const std::string & port_name : registration_options.hidden_ports) {
1335 hidden_ports[node_name].push_back(port_name);
1336 }
1337
1338 // Implicitly hide ports that are specified in port_alias
1339 for (const auto & [port_name, _] : registration_options.port_alias) {
1340 hidden_ports[node_name].push_back(port_name);
1341 }
1342 }
1343
1344 // Apply port hiding
1345 for (const auto & [node_name, ports_to_hide] : hidden_ports) {
1346 auto it = model_map.find(node_name);
1347 if (it == model_map.end()) {
1348 continue;
1349 }
1350 NodeModel & model = it->second;
1351 model.port_infos.erase(
1352 std::remove_if(
1353 model.port_infos.begin(), model.port_infos.end(),
1354 [&ports_to_hide](const NodePortInfo & port_info) {
1355 return std::find(ports_to_hide.begin(), ports_to_hide.end(), port_info.port_name) != ports_to_hide.end();
1356 }),
1357 model.port_infos.end());
1358 }
1359
1360 return model_map;
1361}
1362
1364{
1365 // Generate XML from the factory
1366 std::string model_xml;
1367 try {
1368 model_xml = BT::writeTreeNodesModelXML(factory_, include_native);
1369 } catch (const std::exception & e) {
1370 throw exceptions::TreeDocumentError("Error generating node model XML from factory: " + std::string(e.what()));
1371 }
1372
1373 // Parse the XML
1374 tinyxml2::XMLDocument model_doc;
1375 if (model_doc.Parse(model_xml.c_str()) != tinyxml2::XMLError::XML_SUCCESS) {
1376 throw exceptions::TreeDocumentError(
1377 "Error parsing the model of the currently registered nodes: " + std::string(model_doc.ErrorStr()));
1378 }
1379
1380 // Get the node model with hidden ports applied
1381 return getNodeModel(model_doc, registered_nodes_manifest_);
1382}
1383
1384BT::Result TreeDocument::verify() const
1385{
1386 NodeModelMap model_map = getNodeModel(true);
1387 std::unordered_map<std::string, BT::NodeType> registered_nodes;
1388 for (const auto & [node_name, model] : model_map) {
1389 registered_nodes[node_name] = model.type;
1390 }
1391 try {
1392 BT::VerifyXML(writeToString(), registered_nodes);
1393 } catch (const BT::RuntimeError & e) {
1394 return nonstd::make_unexpected(e.what());
1395 }
1396 return {};
1397}
1398
1400{
1401 tinyxml2::XMLPrinter printer;
1402 Print(&printer);
1403 return printer.CStr();
1404}
1405
1406void TreeDocument::writeToFile(const std::string & path) const
1407{
1408 XMLDocument doc;
1409 DeepCopy(&doc);
1410 tinyxml2::XMLError result = doc.SaveFile(path.c_str());
1411 if (result != tinyxml2::XML_SUCCESS) {
1412 throw exceptions::TreeDocumentError(
1413 "Failed to write tree document to file. Error ID: " + std::string(doc.ErrorIDToName(result)));
1414 }
1415}
1416
1418{
1419 // Reset XML document
1420 Clear();
1421 tinyxml2::XMLElement * root_ele = NewElement(TreeDocument::ROOT_ELEMENT_NAME);
1422 root_ele->SetAttribute(TreeDocument::BTCPP_FORMAT_ATTRIBUTE_NAME, format_version_.c_str());
1423 InsertFirstChild(root_ele);
1424
1425 // Reset node registrations
1426 for (const auto & node_name : getRegisteredNodeNames(false)) {
1427 factory_.unregisterBuilder(node_name);
1428 }
1429 registered_nodes_manifest_ = NodeManifest();
1430 return *this;
1431}
1432
1433const TreeDocument::XMLElement * TreeDocument::getXMLElementForTreeWithName(const std::string & tree_name) const
1434{
1435 return getXMLElementForTreeWithNameImpl<const XMLElement *>(*this, tree_name);
1436}
1437
1438TreeDocument::XMLElement * TreeDocument::getXMLElementForTreeWithName(const std::string & tree_name)
1439{
1440 return getXMLElementForTreeWithNameImpl<XMLElement *>(*this, tree_name);
1441}
1442
1443template <typename ReturnT, typename DocumentT>
1444ReturnT TreeDocument::getXMLElementForTreeWithNameImpl(DocumentT & doc, const std::string & tree_name)
1445{
1446 if (tree_name.empty()) {
1447 throw exceptions::TreeDocumentError("Cannot get tree with an empty name.");
1448 }
1449 if (!doc.hasTreeName(tree_name)) {
1450 throw exceptions::TreeDocumentError("Cannot get tree with name '" + tree_name + "' because it doesn't exist.");
1451 }
1452 auto child = doc.RootElement()->FirstChildElement(TreeDocument::TREE_ELEMENT_NAME);
1453 while (child && !child->Attribute(TreeDocument::TREE_NAME_ATTRIBUTE_NAME, tree_name.c_str())) {
1454 child = child->NextSiblingElement();
1455 }
1456 if (!child) {
1457 throw std::logic_error(
1458 "Unexpected error trying to get tree element with name '" + tree_name +
1459 "'. Since hasTreeName() returned true, there MUST be a corresponding element.");
1460 }
1461 return child;
1462}
1463
1464} // namespace auto_apms_behavior_tree::core
const NodeManifest & getNodeManifest() const
Get the node manifest associated with this resource.
Definition behavior.hpp:430
Data structure for information about which behavior tree node plugin to load and how to configure the...
NodeManifest & add(const std::string &node_name, const RegistrationOptions &opt)
Add registration options for a behavior tree node to the manifest.
bool contains(const std::string &node_name) const
Determine if a behavior tree node has been added to the manifest.
NodeManifest & merge(const NodeManifest &other, bool replace=false)
Merges another NodeManifest with this one.
const Map & map() const
Get a view of the internal map.
A pluginlib::ClassLoader specifically for loading installed behavior tree node plugins.
Additional parameters specific to ROS 2 determined at runtime by TreeBuilder.
Class that encapsulates behavior tree script expressions.
Definition script.hpp:30
std::string str() const
Concatenate all expressions of this instance to a single string.
Definition script.cpp:41
Forward iterator for traversing the first-level children of a node.
ChildIterator & operator++()
Pre-increment: advance to the next sibling element.
value_type operator*() const
Dereference the iterator to obtain a NodeElement handle for the current child.
Handle for a single node of a TreeDocument.
const TreeDocument & getParentDocument() const
Get a const view of this node's parent tree document.
NodeElement insertTreeFromResource(const TreeResource &resource, const std::string &tree_name, const NodeElement *before_this=nullptr)
Concatenate a tree from one of the installed package's behavior tree resources and add its first chil...
const std::vector< std::string > & getPortNames() const
Get the names of the data ports implemented by the node represented by this element.
virtual std::string getRegistrationName() const
Get the name of this node given during registration representing its dynamic type.
XMLElement * getXMLElement()
Get a pointer to the underlying tinyxml2::XMLElement of this node.
std::function< bool(const NodeElement &)> ConstDeepApplyCallback
Callback invoked for every node found under another node. It cannot modify the current node.
NodeElement & resetPorts()
Delete all currently specified port values and reset with the defaults.
std::function< bool(NodeElement &)> DeepApplyCallback
Callback invoked for every node found under another node. It may modify the current node.
NodeElement & setPorts(const PortValues &port_values)
Populate the the node's data ports.
virtual NodeElement & setName(const std::string &instance_name)
Assign a name for this specific node instance.
NodeElement(TreeDocument *doc_ptr, XMLElement *ele_ptr)
Protected constructor intended for internal use only.
std::map< std::string, std::string > PortValues
Mapping of port names and its respective value encoded as string.
const std::vector< NodeElement > deepApplyConst(ConstDeepApplyCallback apply_callback) const
Recursively apply a callback to this node's children.
TreeDocument * doc_ptr_
Pointer to the tree document that created the tree this node belongs to.
ChildIterator end() const
Get a past-the-end iterator for the children of this node.
virtual std::string getName() const
Get the name of this node given to this specific instance by the developer.
PortValues getPorts() const
Assemble the values given to each data port implemented by this node.
NodeElement & setConditionalScript(BT::PreCond type, const Script &script)
Specify a script that is evaluated before this node is ticked.
NodeElement insertTreeFromString(const std::string &tree_str, const std::string &tree_name, const NodeElement *before_this=nullptr)
Concatenate a tree from a document created from a string and add its first child node to the children...
tinyxml2::XMLElement * ele_ptr_
Pointer to the corresponding XMLElement of the base document.
NodeElement insertTree(const TreeElement &tree, const NodeElement *before_this=nullptr)
Concatenate a tree and add its first child node to the children of this node.
std::string getFullyQualifiedName() const
Create a string that uniquely identifies this node considering its registration and its instance name...
NodeElement insertTreeFromDocument(const TreeDocument &doc, const std::string &tree_name, const NodeElement *before_this=nullptr)
Concatenate a tree from a document and add its first child node to the children of this node.
model::SubTree insertSubTreeNode(const std::string &tree_name, const NodeElement *before_this=nullptr)
Add a subtree node for a specific tree element to the children of this node.
bool hasChildren() const
Determine whether any children have been given to this node.
bool operator==(const NodeElement &other) const
Determine if two node elements refer to the same node.
NodeElement getFirstNode(const std::string &registration_name="", const std::string &instance_name="", bool deep_search=false) const
Get the first node with a particular registration and instance name.
ChildIterator begin() const
Get an iterator to the first child of this node.
NodeElement insertNode(const std::string &name, const NodeElement *before_this=nullptr)
Add a new node to the children of this node.
NodeElement & removeFirstChild(const std::string &registration_name="", const std::string &instance_name="", bool deep_search=false)
Remove the first node with a particular registration and instance name.
NodeElement insertTreeFromFile(const std::string &path, const std::string &tree_name, const NodeElement *before_this=nullptr)
Concatenate a tree from a document created from a file and add its first child node to the children o...
NodeElement & removeChildren()
Recursively remove all children of this node element.
NodeElement & operator=(const NodeElement &other)
Replace this node with another.
std::vector< NodeElement > deepApply(DeepApplyCallback apply_callback)
Recursively apply a callback to this node's children.
Handle for a single behavior tree of a TreeDocument.
TreeElement & removeFirstChild(const std::string &registration_name="", const std::string &instance_name="", bool deep_search=false)
TreeElement(TreeDocument *doc_ptr, XMLElement *ele_ptr)
Protected constructor intended to be used only by certain factory methods of TreeDocument.
std::string getName() const override
Get the name of the behavior tree.
BT::Result verify() const
Verify that this behavior tree is structured correctly and can be created successfully.
NodeManifest getRequiredNodeManifest() const
Assemble the node manifest that is required for successfully creating an instance of this tree.
TreeElement & makeRoot()
Set this behavior tree as the root tree of the parent document.
TreeElement & operator=(const TreeElement &other)
Replace the behavior tree represented by this element with another.
TreeElement & setName(const std::string &tree_name) override
Set the name of the behavior tree.
std::string writeToString() const
Write this behavior tree to an XML encoded in a string.
Document Object Model (DOM) for the behavior tree XML schema. This class offers a programmatic approa...
TreeElement getRootTree()
Get the corresponding behavior tree element for the root tree of this document.
TreeDocument & mergeResource(const TreeResource &resource, bool adopt_root_tree=false)
Merge the behavior trees from one of the installed package's behavior tree resources.
std::string getRootTreeName() const
Get the name of this document's root tree.
TreeDocument & mergeTreeDocument(const XMLDocument &other, bool adopt_root_tree=false)
Merge another tree document with this one.
TreeDocument & mergeString(const std::string &tree_str, bool adopt_root_tree=false)
Create a tree document from a string and merge it with this one.
TreeElement newTreeFromDocument(const TreeDocument &other, const std::string &tree_name="")
Create a new behavior tree inside this document with the content of one found inside another tree doc...
void writeToFile(const std::string &path) const
Write the XML of this tree document to a file.
TreeElement newTreeFromFile(const std::string &path, const std::string &tree_name="")
Create a new behavior tree inside this document with the content of one found inside the XML file.
TreeDocument & applyNodeNamespace(const std::string &node_namespace, const std::string &sep=_AUTO_APMS_BEHAVIOR_TREE_CORE__NODE_NAMESPACE_DEFAULT_SEP)
Prepend a namespace to all nodes associated with this document.
BT::Result verify() const
Verify that all behavior trees of this document are structured correctly and can be created successfu...
NodeManifest getRequiredNodeManifest() const
Assemble the node manifest that is required for successfully creating an instance of any of the docum...
std::set< std::string > getRegisteredNodeNames(bool include_native=true) const
Get the names of all nodes that are known to this document.
TreeElement newTreeFromString(const std::string &tree_str, const std::string &tree_name="")
Create a new behavior tree inside this document with the content of one found inside the XML string.
TreeElement newTreeFromResource(const TreeResource &resource, const std::string &tree_name="")
Create a new behavior tree inside this document with the content of one the trees found inside a part...
TreeDocument & reset()
Clear this document and reset it to its initial state.
TreeElement getTree(const std::string &tree_name)
Get the corresponding behavior tree element for a tree inside this document.
virtual TreeDocument & registerNodes(const NodeManifest &tree_node_manifest, bool override=false)
Load behavior tree node plugins and register them with the internal behavior tree factory.
TreeDocument & setRootTreeName(const std::string &tree_name)
Define the root tree of this document.
bool hasTreeName(const std::string &tree_name) const
Determine if this document contains a behavior tree with a particular name.
TreeDocument & mergeFile(const std::string &path, bool adopt_root_tree=false)
Create a tree document from a file and merge it with this one.
std::vector< std::string > getAllTreeNames() const
Get the names of all behavior trees inside this document.
TreeDocument & addNodeModel(NodeModelMap model_map)
Add a behavior tree node model element to the document by parsing the contents of model_map.
TreeDocument & mergeTree(const TreeElement &tree, bool make_root_tree=false)
Merge an existing behavior tree with this tree document.
std::string writeToString() const
Write the XML of this tree document to a string.
bool hasRootTreeName() const
Determine if this document specifies which of its trees is the root tree.
TreeDocument & removeTree(const std::string &tree_name)
Remove a particular behavior tree from this document.
TreeDocument(const std::string &format_version=BTCPP_FORMAT_DEFAULT_VERSION, NodeRegistrationLoader::SharedPtr tree_node_loader=NodeRegistrationLoader::make_shared())
Create a an empty tree document.
static NodeModelMap getNodeModel(tinyxml2::XMLDocument &doc, const NodeManifest &manifest)
Convert a behavior tree node model document to the corresponding data structure.
TreeElement newTree(const std::string &tree_name)
Create a new behavior tree inside this document.
Class containing behavior tree resource data.
std::string getRootTreeName() const
Get the name of the root tree of this behavior tree resource.
bool hasRootTreeName() const
Determine if this behavior tree resource specifies a root tree.
Subtree behavior tree node model.
bool contains(const ContainerT< ValueT, AllocatorT > &c, const ValueT &val)
Check whether a particular container structure contains a value.
Definition container.hpp:36
std::set< KeyT, CompareT, AllocatorT > getCommonElements(std::set< KeyT, CompareT, AllocatorT > c1, std::set< KeyT, CompareT, AllocatorT > c2)
Assemble common elements of two sets.
Definition container.hpp:53
Core API for AutoAPMS's behavior tree implementation.
Definition behavior.hpp:32
Models for all available behavior tree nodes.
Powerful tooling for incorporating behavior trees for task development.
Definition behavior.hpp:32
std::map< std::string, NodeModel > NodeModelMap
Mapping of node registration names and their implementation details.
Data structure encapsulating the information of all ports implemented by a behavior tree node.
Implementation details of a single data port.
std::string port_default
Default value of the port encoded as string.
bool port_has_default
Flag whether the port implements a default value or not.
std::string port_description
Description of the port.
BT::PortDirection port_direction
Direction of the port.
std::string port_type
String representation of the C++ type given to the port.
std::string port_name
Name of the port.
Parameters for loading and registering a behavior tree node class from a shared library using e....