Fabric 0.2.0
Loading...
Searching...
No Matches
xml_parser_plugin.hpp
Go to the documentation of this file.
1#pragma once
2#include <cerrno>
3#include <cstdlib>
4#include <string>
5#include <vector>
6#include <tinyxml2.h>
7
8#include <rclcpp/rclcpp.hpp>
10#include <capabilities2_events/event_parameters.hpp>
11
12namespace fabric
13{
19class XMLParser : public ParserBase
20{
21public:
22 XMLParser() = default;
23 virtual ~XMLParser() = default;
24
30 void initialize(const rclcpp::Node::SharedPtr& node) override
31 {
32 initialize_base(node, "XMLParserPlugin");
33
34 auto* decl = system_doc.NewDeclaration(R"(xml version="1.0" encoding="UTF-8")");
35 system_doc.InsertEndChild(decl);
36 }
37
44 bool load_file(const std::string& file_path, fabric::Plan& plan) override
45 {
46 tinyxml2::XMLError xml_status = default_document.LoadFile(file_path.c_str());
47
48 // check if the file loading failed
49 if (xml_status != tinyxml2::XMLError::XML_SUCCESS)
50 {
51 RCLCPP_ERROR(node_->get_logger(), "[xml_parser] Error loading plan: %s, Error: %s", file_path.c_str(), default_document.ErrorName());
52 return false;
53 }
54
55 RCLCPP_INFO(node_->get_logger(), "Plan loaded from : %s", file_path.c_str());
56
58
59 return true;
60 }
61
68 bool check_compatibility(const fabric::Plan& plan) override
69 {
70 // XML Document to check the validity of the plan
71 tinyxml2::XMLDocument documentChecking;
72
73 // try to parse the std::string plan from fabric_msgs/Plan to the to a XMLDocument file
74 tinyxml2::XMLError xml_status = documentChecking.Parse(plan.plan.c_str());
75
76 // check if the file parsing failed
77 if (xml_status != tinyxml2::XMLError::XML_SUCCESS)
78 {
79 RCLCPP_ERROR(node_->get_logger(), "Parsing the plan from service request message failed with error: %s", documentChecking.ErrorName());
80 return false;
81 }
82
83 return true;
84 }
85
92 void parse(fabric::Plan& plan) override
93 {
94 RCLCPP_INFO(node_->get_logger(), "Plan being parsed: \n\n %s", plan.plan.c_str());
95
96 // parse the fabric::plan into a XML document
97 current_document_.Parse(plan.plan.c_str());
98
99 // Add a completion runner to the plan
100 RCLCPP_INFO(node_->get_logger(), "[xml_parser] Adding completion runner to the plan");
102
103 // Debug: print the modified plan
104 std::string modified_plan;
105 convert_to_string(current_document_, modified_plan);
106 RCLCPP_DEBUG(node_->get_logger(), "[xml_parser] Plan after adding closing event :\n\n %s", modified_plan.c_str());
107
108 RCLCPP_INFO(node_->get_logger(), "[xml_parser] Completion runner added. Extracting the plan element.");
109
110 // extract the plan element
112
113 if (plan_ == nullptr)
114 throw fabric::fabric_exception("No <Plan> element found.");
115
116 RCLCPP_INFO(node_->get_logger(), "[xml_parser] <Plan> element extracted successfully.");
117
118 // check the syntax of the plan to make sure it contains valid XML elements and required attributes
119 std::string error_msg;
120 std::vector<std::string> control_list = { "sequential", "parallel_any", "parallel_all", "recovery" };
121
122 bool syntax_valid = check_syntax(plan_, control_list, plan.rejected_list, error_msg);
123
124 if (!syntax_valid)
125 {
126 throw fabric::fabric_exception("XML plan parsing failed: " + error_msg);
127 }
128 RCLCPP_INFO(node_->get_logger(), "[xml_parser] Plan syntax validation successful");
129
130 // extract connections from the plan
132
133 RCLCPP_INFO(node_->get_logger(), "Finished parsing the plan");
134 }
135
136protected:
144 void convert_to_string(tinyxml2::XMLDocument& document_xml, std::string& document_string)
145 {
146 tinyxml2::XMLPrinter printer;
147 document_xml.Print(&printer);
148 document_string = printer.CStr();
149 }
150
159 bool search(std::vector<std::string> list, std::string value)
160 {
161 return (std::find(list.begin(), list.end(), value) != list.end());
162 }
163
173 tinyxml2::XMLElement* extract_plan(tinyxml2::XMLDocument& document)
174 {
175 std::string plan_tag(document.FirstChildElement()->Name());
176
177 if (plan_tag == "Plan")
178 return document.FirstChildElement("Plan")->FirstChildElement();
179 else
180 return nullptr;
181 }
182
191 inline bool convert_to_string(tinyxml2::XMLElement* element, std::string& parameters)
192 {
193 if (element)
194 {
195 tinyxml2::XMLPrinter printer;
196 element->Accept(&printer);
197 parameters = printer.CStr();
198 return true;
199 }
200 else
201 {
202 parameters = "";
203 return false;
204 }
205 }
206
218 bool check_syntax(tinyxml2::XMLElement* element, std::vector<std::string>& control_list, std::vector<std::string>& rejected, std::string& error)
219 {
220 const char* type = nullptr;
221 const char* interface = nullptr;
222 const char* provider = nullptr;
223
224 std::string elementTag(element->Name());
225
226 std::string parameter_string;
227 convert_to_string(element, parameter_string);
228
229 bool returnValue = true;
230
231 std::string typetag = "";
232 std::string interfacetag = "";
233 std::string providertag = "";
234
235 bool hasChildren = !element->NoChildren();
236 bool hasSiblings = (element->NextSiblingElement() != nullptr);
237
238 if (elementTag == "Control")
239 {
240 element->QueryStringAttribute("type", &type);
241
242 if (type)
243 {
244 typetag = type;
245
246 if (!search(control_list, typetag))
247 {
248 error = "Control tag '" + typetag + "' not available in the valid list";
249 rejected.push_back(parameter_string);
250 return false;
251 }
252 }
253 else
254 {
255 error = "Control tag missing 'type' attribute: " + parameter_string;
256 rejected.push_back(parameter_string);
257 return false;
258 }
259
260 if (hasChildren)
261 returnValue &= check_syntax(element->FirstChildElement(), control_list, rejected, error);
262
263 if (hasSiblings)
264 returnValue &= check_syntax(element->NextSiblingElement(), control_list, rejected, error);
265 }
266 else if (elementTag == "Runner")
267 {
268 element->QueryStringAttribute("interface", &interface);
269 element->QueryStringAttribute("provider", &provider);
270
271 if (not interface)
272 {
273 error = "Runner tag missing 'interface' attribute: " + parameter_string;
274 rejected.push_back(parameter_string);
275 return false;
276 }
277
278 if (not provider)
279 {
280 error = "Runner tag missing 'provider' attribute: " + parameter_string;
281 rejected.push_back(parameter_string);
282 return false;
283 }
284
285 if (hasSiblings)
286 returnValue &= check_syntax(element->NextSiblingElement(), control_list, rejected, error);
287 }
288 else
289 {
290 error = "XML element is not valid :" + parameter_string;
291 rejected.push_back(parameter_string);
292 return false;
293 }
294
295 return returnValue;
296 }
297
306 void add_completion_runner(tinyxml2::XMLDocument& document, const std::string& plan_id)
307 {
308 // Get the root <Plan> element
309 tinyxml2::XMLElement* plan = document.FirstChildElement("Plan");
310
311 // Create the outer <Control type="sequential"> element
312 tinyxml2::XMLElement* outerControl = document.NewElement("Control");
313 outerControl->SetAttribute("type", "sequential");
314 outerControl->SetAttribute("name", "fabric_completion_control");
315
316 // Move all existing children of <Plan> into the new outer control
317 while (tinyxml2::XMLNode* child = plan->FirstChild())
318 outerControl->InsertEndChild(child);
319
320 // Add the new outer control to the <Plan>
321 plan->InsertEndChild(outerControl);
322
323 // Create and append the new <Runner> element
324 tinyxml2::XMLElement* newRunner = document.NewElement("Runner");
325 newRunner->SetAttribute("interface", "fabric_capabilities/FabricCompletionRunner");
326 newRunner->SetAttribute("provider", "fabric_capabilities/FabricCompletionRunner");
327 newRunner->SetAttribute("plan_id", plan_id.c_str());
328 outerControl->InsertEndChild(newRunner);
329
330 RCLCPP_INFO(node_->get_logger(), "[xml_parser] CompletionRunner added to the plan");
331 }
332
341 int add_parallel_all(fabric::Plan& plan, int connection_id = 0, std::string description = "")
342 {
343 int input_count = 0;
344
345 // Since we are adding a parallel all connection, we need to identify the number of parallel runners
346 // that need to be multiplexed, which is equivalent to the number of success connections without a
347 // target on_success interface in the current plan
348 for (const auto& connection : plan.connections)
349 if (connection.second.on_success.interface == "")
350 input_count += 1;
351
353 node.source.interface = "capabilities2_runner/InputMultiplexRunner";
354 node.source.provider = "capabilities2_runner/InputMultiplexRunner";
356 node.source.parameters.set_value("input_count", input_count, capabilities2_events::OptionType::INT);
357
358 plan.connections[connection_id] = node;
359 plan.connections[connection_id].description = description;
360
361 // set the target_on_success for the new connection
362 for (auto& connection : plan.connections)
363 if (connection.second.on_success.interface == "")
364 connection.second.on_success = plan.connections[connection_id].source;
365
366 // increment the index for the next parallel all connection
367 runner_index += 1;
368
369 RCLCPP_INFO(node_->get_logger(), "[xml_parser] ParallelAll connection added to the plan with %d inputs, id %d", input_count, connection_id);
370
371 // return the next connection id
372 return connection_id;
373 }
374
383 int add_parallel_any(fabric::Plan& plan, int connection_id = 0, std::string description = "")
384 {
385 // Since we are adding a parallel_any connection, we only need to wait for a single success
386 // trigger to succeed, so we can set the input count to 1 for the multiplex runner
387 int input_count = 1;
388
389 // create a system runner for parallel any with the identified number of inputs
391 node.source.interface = "capabilities2_runner/InputMultiplexRunner";
392 node.source.provider = "capabilities2_runner/InputMultiplexRunner";
394 node.source.parameters.set_value("input_count", input_count, capabilities2_events::OptionType::INT);
395
396 plan.connections[connection_id] = node;
397 plan.connections[connection_id].description = description;
398
399 // set the on_success for the new connection
400 for (auto& connection : plan.connections)
401 if (connection.second.on_success.interface == "")
402 connection.second.on_success = plan.connections[connection_id].source;
403
404 // increment the index for the next parallel any connection
405 runner_index += 1;
406
407 RCLCPP_INFO(node_->get_logger(), "[xml_parser] ParallelAny connection added to the plan with id %d", connection_id);
408
409 // return the next connection id
410 return connection_id;
411 }
412
422 // void check_and_update_runner_id(fabric::connection& predecessor, fabric::connection& successor)
423 // {
424 // // check if predecessor is a system runner and has parameters
425 // if (predecessor.source.interface.find("capabilities2_runner/InputMultiplexRunner") != std::string::npos)
426 // {
427 // // If the predecessor is a system runner, we need to update the successor's parameters with the predecessor's id
428 // if (predecessor.source.parameters.has_value("id"))
429 // {
430 // // Get the id attribute from the predecessor system runner
431 // int id = std::any_cast<int>(predecessor.source.parameters.get_value("id", 0));
432
433 // // Set the id attribute for the successor system runner
434 // successor.source.parameters.set_value("id", id, capabilities2_events::OptionType::INT);
435
436 // RCLCPP_INFO(node_->get_logger(), "[xml_parser] Updated successor runner id to %d", id);
437 // }
438 // }
439 // }
440
449 capabilities2_events::EventParameters convert_xml_to_event_parameters(tinyxml2::XMLElement* element)
450 {
451 capabilities2_events::EventParameters parameters;
452
453 // Iterate through the attributes of the XML element and add them to the EventParameters
454 const tinyxml2::XMLAttribute* attr = element->FirstAttribute();
455
456 while (attr)
457 {
458 // key will always be a string.
459 std::string key = attr->Name();
460 std::string string_value = attr->Value();
461
462 // skip the interface attribute as it is already used for the connection's source interface
463 if (key == "interface")
464 {
465 attr = attr->Next();
466 continue;
467 }
468
469 // skip the provider attribute as it is already used for the connection's source provider
470 if (key == "provider")
471 {
472 attr = attr->Next();
473 continue;
474 }
475
476 // Treat booleans strictly as literal true/false (avoid misclassifying numeric strings like "0.5").
477 if (string_value == "true" || string_value == "false")
478 {
479 const bool bool_value = (string_value == "true");
480 RCLCPP_INFO(node_->get_logger(), "[xml_parser] Extracted attribute: %s = %s (bool)", key.c_str(), bool_value ? "true" : "false");
481 parameters.set_value(key, bool_value, capabilities2_events::OptionType::BOOL);
482 }
483 else
484 {
485 // Prefer int when the value is an integer; otherwise parse as double if fully consumable.
486 char* int_end = nullptr;
487 errno = 0;
488 const long int_value_long = std::strtol(string_value.c_str(), &int_end, 10);
489
490 if (int_end != nullptr && *int_end == '\0' && errno == 0)
491 {
492 const int int_value = static_cast<int>(int_value_long);
493 RCLCPP_INFO(node_->get_logger(), "[xml_parser] Extracted attribute: %s = %d (int)", key.c_str(), int_value);
494 parameters.set_value(key, int_value, capabilities2_events::OptionType::INT);
495 }
496 else
497 {
498 char* double_end = nullptr;
499 errno = 0;
500 const double double_value = std::strtod(string_value.c_str(), &double_end);
501
502 if (double_end != nullptr && *double_end == '\0' && errno == 0)
503 {
504 RCLCPP_INFO(node_->get_logger(), "[xml_parser] Extracted attribute: %s = %f (double)", key.c_str(), double_value);
505 parameters.set_value(key, double_value, capabilities2_events::OptionType::DOUBLE);
506 }
507 else
508 {
509 RCLCPP_INFO(node_->get_logger(), "[xml_parser] Extracted attribute: %s = %s (string)", key.c_str(), string_value.c_str());
510 parameters.set_value(key, string_value, capabilities2_events::OptionType::STRING);
511 }
512 }
513 }
514
515 attr = attr->Next();
516 }
517
518 return parameters;
519 }
520
527 int extract_connections(tinyxml2::XMLElement* element, fabric::Plan& plan, int connection_id = 0,
528 fabric::event connection_type = fabric::event::ON_SUCCESS, std::string conn_description = "")
529 {
530 int predecessor_id;
531 int last_conn_id = connection_id;
532
533 const char* type = nullptr;
534 const char* name = nullptr;
535 const char* interface = nullptr;
536 const char* provider = nullptr;
537
538 std::string elementTag(element->Name());
539
540 std::string typetag = "";
541 std::string description = "";
542 std::string interfacetag = "";
543 std::string providertag = "";
544
545 bool hasChildren = (element->FirstChildElement() != nullptr);
546 bool hasSiblings = (element->NextSiblingElement() != nullptr);
547
548 if (elementTag == "Control")
549 {
550 type = element->Attribute("type");
551 name = element->Attribute("name");
552
553 if (type)
554 typetag = type;
555
556 if (name)
557 description = name;
558
559 if (typetag == "sequential")
560 {
561 if (hasChildren)
562 last_conn_id = extract_connections(element->FirstChildElement(), plan, connection_id, fabric::event::ON_SUCCESS, description);
563 }
564 else if (typetag == "parallel_any")
565 {
566 if (hasChildren)
567 {
568 // extract plan.connections from the first child element
569 last_conn_id = extract_connections(element->FirstChildElement(), plan, connection_id, fabric::event::ON_START, description);
570
571 // add a system connection for parallel_any to proceed when at least one parallel runner is completed
572 last_conn_id = add_parallel_any(plan, last_conn_id + 1, "parallel_any_for_muxing_outputs");
573 }
574 }
575 else if (typetag == "parallel_all")
576 {
577 if (hasChildren)
578 {
579 // extract connections from the first child element
580 last_conn_id = extract_connections(element->FirstChildElement(), plan, connection_id, fabric::event::ON_START, description);
581
582 // add a system connection for parallel_all to proceed when all parallel runners are completed
583 last_conn_id = add_parallel_all(plan, last_conn_id + 1, "parallel_all_for_muxing_outputs");
584 }
585 }
586 else if (typetag == "recovery")
587 {
588 if (hasChildren)
589 {
590 // extract connections from the first child element
591 last_conn_id = extract_connections(element->FirstChildElement(), plan, connection_id, fabric::event::ON_FAILURE, description);
592
593 // add a system connection for recovery to proceed when the original runner or at least one recovery runner is completed
594 last_conn_id = add_parallel_any(plan, last_conn_id + 1, "parallel_any_for_muxing_recovery");
595 }
596 }
597
598 if (hasSiblings)
599 {
600 // continue extracting connections from the next sibling element
601 last_conn_id = extract_connections(element->NextSiblingElement(), plan, last_conn_id + 1, connection_type, conn_description);
602 }
603
604 return last_conn_id;
605 }
606 else if (elementTag == "Runner")
607 {
608 interface = element->Attribute("interface");
609 provider = element->Attribute("provider");
610
611 if (interface)
612 interfacetag = interface;
613
614 if (provider)
615 providertag = provider;
616
617 RCLCPP_INFO(node_->get_logger(), "[xml_parser] Creating fabric connection: interface = %s, provider = %s", interfacetag.c_str(),
618 providertag.c_str());
619
621
622 connection.source.interface = interfacetag;
623 connection.source.provider = providertag;
626
627 predecessor_id = connection_id - 1;
628
629 plan.connections[connection_id] = connection;
630 plan.connections[connection_id].description = conn_description;
631
632 runner_index += 1;
633
634 if (connection_id != 0)
635 {
636 // if the predecessor is a system runner, we need to update the successor's parameters with the predecessor's id
637 // this->check_and_update_runner_id(connections[predecessor_id], connections[connection_id]);
638
639 if (connection_type == fabric::event::ON_SUCCESS)
640 {
641 // Set the target_on_success for the predecessor connection
642 plan.connections[predecessor_id].on_success = plan.connections[connection_id].source;
643
644 RCLCPP_INFO(node_->get_logger(), "[xml_parser] set on_success for id %d to interface %s", predecessor_id,
645 plan.connections[predecessor_id].on_success.interface.c_str());
646 }
647 else if (connection_type == fabric::event::ON_START)
648 {
649 // Set the target_on_start for the predecessor connection
650 plan.connections[predecessor_id].on_start = plan.connections[connection_id].source;
651 RCLCPP_INFO(node_->get_logger(), "[xml_parser] set on_start for id %d to interface %s", predecessor_id,
652 plan.connections[predecessor_id].on_start.interface.c_str());
653 }
654 else if (connection_type == fabric::event::ON_FAILURE)
655 {
656 // Set the target_on_failure for the predecessor connection
657 plan.connections[predecessor_id].on_failure = plan.connections[connection_id].source;
658 RCLCPP_INFO(node_->get_logger(), "[xml_parser] set on_failure for id %d to interface %s", predecessor_id,
659 plan.connections[predecessor_id].on_failure.interface.c_str());
660 }
661 }
662
663 if (hasSiblings)
664 last_conn_id = this->extract_connections(element->NextSiblingElement(), plan, connection_id + 1, connection_type);
665 else
666 last_conn_id = connection_id;
667
668 return last_conn_id;
669 }
670
671 // Fallback if an unexpected tag appears: return the current id unchanged
672 return last_conn_id;
673 }
674
678 std::vector<std::string> control_list;
679
683 tinyxml2::XMLDocument system_doc;
684
689
693 tinyxml2::XMLElement* plan_ = nullptr;
694
698 tinyxml2::XMLDocument default_document;
699
703 tinyxml2::XMLDocument current_document_;
704};
705} // namespace fabric
Abstract base class for XML plan parsing plugins.
Definition parser_base.hpp:18
void initialize_base(const rclcpp::Node::SharedPtr &node, const std::string &plugin_name)
Base initialization method for derived classes.
Definition parser_base.hpp:64
rclcpp::Node::SharedPtr node_
Shared pointer to the ROS2 node.
Definition parser_base.hpp:73
Syntax validation plugin for XML plans.
Definition xml_parser_plugin.hpp:20
tinyxml2::XMLDocument current_document_
XML Document to hold the current plan.
Definition xml_parser_plugin.hpp:703
std::vector< std::string > control_list
List of valid control tags.
Definition xml_parser_plugin.hpp:678
bool load_file(const std::string &file_path, fabric::Plan &plan) override
load a file into an XML document
Definition xml_parser_plugin.hpp:44
int add_parallel_all(fabric::Plan &plan, int connection_id=0, std::string description="")
Adds a system connection for parallel input multiplexing with waiting for all inputs.
Definition xml_parser_plugin.hpp:341
bool search(std::vector< std::string > list, std::string value)
search a string in a vector of strings
Definition xml_parser_plugin.hpp:159
XMLParser()=default
void parse(fabric::Plan &plan) override
Parse the given XML plan.
Definition xml_parser_plugin.hpp:92
tinyxml2::XMLElement * plan_
Pointer to the plan element in the XML document.
Definition xml_parser_plugin.hpp:693
tinyxml2::XMLElement * extract_plan(tinyxml2::XMLDocument &document)
Extract the <Plan> element from the XML document.
Definition xml_parser_plugin.hpp:173
bool check_compatibility(const fabric::Plan &plan) override
Check the compatibility of the given plan with the given parser.
Definition xml_parser_plugin.hpp:68
tinyxml2::XMLDocument system_doc
XML Document for system runners.
Definition xml_parser_plugin.hpp:683
int runner_index
Index for assigning unique IDs to system runners.
Definition xml_parser_plugin.hpp:688
int add_parallel_any(fabric::Plan &plan, int connection_id=0, std::string description="")
Adds a system connection for parallel input multiplexing with waiting for any inputs.
Definition xml_parser_plugin.hpp:383
tinyxml2::XMLDocument default_document
Default XML document for loading from file.
Definition xml_parser_plugin.hpp:698
virtual ~XMLParser()=default
void add_completion_runner(tinyxml2::XMLDocument &document, const std::string &plan_id)
add a completion runner to the plan This function adds a new <Control> element with type "sequential"...
Definition xml_parser_plugin.hpp:306
void convert_to_string(tinyxml2::XMLDocument &document_xml, std::string &document_string)
convert XMLDocument to std::string
Definition xml_parser_plugin.hpp:144
bool convert_to_string(tinyxml2::XMLElement *element, std::string &parameters)
convert XMLElement to std::string
Definition xml_parser_plugin.hpp:191
void initialize(const rclcpp::Node::SharedPtr &node) override
Initialize the XML parser plugin.
Definition xml_parser_plugin.hpp:30
int extract_connections(tinyxml2::XMLElement *element, fabric::Plan &plan, int connection_id=0, fabric::event connection_type=fabric::event::ON_SUCCESS, std::string conn_description="")
parse through the plan and extract the connections
Definition xml_parser_plugin.hpp:527
capabilities2_events::EventParameters convert_xml_to_event_parameters(tinyxml2::XMLElement *element)
Check and update the system runner id for the successor based on the predecessor's id.
Definition xml_parser_plugin.hpp:449
bool check_syntax(tinyxml2::XMLElement *element, std::vector< std::string > &control_list, std::vector< std::string > &rejected, std::string &error)
check the plan to make sure all control and runner XML elements are valid with minimal required attri...
Definition xml_parser_plugin.hpp:218
Definition parser_base.hpp:9
event
Enumeration for different event types in connections.
Definition structs.hpp:15
@ ON_START
Definition structs.hpp:16
@ ON_FAILURE
Definition structs.hpp:18
@ ON_SUCCESS
Definition structs.hpp:17
Definition structs.hpp:70
std::vector< std::string > rejected_list
Definition structs.hpp:75
std::map< int, connection > connections
Definition structs.hpp:74
std::string plan
Definition structs.hpp:72
std::string plan_id
Definition structs.hpp:71
Structure representing connections between nodes.
Definition structs.hpp:42
node on_success
Definition structs.hpp:46
node source
Definition structs.hpp:43
Base class for driver exceptions.
Definition exception.hpp:14
Structure representing a node in the fabric.
Definition structs.hpp:26
std::string provider
Definition structs.hpp:28
std::string interface
Definition structs.hpp:27
capabilities2_events::EventParameters parameters
Definition structs.hpp:29
int instance_id
Definition structs.hpp:30