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 <cstdio>
19#include <filesystem>
20
21#include "ament_index_cpp/get_package_prefix.hpp"
22#include "ament_index_cpp/get_package_share_path.hpp"
23#include "auto_apms_behavior_tree_core/builder.hpp"
24#include "auto_apms_behavior_tree_core/exceptions.hpp"
25#include "auto_apms_behavior_tree_core/node/node_model_type.hpp"
26#include "auto_apms_behavior_tree_core/tree/tree_resource.hpp"
27#include "auto_apms_util/container.hpp"
28#include "auto_apms_util/logging.hpp"
29#include "auto_apms_util/string.hpp"
30#include "behaviortree_cpp/xml_parsing.h"
31#include "yaml-cpp/yaml.h"
32
34{
35
36std::vector<std::string> getAllTreeNamesImpl(const tinyxml2::XMLDocument & doc)
37{
38 std::vector<std::string> names;
39 if (const tinyxml2::XMLElement * root = doc.RootElement()) {
40 if (strcmp(root->Name(), TreeDocument::ROOT_ELEMENT_NAME) == 0) {
41 // Found root element
42 for (const tinyxml2::XMLElement * child = root->FirstChildElement(); child != nullptr;
43 child = child->NextSiblingElement()) {
44 if (strcmp(TreeDocument::TREE_ELEMENT_NAME, child->Name()) == 0) {
45 if (const char * name = child->Attribute(TreeDocument::TREE_NAME_ATTRIBUTE_NAME)) {
46 names.push_back(name);
47 } else {
48 throw exceptions::TreeDocumentError(
49 "Cannot get tree name, because required attribute '" +
50 std::string(TreeDocument::TREE_NAME_ATTRIBUTE_NAME) + "' is missing.");
51 }
52 }
53 }
54 } else if (strcmp(root->Name(), TreeDocument::TREE_ELEMENT_NAME) == 0) {
55 // Found behavior tree element as root
56 if (const char * name = root->Attribute(TreeDocument::TREE_NAME_ATTRIBUTE_NAME)) names.push_back(name);
57 }
58 }
59 return names;
60}
61
62bool hasInlineRegistrationOptions(const tinyxml2::XMLElement * ele)
63{
64 for (const tinyxml2::XMLAttribute * attr = ele->FirstAttribute(); attr != nullptr; attr = attr->Next()) {
65 if (std::string_view(attr->Name()).rfind(TreeDocument::INLINE_NODE_REGISTRATION_OPTIONS_ATTRIBUTE_PREFIX, 0) == 0)
66 return true;
67 }
68 return false;
69}
70
71std::optional<NodeRegistrationOptions> parseInlineRegistrationOptions(const tinyxml2::XMLElement * ele)
72{
73 YAML::Node reg_opts_node(YAML::NodeType::Map);
74
75 for (const tinyxml2::XMLAttribute * attr = ele->FirstAttribute(); attr != nullptr; attr = attr->Next()) {
76 if (const std::string attr_name = attr->Name();
77 attr_name.rfind(TreeDocument::INLINE_NODE_REGISTRATION_OPTIONS_ATTRIBUTE_PREFIX, 0) == 0) {
78 const std::string option_name =
79 attr_name.substr(strlen(TreeDocument::INLINE_NODE_REGISTRATION_OPTIONS_ATTRIBUTE_PREFIX));
80 try {
81 reg_opts_node[option_name] = YAML::Load(attr->Value());
82 } catch (const std::exception &) {
83 return std::nullopt;
84 }
85 }
86 }
87
88 try {
89 NodeRegistrationOptions options = reg_opts_node.as<NodeRegistrationOptions>();
90 return options.valid() ? std::optional(options) : std::nullopt;
91 } catch (const std::exception &) {
92 return std::nullopt;
93 }
94}
95
97: doc_ptr_(doc_ptr), ele_ptr_(ele_ptr)
98{
99 if (!doc_ptr) {
100 throw exceptions::TreeDocumentError("Cannot create an instance of NodeElement with doc_ptr=nullptr.");
101 }
102 if (!ele_ptr) {
103 throw exceptions::TreeDocumentError("Cannot create an instance of NodeElement with ele_ptr=nullptr.");
104 }
105
106 // There only is a model for this node if a corresponding plugin has been registered previsouly. So if the
107 // constructor is called e.g. before the document registered the respective node, that's not the case and for example
108 // getPorts() or setPorts() won't work as expected. This is a deliberate design decision though. We want the
109 // constructor to be cheap, tolerant, and it shouldn't have registration side effects. It's the developer's
110 // responsibility to coordinate registration properly. IMPORTANT: We must not throw here, since the constructor must
111 // also succeed for the derived TreeElement class for which there is no node model and getPorts() and setPorts() is
112 // deleted anyways. Additionally, we want to allow traversing a document with unregistered nodes.
113 refreshPortInfoCache();
114}
115
116void TreeDocument::NodeElement::refreshPortInfoCache() const
117{
118 const NodeModelMap model = doc_ptr_->getNodeModel(true);
119 NodeModelMap::const_iterator it = model.find(ele_ptr_->Name());
120
121 std::vector<std::string> refreshed_port_names;
122 PortValues refreshed_port_default_values;
123 if (it != model.end()) {
124 const std::vector<NodePortInfo> & port_infos_str_vec = it->second.port_infos;
125 refreshed_port_names.reserve(port_infos_str_vec.size());
126 for (const NodePortInfo & port_info : port_infos_str_vec) {
127 refreshed_port_names.push_back(port_info.port_name);
128 if (!port_info.port_default.empty()) {
129 refreshed_port_default_values[port_info.port_name] = port_info.port_default;
130 }
131 }
132 }
133
134 if (refreshed_port_names == port_names_ && refreshed_port_default_values == port_default_values_) {
135 return;
136 }
137
138 port_names_ = std::move(refreshed_port_names);
139 port_default_values_ = std::move(refreshed_port_default_values);
140
141 // Set the node's attributes according to the default values for all the ports that have a default value and have
142 // not been set previously.
143 if (!port_default_values_.empty()) {
144 PortValues existing_port_values;
145 for (const tinyxml2::XMLAttribute * attr = ele_ptr_->FirstAttribute(); attr != nullptr; attr = attr->Next()) {
146 if (const std::string attr_name = attr->Name(); auto_apms_util::contains(port_names_, attr_name)) {
147 existing_port_values[attr_name] = attr->Value();
148 }
149 }
150 for (const auto & [name, val] : port_default_values_) {
151 if (existing_port_values.find(name) == existing_port_values.end()) {
152 ele_ptr_->SetAttribute(name.c_str(), val.c_str());
153 }
154 }
155 }
156}
157
159{
160 XMLElement * other_ele = other.ele_ptr_;
161 if (ele_ptr_ != other_ele) {
162 // Copy the other element and all its children
163 other_ele = other_ele->DeepClone(doc_ptr_)->ToElement();
164 // Insert new element in place of this
165 XMLElement * prev = ele_ptr_->PreviousSiblingElement();
166 if (prev) {
167 ele_ptr_->Parent()->InsertAfterChild(ele_ptr_->PreviousSibling(), other_ele);
168 } else {
169 ele_ptr_->Parent()->InsertFirstChild(other_ele);
170 }
171 // Delete this element
172 doc_ptr_->DeleteNode(ele_ptr_);
173 // Update the internal pointer
174 ele_ptr_ = other_ele;
175 }
176 port_names_ = other.port_names_;
177 port_default_values_ = other.port_default_values_;
178 return *this;
179}
180
181bool TreeDocument::NodeElement::operator==(const NodeElement & other) const { return ele_ptr_ == other.ele_ptr_; }
182
183bool TreeDocument::NodeElement::operator!=(const NodeElement & other) const { return !this->operator==(other); }
184
186 const std::string & name, const NodeElement * before_this)
187{
188 // No registration check — the node is inserted unconditionally. Port info is populated lazily
189 // once the node has been registered (consistent with the tolerant NodeElement constructor).
190 XMLElement * ele = doc_ptr_->NewElement(name.c_str());
191 return insertBeforeImpl(before_this, ele);
192}
193
195 const std::string & name, const NodeRegistrationOptions & registration_options, const NodeElement * before_this)
196{
197 // Check if the node name is known. If the user tries to register a node with the same name as one of the native
198 // nodes, we want registerNodes to throw since those names are reserved (Therefore we set include_native to false).
199 if (const std::set<std::string> names = doc_ptr_->getRegisteredNodeNames(false); names.find(name) == names.end()) {
200 doc_ptr_->registerNodes(NodeManifest({{name, registration_options}}));
201 }
202 return insertNode(name, before_this);
203}
204
206 const std::string & tree_name, const NodeElement * before_this)
207{
208 if (!doc_ptr_->hasTreeName(tree_name)) {
209 throw exceptions::TreeDocumentError(
210 "Cannot insert subtree node for tree '" + tree_name + "' because no tree with that name exists.");
211 }
212 NodeElement ele = insertNode(SUBTREE_ELEMENT_NAME, before_this);
213 ele.ele_ptr_->SetAttribute(TREE_NAME_ATTRIBUTE_NAME, tree_name.c_str());
214 return model::SubTree(ele.doc_ptr_, ele.ele_ptr_);
215}
216
218{
219 const std::string tree_name = tree.getName();
220 if (doc_ptr_->hasTreeName(tree_name)) {
221 // A tree with this name already exists in the document.
222 // If the provided tree element is from THIS document, we can compare directly using pointer equality.
223 // If it's from an external document, we compare the XML content to detect conflicting trees.
224 const TreeElement existing_tree = doc_ptr_->getTree(tree_name);
225 if (tree.doc_ptr_ == doc_ptr_) {
226 // Tree element is from this document - they must be the same tree element
227 if (existing_tree != tree) {
228 throw exceptions::TreeDocumentError(
229 "Cannot insert subtree node using the provided tree element, because another tree with name '" + tree_name +
230 "' already exists.");
231 }
232 } else {
233 // Tree element is from an external document - compare XML content
234 const std::string existing_xml = existing_tree.writeToString();
235 const std::string incoming_xml = tree.writeToString();
236 if (existing_xml != incoming_xml) {
237 throw exceptions::TreeDocumentError(
238 "Cannot insert subtree node using the provided tree element, because another tree with name '" + tree_name +
239 "' already exists with different content.");
240 }
241 }
242 // If we reach here, the trees are equivalent - proceed to insert the subtree node
243 } else {
244 // Add the tree provided as argument to the document
245 doc_ptr_->mergeTree(tree, false);
246 }
247 return insertSubTreeNode(tree_name, before_this);
248}
249
251 const TreeElement & tree, const NodeElement * before_this, bool auto_register_nodes)
252{
253 const XMLElement * root_child = tree.ele_ptr_->FirstChildElement();
254 if (!root_child) {
255 throw exceptions::TreeDocumentError(
256 "Cannot insert tree element '" + tree.getFullyQualifiedName() + "' because it has no child nodes.");
257 }
258 if (root_child->NextSibling()) {
259 throw exceptions::TreeDocumentError(
260 "Cannot insert tree element '" + tree.getFullyQualifiedName() + "' because it has more than one child node.");
261 }
262 return insertTreeFromDocument(*tree.doc_ptr_, tree.getName(), before_this, auto_register_nodes);
263}
264
266 const TreeDocument & doc, const std::string & tree_name, const NodeElement * before_this, bool auto_register_nodes)
267{
268 const std::vector<std::string> other_tree_names = doc.getAllTreeNames();
269 if (other_tree_names.empty()) {
270 throw exceptions::TreeDocumentError(
271 "Cannot insert tree '" + tree_name + "' because document has no <" + TREE_ELEMENT_NAME + "> elements.");
272 }
273 if (!auto_apms_util::contains(other_tree_names, tree_name)) {
274 throw exceptions::TreeDocumentError(
275 "Cannot insert tree '" + tree_name + "' because document doesn't specify a tree with that name.");
276 }
277
278 if (auto_register_nodes) {
279 // We register all associated node plugins beforehand, so that the user doesn't have to do that manually. This
280 // means, that also potentially unused nodes are available and registered with the factory. This seems unnecessary,
281 // but it's very convenient and the performance probably doesn't suffer too much.
282 doc_ptr_->registerNodes(doc.getRequiredNodeManifest(tree_name), false);
283 }
284
285 // List including the target tree name and the names of its dependencies (Trees required by SubTree nodes)
286 std::set<std::string> required_tree_names = {tree_name};
287
288 // Find all subtree nodes and push back the associated tree to required_tree_names.
289 // NOTE: All tree elements within a TreeDocument instance always have exactly one child, so we don't have to verify
290 // that here again
291 TreeDocument temp_doc(doc_ptr_->format_version_, doc_ptr_->tree_node_loader_ptr_); // Temporary working document
292 doc.DeepCopy(&temp_doc);
293 const TreeElement other_tree(&temp_doc, temp_doc.getXMLElementForTreeWithName(tree_name));
294 ConstDeepApplyCallback collect_dependency_tree_names;
295 collect_dependency_tree_names = [&required_tree_names, &collect_dependency_tree_names](const NodeElement & ele) {
296 if (ele.getRegistrationName() == SUBTREE_ELEMENT_NAME) {
297 if (const char * name = ele.ele_ptr_->Attribute(TREE_NAME_ATTRIBUTE_NAME)) {
298 // Add the tree name to the list
299 required_tree_names.insert(name);
300
301 // Search for more tree dependencies under the tree pointed by this subtree node
302 ele.doc_ptr_->getTree(name).deepApplyConst(collect_dependency_tree_names);
303 } else {
304 throw exceptions::TreeDocumentError("Subtree element has no name attribute.");
305 };
306 }
307 // The return value doesn't matter here
308 return false;
309 };
310 other_tree.deepApplyConst(collect_dependency_tree_names);
311
312 // Verify that all child nodes of the trees listed in required_tree_names are known to the builder
313 ConstDeepApplyCallback apply = [available_names = doc_ptr_->getRegisteredNodeNames(true)](const NodeElement & ele) {
314 return available_names.find(ele.getRegistrationName()) == available_names.end();
315 };
316 for (const std::string & name : required_tree_names) {
317 const NodeElement ele(&temp_doc, temp_doc.getXMLElementForTreeWithName(name)->FirstChildElement());
318 if (const std::vector<NodeElement> found = ele.deepApplyConst(apply); !found.empty()) {
319 std::vector<std::string> names;
320 for (const NodeElement & ele : found) names.push_back(ele.getFullyQualifiedName());
321 throw exceptions::TreeDocumentError(
322 "Cannot insert tree '" + tree_name + "' because the following nodes found in tree '" + name +
323 "' are unknown to the builder:\n\t- " + auto_apms_util::join(names, "\n\t- "));
324 }
325 }
326
327 // Remove the data from each tree that the target tree doesn't directly depend on and merge the working document. This
328 // approach ensures, that the verification logic inside merge() is applied to the working document.
329 required_tree_names.erase(tree_name); // Don't add the target tree twice
330 for (const std::string & name : other_tree_names) {
331 if (required_tree_names.find(name) == required_tree_names.end()) {
332 // Remove if not required
333 temp_doc.removeTree(name);
334 }
335 }
336 doc_ptr_->mergeTreeDocument(static_cast<const XMLDocument &>(temp_doc), false);
337
338 // Clone and insert the target tree
339 return insertBeforeImpl(
340 before_this, doc.getXMLElementForTreeWithName(tree_name)->FirstChildElement()->DeepClone(doc_ptr_)->ToElement());
341}
342
344 const TreeDocument & doc, const NodeElement * before_this, bool auto_register_nodes)
345{
346 return insertTreeFromDocument(doc, doc.getRootTreeName(), before_this, auto_register_nodes);
347}
348
350 const std::string & tree_str, const std::string & tree_name, const NodeElement * before_this)
351{
352 TreeDocument insert_doc(doc_ptr_->format_version_, doc_ptr_->tree_node_loader_ptr_);
353 insert_doc.mergeString(tree_str);
354 return insertTreeFromDocument(insert_doc, tree_name, before_this);
355}
356
358 const std::string & tree_str, const NodeElement * before_this)
359{
360 TreeDocument insert_doc(doc_ptr_->format_version_, doc_ptr_->tree_node_loader_ptr_);
361 insert_doc.mergeString(tree_str);
362 return insertTreeFromDocument(insert_doc, before_this);
363}
364
366 const std::string & path, const std::string & tree_name, const NodeElement * before_this)
367{
368 TreeDocument insert_doc(doc_ptr_->format_version_, doc_ptr_->tree_node_loader_ptr_);
369 insert_doc.mergeFile(path);
370 return insertTreeFromDocument(insert_doc, tree_name, before_this);
371}
372
374 const std::string & path, const NodeElement * before_this)
375{
376 TreeDocument insert_doc(doc_ptr_->format_version_, doc_ptr_->tree_node_loader_ptr_);
377 insert_doc.mergeFile(path);
378 return insertTreeFromDocument(insert_doc, before_this);
379}
380
382 const TreeResource & resource, const std::string & tree_name, const NodeElement * before_this,
383 bool auto_register_nodes)
384{
385 TreeDocument insert_doc(doc_ptr_->format_version_, doc_ptr_->tree_node_loader_ptr_);
386 insert_doc.mergeFile(resource.build_request_file_path_);
387 return insertTreeFromDocument(insert_doc, tree_name, before_this, auto_register_nodes);
388}
389
391 const TreeResource & resource, const NodeElement * before_this, bool auto_register_nodes)
392{
393 return insertTreeFromResource(resource, resource.getRootTreeName(), before_this, auto_register_nodes);
394}
395
396bool TreeDocument::NodeElement::hasChildren() const { return ele_ptr_->FirstChild() == nullptr ? false : true; }
397
399 const std::string & registration_name, const std::string & instance_name, bool deep_search) const
400{
401 if (registration_name.empty() && instance_name.empty()) return NodeElement(doc_ptr_, ele_ptr_->FirstChildElement());
402
403 ConstDeepApplyCallback apply = [&registration_name, &instance_name](const NodeElement & ele) {
404 if (registration_name.empty()) return ele.getName() == instance_name;
405 if (instance_name.empty()) return ele.getRegistrationName() == registration_name;
406 return ele.getRegistrationName() == registration_name && ele.getName() == instance_name;
407 };
408
409 if (deep_search) {
410 if (const std::vector<NodeElement> found = deepApplyConst(apply); !found.empty()) return found[0];
411 } else {
412 for (auto child : *this) {
413 if (apply(child)) return child;
414 }
415 }
416
417 // Cannot find node in children of this
418 throw exceptions::TreeDocumentError(
419 "Cannot find a child node that matches the search arguments (registration_name: '" + registration_name +
420 "' - instance_name: '" + instance_name + "') in parent element '" + getFullyQualifiedName() + "'.");
421}
422
424 const std::string & registration_name, const std::string & instance_name, bool deep_search)
425{
426 XMLElement * found = getFirstNode(registration_name, instance_name, deep_search).ele_ptr_;
427 found->Parent()->DeleteChild(found);
428 return *this;
429}
430
432{
433 ele_ptr_->DeleteChildren();
434 return *this;
435}
436
437const std::vector<std::string> & TreeDocument::NodeElement::getPortNames() const
438{
439 refreshPortInfoCache();
440 return port_names_;
441}
442
444{
445 refreshPortInfoCache();
446 PortValues values;
447 for (const tinyxml2::XMLAttribute * attr = ele_ptr_->FirstAttribute(); attr != nullptr; attr = attr->Next()) {
448 if (const std::string attr_name = attr->Name(); auto_apms_util::contains(port_names_, attr_name)) {
449 values[attr_name] = attr->Value();
450 }
451 }
452 return values;
453}
454
455std::optional<NodeRegistrationOptions> TreeDocument::NodeElement::getInlineRegistrationOptions() const
456{
457 return parseInlineRegistrationOptions(ele_ptr_);
458}
459
461{
462 refreshPortInfoCache();
463
464 // Verify port_values
465 std::vector<std::string> unknown_keys;
466 for (const auto & [key, _] : port_values) {
467 if (!auto_apms_util::contains(port_names_, key)) unknown_keys.push_back(key);
468 }
469 if (!unknown_keys.empty()) {
470 throw exceptions::TreeDocumentError(
471 "Cannot set ports. According to the node model, the following ports are not implemented by '" +
472 std::string(ele_ptr_->Name()) + "': [ " + auto_apms_util::join(unknown_keys, ", ") + " ].");
473 }
474
475 // Populate attributes according to the content of port_values
476 for (const auto & [key, val] : port_values) {
477 ele_ptr_->SetAttribute(key.c_str(), val.c_str());
478 }
479 return *this;
480}
481
483 const NodeRegistrationOptions & registration_options)
484{
485 if (!registration_options.valid()) {
486 throw exceptions::TreeDocumentError(
487 "Cannot set inline registration options for node '" + getFullyQualifiedName() +
488 "': Registration options are invalid.");
489 }
490 const YAML::Node node = YAML::convert<NodeRegistrationOptions>::encode(registration_options);
491 for (YAML::const_iterator it = node.begin(); it != node.end(); ++it) {
492 const std::string attr_name =
493 std::string(INLINE_NODE_REGISTRATION_OPTIONS_ATTRIBUTE_PREFIX) + it->first.as<std::string>();
494 // Use flow notation so that sequences and maps are encoded as compact JSON (e.g. [a, b] / {k: v}),
495 // which is valid YAML and can be parsed back with YAML::Load()
496 YAML::Emitter emitter;
497 emitter << YAML::Flow << it->second;
498 ele_ptr_->SetAttribute(attr_name.c_str(), emitter.c_str());
499 }
500 return *this;
501}
502
504{
505 const std::string name = getRegistrationName();
506 if (!doc_ptr_->registered_nodes_manifest_.contains(name)) {
507 throw exceptions::TreeDocumentError(
508 "Cannot set inline registration options for node '" + getFullyQualifiedName() + "': Node '" + name +
509 "' has not been registered with the parent document.");
510 }
511 return setInlineRegistrationOptions(doc_ptr_->registered_nodes_manifest_[name]);
512}
513
515{
516 refreshPortInfoCache();
517 for (const std::string & name : port_names_) {
518 PortValues::const_iterator it = port_default_values_.find(name);
519 if (it != port_default_values_.end()) {
520 ele_ptr_->SetAttribute(it->first.c_str(), it->second.c_str());
521 } else {
522 ele_ptr_->DeleteAttribute(name.c_str());
523 }
524 }
525 return *this;
526}
527
529{
530 ele_ptr_->SetAttribute(BT::toStr(type).c_str(), script.str().c_str());
531 return *this;
532}
533
535{
536 ele_ptr_->SetAttribute(BT::toStr(type).c_str(), script.str().c_str());
537 return *this;
538}
539
541{
542 ele_ptr_->SetAttribute(NODE_INSTANCE_NAME_ATTRIBUTE_NAME, instance_name.c_str());
543 return *this;
544}
545
546std::string TreeDocument::NodeElement::getRegistrationName() const { return ele_ptr_->Name(); }
547
549{
550 if (const char * name = ele_ptr_->Attribute(NODE_INSTANCE_NAME_ATTRIBUTE_NAME)) return name;
551 return getRegistrationName();
552}
553
555{
556 std::string registration_name = getRegistrationName();
557 std::string instance_name = getName();
558 if (registration_name == instance_name) return registration_name;
559 return instance_name + " (" + registration_name + ")";
560}
561
563
564TreeDocument::XMLElement * TreeDocument::NodeElement::getXMLElement() { return ele_ptr_; }
565
566const std::vector<TreeDocument::NodeElement> TreeDocument::NodeElement::deepApplyConst(
567 ConstDeepApplyCallback apply_callback) const
568{
569 std::vector<NodeElement> found;
570 deepApplyImpl(*this, apply_callback, found);
571 return found;
572}
573
574std::vector<TreeDocument::NodeElement> TreeDocument::NodeElement::deepApply(DeepApplyCallback apply_callback)
575{
576 std::vector<NodeElement> found;
577 deepApplyImpl(*this, apply_callback, found);
578 return found;
579}
580
581TreeDocument::NodeElement TreeDocument::NodeElement::insertBeforeImpl(
582 const NodeElement * before_this, XMLElement * add_this)
583{
584 if (before_this) {
585 XMLElement * prev = nullptr;
586 XMLElement * curr = ele_ptr_->FirstChildElement();
587
588 // Traverse through siblings of first child to find the before_this node.
589 // If there are no siblings, add the first child to this;
590 bool found = false;
591 while (curr) {
592 if (curr == before_this->ele_ptr_) {
593 found = true;
594 break;
595 }
596 prev = curr;
597 curr = curr->NextSiblingElement();
598 }
599 if (prev) {
600 if (found) {
601 ele_ptr_->InsertAfterChild(prev, add_this);
602 } else {
603 throw exceptions::TreeDocumentError(
604 "NodeElement before_this (" + before_this->getFullyQualifiedName() + ") is not a child of " +
605 getFullyQualifiedName() + ".");
606 }
607 } else {
608 ele_ptr_->InsertFirstChild(add_this);
609 }
610 } else {
611 ele_ptr_->InsertEndChild(add_this);
612 }
613 return NodeElement(doc_ptr_, add_this);
614}
615
616void TreeDocument::NodeElement::deepApplyImpl(
617 const NodeElement & parent, ConstDeepApplyCallback apply_callback, std::vector<NodeElement> & vec)
618{
619 for (XMLElement * child = parent.ele_ptr_->FirstChildElement(); child != nullptr;
620 child = child->NextSiblingElement()) {
621 const NodeElement child_ele(parent.doc_ptr_, child);
622
623 // Apply on current child element
624 if (apply_callback(child_ele)) vec.push_back(child_ele);
625
626 // Search children of child before evaluating the siblings
627 deepApplyImpl(child_ele, apply_callback, vec);
628 }
629}
630
631void TreeDocument::NodeElement::deepApplyImpl(
632 NodeElement & parent, DeepApplyCallback apply_callback, std::vector<NodeElement> & vec)
633{
634 for (XMLElement * child = parent.ele_ptr_->FirstChildElement(); child != nullptr;
635 child = child->NextSiblingElement()) {
636 NodeElement child_ele(parent.doc_ptr_, child);
637
638 // Apply on current child element
639 if (apply_callback(child_ele)) vec.push_back(child_ele);
640
641 // Search children of child before evaluating the siblings
642 deepApplyImpl(child_ele, apply_callback, vec);
643 }
644}
645
646TreeDocument::NodeElement::ChildIterator::ChildIterator() : doc_ptr_(nullptr), current_(nullptr) {}
647
648TreeDocument::NodeElement::ChildIterator::ChildIterator(TreeDocument * doc_ptr, tinyxml2::XMLElement * current)
649: doc_ptr_(doc_ptr), current_(current)
650{
651}
652
653TreeDocument::NodeElement::ChildIterator::value_type TreeDocument::NodeElement::ChildIterator::operator*() const
654{
655 return NodeElement(doc_ptr_, current_);
656}
657
659{
660 if (current_) current_ = current_->NextSiblingElement();
661 return *this;
662}
663
670
671bool TreeDocument::NodeElement::ChildIterator::operator==(const ChildIterator & other) const
672{
673 return current_ == other.current_;
674}
675
676bool TreeDocument::NodeElement::ChildIterator::operator!=(const ChildIterator & other) const
677{
678 return current_ != other.current_;
679}
680
685
687
688TreeDocument::TreeElement::TreeElement(TreeDocument * doc_ptr, XMLElement * ele_ptr) : NodeElement(doc_ptr, ele_ptr)
689{
690 if (!ele_ptr->Attribute(TREE_NAME_ATTRIBUTE_NAME)) {
691 throw exceptions::TreeDocumentError(
692 "Cannot create tree element without a '" + std::string(TREE_NAME_ATTRIBUTE_NAME) + "' attribute.");
693 }
694}
695
697{
698 const std::string other_tree_name = other.getName();
699 // For a tree replacement to be allowed, the other tree name must be the same or at least not already taken by another
700 // tree inside this document.
701 if (getName() != other_tree_name && doc_ptr_->hasTreeName(other.getName())) {
702 throw exceptions::TreeDocumentError(
703 "Cannot copy tree '" + other.getName() + "' because another tree with this name already exists.");
704 }
706 return *this;
707}
708
710{
711 ele_ptr_->SetAttribute(TREE_NAME_ATTRIBUTE_NAME, tree_name.c_str());
712 return *this;
713}
714
716{
717 if (const char * name = ele_ptr_->Attribute(TREE_NAME_ATTRIBUTE_NAME)) return name;
718 return "unknown";
719}
720
722{
723 doc_ptr_->setRootTreeName(getName());
724 return *this;
725}
726
728{
729 NodeManifest m;
730 deepApplyConst([this, &m](const NodeElement & node) {
731 const std::string name = node.getRegistrationName();
732 const bool is_native_node = doc_ptr_->native_node_names_.find(name) != doc_ptr_->native_node_names_.end();
733 if (!is_native_node && !m.contains(name)) {
734 if (!doc_ptr_->registered_nodes_manifest_.contains(name)) {
735 throw exceptions::NodeManifestError(
736 "Cannot assemble the required node manifest for tree '" + getName() +
737 "' since there are no registration options for node '" + name + "'.");
738 }
739 m.add(name, doc_ptr_->registered_nodes_manifest_[name]);
740 }
741 return false;
742 });
743 return m;
744}
745
747{
748 deepApply([this](NodeElement & node) {
749 const std::string name = node.getRegistrationName();
750 const bool is_native_node = doc_ptr_->native_node_names_.find(name) != doc_ptr_->native_node_names_.end();
751 if (!is_native_node) {
753 }
754 return false;
755 });
756 return *this;
757}
758
760{
761 TreeDocument doc(doc_ptr_->format_version_, doc_ptr_->tree_node_loader_ptr_);
762 return doc.mergeTree(*this, true).verify();
763}
764
765std::string TreeDocument::TreeElement::writeToString(bool remove_whitespace) const
766{
767 XMLDocument tree_doc;
768 tree_doc.InsertEndChild(ele_ptr_->DeepClone(&tree_doc));
769 tinyxml2::XMLPrinter printer(nullptr, remove_whitespace);
770 tree_doc.Print(&printer);
771 return printer.CStr();
772}
773
775 const std::string & registration_name, const std::string & instance_name, bool deep_search)
776{
777 NodeElement::removeFirstChild(registration_name, instance_name, deep_search);
778 return *this;
779}
780
786
787TreeDocument::TreeDocument(const std::string & format_version, NodeRegistrationLoader::SharedPtr tree_node_loader)
788// It's important to initialize XMLDocument using PRESERVE_WHITESPACE, since encoded port data (like
789// NodeRegistrationOptions) may be sensitive to changes in the whitespaces (think of the YAML format).
790: XMLDocument(true, tinyxml2::PRESERVE_WHITESPACE),
791 all_node_classes_package_map_(auto_apms_behavior_tree::core::NodeRegistrationLoader().getClassPackageMap()),
792 native_node_names_(BT::BehaviorTreeFactory().builtinNodes()),
793 format_version_(format_version),
794 tree_node_loader_ptr_(tree_node_loader),
795 registered_nodes_manifest_(),
796 factory_(),
797 logger_(rclcpp::get_logger(LOGGER_NAME))
798{
799 reset();
800}
801
802TreeDocument & TreeDocument::mergeTreeDocumentImpl(
803 const XMLDocument & other, bool adopt_root_tree, std::set<std::string> & include_stack)
804{
805 const XMLElement * other_root = other.RootElement();
806 if (!other_root) {
807 throw exceptions::TreeDocumentError("Cannot merge tree documents: other_root is nullptr.");
808 };
809
810 auto verify_tree_structure = [](const XMLElement * tree_ele) {
811 const char * tree_id = tree_ele->Attribute(TREE_NAME_ATTRIBUTE_NAME);
812 if (!tree_id) {
813 throw exceptions::TreeDocumentError(
814 "Cannot merge tree document: Found a <" + std::string(TREE_ELEMENT_NAME) +
815 "> element that doesn't specify the required attribute '" + TREE_NAME_ATTRIBUTE_NAME + "'.");
816 }
817 const XMLElement * tree_root_child = tree_ele->FirstChildElement();
818 if (!tree_root_child) {
819 throw exceptions::TreeDocumentError(
820 "Cannot merge tree document: Tree '" + std::string(tree_id) + "' has no child nodes.");
821 }
822 if (tree_root_child->NextSibling()) {
823 throw exceptions::TreeDocumentError(
824 "Cannot merge tree document: Tree '" + std::string(tree_id) + "' has more than one child node.");
825 }
826 };
827
828 // Helper lambda to check for duplicate tree names and throw with a descriptive message
829 auto check_duplicates = [this](const std::vector<std::string> & new_tree_names, const std::string & source) {
830 const auto common = auto_apms_util::getCommonElements(getAllTreeNames(), new_tree_names);
831 if (!common.empty()) {
832 throw exceptions::TreeDocumentError(
833 "Cannot merge tree document: The following trees from " + source + " are already defined: [ " +
834 auto_apms_util::join(common, ", ") + " ].");
835 }
836 };
837
838 auto register_inline_nodes_for_tree = [this](const XMLElement * tree_ele) {
839 const char * tree_name_attr = tree_ele->Attribute(TREE_NAME_ATTRIBUTE_NAME);
840 const std::string tree_name = tree_name_attr ? tree_name_attr : "<unknown>";
841 NodeManifest inline_manifest;
842
843 auto collect_inline_registration_options = [&](auto && self, const XMLElement * node_ele) -> void {
844 if (!node_ele) {
845 return;
846 }
847
848 if (strcmp(node_ele->Name(), SUBTREE_ELEMENT_NAME) != 0 && hasInlineRegistrationOptions(node_ele)) {
849 const std::string registration_name = node_ele->Name();
850 const std::optional<NodeRegistrationOptions> parsed_options = parseInlineRegistrationOptions(node_ele);
851 if (!parsed_options.has_value()) {
852 throw exceptions::TreeDocumentError(
853 "Cannot merge tree document: Invalid inline registration options for node '" + registration_name +
854 "' in tree '" + tree_name + "'.");
855 }
856
857 const NodeRegistrationOptions & inline_options = parsed_options.value();
858
859 if (registered_nodes_manifest_.contains(registration_name)) {
860 if (registered_nodes_manifest_[registration_name] != inline_options) {
861 throw exceptions::TreeDocumentError(
862 "Cannot merge tree document: Inline registration options for node '" + registration_name + "' in tree '" +
863 tree_name + "' conflict with an existing registration.");
864 }
865 } else if (inline_manifest.contains(registration_name)) {
866 // Another element in this tree already declared the same node inline — tolerate an identical
867 // duplicate but reject conflicting options.
868 if (inline_manifest[registration_name] != inline_options) {
869 throw exceptions::TreeDocumentError(
870 "Cannot merge tree document: Found conflicting inline registration options for node '" +
871 registration_name + "' in tree '" + tree_name + "'.");
872 }
873 } else {
874 inline_manifest.add(registration_name, inline_options);
875 }
876 }
877
878 for (const XMLElement * child = node_ele->FirstChildElement(); child != nullptr;
879 child = child->NextSiblingElement()) {
880 self(self, child);
881 }
882 };
883
884 collect_inline_registration_options(collect_inline_registration_options, tree_ele->FirstChildElement());
885
886 if (!inline_manifest.empty()) {
887 registerNodes(inline_manifest, false);
888 }
889 };
890
891 // Get tree names from the other document (used for duplicate check and adopt_root_tree logic)
892 const std::vector<std::string> other_tree_names = getAllTreeNamesImpl(other);
893
894 if (strcmp(other_root->Name(), ROOT_ELEMENT_NAME) == 0) {
895 // Verify format
896 if (const char * ver = other_root->Attribute(BTCPP_FORMAT_ATTRIBUTE_NAME)) {
897 if (std::string(ver) != format_version_) {
898 throw exceptions::TreeDocumentError(
899 "Cannot merge tree document: Format of other document (" + std::string(BTCPP_FORMAT_ATTRIBUTE_NAME) + ": " +
900 ver + ") is not compatible with this document (" + std::string(BTCPP_FORMAT_ATTRIBUTE_NAME) + ": " +
901 format_version_ + ").");
902 }
903 } else {
904 throw exceptions::TreeDocumentError(
905 "Cannot merge tree document: Root element of other document doesn't have required attribute '" +
906 std::string(BTCPP_FORMAT_ATTRIBUTE_NAME) + "'.");
907 }
908
909 // Iterate over all include elements and store the associated content to a temporary buffer document
910 TreeDocument include_doc(format_version_, tree_node_loader_ptr_);
911 for (const XMLElement * include_ele = other_root->FirstChildElement(INCLUDE_ELEMENT_NAME); include_ele != nullptr;
912 include_ele = include_ele->NextSiblingElement(INCLUDE_ELEMENT_NAME)) {
913 const char * path = include_ele->Attribute(INCLUDE_PATH_ATTRIBUTE_NAME);
914 const char * resource_identity = include_ele->Attribute(INCLUDE_RESOURCE_ATTRIBUTE_NAME);
915
916 if (path && resource_identity) {
917 throw exceptions::TreeDocumentError(
918 "Cannot merge tree document: Found an <" + std::string(INCLUDE_ELEMENT_NAME) +
919 "> element that specifies both the '" + INCLUDE_PATH_ATTRIBUTE_NAME + "' and the '" +
920 INCLUDE_RESOURCE_ATTRIBUTE_NAME + "' attribute. Please specify only one of them.");
921 }
922
923 std::string absolute_path;
924 // For a resource include this holds the single tree to cherry-pick from the file; it stays empty for a path
925 // include, which merges the whole file.
926 std::string cherry_pick_tree_name;
927 if (resource_identity) {
928 // Resolve the include by looking up an installed tree resource by its identity. Unlike a file path, this
929 // decouples the include from any concrete install location: the resource system knows where the associated tree
930 // file lives. The 'ros_pkg' attribute is meaningless here (the package is already part of the identity).
931 if (std::string(resource_identity).empty()) {
932 throw exceptions::TreeDocumentError(
933 "Cannot merge tree document: Found an <" + std::string(INCLUDE_ELEMENT_NAME) +
934 "> element that specifies an empty resource identity in attribute '" + INCLUDE_RESOURCE_ATTRIBUTE_NAME +
935 "'.");
936 }
937 if (include_ele->Attribute(INCLUDE_ROS_PKG_ATTRIBUTE_NAME)) {
938 throw exceptions::TreeDocumentError(
939 "Cannot merge tree document: Found an <" + std::string(INCLUDE_ELEMENT_NAME) +
940 "> element that specifies the '" + INCLUDE_ROS_PKG_ATTRIBUTE_NAME + "' attribute together with '" +
941 INCLUDE_RESOURCE_ATTRIBUTE_NAME + "'. The '" + INCLUDE_ROS_PKG_ATTRIBUTE_NAME +
942 "' attribute is only allowed in combination with '" + INCLUDE_PATH_ATTRIBUTE_NAME + "'.");
943 }
944 try {
945 const TreeResource resource{std::string(resource_identity)};
946 absolute_path = resource.build_request_file_path_;
947 // The identity always carries a <tree_name> (a fully qualified tree resource identity requires it), so this
948 // yields the specific tree to cherry-pick rather than parsing the file for a root tree.
949 cherry_pick_tree_name = resource.getRootTreeName();
950 } catch (const std::exception & e) {
951 throw exceptions::TreeDocumentError(
952 "Cannot merge tree document: Found an <" + std::string(INCLUDE_ELEMENT_NAME) +
953 "> element that specifies the tree resource identity '" + std::string(resource_identity) +
954 "' in attribute '" + INCLUDE_RESOURCE_ATTRIBUTE_NAME + "' which cannot be resolved: " + e.what());
955 }
956 } else if (path) {
957 if (std::string(path).empty()) {
958 throw exceptions::TreeDocumentError(
959 "Cannot merge tree document: Found an <" + std::string(INCLUDE_ELEMENT_NAME) +
960 "> element that specifies an empty path in attribute '" + INCLUDE_PATH_ATTRIBUTE_NAME + "'.");
961 }
962 absolute_path = path;
963 if (const char * ros_pkg = include_ele->Attribute(INCLUDE_ROS_PKG_ATTRIBUTE_NAME)) {
964 if (std::string(ros_pkg).empty()) {
965 throw exceptions::TreeDocumentError(
966 "Cannot merge tree document: Found an <" + std::string(INCLUDE_ELEMENT_NAME) +
967 "> element that specifies an empty ROS package name in attribute '" + INCLUDE_ROS_PKG_ATTRIBUTE_NAME +
968 "'.");
969 }
970 if (std::filesystem::path(path).is_absolute()) {
971 throw exceptions::TreeDocumentError(
972 "Cannot merge tree document: Found an <" + std::string(INCLUDE_ELEMENT_NAME) +
973 "> element that specifies an absolute path '" + std::string(path) + "' in attribute '" +
974 INCLUDE_PATH_ATTRIBUTE_NAME + "' together with a ROS package name in attribute '" +
975 INCLUDE_ROS_PKG_ATTRIBUTE_NAME + "'. Please remove the attribute " + INCLUDE_ROS_PKG_ATTRIBUTE_NAME +
976 " or use a relative path if you want to refer to a location "
977 "relative to the package's share directory.");
978 }
979 try {
980 absolute_path = (ament_index_cpp::get_package_share_path(ros_pkg) / path).string();
981 } catch (const ament_index_cpp::PackageNotFoundError & e) {
982 throw exceptions::TreeDocumentError(
983 "Cannot merge tree document: Found an <" + std::string(INCLUDE_ELEMENT_NAME) +
984 "> element that specifies a non-existing ROS package '" + std::string(ros_pkg) + "' in attribute '" +
985 INCLUDE_ROS_PKG_ATTRIBUTE_NAME + "'.");
986 }
987 }
988 } else {
989 throw exceptions::TreeDocumentError(
990 "Cannot merge tree document: Found an <" + std::string(INCLUDE_ELEMENT_NAME) +
991 "> element that doesn't specify any of the required attributes '" + INCLUDE_PATH_ATTRIBUTE_NAME + "' or '" +
992 INCLUDE_RESOURCE_ATTRIBUTE_NAME + "'.");
993 }
994
995 // Load the included file into a temporary working document (resolving its own nested includes and passing the
996 // include stack for circular detection). Both include kinds go through this same working document; a resource
997 // include additionally narrows it down to a single tree below.
998 TreeDocument work_doc(format_version_, tree_node_loader_ptr_);
999 try {
1000 work_doc.mergeFileImpl(absolute_path, false, include_stack);
1001 } catch (const std::exception & e) {
1002 throw exceptions::TreeDocumentError(
1003 "Cannot merge tree document: Failed to include file '" + absolute_path + "': " + e.what());
1004 }
1005
1006 // For a resource include, cherry-pick: reduce the working document to the referenced tree and the trees it
1007 // transitively depends on via <SubTree> nodes. A path include keeps the whole file (empty cherry_pick_tree_name).
1008 if (!cherry_pick_tree_name.empty()) {
1009 if (!work_doc.hasTreeName(cherry_pick_tree_name)) {
1010 throw exceptions::TreeDocumentError(
1011 "Cannot merge tree document: The tree resource '" + std::string(resource_identity) + "' refers to tree '" +
1012 cherry_pick_tree_name + "' which is not defined in file '" + absolute_path + "'.");
1013 }
1014
1015 // Compute the dependency closure of the referenced tree. References to trees not defined in this file are left
1016 // out, so they can be resolved by the including document or another include.
1017 std::set<std::string> required_tree_names;
1018 std::vector<std::string> pending{cherry_pick_tree_name};
1019 while (!pending.empty()) {
1020 const std::string current = pending.back();
1021 pending.pop_back();
1022 if (!work_doc.hasTreeName(current)) continue; // resolved elsewhere
1023 if (!required_tree_names.insert(current).second) continue; // already visited
1024 std::function<void(const XMLElement *)> collect_subtree_refs = [&](const XMLElement * ele) {
1025 for (const XMLElement * child = ele->FirstChildElement(); child != nullptr;
1026 child = child->NextSiblingElement()) {
1027 if (strcmp(child->Name(), SUBTREE_ELEMENT_NAME) == 0) {
1028 if (const char * ref = child->Attribute(TREE_NAME_ATTRIBUTE_NAME)) pending.push_back(ref);
1029 }
1030 collect_subtree_refs(child);
1031 }
1032 };
1033 collect_subtree_refs(work_doc.getXMLElementForTreeWithName(current));
1034 }
1035
1036 for (const std::string & name : work_doc.getAllTreeNames()) {
1037 if (required_tree_names.count(name) == 0) work_doc.removeTree(name);
1038 }
1039 }
1040
1041 // Merge the working document into the buffer. A tree that already exists in the buffer with identical content is
1042 // dropped beforehand (this lets several includes share a common helper subtree - e.g. two behaviors reusing the
1043 // same subtree); a same-named tree with differing content is left in place so the merge below reports the clash.
1044 for (const std::string & name : work_doc.getAllTreeNames()) {
1045 if (
1046 include_doc.hasTreeName(name) &&
1047 include_doc.getTree(name).writeToString() == work_doc.getTree(name).writeToString()) {
1048 work_doc.removeTree(name);
1049 }
1050 }
1051 include_doc.mergeTreeDocument(static_cast<const XMLDocument &>(work_doc), false);
1052 }
1053
1054 // Check for duplicates from included files before inserting
1055 check_duplicates(include_doc.getAllTreeNames(), "included files");
1056
1057 // Iterate over all the tree elements of the include buffer and include them in this document
1058 for (const XMLElement * child = include_doc.RootElement()->FirstChildElement(TREE_ELEMENT_NAME); child != nullptr;
1059 child = child->NextSiblingElement(TREE_ELEMENT_NAME)) {
1060 // We do not need to verify the tree structure here, since it has already been verified during the recursive
1061 // mergeFile() calls by the for loop below
1062
1063 register_inline_nodes_for_tree(child);
1064
1065 // If tree element is valid, append to this document
1066 RootElement()->InsertEndChild(child->DeepClone(this));
1067 }
1068
1069 // Check for duplicates from other document's direct trees before inserting
1070 check_duplicates(other_tree_names, "the merged document");
1071
1072 // Iterate over all the tree elements of other root and verify them
1073 for (const XMLElement * child = other_root->FirstChildElement(TREE_ELEMENT_NAME); child != nullptr;
1074 child = child->NextSiblingElement(TREE_ELEMENT_NAME)) {
1075 verify_tree_structure(child);
1076
1077 register_inline_nodes_for_tree(child);
1078
1079 // If tree element is valid, append to this document
1080 RootElement()->InsertEndChild(child->DeepClone(this));
1081 }
1082
1083 if (adopt_root_tree) {
1084 // After (!) all tree elements have been inserted, adopt the name specified in the corresponding attribute of the
1085 // root element
1086 if (const char * name = other_root->Attribute(ROOT_TREE_ATTRIBUTE_NAME)) setRootTreeName(name);
1087 }
1088 } else if (strcmp(other_root->Name(), TREE_ELEMENT_NAME) == 0) {
1089 // Allow a single behavior tree without <root> element.
1090 // We assume that the the format complies with the version configured at construction time
1091 verify_tree_structure(other_root);
1092
1093 // Check for duplicates before inserting
1094 check_duplicates(other_tree_names, "the merged document");
1095
1096 register_inline_nodes_for_tree(other_root);
1097
1098 // If tree element is valid, append to this document
1099 RootElement()->InsertEndChild(other_root->DeepClone(this));
1100 } else {
1101 throw exceptions::TreeDocumentError(
1102 "Cannot merge tree document: Root element of other document must either be <" + std::string(ROOT_ELEMENT_NAME) +
1103 "> or <" + TREE_ELEMENT_NAME + ">.");
1104 }
1105
1106 if (adopt_root_tree && other_tree_names.size() == 1) {
1107 // If there is only one tree element, we change the root tree to the respective tree after (!) all tree elements
1108 // have been inserted
1109 setRootTreeName(other_tree_names[0]);
1110 }
1111
1112 return *this;
1113}
1114
1115TreeDocument & TreeDocument::mergeTreeDocument(const XMLDocument & other, bool adopt_root_tree)
1116{
1117 std::set<std::string> include_stack;
1118 return mergeTreeDocumentImpl(other, adopt_root_tree, include_stack);
1119}
1120
1122 const TreeDocument & other, bool adopt_root_tree, bool auto_register_nodes)
1123{
1124 if (auto_register_nodes) {
1125 // We register all associated node plugins beforehand, so that the user doesn't have to do that manually. This
1126 // means, that also potentially unused nodes are available and registered with the factory. This seems unnecessary,
1127 // but it's very convenient and the performance probably doesn't suffer too much.
1128 registerNodes(other.getRequiredNodeManifest(), false);
1129 }
1130 std::set<std::string> include_stack;
1131 return mergeTreeDocumentImpl(static_cast<const XMLDocument &>(other), adopt_root_tree, include_stack);
1132}
1133
1134TreeDocument & TreeDocument::mergeString(const std::string & tree_str, bool adopt_root_tree)
1135{
1136 XMLDocument other_doc;
1137 if (other_doc.Parse(tree_str.c_str()) != tinyxml2::XMLError::XML_SUCCESS) {
1138 throw exceptions::TreeDocumentError("Cannot merge tree document from string: " + std::string(other_doc.ErrorStr()));
1139 }
1140 std::set<std::string> include_stack;
1141 return mergeTreeDocumentImpl(other_doc, adopt_root_tree, include_stack);
1142}
1143
1144TreeDocument & TreeDocument::mergeFile(const std::string & path, bool adopt_root_tree)
1145{
1146 std::set<std::string> include_stack;
1147 return mergeFileImpl(path, adopt_root_tree, include_stack);
1148}
1149
1150TreeDocument & TreeDocument::mergeFileImpl(
1151 const std::string & path, bool adopt_root_tree, std::set<std::string> & include_stack)
1152{
1153 // Get canonical path for consistent circular include detection
1154 std::string canonical_path;
1155 try {
1156 canonical_path = std::filesystem::canonical(path).string();
1157 } catch (const std::filesystem::filesystem_error &) {
1158 // If canonical fails (e.g., file doesn't exist), use the original path
1159 // The error will be caught later when loading the file
1160 canonical_path = path;
1161 }
1162
1163 // Check for circular includes
1164 if (include_stack.count(canonical_path) > 0) {
1165 throw exceptions::TreeDocumentError(
1166 "Cannot merge tree document: Circular include detected for file '" + path + "'.");
1167 }
1168
1169 // Add to include stack
1170 include_stack.insert(canonical_path);
1171
1172 XMLDocument other_doc;
1173 if (other_doc.LoadFile(path.c_str()) != tinyxml2::XMLError::XML_SUCCESS) {
1174 throw exceptions::TreeDocumentError("Cannot create tree document from file " + path + ": " + other_doc.ErrorStr());
1175 }
1176
1177 mergeTreeDocumentImpl(other_doc, adopt_root_tree, include_stack);
1178
1179 // Remove from include stack (for recursive calls that continue after this one)
1180 include_stack.erase(canonical_path);
1181 return *this;
1182}
1183
1185 const TreeResource & resource, bool adopt_root_tree, bool auto_register_nodes)
1186{
1187 if (auto_register_nodes) {
1188 // We register all associated node plugins beforehand, so that the user doesn't have to do that manually. This
1189 // means, that also potentially unused nodes are available and registered with the factory. This seems unnecessary,
1190 // but it's very convenient and the performance probably doesn't suffer too much.
1191 registerNodes(resource.getNodeManifest(), false);
1192 }
1193 return mergeFile(resource.build_request_file_path_, adopt_root_tree);
1194}
1195
1196TreeDocument & TreeDocument::mergeTree(const TreeElement & tree, bool make_root_tree, bool auto_register_nodes)
1197{
1198 XMLDocument tree_doc;
1199 tree_doc.InsertEndChild(tree.ele_ptr_->DeepClone(&tree_doc));
1200 if (auto_register_nodes) {
1201 // We register all associated node plugins beforehand, so that the user doesn't have to do that manually. This
1202 // means, that also potentially unused nodes are available and registered with the factory. This seems unnecessary,
1203 // but it's very convenient and the performance probably doesn't suffer too much.
1205 }
1206 mergeTreeDocument(tree_doc, make_root_tree);
1207 return *this;
1208}
1209
1211{
1212 if (tree_name.empty()) {
1213 throw exceptions::TreeDocumentError("Cannot create a new tree with an empty name");
1214 }
1215 if (hasTreeName(tree_name)) {
1216 throw exceptions::TreeDocumentError(
1217 "Cannot create a new tree with name '" + tree_name + "' because it already exists.");
1218 }
1219 TreeDocument::XMLElement * new_ele = RootElement()->InsertNewChildElement(TreeDocument::TREE_ELEMENT_NAME);
1220 new_ele->SetAttribute(TreeDocument::TREE_NAME_ATTRIBUTE_NAME, tree_name.c_str());
1221 return TreeElement(this, new_ele);
1222}
1223
1224TreeDocument::TreeElement TreeDocument::newTree(const TreeElement & other, bool auto_register_nodes)
1225{
1226 return mergeTree(other, false, auto_register_nodes).getTree(other.getName());
1227}
1228
1230 const TreeDocument & other, const std::string & tree_name, bool auto_register_nodes)
1231{
1232 std::string name(tree_name);
1233 if (name.empty()) {
1234 if (other.hasRootTreeName()) {
1235 name = other.getRootTreeName();
1236 } else if (const std::vector<std::string> names = other.getAllTreeNames(); names.size() == 1) {
1237 name = names[0];
1238 } else {
1239 throw exceptions::TreeDocumentError(
1240 "Failed to create new tree element from another document because argument tree_name was omitted and it was not "
1241 "possible to determine the root tree automatically.");
1242 }
1243 }
1244 TreeElement tree_ele = newTree(name);
1245 tree_ele.insertTreeFromDocument(other, name, nullptr, auto_register_nodes);
1246 return tree_ele;
1247}
1248
1249TreeDocument::TreeElement TreeDocument::newTreeFromString(const std::string & tree_str, const std::string & tree_name)
1250{
1251 TreeDocument new_doc(format_version_, tree_node_loader_ptr_);
1252 new_doc.mergeString(tree_str, true);
1253 return newTreeFromDocument(new_doc, tree_name, false);
1254}
1255
1256TreeDocument::TreeElement TreeDocument::newTreeFromFile(const std::string & path, const std::string & tree_name)
1257{
1258 TreeDocument new_doc(format_version_, tree_node_loader_ptr_);
1259 new_doc.mergeFile(path, true);
1260 return newTreeFromDocument(new_doc, tree_name, false);
1261}
1262
1264 const TreeResource & resource, const std::string & tree_name, bool auto_register_nodes)
1265{
1266 TreeDocument new_doc(format_version_, tree_node_loader_ptr_);
1267 new_doc.mergeFile(resource.build_request_file_path_);
1268 if (resource.hasRootTreeName()) new_doc.setRootTreeName(resource.getRootTreeName());
1269 if (auto_register_nodes) {
1270 // We register all associated node plugins beforehand, so that the user doesn't have to do that manually. This
1271 // means, that also potentially unused nodes are available and registered with the factory. This seems unnecessary,
1272 // but it's very convenient and the performance probably doesn't suffer too much.
1273 registerNodes(resource.getNodeManifest(), false);
1274 }
1275 return newTreeFromDocument(new_doc, tree_name, false);
1276}
1277
1278bool TreeDocument::hasTreeName(const std::string & tree_name) const
1279{
1280 if (auto_apms_util::contains(getAllTreeNames(), tree_name)) return true;
1281 return false;
1282}
1283
1285{
1286 return TreeElement(this, getXMLElementForTreeWithName(tree_name));
1287}
1288
1289TreeDocument & TreeDocument::setRootTreeName(const std::string & tree_name)
1290{
1291 if (tree_name.empty()) {
1292 throw exceptions::TreeDocumentError("Cannot set root tree name with empty string.");
1293 }
1294 if (!hasTreeName(tree_name)) {
1295 throw exceptions::TreeDocumentError(
1296 "Cannot make tree with name '" + tree_name + "' the root tree because it doesn't exist.");
1297 }
1298 RootElement()->SetAttribute(ROOT_TREE_ATTRIBUTE_NAME, tree_name.c_str());
1299 return *this;
1300}
1301
1303{
1304 if (RootElement()->Attribute(ROOT_TREE_ATTRIBUTE_NAME)) return true;
1305 return false;
1306}
1307
1309{
1310 if (const auto tree_name = RootElement()->Attribute(ROOT_TREE_ATTRIBUTE_NAME)) return tree_name;
1311 throw exceptions::TreeDocumentError(
1312 "Cannot get root tree name because the document's root element has no attribute '" +
1313 std::string(ROOT_TREE_ATTRIBUTE_NAME) + "'.");
1314}
1315
1317
1318TreeDocument & TreeDocument::removeTree(const std::string & tree_name)
1319{
1320 RootElement()->DeleteChild(getXMLElementForTreeWithName(tree_name));
1321 return *this;
1322}
1323
1325
1326std::vector<std::string> TreeDocument::getAllTreeNames() const { return getAllTreeNamesImpl(*this); }
1327
1328TreeDocument & TreeDocument::applyNodeNamespace(const std::string & node_namespace, const std::string & sep)
1329{
1330 // Get the old node names before applying namespace
1331 std::set<std::string> old_node_names;
1332 for (const auto & [name, _] : registered_nodes_manifest_.map()) {
1333 old_node_names.insert(name);
1334 }
1335
1336 // Apply namespace to the registered nodes manifest
1337 registered_nodes_manifest_.applyNodeNamespace(node_namespace, sep);
1338
1339 // Helper to recursively rename nodes in the XML
1340 auto rename_recursive = [&old_node_names, &node_namespace, &sep](XMLElement * parent, auto & self) -> void {
1341 for (XMLElement * child = parent->FirstChildElement(); child != nullptr; child = child->NextSiblingElement()) {
1342 const char * name = child->Name();
1343 // Rename if the node name is in the old registered nodes - we don't want to rename native nodes.
1344 if (name && old_node_names.count(name) > 0) {
1345 child->SetName((node_namespace + sep + name).c_str());
1346 }
1347 self(child, self);
1348 }
1349 };
1350
1351 // Process all trees
1352 for (XMLElement * tree_ele = RootElement()->FirstChildElement(TREE_ELEMENT_NAME); tree_ele != nullptr;
1353 tree_ele = tree_ele->NextSiblingElement(TREE_ELEMENT_NAME)) {
1354 rename_recursive(tree_ele, rename_recursive);
1355 }
1356
1357 // Also update the BehaviorTreeFactory registrations so that the factory
1358 // recognizes the new (namespaced) node IDs used in the XML.
1359 // We must copy the manifest and builder data BEFORE unregistering, as unregisterBuilder
1360 // invalidates any references to the internal maps.
1361 std::vector<std::pair<std::string, std::pair<BT::TreeNodeManifest, BT::NodeBuilder>>> registrations_to_update;
1362 const auto & factory_manifests = factory_.manifests();
1363 const auto & factory_builders = factory_.builders();
1364 for (const auto & old_name : old_node_names) {
1365 const auto manifest_it = factory_manifests.find(old_name);
1366 const auto builder_it = factory_builders.find(old_name);
1367 if (manifest_it == factory_manifests.end() || builder_it == factory_builders.end()) {
1368 // This should never happen since we derived old_node_names from registered_nodes_manifest_
1369 throw exceptions::TreeDocumentError(
1370 "Internal error while applying node namespace: Node '" + old_name +
1371 "' not found in BehaviorTreeFactory registrations.");
1372 }
1373 // Copy the data before we modify the factory
1374 registrations_to_update.emplace_back(old_name, std::make_pair(manifest_it->second, builder_it->second));
1375 }
1376
1377 // Now perform the unregister/register operations using the copied data
1378 for (const auto & [old_name, manifest_builder_pair] : registrations_to_update) {
1379 factory_.unregisterBuilder(old_name);
1380 BT::TreeNodeManifest new_manifest = manifest_builder_pair.first;
1381 new_manifest.registration_ID = node_namespace + sep + old_name;
1382 factory_.registerBuilder(new_manifest, manifest_builder_pair.second);
1383 }
1384
1385 return *this;
1386}
1387
1388TreeDocument & TreeDocument::registerNodes(const NodeManifest & node_manifest, bool override)
1389{
1390 // Make sure that there are no node names that are reserved for native nodes
1391 std::set<std::string> all_registration_names;
1392 for (const auto & [name, _] : node_manifest.map()) all_registration_names.insert(name);
1393 if (const std::set<std::string> common =
1394 auto_apms_util::getCommonElements(all_registration_names, native_node_names_);
1395 !common.empty()) {
1396 throw exceptions::TreeDocumentError(
1397 "Found reserved node registration names in the node manifest. The following names are not allowed, because "
1398 "they refer to native behavior tree nodes: [ " +
1399 auto_apms_util::join(std::vector<std::string>(common.begin(), common.end()), ", ") + " ].");
1400 }
1401
1402 for (const auto & [node_name, params] : node_manifest.map()) {
1403 if (registered_nodes_manifest_.contains(node_name)) {
1404 if (override) {
1405 // If override is true, register the new node plugin instead of the current one
1406 factory_.unregisterBuilder(node_name);
1407 registered_nodes_manifest_.remove(node_name);
1408 } else {
1409 // Tolerate identical re-registrations as a no-op so that manifests
1410 // assembled from multiple independent sources can overlap without error.
1411 if (registered_nodes_manifest_[node_name] == params) {
1412 continue;
1413 }
1414 throw exceptions::TreeDocumentError(
1415 "Tried to register node '" + node_name + "' (Class: " + params.class_name +
1416 ") which is already registered with different options. You must make sure that the registration names are "
1417 "unique, or that re-registrations use the same options, or explicitly allow overriding previously registered "
1418 "nodes with the same name by setting override=true.");
1419 }
1420 }
1421
1422 // Check if the class we search for is actually available with the loader.
1423 if (!tree_node_loader_ptr_->isClassAvailable(params.class_name)) {
1424 if (all_node_classes_package_map_.find(params.class_name) == all_node_classes_package_map_.end()) {
1425 throw exceptions::TreeDocumentError(
1426 "Node '" + node_name + " (" + params.class_name +
1427 ")' cannot be registered, because the class name is not known to the class loader. "
1428 "Make sure that it's spelled correctly and registered by calling "
1429 "auto_apms_behavior_tree_register_nodes() in the CMakeLists.txt of the "
1430 "corresponding package.");
1431 }
1432 throw exceptions::TreeDocumentError(
1433 "Node '" + node_name + " (" + params.class_name +
1434 ")' cannot be registered, because the corresponding resource belongs to excluded package '" +
1435 all_node_classes_package_map_.at(params.class_name) + "'.");
1436 }
1437
1438 pluginlib::UniquePtr<NodeRegistrationInterface> plugin_instance;
1439 try {
1440 plugin_instance = tree_node_loader_ptr_->createUniqueInstance(params.class_name);
1441 } catch (const pluginlib::CreateClassException & e) {
1442 throw pluginlib::CreateClassException(
1443 "Failed to create an instance of node '" + node_name + " (" + params.class_name +
1444 ")'. Remember that the AUTO_APMS_BEHAVIOR_TREE_REGISTER_NODE "
1445 "macro must be called in the source file for the node class to be discoverable. "
1446 "Error message: " +
1447 e.what() + ".");
1448 }
1449
1450 try {
1451 if (plugin_instance->requiresRosNodeContext()) {
1452 if (only_non_ros_nodes_) {
1453 throw exceptions::NodeRegistrationError(
1454 "Node '" + node_name +
1455 "' relies on ROS 2 functionality but this instance only allows to use non-ROS nodes.");
1456 }
1457 RosNodeContext ros_node_context(
1458 ros_node_wptr_.lock(), tree_node_waitables_callback_group_wptr_.lock(),
1459 tree_node_waitables_executor_wptr_.lock(), params);
1460 plugin_instance->registerWithBehaviorTreeFactory(factory_, node_name, &ros_node_context);
1461 } else {
1462 // Create a dummy object of RosNodeContext to allow for parsing the registration params nevertheless
1463 RosNodeContext ros_node_context(nullptr, nullptr, nullptr, params);
1464 plugin_instance->registerWithBehaviorTreeFactory(factory_, node_name, &ros_node_context);
1465 }
1466 } catch (const std::exception & e) {
1467 throw exceptions::NodeRegistrationError(
1468 "Cannot register node '" + node_name + " (" + params.class_name + ")': " + e.what() + ".");
1469 }
1470 registered_nodes_manifest_.add(node_name, params);
1471 }
1472 return *this;
1473}
1474
1475std::set<std::string> TreeDocument::getRegisteredNodeNames(bool include_native) const
1476{
1477 std::set<std::string> names;
1478 if (include_native) names = native_node_names_;
1479 for (const auto & [name, _] : registered_nodes_manifest_.map()) names.insert(name);
1480 return names;
1481}
1482
1483NodeManifest TreeDocument::getRequiredNodeManifest(const std::string & tree_name) const
1484{
1485 NodeManifest m;
1486 TreeDocument * doc = const_cast<TreeDocument *>(this);
1487 std::vector<std::string> tree_names_to_process =
1488 tree_name.empty() ? getAllTreeNames() : std::vector<std::string>{tree_name};
1489 for (const std::string & tree_name : tree_names_to_process) {
1490 XMLElement * ptr = const_cast<XMLElement *>(getXMLElementForTreeWithName(tree_name));
1491 const TreeElement ele(doc, ptr);
1492 m.merge(ele.getRequiredNodeManifest(), true);
1493 }
1494 return m;
1495}
1496
1498{
1499 const std::vector<std::string> names_to_process =
1500 tree_name.empty() ? getAllTreeNames() : std::vector<std::string>{tree_name};
1501 for (const std::string & name : names_to_process) {
1503 }
1504 return *this;
1505}
1506
1508{
1509 // Create or get the TreeNodesModel element
1510 tinyxml2::XMLElement * model_root = RootElement()->FirstChildElement(TREE_NODE_MODEL_ELEMENT_NAME);
1511
1512 // If no TreeNodesModel element exists, create one
1513 if (!model_root) {
1514 model_root = NewElement(TREE_NODE_MODEL_ELEMENT_NAME);
1515 RootElement()->InsertEndChild(model_root);
1516 }
1517
1518 // Iterate through the model_map and create XML elements for each node
1519 for (const auto & [node_name, model] : model_map) {
1520 // Create the element with the node type as the tag name
1521 tinyxml2::XMLElement * node_element = NewElement(BT::toStr(model.type).c_str());
1522 node_element->SetAttribute("ID", node_name.c_str());
1523
1524 // Add port information
1525 for (const auto & port_info : model.port_infos) {
1526 tinyxml2::XMLElement * port_element = nullptr;
1527
1528 // Create the appropriate port element based on direction
1529 switch (port_info.port_direction) {
1530 case BT::PortDirection::INPUT:
1531 port_element = NewElement("input_port");
1532 break;
1533 case BT::PortDirection::OUTPUT:
1534 port_element = NewElement("output_port");
1535 break;
1536 case BT::PortDirection::INOUT:
1537 port_element = NewElement("inout_port");
1538 break;
1539 }
1540
1541 // Set port attributes
1542 port_element->SetAttribute("name", port_info.port_name.c_str());
1543
1544 if (!port_info.port_type.empty()) {
1545 port_element->SetAttribute("type", port_info.port_type.c_str());
1546 }
1547
1548 if (port_info.port_has_default) {
1549 port_element->SetAttribute("default", port_info.port_default.c_str());
1550 }
1551
1552 // Set port description as text content
1553 if (!port_info.port_description.empty()) {
1554 port_element->SetText(port_info.port_description.c_str());
1555 }
1556
1557 node_element->InsertEndChild(port_element);
1558 }
1559
1560 model_root->InsertEndChild(node_element);
1561 }
1562
1563 return *this;
1564}
1565
1566NodeModelMap TreeDocument::getNodeModel(tinyxml2::XMLDocument & doc, const NodeManifest & manifest)
1567{
1568 const tinyxml2::XMLElement * root = doc.RootElement();
1569 if (!root) {
1570 throw exceptions::TreeDocumentError("Node model document has no root element.");
1571 }
1572 if (const char * ver = root->Attribute(TreeDocument::BTCPP_FORMAT_ATTRIBUTE_NAME)) {
1573 const std::string expected_format = TreeDocument::BTCPP_FORMAT_DEFAULT_VERSION;
1574 if (std::string(ver) != expected_format) {
1575 throw exceptions::TreeDocumentError(
1576 "Cannot parse node model document: Format of model document (" +
1577 std::string(TreeDocument::BTCPP_FORMAT_ATTRIBUTE_NAME) + ": " + ver +
1578 ") doesn't comply with the expected format (" + std::string(TreeDocument::BTCPP_FORMAT_ATTRIBUTE_NAME) + ": " +
1579 expected_format + ").");
1580 }
1581 } else {
1582 throw exceptions::TreeDocumentError(
1583 "Cannot parse node model document: Root element of model document doesn't have required attribute '" +
1584 std::string(TreeDocument::BTCPP_FORMAT_ATTRIBUTE_NAME) + "'.");
1585 }
1586 tinyxml2::XMLElement * model_ele = doc.RootElement()->FirstChildElement(TreeDocument::TREE_NODE_MODEL_ELEMENT_NAME);
1587 if (!model_ele) {
1588 throw exceptions::TreeDocumentError(
1589 "Element <" + std::string(TreeDocument::TREE_NODE_MODEL_ELEMENT_NAME) +
1590 "> doesn't exist in node model document.");
1591 }
1592
1593 NodeModelMap model_map;
1594 for (tinyxml2::XMLElement * ele = model_ele->FirstChildElement(); ele != nullptr; ele = ele->NextSiblingElement()) {
1595 const char * node_name = ele->Attribute("ID");
1596 if (!node_name) {
1597 throw exceptions::TreeDocumentError(
1598 "Element '" + std::string(ele->Name()) + "' in node model document is missing the required attribute 'ID'");
1599 }
1600 NodeModel & model = model_map[node_name];
1601 model.type = BT::convertFromString<BT::NodeType>(ele->Name());
1602 for (const tinyxml2::XMLElement * port_ele = ele->FirstChildElement(); port_ele != nullptr;
1603 port_ele = port_ele->NextSiblingElement()) {
1604 const std::string direction = port_ele->Name();
1605 NodePortInfo port_info;
1606 if (direction == "input_port") {
1607 port_info.port_direction = BT::PortDirection::INPUT;
1608 } else if (direction == "output_port") {
1609 port_info.port_direction = BT::PortDirection::OUTPUT;
1610 } else if (direction == "inout_port") {
1611 port_info.port_direction = BT::PortDirection::INOUT;
1612 } else {
1613 throw exceptions::TreeDocumentError(
1614 "Unknown port direction in node model for '" + std::string(node_name) + "': " + direction);
1615 }
1616 if (const char * c = port_ele->Attribute("name")) {
1617 port_info.port_name = c;
1618 }
1619 if (const char * c = port_ele->Attribute("type")) {
1620 port_info.port_type = c;
1621 }
1622 if (const char * c = port_ele->Attribute("default")) {
1623 port_info.port_has_default = true;
1624 port_info.port_default = c;
1625 } else {
1626 port_info.port_has_default = false;
1627 }
1628 if (const char * c = port_ele->GetText()) {
1629 port_info.port_description = c;
1630 }
1631 model.port_infos.push_back(std::move(port_info));
1632 }
1633 // Port infos may be empty if there are no ports
1634 }
1635
1636 // Collect hidden ports from the node manifest
1637 std::map<std::string, std::vector<std::string>> hidden_ports;
1638 for (const auto & [node_name, registration_options] : manifest.map()) {
1639 // Extract hidden_ports from registration options
1640 for (const std::string & port_name : registration_options.hidden_ports) {
1641 hidden_ports[node_name].push_back(port_name);
1642 }
1643
1644 // Implicitly hide ports that are specified in port_alias
1645 for (const auto & [port_name, _] : registration_options.port_alias) {
1646 hidden_ports[node_name].push_back(port_name);
1647 }
1648 }
1649
1650 // Apply port hiding
1651 for (const auto & [node_name, ports_to_hide] : hidden_ports) {
1652 auto it = model_map.find(node_name);
1653 if (it == model_map.end()) {
1654 continue;
1655 }
1656 NodeModel & model = it->second;
1657 model.port_infos.erase(
1658 std::remove_if(
1659 model.port_infos.begin(), model.port_infos.end(),
1660 [&ports_to_hide](const NodePortInfo & port_info) {
1661 return std::find(ports_to_hide.begin(), ports_to_hide.end(), port_info.port_name) != ports_to_hide.end();
1662 }),
1663 model.port_infos.end());
1664 }
1665
1666 return model_map;
1667}
1668
1670{
1671 // Generate XML from the factory
1672 std::string model_xml;
1673 try {
1674 model_xml = BT::writeTreeNodesModelXML(factory_, include_native);
1675 } catch (const std::exception & e) {
1676 throw exceptions::TreeDocumentError("Error generating node model XML from factory: " + std::string(e.what()));
1677 }
1678
1679 // Parse the XML
1680 tinyxml2::XMLDocument model_doc;
1681 if (model_doc.Parse(model_xml.c_str()) != tinyxml2::XMLError::XML_SUCCESS) {
1682 throw exceptions::TreeDocumentError(
1683 "Error parsing the model of the currently registered nodes: " + std::string(model_doc.ErrorStr()));
1684 }
1685
1686 // Get the node model with hidden ports applied
1687 return getNodeModel(model_doc, registered_nodes_manifest_);
1688}
1689
1690BT::Result TreeDocument::verify() const
1691{
1692 NodeModelMap model_map = getNodeModel(true);
1693 std::unordered_map<std::string, BT::NodeType> registered_nodes;
1694 for (const auto & [node_name, model] : model_map) {
1695 registered_nodes[node_name] = model.type;
1696 }
1697 try {
1698 BT::VerifyXML(writeToString(), registered_nodes);
1699 } catch (const BT::RuntimeError & e) {
1700 return nonstd::make_unexpected(e.what());
1701 }
1702 return {};
1703}
1704
1705std::string TreeDocument::writeToString(bool remove_whitespace) const
1706{
1707 tinyxml2::XMLPrinter printer(nullptr, remove_whitespace);
1708 Print(&printer);
1709 return printer.CStr();
1710}
1711
1712void TreeDocument::writeToFile(const std::string & path, bool remove_whitespace) const
1713{
1714 const std::string xml = writeToString(remove_whitespace);
1715 FILE * f = std::fopen(path.c_str(), "w");
1716 if (!f) {
1717 throw exceptions::TreeDocumentError("Failed to open file for writing: " + path);
1718 }
1719 const std::size_t written = std::fwrite(xml.data(), 1, xml.size(), f);
1720 const int close_err = std::fclose(f);
1721 if (written != xml.size() || close_err != 0) {
1722 throw exceptions::TreeDocumentError("Failed to write tree document to file: " + path);
1723 }
1724}
1725
1726TreeDocument & TreeDocument::reset(bool unregister_nodes)
1727{
1728 // Reset XML document
1729 Clear();
1730 tinyxml2::XMLElement * root_ele = NewElement(TreeDocument::ROOT_ELEMENT_NAME);
1731 root_ele->SetAttribute(TreeDocument::BTCPP_FORMAT_ATTRIBUTE_NAME, format_version_.c_str());
1732 InsertFirstChild(root_ele);
1733
1734 // Reset node registration state if desired
1735 if (unregister_nodes) {
1736 for (const auto & node_name : getRegisteredNodeNames(false)) {
1737 factory_.unregisterBuilder(node_name);
1738 }
1739 registered_nodes_manifest_ = NodeManifest();
1740 }
1741 return *this;
1742}
1743
1744const TreeDocument::XMLElement * TreeDocument::getXMLElementForTreeWithName(const std::string & tree_name) const
1745{
1746 return getXMLElementForTreeWithNameImpl<const XMLElement *>(*this, tree_name);
1747}
1748
1749TreeDocument::XMLElement * TreeDocument::getXMLElementForTreeWithName(const std::string & tree_name)
1750{
1751 return getXMLElementForTreeWithNameImpl<XMLElement *>(*this, tree_name);
1752}
1753
1754template <typename ReturnT, typename DocumentT>
1755ReturnT TreeDocument::getXMLElementForTreeWithNameImpl(DocumentT & doc, const std::string & tree_name)
1756{
1757 if (tree_name.empty()) {
1758 throw exceptions::TreeDocumentError("Cannot get tree with an empty name.");
1759 }
1760 if (!doc.hasTreeName(tree_name)) {
1761 throw exceptions::TreeDocumentError("Cannot get tree with name '" + tree_name + "' because it doesn't exist.");
1762 }
1763 auto child = doc.RootElement()->FirstChildElement(TreeDocument::TREE_ELEMENT_NAME);
1764 while (child && !child->Attribute(TreeDocument::TREE_NAME_ATTRIBUTE_NAME, tree_name.c_str())) {
1765 child = child->NextSiblingElement();
1766 }
1767 if (!child) {
1768 throw std::logic_error(
1769 "Unexpected error trying to get tree element with name '" + tree_name +
1770 "'. Since hasTreeName() returned true, there MUST be a corresponding element.");
1771 }
1772 return child;
1773}
1774
1775} // namespace auto_apms_behavior_tree::core
const NodeManifest & getNodeManifest() const
Get the node manifest associated with this resource.
Definition behavior.hpp:472
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.
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 insertTreeFromResource(const TreeResource &resource, const std::string &tree_name, const NodeElement *before_this=nullptr, bool auto_register_nodes=true)
Concatenate a tree from one of the installed package's behavior tree resources and add its first chil...
NodeElement(TreeDocument *doc_ptr, XMLElement *ele_ptr)
Protected constructor intended for internal use only.
NodeElement insertTreeFromDocument(const TreeDocument &doc, const std::string &tree_name, const NodeElement *before_this=nullptr, bool auto_register_nodes=true)
Concatenate a tree from a document and add its first child node to the children of this node.
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.
NodeElement & setInlineRegistrationOptions()
Encode the registration options for this node element based on the currently registered node manifest...
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.
std::string getFullyQualifiedName() const
Create a string that uniquely identifies this node considering its registration and its instance name...
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.
std::optional< NodeRegistrationOptions > getInlineRegistrationOptions() const
Extract the node registration options by parsing the attributes of this node element.
bool hasChildren() const
Determine whether any children have been given to this node.
NodeElement insertTree(const TreeElement &tree, const NodeElement *before_this=nullptr, bool auto_register_nodes=true)
Concatenate a tree and add its first child node to the children of 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.
NodeElement & setInlineRegistrationOptions(const NodeRegistrationOptions &registration_options)
Encode the given node registration options as attributes of this node element.
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.
std::string writeToString(bool remove_whitespace=false) const
Write this behavior tree to an XML encoded in a string.
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 & setInlineRegistrationOptions()
Encode the registration options for all nodes in this tree based on the currently registered node man...
TreeElement & setName(const std::string &tree_name) override
Set the name of the behavior tree.
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.
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 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.
std::string writeToString(bool remove_whitespace=false) const
Write the XML of this tree document to a string.
BT::Result verify() const
Verify that all behavior trees of this document are structured correctly and can be created successfu...
std::set< std::string > getRegisteredNodeNames(bool include_native=true) const
Get the names of all nodes that are known to this document.
TreeDocument & mergeResource(const TreeResource &resource, bool adopt_root_tree=false, bool auto_register_nodes=true)
Merge the behavior trees from one of the installed package's behavior tree resources.
TreeDocument & reset(bool unregister_nodes=true)
Clear this document and reset it to its initial state.
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.
void writeToFile(const std::string &path, bool remove_whitespace=false) const
Write the XML of this tree document to a file.
virtual TreeDocument & registerNodes(const NodeManifest &node_manifest, bool override=false)
Load behavior tree node plugins and register them with the internal behavior tree factory.
TreeElement newTreeFromDocument(const TreeDocument &other, const std::string &tree_name="", bool auto_register_nodes=true)
Create a new behavior tree inside this document with the content of one found inside another tree doc...
TreeElement getTree(const std::string &tree_name)
Get the corresponding behavior tree element for a tree inside this document.
TreeDocument & setInlineRegistrationOptions(const std::string &tree_name="")
Encode the registration options for all nodes in this document based on the currently registered node...
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.
NodeManifest getRequiredNodeManifest(const std::string &tree_name="") const
Assemble the node manifest that is required for successfully creating an instance of any of the docum...
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.
TreeElement newTreeFromResource(const TreeResource &resource, const std::string &tree_name="", bool auto_register_nodes=true)
Create a new behavior tree inside this document with the content of one the trees found inside a part...
TreeDocument & mergeTree(const TreeElement &tree, bool make_root_tree=false, bool auto_register_nodes=true)
Merge an existing behavior tree with this tree document.
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....
bool valid() const
Verify that the options are valid (e.g. all required values are set).