Skip to content

Commit 9d78398

Browse files
author
Gaël Écorchard
committed
Implement writeTreeXSD() to generate an XSD
Implement writeTreeXSD() to generate an XSD schema for the nodes defined in the factory. Signed-off-by: Gaël Écorchard <[email protected]>
1 parent 01e7f59 commit 9d78398

File tree

2 files changed

+306
-0
lines changed

2 files changed

+306
-0
lines changed

include/behaviortree_cpp/xml_parsing.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,16 @@ void VerifyXML(const std::string& xml_text,
6060
std::string writeTreeNodesModelXML(const BehaviorTreeFactory& factory,
6161
bool include_builtin = false);
6262

63+
/**
64+
* @brief writeTreeXSD generates an XSD for the nodes defined in the factory
65+
*
66+
* @param factory the factory with the registered types
67+
*
68+
* @return string containing the XML.
69+
*/
70+
[[nodiscard]]
71+
std::string writeTreeXSD(const BehaviorTreeFactory& factory);
72+
6373
/**
6474
* @brief WriteTreeToXML create a string that contains the XML that corresponds to a given tree.
6575
* When using this function with a logger, you should probably set both add_metadata and

src/xml_parsing.cpp

Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,13 @@
1010
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
1111
*/
1212

13+
#include <cstring>
1314
#include <functional>
15+
#include <iostream>
1416
#include <list>
17+
#include <sstream>
18+
#include <string>
19+
#include <typeindex>
1520

1621
#if defined(__linux) || defined(__linux__)
1722
#pragma GCC diagnostic push
@@ -39,6 +44,34 @@
3944
#include "behaviortree_cpp/tree_node.h"
4045
#include "behaviortree_cpp/utils/demangle_util.h"
4146

47+
namespace
48+
{
49+
std::string
50+
xsdAttributeType(const BT::PortInfo& port_info)
51+
{
52+
const auto& type_info = port_info.type();
53+
if ((type_info == typeid(int)) or (type_info == typeid(unsigned int)))
54+
{
55+
return "integerOrBlackboardType";
56+
}
57+
else if (type_info == typeid(double))
58+
{
59+
return "decimalOrBlackboardType";
60+
}
61+
else if (type_info == typeid(bool))
62+
{
63+
return "xs:boolean";
64+
}
65+
else if (type_info == typeid(std::string))
66+
{
67+
return "xs:string";
68+
}
69+
70+
return std::string();
71+
}
72+
73+
} // Anonymous workspace
74+
4275
namespace BT
4376
{
4477
using namespace tinyxml2;
@@ -1117,6 +1150,269 @@ std::string writeTreeNodesModelXML(const BehaviorTreeFactory& factory,
11171150
return std::string(printer.CStr(), size_t(printer.CStrSize() - 1));
11181151
}
11191152

1153+
std::string writeTreeXSD(const BehaviorTreeFactory& factory)
1154+
{
1155+
// There are 2 forms of representation for a node:
1156+
// compact: <Sequence .../> and explicit: <Control ID="Sequence" ... />
1157+
// Only the compact form is supported because the explicit form doesn't
1158+
// make sense with XSD since we would need to allow any attribute.
1159+
// Prepare the data
1160+
1161+
std::map<std::string, const TreeNodeManifest*> ordered_models;
1162+
for (const auto& [registration_id, model] : factory.manifests())
1163+
{
1164+
ordered_models.insert({registration_id, &model});
1165+
}
1166+
1167+
XMLDocument doc;
1168+
1169+
// Add the XML declaration
1170+
XMLDeclaration* declaration = doc.NewDeclaration("xml version=\"1.0\" encoding=\"UTF-8\"");
1171+
doc.InsertFirstChild(declaration);
1172+
1173+
// Create the root element with namespace and attributes
1174+
// To validate a BT XML file with `schema.xsd` in the same directory:
1175+
// <root BTCPP_format="4" main_tree_to_execute="MainTree"
1176+
// xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
1177+
// xsi:noNamespaceSchemaLocation="schema.xsd">
1178+
XMLElement* schema_element = doc.NewElement("xs:schema");
1179+
schema_element->SetAttribute("xmlns:xs", "http://www.w3.org/2001/XMLSchema");
1180+
schema_element->SetAttribute("elementFormDefault", "qualified");
1181+
doc.InsertEndChild(schema_element);
1182+
1183+
auto parse_and_insert = [&doc](XMLElement* parent_elem, const char* str) {
1184+
XMLDocument tmp_doc;
1185+
tmp_doc.Parse(str);
1186+
if (tmp_doc.Error())
1187+
{
1188+
std::cerr << "Internal error parsing existing XML: " << tmp_doc.ErrorStr() << std::endl;
1189+
return;
1190+
}
1191+
for (auto child = tmp_doc.FirstChildElement(); child != nullptr;
1192+
child = child->NextSiblingElement())
1193+
{
1194+
parent_elem->InsertEndChild(child->DeepClone(&doc));
1195+
}
1196+
};
1197+
1198+
// Common elements.
1199+
XMLComment* comment = doc.NewComment("Define the common elements");
1200+
schema_element->InsertEndChild(comment);
1201+
1202+
// TODO: add <xs:whiteSpace value="preserve"/> for `inputPortType` and `outputPortType`.
1203+
parse_and_insert(schema_element, R"(
1204+
<xs:simpleType name="decimalOrBlackboardType">
1205+
<xs:restriction base="xs:string">
1206+
<xs:pattern value="[+-]?[0-9]*(\.[0-9]+)?"/>
1207+
<xs:pattern value="\{[a-zA-Z][a-zA-Z0-9_]*(\.[a-zA-Z][a-zA-Z0-9_]*)*\}"/>
1208+
</xs:restriction>
1209+
</xs:simpleType>
1210+
<xs:simpleType name="integerOrBlackboardType">
1211+
<xs:restriction base="xs:string">
1212+
<xs:pattern value="[+-]?[0-9]+"/>
1213+
<xs:pattern value="\{[a-zA-Z][a-zA-Z0-9_]*(\.[a-zA-Z][a-zA-Z0-9_]*)*\}"/>
1214+
</xs:restriction>
1215+
</xs:simpleType>
1216+
<xs:simpleType name="descriptionType">
1217+
<xs:restriction base="xs:string">
1218+
<xs:whiteSpace value="preserve"/>
1219+
</xs:restriction>
1220+
</xs:simpleType>
1221+
<xs:complexType name="inputPortType">
1222+
<xs:simpleContent>
1223+
<xs:extension base="xs:string">
1224+
<xs:attribute name="name" type="xs:string" use="required"/>
1225+
<xs:attribute name="type" type="xs:string" use="optional"/>
1226+
<xs:attribute name="default" type="xs:string" use="optional"/>
1227+
</xs:extension>
1228+
</xs:simpleContent>
1229+
</xs:complexType>
1230+
<xs:complexType name="outputPortType">
1231+
<xs:simpleContent>
1232+
<xs:extension base="xs:string">
1233+
<xs:attribute name="name" type="xs:string" use="required"/>
1234+
<xs:attribute name="type" type="xs:string" use="optional"/>
1235+
</xs:extension>
1236+
</xs:simpleContent>
1237+
</xs:complexType>
1238+
<xs:attributeGroup name="preconditionAttributeGroup">
1239+
<xs:attribute name="_failureIf" type="xs:string" use="optional"/>
1240+
<xs:attribute name="_skipIf" type="xs:string" use="optional"/>
1241+
<xs:attribute name="_successIf" type="xs:string" use="optional"/>
1242+
<xs:attribute name="_while" type="xs:string" use="optional"/>
1243+
</xs:attributeGroup>
1244+
<xs:attributeGroup name="postconditionAttributeGroup">
1245+
<xs:attribute name="_onSuccess" type="xs:string" use="optional"/>
1246+
<xs:attribute name="_onFailure" type="xs:string" use="optional"/>
1247+
<xs:attribute name="_post" type="xs:string" use="optional"/>
1248+
<xs:attribute name="_onHalted" type="xs:string" use="optional"/>
1249+
</xs:attributeGroup>)");
1250+
1251+
// Common attributes
1252+
// Note that we do not add the `ID` attribute because we do not
1253+
// support the explicit notation (e.g. <Action ID="Saysomething">).
1254+
// Cf. https://www.behaviortree.dev/docs/learn-the-basics/xml_format/#compact-vs-explicit-representation
1255+
// There is no way to check attribute validity with the explicit notation with XSD.
1256+
// The `ID` attribute for `<SubTree>` is handled separately.
1257+
parse_and_insert(schema_element, R"(
1258+
<xs:attributeGroup name="commonAttributeGroup">
1259+
<xs:attribute name="name" type="xs:string" use="optional"/>
1260+
<xs:attributeGroup ref="preconditionAttributeGroup"/>
1261+
<xs:attributeGroup ref="postconditionAttributeGroup"/>
1262+
</xs:attributeGroup>)");
1263+
1264+
// Basic node types
1265+
parse_and_insert(schema_element, R"(
1266+
<xs:complexType name="treeNodesModelNodeType">
1267+
<xs:sequence>
1268+
<xs:choice minOccurs="0" maxOccurs="unbounded">
1269+
<xs:element name="input_port" type="inputPortType"/>
1270+
<xs:element name="output_port" type="outputPortType"/>
1271+
</xs:choice>
1272+
<xs:element name="description" type="descriptionType" minOccurs="0" maxOccurs="1"/>
1273+
</xs:sequence>
1274+
<xs:attribute name="ID" type="xs:string" use="required"/>
1275+
</xs:complexType>
1276+
<xs:group name="treeNodesModelNodeGroup">
1277+
<xs:choice>
1278+
<xs:element name="Action" type="treeNodesModelNodeType"/>
1279+
<xs:element name="Condition" type="treeNodesModelNodeType"/>
1280+
<xs:element name="Control" type="treeNodesModelNodeType"/>
1281+
<xs:element name="Decorator" type="treeNodesModelNodeType"/>
1282+
</xs:choice>
1283+
</xs:group>
1284+
)");
1285+
1286+
// `root` element
1287+
const auto root_element_xsd = R"(
1288+
<xs:element name="root">
1289+
<xs:complexType>
1290+
<xs:sequence>
1291+
<xs:choice minOccurs="0" maxOccurs="unbounded">
1292+
<xs:element ref="include"/>
1293+
<xs:element ref="BehaviorTree"/>
1294+
</xs:choice>
1295+
<xs:element ref="TreeNodesModel" minOccurs="0" maxOccurs="1"/>
1296+
</xs:sequence>
1297+
<xs:attribute name="BTCPP_format" type="xs:string" use="required"/>
1298+
<xs:attribute name="main_tree_to_execute" type="xs:string" use="required"/>
1299+
</xs:complexType>
1300+
</xs:element>
1301+
)";
1302+
parse_and_insert(schema_element, root_element_xsd);
1303+
1304+
// Group definition for a single node of any of the existing node types.
1305+
XMLElement* one_node_group = doc.NewElement("xs:group");
1306+
{
1307+
one_node_group->SetAttribute("name", "oneNodeGroup");
1308+
std::ostringstream xsd;
1309+
xsd << "<xs:choice>";
1310+
for (const auto& [registration_id, model] : ordered_models)
1311+
{
1312+
xsd << "<xs:element name=\"" << registration_id << "\" type=\"" << registration_id << "Type\"/>";
1313+
}
1314+
xsd << "</xs:choice>";
1315+
parse_and_insert(one_node_group, xsd.str().c_str());
1316+
schema_element->InsertEndChild(one_node_group);
1317+
}
1318+
1319+
// `include` element
1320+
parse_and_insert(schema_element, R"(
1321+
<xs:element name="include">
1322+
<xs:complexType>
1323+
<xs:attribute name="path" type="xs:string" use="required"/>
1324+
<xs:attribute name="ros_pkg" type="xs:string" use="optional"/>
1325+
</xs:complexType>
1326+
</xs:element>
1327+
)");
1328+
1329+
// `BehaviorTree` element
1330+
parse_and_insert(schema_element, R"(
1331+
<xs:element name="BehaviorTree">
1332+
<xs:complexType>
1333+
<xs:group ref="oneNodeGroup"/>
1334+
<xs:attribute name="ID" type="xs:string" use="required"/>
1335+
</xs:complexType>
1336+
</xs:element>
1337+
)");
1338+
1339+
// `TreeNodesModel` element
1340+
parse_and_insert(schema_element, R"(
1341+
<xs:element name="TreeNodesModel">
1342+
<xs:complexType>
1343+
<xs:group ref="treeNodesModelNodeGroup" minOccurs="0" maxOccurs="unbounded"/>
1344+
</xs:complexType>
1345+
</xs:element>
1346+
)");
1347+
1348+
// Definitions for all node types.
1349+
for (const auto& [registration_id, model] : ordered_models)
1350+
{
1351+
XMLElement* type = doc.NewElement("xs:complexType");
1352+
type->SetAttribute("name", (model->registration_ID + "Type").c_str());
1353+
if ((model->type == NodeType::CONDITION) or (model->type == NodeType::ACTION))
1354+
{
1355+
/* No children, nothing to add. */
1356+
}
1357+
else if ((model->type == NodeType::DECORATOR)
1358+
or (model->type == NodeType::SUBTREE))
1359+
{
1360+
/* One child. */
1361+
// <xs:group ref="oneNodeGroup" minOccurs="0" maxOccurs="1"/>
1362+
XMLElement* group = doc.NewElement("xs:group");
1363+
group->SetAttribute("ref", "oneNodeGroup");
1364+
group->SetAttribute("minOccurs", "0");
1365+
group->SetAttribute("maxOccurs", "1");
1366+
type->InsertEndChild(group);
1367+
}
1368+
else
1369+
{
1370+
/* NodeType::CONTROL. */
1371+
// TODO: check the code, the doc says 1..N but why not 0..N?
1372+
// <xs:group ref="oneNodeGroup" minOccurs="0" maxOccurs="unbounded"/>
1373+
XMLElement* group = doc.NewElement("xs:group");
1374+
group->SetAttribute("ref", "oneNodeGroup");
1375+
group->SetAttribute("minOccurs", "0");
1376+
group->SetAttribute("maxOccurs", "unbounded");
1377+
type->InsertEndChild(group);
1378+
}
1379+
XMLElement* common_attr_group = doc.NewElement("xs:attributeGroup");
1380+
common_attr_group->SetAttribute("ref", "commonAttributeGroup");
1381+
type->InsertEndChild(common_attr_group);
1382+
for (const auto& [port_name, port_info] : model->ports)
1383+
{
1384+
XMLElement* attr = doc.NewElement("xs:attribute");
1385+
attr->SetAttribute("name", port_name.c_str());
1386+
const auto xsd_attribute_type = xsdAttributeType(port_info);
1387+
if (not xsd_attribute_type.empty())
1388+
{
1389+
attr->SetAttribute("type", xsd_attribute_type.c_str());
1390+
}
1391+
if (not port_info.defaultValue().empty())
1392+
{
1393+
attr->SetAttribute("default", port_info.defaultValueString().c_str());
1394+
}
1395+
else
1396+
{
1397+
attr->SetAttribute("use", "required");
1398+
}
1399+
type->InsertEndChild(attr);
1400+
}
1401+
if (model->registration_ID == "SubTree")
1402+
{
1403+
parse_and_insert(type, R"(
1404+
<xs:attribute name="ID" type="xs:string" use="required"/>
1405+
<xs:anyAttribute processContents="skip"/>
1406+
)");
1407+
}
1408+
schema_element->InsertEndChild(type);
1409+
}
1410+
1411+
XMLPrinter printer;
1412+
doc.Print(&printer);
1413+
return std::string(printer.CStr(), size_t(printer.CStrSize() - 1));
1414+
}
1415+
11201416
Tree buildTreeFromText(const BehaviorTreeFactory& factory, const std::string& text,
11211417
const Blackboard::Ptr& blackboard)
11221418
{

0 commit comments

Comments
 (0)