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 unkown 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 unkown 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) const
340{
341 if (registration_name.empty() && instance_name.empty()) return NodeElement(doc_ptr_, ele_ptr_->FirstChildElement());
342
343 // If name is given, recursively search for the first node with this name
344 ConstDeepApplyCallback apply = [&registration_name, &instance_name](const NodeElement & ele) {
345 if (registration_name.empty()) return ele.getName() == instance_name;
346 if (instance_name.empty()) return ele.getRegistrationName() == registration_name;
347 return ele.getRegistrationName() == registration_name && ele.getName() == instance_name;
348 };
349 if (const std::vector<NodeElement> found = deepApplyConst(apply); !found.empty()) return found[0];
350
351 // Cannot find node in children of this
352 throw exceptions::TreeDocumentError(
353 "Cannot find a child node that matches the search arguments (registration_name: '" + registration_name +
354 "' - instance_name: '" + instance_name + "') in parent element '" + getFullyQualifiedName() + "'.");
355}
356
358 const std::string & registration_name, const std::string & instance_name)
359{
360 ele_ptr_->DeleteChild(getFirstNode(registration_name, instance_name).ele_ptr_);
361 return *this;
362}
363
365{
366 ele_ptr_->DeleteChildren();
367 return *this;
368}
369
370const std::vector<std::string> & TreeDocument::NodeElement::getPortNames() const { return port_names_; }
371
373{
374 PortValues values;
375 for (const tinyxml2::XMLAttribute * attr = ele_ptr_->FirstAttribute(); attr != nullptr; attr = attr->Next()) {
376 if (const std::string attr_name = attr->Name(); auto_apms_util::contains(port_names_, attr_name)) {
377 values[attr_name] = attr->Value();
378 }
379 }
380 return values;
381}
382
384{
385 // Verify port_values
386 std::vector<std::string> unkown_keys;
387 for (const auto & [key, _] : port_values) {
388 if (!auto_apms_util::contains(port_names_, key)) unkown_keys.push_back(key);
389 }
390 if (!unkown_keys.empty()) {
391 throw exceptions::TreeDocumentError(
392 "Cannot set ports. According to the node model, the following ports are not implemented by '" +
393 std::string(ele_ptr_->Name()) + "': [ " + auto_apms_util::join(unkown_keys, ", ") + " ].");
394 }
395
396 // Populate attributes according to the content of port_values
397 for (const auto & [key, val] : port_values) {
398 ele_ptr_->SetAttribute(key.c_str(), val.c_str());
399 }
400 return *this;
401}
402
404{
405 for (const std::string & name : port_names_) {
406 PortValues::const_iterator it = port_default_values_.find(name);
407 if (it != port_default_values_.end()) {
408 ele_ptr_->SetAttribute(it->first.c_str(), it->second.c_str());
409 } else {
410 ele_ptr_->DeleteAttribute(name.c_str());
411 }
412 }
413 return *this;
414}
415
417{
418 ele_ptr_->SetAttribute(BT::toStr(type).c_str(), script.str().c_str());
419 return *this;
420}
421
423{
424 ele_ptr_->SetAttribute(BT::toStr(type).c_str(), script.str().c_str());
425 return *this;
426}
427
429{
430 ele_ptr_->SetAttribute(NODE_INSTANCE_NAME_ATTRIBUTE_NAME, instance_name.c_str());
431 return *this;
432}
433
434std::string TreeDocument::NodeElement::getRegistrationName() const { return ele_ptr_->Name(); }
435
437{
438 if (const char * name = ele_ptr_->Attribute(NODE_INSTANCE_NAME_ATTRIBUTE_NAME)) return name;
439 return getRegistrationName();
440}
441
443{
444 std::string registration_name = getRegistrationName();
445 std::string instance_name = getName();
446 if (registration_name == instance_name) return registration_name;
447 return instance_name + " (" + registration_name + ")";
448}
449
451
452const std::vector<TreeDocument::NodeElement> TreeDocument::NodeElement::deepApplyConst(
453 ConstDeepApplyCallback apply_callback) const
454{
455 std::vector<NodeElement> found;
456 deepApplyImpl(*this, apply_callback, found);
457 return found;
458}
459
460std::vector<TreeDocument::NodeElement> TreeDocument::NodeElement::deepApply(DeepApplyCallback apply_callback)
461{
462 std::vector<NodeElement> found;
463 deepApplyImpl(*this, apply_callback, found);
464 return found;
465}
466
467TreeDocument::NodeElement TreeDocument::NodeElement::insertBeforeImpl(
468 const NodeElement * before_this, XMLElement * add_this)
469{
470 if (before_this) {
471 XMLElement * prev = nullptr;
472 XMLElement * curr = ele_ptr_->FirstChildElement();
473
474 // Traverse through siblings of first child to find the before_this node.
475 // If there are no siblings, add the first child to this;
476 bool found = false;
477 while (curr) {
478 if (curr == before_this->ele_ptr_) {
479 found = true;
480 break;
481 }
482 prev = curr;
483 curr = curr->NextSiblingElement();
484 }
485 if (prev) {
486 if (found) {
487 ele_ptr_->InsertAfterChild(prev, add_this);
488 } else {
489 throw exceptions::TreeDocumentError(
490 "NodeElement before_this (" + before_this->getFullyQualifiedName() + ") is not a child of " +
491 getFullyQualifiedName() + ".");
492 }
493 } else {
494 ele_ptr_->InsertFirstChild(add_this);
495 }
496 } else {
497 ele_ptr_->InsertEndChild(add_this);
498 }
499 return NodeElement(doc_ptr_, add_this);
500}
501
502void TreeDocument::NodeElement::deepApplyImpl(
503 const NodeElement & parent, ConstDeepApplyCallback apply_callback, std::vector<NodeElement> & vec)
504{
505 for (XMLElement * child = parent.ele_ptr_->FirstChildElement(); child != nullptr;
506 child = child->NextSiblingElement()) {
507 const NodeElement child_ele(parent.doc_ptr_, child);
508
509 // Apply on current child element
510 if (apply_callback(child_ele)) vec.push_back(child_ele);
511
512 // Search children of child before evaluating the siblings
513 deepApplyImpl(child_ele, apply_callback, vec);
514 }
515}
516
517void TreeDocument::NodeElement::deepApplyImpl(
518 NodeElement & parent, DeepApplyCallback apply_callback, std::vector<NodeElement> & vec)
519{
520 for (XMLElement * child = parent.ele_ptr_->FirstChildElement(); child != nullptr;
521 child = child->NextSiblingElement()) {
522 NodeElement child_ele(parent.doc_ptr_, child);
523
524 // Apply on current child element
525 if (apply_callback(child_ele)) vec.push_back(child_ele);
526
527 // Search children of child before evaluating the siblings
528 deepApplyImpl(child_ele, apply_callback, vec);
529 }
530}
531
532TreeDocument::TreeElement::TreeElement(TreeDocument * doc_ptr, XMLElement * ele_ptr) : NodeElement(doc_ptr, ele_ptr)
533{
534 if (!ele_ptr->Attribute(TREE_NAME_ATTRIBUTE_NAME)) {
535 throw exceptions::TreeDocumentError(
536 "Cannot create tree element without a '" + std::string(TREE_NAME_ATTRIBUTE_NAME) + "' attribute.");
537 }
538}
539
541{
542 const std::string other_tree_name = other.getName();
543 // For a tree replacement to be allowed, the other tree name must be the same or at least not already taken by another
544 // tree inside this document.
545 if (getName() != other_tree_name && doc_ptr_->hasTreeName(other.getName())) {
546 throw exceptions::TreeDocumentError(
547 "Cannot copy tree '" + other.getName() + "' because another tree with this name already exists.");
548 }
550 return *this;
551}
552
554{
555 ele_ptr_->SetAttribute(TREE_NAME_ATTRIBUTE_NAME, tree_name.c_str());
556 return *this;
557}
558
560{
561 if (const char * name = ele_ptr_->Attribute(TREE_NAME_ATTRIBUTE_NAME)) return name;
562 return "unkown";
563}
564
566{
567 doc_ptr_->setRootTreeName(getName());
568 return *this;
569}
570
572{
573 NodeManifest m;
574 deepApplyConst([this, &m](const NodeElement & node) {
575 const std::string name = node.getRegistrationName();
576 const bool is_native_node = doc_ptr_->native_node_names_.find(name) != doc_ptr_->native_node_names_.end();
577 if (!is_native_node && !m.contains(name)) {
578 if (!doc_ptr_->registered_nodes_manifest_.contains(name)) {
579 throw exceptions::NodeManifestError(
580 "Cannot assemble the required node manifest for tree '" + getName() +
581 "' since there are no registration options for node '" + name + "'.");
582 }
583 m.add(name, doc_ptr_->registered_nodes_manifest_[name]);
584 }
585 return false;
586 });
587 return m;
588}
589
591{
592 TreeDocument doc(doc_ptr_->format_version_, doc_ptr_->tree_node_loader_ptr_);
593 return doc.mergeTree(*this, true).verify();
594}
595
597{
598 XMLDocument tree_doc;
599 tree_doc.InsertEndChild(ele_ptr_->DeepClone(&tree_doc));
600 tinyxml2::XMLPrinter printer;
601 tree_doc.Print(&printer);
602 return printer.CStr();
603}
604
606 const std::string & registration_name, const std::string & instance_name)
607{
608 NodeElement::removeFirstChild(registration_name, instance_name);
609 return *this;
610}
611
617
618TreeDocument::TreeDocument(const std::string & format_version, NodeRegistrationLoader::SharedPtr tree_node_loader)
619// It's important to initialize XMLDocument using PRESERVE_WHITESPACE, since encoded port data (like
620// NodeRegistrationOptions) may be sensitive to changes in the whitespaces (think of the YAML format).
621: XMLDocument(true, tinyxml2::PRESERVE_WHITESPACE),
622 all_node_classes_package_map_(auto_apms_behavior_tree::core::NodeRegistrationLoader().getClassPackageMap()),
623 native_node_names_(BT::BehaviorTreeFactory().builtinNodes()),
624 format_version_(format_version),
625 tree_node_loader_ptr_(tree_node_loader),
626 registered_nodes_manifest_(),
627 factory_(),
628 logger_(rclcpp::get_logger(LOGGER_NAME))
629{
630 reset();
631}
632
633TreeDocument & TreeDocument::mergeTreeDocument(const XMLDocument & other, bool adopt_root_tree)
634{
635 std::set<std::string> include_stack;
636 return mergeTreeDocumentImpl(other, adopt_root_tree, include_stack);
637}
638
639TreeDocument & TreeDocument::mergeTreeDocumentImpl(
640 const XMLDocument & other, bool adopt_root_tree, std::set<std::string> & include_stack)
641{
642 const XMLElement * other_root = other.RootElement();
643 if (!other_root) {
644 throw exceptions::TreeDocumentError("Cannot merge tree documents: other_root is nullptr.");
645 };
646
647 auto verify_tree_structure = [](const XMLElement * tree_ele) {
648 const char * tree_id = tree_ele->Attribute(TREE_NAME_ATTRIBUTE_NAME);
649 if (!tree_id) {
650 throw exceptions::TreeDocumentError(
651 "Cannot merge tree document: Found a <" + std::string(TREE_ELEMENT_NAME) +
652 "> element that doesn't specify the required attribute '" + TREE_NAME_ATTRIBUTE_NAME + "'.");
653 }
654 const XMLElement * tree_root_child = tree_ele->FirstChildElement();
655 if (!tree_root_child) {
656 throw exceptions::TreeDocumentError(
657 "Cannot merge tree document: Tree '" + std::string(tree_id) + "' has no child nodes.");
658 }
659 if (tree_root_child->NextSibling()) {
660 throw exceptions::TreeDocumentError(
661 "Cannot merge tree document: Tree '" + std::string(tree_id) + "' has more than one child node.");
662 }
663 };
664
665 // Helper lambda to check for duplicate tree names and throw with a descriptive message
666 auto check_duplicates = [this](const std::vector<std::string> & new_tree_names, const std::string & source) {
667 const auto common = auto_apms_util::getCommonElements(getAllTreeNames(), new_tree_names);
668 if (!common.empty()) {
669 throw exceptions::TreeDocumentError(
670 "Cannot merge tree document: The following trees from " + source + " are already defined: [ " +
671 auto_apms_util::join(common, ", ") + " ].");
672 }
673 };
674
675 // Get tree names from the other document (used for duplicate check and adopt_root_tree logic)
676 const std::vector<std::string> other_tree_names = getAllTreeNamesImpl(other);
677
678 if (strcmp(other_root->Name(), ROOT_ELEMENT_NAME) == 0) {
679 // Verify format
680 if (const char * ver = other_root->Attribute(BTCPP_FORMAT_ATTRIBUTE_NAME)) {
681 if (std::string(ver) != format_version_) {
682 throw exceptions::TreeDocumentError(
683 "Cannot merge tree document: Format of other document (" + std::string(BTCPP_FORMAT_ATTRIBUTE_NAME) + ": " +
684 ver + ") is not compatible with this document (" + std::string(BTCPP_FORMAT_ATTRIBUTE_NAME) + ": " +
685 format_version_ + ").");
686 }
687 } else {
688 throw exceptions::TreeDocumentError(
689 "Cannot merge tree document: Root element of other document doesn't have required attribute '" +
690 std::string(BTCPP_FORMAT_ATTRIBUTE_NAME) + "'.");
691 }
692
693 // Iterate over all include elements and store the associated content to a temporary buffer document
694 TreeDocument include_doc(format_version_, tree_node_loader_ptr_);
695 for (const XMLElement * include_ele = other_root->FirstChildElement(INCLUDE_ELEMENT_NAME); include_ele != nullptr;
696 include_ele = include_ele->NextSiblingElement(INCLUDE_ELEMENT_NAME)) {
697 if (const char * path = include_ele->Attribute(INCLUDE_PATH_ATTRIBUTE_NAME)) {
698 if (std::string(path).empty()) {
699 throw exceptions::TreeDocumentError(
700 "Cannot merge tree document: Found an <" + std::string(INCLUDE_ELEMENT_NAME) +
701 "> element that specifies an empty path in attribute '" + INCLUDE_PATH_ATTRIBUTE_NAME + "'.");
702 }
703 std::string absolute_path = path;
704 if (const char * ros_pkg = include_ele->Attribute(INCLUDE_ROS_PKG_ATTRIBUTE_NAME)) {
705 if (std::string(ros_pkg).empty()) {
706 throw exceptions::TreeDocumentError(
707 "Cannot merge tree document: Found an <" + std::string(INCLUDE_ELEMENT_NAME) +
708 "> element that specifies an empty ROS package name in attribute '" + INCLUDE_ROS_PKG_ATTRIBUTE_NAME +
709 "'.");
710 }
711 if (std::filesystem::path(path).is_absolute()) {
712 throw exceptions::TreeDocumentError(
713 "Cannot merge tree document: Found an <" + std::string(INCLUDE_ELEMENT_NAME) +
714 "> element that specifies an absolute path '" + std::string(path) + "' in attribute '" +
715 INCLUDE_PATH_ATTRIBUTE_NAME + "' together with a ROS package name in attribute '" +
716 INCLUDE_ROS_PKG_ATTRIBUTE_NAME + "'. Please remove the attribute " + INCLUDE_ROS_PKG_ATTRIBUTE_NAME +
717 " or use a relative path if you want to refer to a location "
718 "relative to the package's share directory.");
719 }
720 try {
721 absolute_path =
722 (std::filesystem::path(ament_index_cpp::get_package_share_directory(ros_pkg)) / path).string();
723 } catch (const ament_index_cpp::PackageNotFoundError & e) {
724 throw exceptions::TreeDocumentError(
725 "Cannot merge tree document: Found an <" + std::string(INCLUDE_ELEMENT_NAME) +
726 "> element that specifies a non-existing ROS package '" + std::string(ros_pkg) + "' in attribute '" +
727 INCLUDE_ROS_PKG_ATTRIBUTE_NAME + "'.");
728 }
729 }
730
731 // Recursively merge the included file to the buffer document, passing the include stack for circular detection
732 try {
733 include_doc.mergeFileImpl(absolute_path, false, include_stack);
734 } catch (const std::exception & e) {
735 throw exceptions::TreeDocumentError(
736 "Cannot merge tree document: Failed to include file '" + absolute_path + "': " + e.what());
737 }
738 } else {
739 throw exceptions::TreeDocumentError(
740 "Cannot merge tree document: Found an <" + std::string(INCLUDE_ELEMENT_NAME) +
741 "> element that doesn't specify the required attribute '" + INCLUDE_PATH_ATTRIBUTE_NAME + "'.");
742 }
743 }
744
745 // Check for duplicates from included files before inserting
746 check_duplicates(include_doc.getAllTreeNames(), "included files");
747
748 // Iterate over all the tree elements of the include buffer and include them in this document
749 for (const XMLElement * child = include_doc.RootElement()->FirstChildElement(TREE_ELEMENT_NAME); child != nullptr;
750 child = child->NextSiblingElement(TREE_ELEMENT_NAME)) {
751 // We do not need to verify the tree structure here, since it has already been verified during the recursive
752 // mergeFile() calls by the for loop below
753
754 // If tree element is valid, append to this document
755 RootElement()->InsertEndChild(child->DeepClone(this));
756 }
757
758 // Check for duplicates from other document's direct trees before inserting
759 check_duplicates(other_tree_names, "the merged document");
760
761 // Iterate over all the tree elements of other root and verify them
762 for (const XMLElement * child = other_root->FirstChildElement(TREE_ELEMENT_NAME); child != nullptr;
763 child = child->NextSiblingElement(TREE_ELEMENT_NAME)) {
764 verify_tree_structure(child);
765
766 // If tree element is valid, append to this document
767 RootElement()->InsertEndChild(child->DeepClone(this));
768 }
769
770 if (adopt_root_tree) {
771 // After (!) all tree elements have been inserted, adopt the name specified in the corresponding attribute of the
772 // root element
773 if (const char * name = other_root->Attribute(ROOT_TREE_ATTRIBUTE_NAME)) setRootTreeName(name);
774 }
775 } else if (strcmp(other_root->Name(), TREE_ELEMENT_NAME) == 0) {
776 // Allow a single behavior tree without <root> element.
777 // We assume that the the format complies with the version configured at construction time
778 verify_tree_structure(other_root);
779
780 // Check for duplicates before inserting
781 check_duplicates(other_tree_names, "the merged document");
782
783 // If tree element is valid, append to this document
784 RootElement()->InsertEndChild(other_root->DeepClone(this));
785 } else {
786 throw exceptions::TreeDocumentError(
787 "Cannot merge tree document: Root element of other document must either be <" + std::string(ROOT_ELEMENT_NAME) +
788 "> or <" + TREE_ELEMENT_NAME + ">.");
789 }
790
791 if (adopt_root_tree && other_tree_names.size() == 1) {
792 // If there is only one tree element, we change the root tree to the respective tree after (!) all tree elements
793 // have been inserted
794 setRootTreeName(other_tree_names[0]);
795 }
796
797 return *this;
798}
799
800TreeDocument & TreeDocument::mergeTreeDocument(const TreeDocument & other, bool adopt_root_tree)
801{
803 std::set<std::string> include_stack;
804 return mergeTreeDocumentImpl(static_cast<const XMLDocument &>(other), adopt_root_tree, include_stack);
805}
806
807TreeDocument & TreeDocument::mergeString(const std::string & tree_str, bool adopt_root_tree)
808{
809 XMLDocument other_doc;
810 if (other_doc.Parse(tree_str.c_str()) != tinyxml2::XMLError::XML_SUCCESS) {
811 throw exceptions::TreeDocumentError("Cannot merge tree document from string: " + std::string(other_doc.ErrorStr()));
812 }
813 std::set<std::string> include_stack;
814 return mergeTreeDocumentImpl(other_doc, adopt_root_tree, include_stack);
815}
816
817TreeDocument & TreeDocument::mergeFile(const std::string & path, bool adopt_root_tree)
818{
819 std::set<std::string> include_stack;
820 return mergeFileImpl(path, adopt_root_tree, include_stack);
821}
822
823TreeDocument & TreeDocument::mergeFileImpl(
824 const std::string & path, bool adopt_root_tree, std::set<std::string> & include_stack)
825{
826 // Get canonical path for consistent circular include detection
827 std::string canonical_path;
828 try {
829 canonical_path = std::filesystem::canonical(path).string();
830 } catch (const std::filesystem::filesystem_error &) {
831 // If canonical fails (e.g., file doesn't exist), use the original path
832 // The error will be caught later when loading the file
833 canonical_path = path;
834 }
835
836 // Check for circular includes
837 if (include_stack.count(canonical_path) > 0) {
838 throw exceptions::TreeDocumentError(
839 "Cannot merge tree document: Circular include detected for file '" + path + "'.");
840 }
841
842 // Add to include stack
843 include_stack.insert(canonical_path);
844
845 XMLDocument other_doc;
846 if (other_doc.LoadFile(path.c_str()) != tinyxml2::XMLError::XML_SUCCESS) {
847 throw exceptions::TreeDocumentError("Cannot create tree document from file " + path + ": " + other_doc.ErrorStr());
848 }
849
850 mergeTreeDocumentImpl(other_doc, adopt_root_tree, include_stack);
851
852 // Remove from include stack (for recursive calls that continue after this one)
853 include_stack.erase(canonical_path);
854 return *this;
855}
856
857TreeDocument & TreeDocument::mergeResource(const TreeResource & resource, bool adopt_root_tree)
858{
859 registerNodes(resource.getNodeManifest(), false);
860 return mergeFile(resource.build_request_file_path_, adopt_root_tree);
861}
862
863TreeDocument & TreeDocument::mergeTree(const TreeElement & tree, bool make_root_tree)
864{
865 XMLDocument tree_doc;
866 tree_doc.InsertEndChild(tree.ele_ptr_->DeepClone(&tree_doc));
868 mergeTreeDocument(tree_doc, make_root_tree);
869 return *this;
870}
871
873{
874 if (tree_name.empty()) {
875 throw exceptions::TreeDocumentError("Cannot create a new tree with an empty name");
876 }
877 if (hasTreeName(tree_name)) {
878 throw exceptions::TreeDocumentError(
879 "Cannot create a new tree with name '" + tree_name + "' because it already exists.");
880 }
881 TreeDocument::XMLElement * new_ele = RootElement()->InsertNewChildElement(TreeDocument::TREE_ELEMENT_NAME);
882 new_ele->SetAttribute(TreeDocument::TREE_NAME_ATTRIBUTE_NAME, tree_name.c_str());
883 return TreeElement(this, new_ele);
884}
885
887{
888 return mergeTree(other_tree).getTree(other_tree.getName());
889}
890
892{
893 std::string name(tree_name);
894 if (name.empty()) {
895 if (other.hasRootTreeName()) {
896 name = other.getRootTreeName();
897 } else if (const std::vector<std::string> names = other.getAllTreeNames(); names.size() == 1) {
898 name = names[0];
899 } else {
900 throw exceptions::TreeDocumentError(
901 "Failed to create new tree element from another document because argument tree_name was omitted and it was not "
902 "possible to determine the root tree automatically.");
903 }
904 }
905 TreeElement tree_ele = newTree(name);
906 tree_ele.insertTreeFromDocument(other, name);
907 return tree_ele;
908}
909
910TreeDocument::TreeElement TreeDocument::newTreeFromString(const std::string & tree_str, const std::string & tree_name)
911{
912 TreeDocument new_doc(format_version_, tree_node_loader_ptr_);
913 new_doc.mergeString(tree_str, true);
914 return newTreeFromDocument(new_doc, tree_name);
915}
916
917TreeDocument::TreeElement TreeDocument::newTreeFromFile(const std::string & path, const std::string & tree_name)
918{
919 TreeDocument new_doc(format_version_, tree_node_loader_ptr_);
920 new_doc.mergeFile(path, true);
921 return newTreeFromDocument(new_doc, tree_name);
922}
923
925 const TreeResource & resource, const std::string & tree_name)
926{
927 TreeDocument new_doc(format_version_, tree_node_loader_ptr_);
928 new_doc.mergeFile(resource.build_request_file_path_);
929 if (resource.hasRootTreeName()) new_doc.setRootTreeName(resource.getRootTreeName());
930 registerNodes(resource.getNodeManifest(), false);
931 return newTreeFromDocument(new_doc, tree_name);
932}
933
934bool TreeDocument::hasTreeName(const std::string & tree_name) const
935{
936 if (auto_apms_util::contains(getAllTreeNames(), tree_name)) return true;
937 return false;
938}
939
941{
942 return TreeElement(this, getXMLElementForTreeWithName(tree_name));
943}
944
945TreeDocument & TreeDocument::setRootTreeName(const std::string & tree_name)
946{
947 if (tree_name.empty()) {
948 throw exceptions::TreeDocumentError("Cannot set root tree name with empty string.");
949 }
950 if (!hasTreeName(tree_name)) {
951 throw exceptions::TreeDocumentError(
952 "Cannot make tree with name '" + tree_name + "' the root tree because it doesn't exist.");
953 }
954 RootElement()->SetAttribute(ROOT_TREE_ATTRIBUTE_NAME, tree_name.c_str());
955 return *this;
956}
957
959{
960 if (RootElement()->Attribute(ROOT_TREE_ATTRIBUTE_NAME)) return true;
961 return false;
962}
963
965{
966 if (const auto tree_name = RootElement()->Attribute(ROOT_TREE_ATTRIBUTE_NAME)) return tree_name;
967 throw exceptions::TreeDocumentError(
968 "Cannot get root tree name because the document's root element has no attribute '" +
969 std::string(ROOT_TREE_ATTRIBUTE_NAME) + "'.");
970}
971
973
974TreeDocument & TreeDocument::removeTree(const std::string & tree_name)
975{
976 RootElement()->DeleteChild(getXMLElementForTreeWithName(tree_name));
977 return *this;
978}
979
981
982std::vector<std::string> TreeDocument::getAllTreeNames() const { return getAllTreeNamesImpl(*this); }
983
984TreeDocument & TreeDocument::applyNodeNamespace(const std::string & node_namespace, const std::string & sep)
985{
986 // Get the old node names before applying namespace
987 std::set<std::string> old_node_names;
988 for (const auto & [name, _] : registered_nodes_manifest_.map()) {
989 old_node_names.insert(name);
990 }
991
992 // Apply namespace to the registered nodes manifest
993 registered_nodes_manifest_.applyNodeNamespace(node_namespace, sep);
994
995 // Helper to recursively rename nodes in the XML
996 auto rename_recursive = [&old_node_names, &node_namespace, &sep](XMLElement * parent, auto & self) -> void {
997 for (XMLElement * child = parent->FirstChildElement(); child != nullptr; child = child->NextSiblingElement()) {
998 const char * name = child->Name();
999 // Rename if the node name is in the old registered nodes - we don't want to rename native nodes.
1000 if (name && old_node_names.count(name) > 0) {
1001 child->SetName((node_namespace + sep + name).c_str());
1002 }
1003 self(child, self);
1004 }
1005 };
1006
1007 // Process all trees
1008 for (XMLElement * tree_ele = RootElement()->FirstChildElement(TREE_ELEMENT_NAME); tree_ele != nullptr;
1009 tree_ele = tree_ele->NextSiblingElement(TREE_ELEMENT_NAME)) {
1010 rename_recursive(tree_ele, rename_recursive);
1011 }
1012
1013 // Also update the BehaviorTreeFactory registrations so that the factory
1014 // recognizes the new (namespaced) node IDs used in the XML.
1015 // We must copy the manifest and builder data BEFORE unregistering, as unregisterBuilder
1016 // invalidates any references to the internal maps.
1017 std::vector<std::pair<std::string, std::pair<BT::TreeNodeManifest, BT::NodeBuilder>>> registrations_to_update;
1018 const auto & factory_manifests = factory_.manifests();
1019 const auto & factory_builders = factory_.builders();
1020 for (const auto & old_name : old_node_names) {
1021 const auto manifest_it = factory_manifests.find(old_name);
1022 const auto builder_it = factory_builders.find(old_name);
1023 if (manifest_it == factory_manifests.end() || builder_it == factory_builders.end()) {
1024 // This should never happen since we derived old_node_names from registered_nodes_manifest_
1025 throw exceptions::TreeDocumentError(
1026 "Internal error while applying node namespace: Node '" + old_name +
1027 "' not found in BehaviorTreeFactory registrations.");
1028 }
1029 // Copy the data before we modify the factory
1030 registrations_to_update.emplace_back(old_name, std::make_pair(manifest_it->second, builder_it->second));
1031 }
1032
1033 // Now perform the unregister/register operations using the copied data
1034 for (const auto & [old_name, manifest_builder_pair] : registrations_to_update) {
1035 factory_.unregisterBuilder(old_name);
1036 BT::TreeNodeManifest new_manifest = manifest_builder_pair.first;
1037 new_manifest.registration_ID = node_namespace + sep + old_name;
1038 factory_.registerBuilder(new_manifest, manifest_builder_pair.second);
1039 }
1040
1041 return *this;
1042}
1043
1044TreeDocument & TreeDocument::registerNodes(const NodeManifest & tree_node_manifest, bool override)
1045{
1046 // Make sure that there are no node names that are reserved for native nodes
1047 std::set<std::string> all_registration_names;
1048 for (const auto & [name, _] : tree_node_manifest.map()) all_registration_names.insert(name);
1049 if (const std::set<std::string> common =
1050 auto_apms_util::getCommonElements(all_registration_names, native_node_names_);
1051 !common.empty()) {
1052 throw exceptions::TreeDocumentError(
1053 "Found reserved node registration names in the node manifest. The following names are not allowed, because "
1054 "they refer to native behavior tree nodes: [ " +
1055 auto_apms_util::join(std::vector<std::string>(common.begin(), common.end()), ", ") + " ].");
1056 }
1057
1058 for (const auto & [node_name, params] : tree_node_manifest.map()) {
1059 // By design, we require the user to maintain unique registration names for nodes. Theoretically, we could tolerate
1060 // multiple registrations of the same node name if both registration options are identical, but this would
1061 // complicate the implementation (comparing every option one by one) and indicates bad structuring of node manifests
1062 // anyways. So we discourage this practice by throwing an error.
1063 if (registered_nodes_manifest_.contains(node_name)) {
1064 if (override) {
1065 // If override is true, register the new node plugin instead of the current one
1066 factory_.unregisterBuilder(node_name);
1067 registered_nodes_manifest_.remove(node_name);
1068 } else {
1069 // If overriding is not explicitly wanted, we must throw
1070 throw exceptions::TreeDocumentError(
1071 "Tried to register node '" + node_name + "' (Class: " + params.class_name +
1072 ") which is already known. You must make sure that the registration names are unique or explicitly allow "
1073 "overriding previously registered nodes with the same name by setting override=true.");
1074 }
1075 }
1076
1077 // Check if the class we search for is actually available with the loader.
1078 if (!tree_node_loader_ptr_->isClassAvailable(params.class_name)) {
1079 if (all_node_classes_package_map_.find(params.class_name) == all_node_classes_package_map_.end()) {
1080 throw exceptions::TreeDocumentError(
1081 "Node '" + node_name + " (" + params.class_name +
1082 ")' cannot be registered, because the class name is not known to the class loader. "
1083 "Make sure that it's spelled correctly and registered by calling "
1084 "auto_apms_behavior_tree_register_nodes() in the CMakeLists.txt of the "
1085 "corresponding package.");
1086 }
1087 throw exceptions::TreeDocumentError(
1088 "Node '" + node_name + " (" + params.class_name +
1089 ")' cannot be registered, because the corresponding resource belongs to excluded package '" +
1090 all_node_classes_package_map_.at(params.class_name) + "'.");
1091 }
1092
1093 pluginlib::UniquePtr<NodeRegistrationInterface> plugin_instance;
1094 try {
1095 plugin_instance = tree_node_loader_ptr_->createUniqueInstance(params.class_name);
1096 } catch (const pluginlib::CreateClassException & e) {
1097 throw pluginlib::CreateClassException(
1098 "Failed to create an instance of node '" + node_name + " (" + params.class_name +
1099 ")'. Remember that the AUTO_APMS_BEHAVIOR_TREE_REGISTER_NODE "
1100 "macro must be called in the source file for the node class to be discoverable. "
1101 "Error message: " +
1102 e.what() + ".");
1103 }
1104
1105 try {
1106 if (plugin_instance->requiresRosNodeContext()) {
1107 if (only_non_ros_nodes_) {
1108 throw exceptions::NodeRegistrationError(
1109 "Node '" + node_name +
1110 "' relies on ROS 2 functionality but this instance only allows to use non-ROS nodes.");
1111 }
1112 RosNodeContext ros_node_context(
1113 ros_node_wptr_.lock(), tree_node_waitables_callback_group_wptr_.lock(),
1114 tree_node_waitables_executor_wptr_.lock(), params);
1115 plugin_instance->registerWithBehaviorTreeFactory(factory_, node_name, &ros_node_context);
1116 } else {
1117 // Create a dummy object of RosNodeContext to allow for parsing the registration params nevertheless
1118 RosNodeContext ros_node_context(nullptr, nullptr, nullptr, params);
1119 plugin_instance->registerWithBehaviorTreeFactory(factory_, node_name, &ros_node_context);
1120 }
1121 } catch (const std::exception & e) {
1122 throw exceptions::NodeRegistrationError(
1123 "Cannot register node '" + node_name + " (" + params.class_name + ")': " + e.what() + ".");
1124 }
1125 registered_nodes_manifest_.add(node_name, params);
1126 }
1127 return *this;
1128}
1129
1130std::set<std::string> TreeDocument::getRegisteredNodeNames(bool include_native) const
1131{
1132 std::set<std::string> names;
1133 if (include_native) names = native_node_names_;
1134 for (const auto & [name, _] : registered_nodes_manifest_.map()) names.insert(name);
1135 return names;
1136}
1137
1139{
1140 NodeManifest m;
1141 TreeDocument * doc = const_cast<TreeDocument *>(this);
1142 for (const std::string & tree_name : getAllTreeNames()) {
1143 XMLElement * ptr = const_cast<XMLElement *>(getXMLElementForTreeWithName(tree_name));
1144 const TreeElement ele(doc, ptr);
1145 m.merge(ele.getRequiredNodeManifest(), true);
1146 }
1147 return m;
1148}
1149
1151{
1152 // Create or get the TreeNodesModel element
1153 tinyxml2::XMLElement * model_root = RootElement()->FirstChildElement(TREE_NODE_MODEL_ELEMENT_NAME);
1154
1155 // If no TreeNodesModel element exists, create one
1156 if (!model_root) {
1157 model_root = NewElement(TREE_NODE_MODEL_ELEMENT_NAME);
1158 RootElement()->InsertEndChild(model_root);
1159 }
1160
1161 // Iterate through the model_map and create XML elements for each node
1162 for (const auto & [node_name, model] : model_map) {
1163 // Create the element with the node type as the tag name
1164 tinyxml2::XMLElement * node_element = NewElement(BT::toStr(model.type).c_str());
1165 node_element->SetAttribute("ID", node_name.c_str());
1166
1167 // Add port information
1168 for (const auto & port_info : model.port_infos) {
1169 tinyxml2::XMLElement * port_element = nullptr;
1170
1171 // Create the appropriate port element based on direction
1172 switch (port_info.port_direction) {
1173 case BT::PortDirection::INPUT:
1174 port_element = NewElement("input_port");
1175 break;
1176 case BT::PortDirection::OUTPUT:
1177 port_element = NewElement("output_port");
1178 break;
1179 case BT::PortDirection::INOUT:
1180 port_element = NewElement("inout_port");
1181 break;
1182 }
1183
1184 // Set port attributes
1185 port_element->SetAttribute("name", port_info.port_name.c_str());
1186
1187 if (!port_info.port_type.empty()) {
1188 port_element->SetAttribute("type", port_info.port_type.c_str());
1189 }
1190
1191 if (port_info.port_has_default) {
1192 port_element->SetAttribute("default", port_info.port_default.c_str());
1193 }
1194
1195 // Set port description as text content
1196 if (!port_info.port_description.empty()) {
1197 port_element->SetText(port_info.port_description.c_str());
1198 }
1199
1200 node_element->InsertEndChild(port_element);
1201 }
1202
1203 model_root->InsertEndChild(node_element);
1204 }
1205
1206 return *this;
1207}
1208
1209NodeModelMap TreeDocument::getNodeModel(tinyxml2::XMLDocument & doc, const NodeManifest & manifest)
1210{
1211 const tinyxml2::XMLElement * root = doc.RootElement();
1212 if (!root) {
1213 throw exceptions::TreeDocumentError("Node model document has no root element.");
1214 }
1215 if (const char * ver = root->Attribute(TreeDocument::BTCPP_FORMAT_ATTRIBUTE_NAME)) {
1216 const std::string expected_format = TreeDocument::BTCPP_FORMAT_DEFAULT_VERSION;
1217 if (std::string(ver) != expected_format) {
1218 throw exceptions::TreeDocumentError(
1219 "Cannot parse node model document: Format of model document (" +
1220 std::string(TreeDocument::BTCPP_FORMAT_ATTRIBUTE_NAME) + ": " + ver +
1221 ") doesn't comply with the expected format (" + std::string(TreeDocument::BTCPP_FORMAT_ATTRIBUTE_NAME) + ": " +
1222 expected_format + ").");
1223 }
1224 } else {
1225 throw exceptions::TreeDocumentError(
1226 "Cannot parse node model document: Root element of model document doesn't have required attribute '" +
1227 std::string(TreeDocument::BTCPP_FORMAT_ATTRIBUTE_NAME) + "'.");
1228 }
1229 tinyxml2::XMLElement * model_ele = doc.RootElement()->FirstChildElement(TreeDocument::TREE_NODE_MODEL_ELEMENT_NAME);
1230 if (!model_ele) {
1231 throw exceptions::TreeDocumentError(
1232 "Element <" + std::string(TreeDocument::TREE_NODE_MODEL_ELEMENT_NAME) +
1233 "> doesn't exist in node model document.");
1234 }
1235
1236 NodeModelMap model_map;
1237 for (tinyxml2::XMLElement * ele = model_ele->FirstChildElement(); ele != nullptr; ele = ele->NextSiblingElement()) {
1238 const char * node_name = ele->Attribute("ID");
1239 if (!node_name) {
1240 throw exceptions::TreeDocumentError(
1241 "Element '" + std::string(ele->Name()) + "' in node model document is missing the required attribute 'ID'");
1242 }
1243 NodeModel & model = model_map[node_name];
1244 model.type = BT::convertFromString<BT::NodeType>(ele->Name());
1245 for (const tinyxml2::XMLElement * port_ele = ele->FirstChildElement(); port_ele != nullptr;
1246 port_ele = port_ele->NextSiblingElement()) {
1247 const std::string direction = port_ele->Name();
1248 NodePortInfo port_info;
1249 if (direction == "input_port") {
1250 port_info.port_direction = BT::PortDirection::INPUT;
1251 } else if (direction == "output_port") {
1252 port_info.port_direction = BT::PortDirection::OUTPUT;
1253 } else if (direction == "inout_port") {
1254 port_info.port_direction = BT::PortDirection::INOUT;
1255 } else {
1256 throw exceptions::TreeDocumentError(
1257 "Unkown port direction in node model for '" + std::string(node_name) + "': " + direction);
1258 }
1259 if (const char * c = port_ele->Attribute("name")) {
1260 port_info.port_name = c;
1261 }
1262 if (const char * c = port_ele->Attribute("type")) {
1263 port_info.port_type = c;
1264 }
1265 if (const char * c = port_ele->Attribute("default")) {
1266 port_info.port_has_default = true;
1267 port_info.port_default = c;
1268 } else {
1269 port_info.port_has_default = false;
1270 }
1271 if (const char * c = port_ele->GetText()) {
1272 port_info.port_description = c;
1273 }
1274 model.port_infos.push_back(std::move(port_info));
1275 }
1276 // Port infos may be empty if there are no ports
1277 }
1278
1279 // Collect hidden ports from the node manifest
1280 std::map<std::string, std::vector<std::string>> hidden_ports;
1281 for (const auto & [node_name, registration_options] : manifest.map()) {
1282 // Extract hidden_ports from registration options
1283 for (const std::string & port_name : registration_options.hidden_ports) {
1284 hidden_ports[node_name].push_back(port_name);
1285 }
1286
1287 // Implicitly hide ports that are specified in port_alias
1288 for (const auto & [port_name, _] : registration_options.port_alias) {
1289 hidden_ports[node_name].push_back(port_name);
1290 }
1291 }
1292
1293 // Apply port hiding
1294 for (const auto & [node_name, ports_to_hide] : hidden_ports) {
1295 auto it = model_map.find(node_name);
1296 if (it == model_map.end()) {
1297 continue;
1298 }
1299 NodeModel & model = it->second;
1300 model.port_infos.erase(
1301 std::remove_if(
1302 model.port_infos.begin(), model.port_infos.end(),
1303 [&ports_to_hide](const NodePortInfo & port_info) {
1304 return std::find(ports_to_hide.begin(), ports_to_hide.end(), port_info.port_name) != ports_to_hide.end();
1305 }),
1306 model.port_infos.end());
1307 }
1308
1309 return model_map;
1310}
1311
1313{
1314 // Generate XML from the factory
1315 std::string model_xml;
1316 try {
1317 model_xml = BT::writeTreeNodesModelXML(factory_, include_native);
1318 } catch (const std::exception & e) {
1319 throw exceptions::TreeDocumentError("Error generating node model XML from factory: " + std::string(e.what()));
1320 }
1321
1322 // Parse the XML
1323 tinyxml2::XMLDocument model_doc;
1324 if (model_doc.Parse(model_xml.c_str()) != tinyxml2::XMLError::XML_SUCCESS) {
1325 throw exceptions::TreeDocumentError(
1326 "Error parsing the model of the currently registered nodes: " + std::string(model_doc.ErrorStr()));
1327 }
1328
1329 // Get the node model with hidden ports applied
1330 return getNodeModel(model_doc, registered_nodes_manifest_);
1331}
1332
1333BT::Result TreeDocument::verify() const
1334{
1335 NodeModelMap model_map = getNodeModel(true);
1336 std::unordered_map<std::string, BT::NodeType> registered_nodes;
1337 for (const auto & [node_name, model] : model_map) {
1338 registered_nodes[node_name] = model.type;
1339 }
1340 try {
1341 BT::VerifyXML(writeToString(), registered_nodes);
1342 } catch (const BT::RuntimeError & e) {
1343 return nonstd::make_unexpected(e.what());
1344 }
1345 return {};
1346}
1347
1349{
1350 tinyxml2::XMLPrinter printer;
1351 Print(&printer);
1352 return printer.CStr();
1353}
1354
1355void TreeDocument::writeToFile(const std::string & path) const
1356{
1357 XMLDocument doc;
1358 DeepCopy(&doc);
1359 tinyxml2::XMLError result = doc.SaveFile(path.c_str());
1360 if (result != tinyxml2::XML_SUCCESS) {
1361 throw exceptions::TreeDocumentError(
1362 "Failed to write tree document to file. Error ID: " + std::string(doc.ErrorIDToName(result)));
1363 }
1364}
1365
1367{
1368 Clear();
1369 tinyxml2::XMLElement * root_ele = NewElement(TreeDocument::ROOT_ELEMENT_NAME);
1370 root_ele->SetAttribute(TreeDocument::BTCPP_FORMAT_ATTRIBUTE_NAME, format_version_.c_str());
1371 InsertFirstChild(root_ele);
1372 return *this;
1373}
1374
1375const TreeDocument::XMLElement * TreeDocument::getXMLElementForTreeWithName(const std::string & tree_name) const
1376{
1377 return getXMLElementForTreeWithNameImpl<const XMLElement *>(*this, tree_name);
1378}
1379
1380TreeDocument::XMLElement * TreeDocument::getXMLElementForTreeWithName(const std::string & tree_name)
1381{
1382 return getXMLElementForTreeWithNameImpl<XMLElement *>(*this, tree_name);
1383}
1384
1385template <typename ReturnT, typename DocumentT>
1386ReturnT TreeDocument::getXMLElementForTreeWithNameImpl(DocumentT & doc, const std::string & tree_name)
1387{
1388 if (tree_name.empty()) {
1389 throw exceptions::TreeDocumentError("Cannot get tree with an empty name.");
1390 }
1391 if (!doc.hasTreeName(tree_name)) {
1392 throw exceptions::TreeDocumentError("Cannot get tree with name '" + tree_name + "' because it doesn't exist.");
1393 }
1394 auto child = doc.RootElement()->FirstChildElement(TreeDocument::TREE_ELEMENT_NAME);
1395 while (child && !child->Attribute(TreeDocument::TREE_NAME_ATTRIBUTE_NAME, tree_name.c_str())) {
1396 child = child->NextSiblingElement();
1397 }
1398 if (!child) {
1399 throw std::logic_error(
1400 "Unexpected error trying to get tree element with name '" + tree_name +
1401 "'. Since hasTreeName() returned true, there MUST be a corresponding element.");
1402 }
1403 return child;
1404}
1405
1406} // 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
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.
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 & removeFirstChild(const std::string &registration_name="", const std::string &instance_name="")
Recursively visit this node's children in execution order and remove the first node with a particular...
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 getFirstNode(const std::string &registration_name="", const std::string &instance_name="") const
Recursively visit this node's children in execution order and get the first node with a particular re...
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.
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 insertNode(const std::string &name, const NodeElement *before_this=nullptr)
Add a new node to the children of this node.
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(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 & removeFirstChild(const std::string &registration_name="", const std::string &instance_name="")
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....