1#!/usr/bin/env python3
2#
3# Copyright (C) 2022 Intel Corporation.
4#
5# SPDX-License-Identifier: BSD-3-Clause
6#
7
8import os
9import argparse
10import elementpath
11
12from scenario_transformer import ScenarioTransformer
13
14from pipeline import PipelineObject, PipelineStage, PipelineEngine
15from schema_slicer import SlicingSchemaByVMTypeStage
16
17class DefaultValuePopulator(ScenarioTransformer):
18    def get_default_value(self, xsd_element_node, xml_parent_node):
19        # The attribute @default of the xsd:element node
20        v = xsd_element_node.get("default")
21        if v is not None:
22            return v
23
24        # The acrn:defaults and acrn:unique-among annotations which define a set of default values that shall be unique
25        # among a collection of nodes
26        annot_node = self.get_node(xsd_element_node, "xs:annotation")
27        if annot_node is not None:
28            defaults = annot_node.get("{https://projectacrn.org}defaults")
29            unique_among = annot_node.get("{https://projectacrn.org}unique-among")
30            if defaults is not None and unique_among is not None:
31                try:
32                    default_values = set(eval(defaults))
33                    existing_values = set(elementpath.select(self.xml_etree, unique_among, variables={"parent": xml_parent_node}))
34                    available_defaults = default_values - existing_values
35                    return sorted(list(available_defaults))[0]
36                except:
37                    pass
38
39        return None
40
41    def add_missing_nodes(self, xsd_element_node, xml_parent_node, new_node_index):
42        element_name = xsd_element_node.get("name")
43        default_value = self.get_default_value(xsd_element_node, xml_parent_node)
44
45        # If the node is neither of a complex type (i.e. it does not have an child node) nor has a default value, do not
46        # create the node at all. Users are required to fill in proper values in such nodes, and missing any of them
47        # shall trigger a validation error.
48        if self.complex_type_of_element(xsd_element_node) is None and default_value is None:
49            return []
50
51        new_node = xml_parent_node.makeelement(element_name, {})
52        new_node.text = default_value
53
54        if new_node_index is not None:
55            xml_parent_node.insert(new_node_index, new_node)
56        else:
57            xml_parent_node.append(new_node)
58
59        return [new_node]
60
61    def fill_empty_node(self, xsd_element_node, xml_parent_node, xml_empty_node):
62        default_value = self.get_default_value(xsd_element_node, xml_parent_node)
63        if default_value is not None:
64            xml_empty_node.text = default_value
65
66class DefaultValuePopulatingStage(PipelineStage):
67    uses = {"schema_etree", "scenario_etree"}
68    provides = {"scenario_etree"}
69
70    def run(self, obj):
71        populator = DefaultValuePopulator(obj.get("schema_etree"))
72        etree = obj.get("scenario_etree")
73        populator.transform(etree)
74        obj.set("scenario_etree", etree)
75
76def main(args):
77    from xml_loader import XMLLoadStage
78    from lxml_loader import LXMLLoadStage
79
80    pipeline = PipelineEngine(["schema_path", "scenario_path"])
81    pipeline.add_stages([
82        LXMLLoadStage("schema"),
83        XMLLoadStage("scenario"),
84        SlicingSchemaByVMTypeStage(),
85        DefaultValuePopulatingStage(),
86    ])
87
88    obj = PipelineObject(schema_path = args.schema, scenario_path = args.scenario)
89    pipeline.run(obj)
90    obj.get("scenario_etree").write(args.out)
91
92if __name__ == "__main__":
93    config_tools_dir = os.path.join(os.path.dirname(__file__), "..")
94    schema_dir = os.path.join(config_tools_dir, "schema")
95
96    parser = argparse.ArgumentParser(description="Populate a given scenario XML with default values of nonexistent nodes")
97    parser.add_argument("scenario", help="Path to the scenario XML file from users")
98    parser.add_argument("out", nargs="?", default="out.xml", help="Path where the output is placed")
99    parser.add_argument("--schema", default=os.path.join(schema_dir, "config.xsd"), help="the XML schema that defines the syntax of scenario XMLs")
100    args = parser.parse_args()
101
102    main(args)
103