From dddff95212339569c2edbbde8228c43bf3eaea49 Mon Sep 17 00:00:00 2001
From: Andreas Lauser <andreas.lauser@mercedes-benz.com>
Date: Thu, 5 Dec 2024 14:18:21 +0100
Subject: [PATCH 1/2] resolve SNREFs in comparam subsets and comparam specs

since it seems that nobody ever tried to use odxtools with comparam subsets and
comparam specs that use SNREFs, this did not crop up earlier...

Signed-off-by: Andreas Lauser <andreas.lauser@mercedes-benz.com>
Signed-off-by: Gerrit Ecke <gerrit.ecke@mercedes-benz.com>
---
 odxtools/database.py | 11 +++++++++++
 1 file changed, 11 insertions(+)

diff --git a/odxtools/database.py b/odxtools/database.py
index f8d81801..74b9edcd 100644
--- a/odxtools/database.py
+++ b/odxtools/database.py
@@ -20,6 +20,7 @@
 from .exceptions import odxraise, odxrequire
 from .nameditemlist import NamedItemList
 from .odxlink import OdxLinkDatabase, OdxLinkId
+from .snrefcontext import SnRefContext
 
 
 class Database:
@@ -139,6 +140,16 @@ def refresh(self) -> None:
         for dlc in self.diag_layer_containers:
             dlc._resolve_odxlinks(self._odxlinks)
 
+        # resolve short name references for containers which do not do
+        # inheritance (we can call directly call _resolve_snrefs())
+        context = SnRefContext()
+        context.database = self
+
+        for subset in self.comparam_subsets:
+            subset._resolve_snrefs(context)
+        for spec in self.comparam_specs:
+            spec._resolve_snrefs(context)
+
         # let the diaglayers sort out the inherited objects and the
         # short name references
         for dlc in self.diag_layer_containers:

From 8361b71d7923410706eb74a766509d4b34f9746b Mon Sep 17 00:00:00 2001
From: Andreas Lauser <andreas.lauser@mercedes-benz.com>
Date: Thu, 5 Dec 2024 12:44:40 +0100
Subject: [PATCH 2/2] Introduce `OdxCategory`

The spec defines `OdxCategory` as the base for any top-level content
tag in an ODX XML file. odxtools currently implements
`DIAG-LAYER-CONTAINER`, `COMPARAM-SPEC` and `COMPARAM-SUBSET` (all of
them are relevant for diagnostics based on UDS). Currently missing are
the categories `ECU-CONFIG` (variant coding), `FLASH` (firmware blobs
for flashing), `FUNCTION-DICTIONARY` (functionality distributed over
multiple ECUs) and `MULTIPLE-ECU-JOB-SPEC` (multiple-ecu jobs).

Signed-off-by: Andreas Lauser <andreas.lauser@mercedes-benz.com>
Signed-off-by: Christian Hackenbeck <christian.hackenbeck@mercedes-benz.com>
---
 odxtools/comparamspec.py                      | 70 ++++------------
 odxtools/comparamsubset.py                    | 81 +++++-------------
 odxtools/database.py                          | 13 ++-
 odxtools/diaglayercontainer.py                | 54 ++++--------
 odxtools/odxcategory.py                       | 83 +++++++++++++++++++
 .../templates/comparam-spec.odx-c.xml.jinja2  | 25 +-----
 .../comparam-subset.odx-cs.xml.jinja2         | 26 +-----
 .../diag_layer_container.odx-d.xml.jinja2     | 29 +------
 .../macros/printOdxCategory.xml.jinja2        | 28 +++++++
 9 files changed, 182 insertions(+), 227 deletions(-)
 create mode 100644 odxtools/odxcategory.py
 create mode 100644 odxtools/templates/macros/printOdxCategory.xml.jinja2

diff --git a/odxtools/comparamspec.py b/odxtools/comparamspec.py
index d02f35b7..2cc93bfe 100644
--- a/odxtools/comparamspec.py
+++ b/odxtools/comparamspec.py
@@ -1,66 +1,39 @@
 # SPDX-License-Identifier: MIT
 from dataclasses import dataclass
-from typing import Any, Dict, List, Optional
+from typing import TYPE_CHECKING, Any, Dict, List
 from xml.etree import ElementTree
 
-from .admindata import AdminData
-from .companydata import CompanyData
-from .element import IdentifiableElement
-from .exceptions import odxrequire
 from .nameditemlist import NamedItemList
+from .odxcategory import OdxCategory
 from .odxlink import OdxDocFragment, OdxLinkDatabase, OdxLinkId
 from .protstack import ProtStack
 from .snrefcontext import SnRefContext
-from .specialdatagroup import SpecialDataGroup
 from .utils import dataclass_fields_asdict
 
+if TYPE_CHECKING:
+    from .database import Database
+
 
 @dataclass
-class ComparamSpec(IdentifiableElement):
-    admin_data: Optional[AdminData]
-    company_datas: NamedItemList[CompanyData]
-    sdgs: List[SpecialDataGroup]
+class ComparamSpec(OdxCategory):
     prot_stacks: NamedItemList[ProtStack]
 
     @staticmethod
     def from_et(et_element: ElementTree.Element, doc_frags: List[OdxDocFragment]) -> "ComparamSpec":
 
-        short_name = odxrequire(et_element.findtext("SHORT-NAME"))
-        doc_frags = [OdxDocFragment(short_name, str(et_element.tag))]
-        kwargs = dataclass_fields_asdict(IdentifiableElement.from_et(et_element, doc_frags))
+        cat = OdxCategory.category_from_et(et_element, doc_frags, doc_type="COMPARAM-SPEC")
+        doc_frags = cat.odx_id.doc_fragments
+        kwargs = dataclass_fields_asdict(cat)
 
-        admin_data = AdminData.from_et(et_element.find("ADMIN-DATA"), doc_frags)
-        company_datas = NamedItemList([
-            CompanyData.from_et(cde, doc_frags)
-            for cde in et_element.iterfind("COMPANY-DATAS/COMPANY-DATA")
-        ])
-        sdgs = [
-            SpecialDataGroup.from_et(sdge, doc_frags) for sdge in et_element.iterfind("SDGS/SDG")
-        ]
         prot_stacks = NamedItemList([
             ProtStack.from_et(dl_element, doc_frags)
             for dl_element in et_element.iterfind("PROT-STACKS/PROT-STACK")
         ])
 
-        return ComparamSpec(
-            admin_data=admin_data,
-            company_datas=company_datas,
-            sdgs=sdgs,
-            prot_stacks=prot_stacks,
-            **kwargs)
+        return ComparamSpec(prot_stacks=prot_stacks, **kwargs)
 
     def _build_odxlinks(self) -> Dict[OdxLinkId, Any]:
-        odxlinks: Dict[OdxLinkId, Any] = {}
-        odxlinks[self.odx_id] = self
-
-        if self.admin_data is not None:
-            odxlinks.update(self.admin_data._build_odxlinks())
-
-        for cd in self.company_datas:
-            odxlinks.update(cd._build_odxlinks())
-
-        for sdg in self.sdgs:
-            odxlinks.update(sdg._build_odxlinks())
+        odxlinks = super()._build_odxlinks()
 
         for ps in self.prot_stacks:
             odxlinks.update(ps._build_odxlinks())
@@ -68,27 +41,16 @@ def _build_odxlinks(self) -> Dict[OdxLinkId, Any]:
         return odxlinks
 
     def _resolve_odxlinks(self, odxlinks: OdxLinkDatabase) -> None:
-        if self.admin_data is not None:
-            self.admin_data._resolve_odxlinks(odxlinks)
-
-        for cd in self.company_datas:
-            cd._resolve_odxlinks(odxlinks)
-
-        for sdg in self.sdgs:
-            sdg._resolve_odxlinks(odxlinks)
+        super()._resolve_odxlinks(odxlinks)
 
         for ps in self.prot_stacks:
             ps._resolve_odxlinks(odxlinks)
 
-    def _resolve_snrefs(self, context: SnRefContext) -> None:
-        if self.admin_data is not None:
-            self.admin_data._resolve_snrefs(context)
+    def _finalize_init(self, database: "Database", odxlinks: OdxLinkDatabase) -> None:
+        super()._finalize_init(database, odxlinks)
 
-        for cd in self.company_datas:
-            cd._resolve_snrefs(context)
-
-        for sdg in self.sdgs:
-            sdg._resolve_snrefs(context)
+    def _resolve_snrefs(self, context: SnRefContext) -> None:
+        super()._resolve_snrefs(context)
 
         for ps in self.prot_stacks:
             ps._resolve_snrefs(context)
diff --git a/odxtools/comparamsubset.py b/odxtools/comparamsubset.py
index 5e6ecad8..2c27d44d 100644
--- a/odxtools/comparamsubset.py
+++ b/odxtools/comparamsubset.py
@@ -1,50 +1,41 @@
 # SPDX-License-Identifier: MIT
 from dataclasses import dataclass
-from typing import Any, Dict, List, Optional
+from typing import TYPE_CHECKING, Any, Dict, List, Optional
 from xml.etree import ElementTree
 
-from .admindata import AdminData
-from .companydata import CompanyData
 from .comparam import Comparam
 from .complexcomparam import ComplexComparam
 from .dataobjectproperty import DataObjectProperty
-from .element import IdentifiableElement
-from .exceptions import odxrequire
 from .nameditemlist import NamedItemList
+from .odxcategory import OdxCategory
 from .odxlink import OdxDocFragment, OdxLinkDatabase, OdxLinkId
 from .snrefcontext import SnRefContext
-from .specialdatagroup import SpecialDataGroup
 from .unitspec import UnitSpec
 from .utils import dataclass_fields_asdict
 
+if TYPE_CHECKING:
+    from .database import Database
+
 
 @dataclass
-class ComparamSubset(IdentifiableElement):
-    # mandatory in ODX 2.2, but non existent in ODX 2.0
+class ComparamSubset(OdxCategory):
+    # mandatory in ODX 2.2, but non-existent in ODX 2.0
     category: Optional[str]
-    data_object_props: NamedItemList[DataObjectProperty]
+
     comparams: NamedItemList[Comparam]
     complex_comparams: NamedItemList[ComplexComparam]
+    data_object_props: NamedItemList[DataObjectProperty]
     unit_spec: Optional[UnitSpec]
-    admin_data: Optional[AdminData]
-    company_datas: NamedItemList[CompanyData]
-    sdgs: List[SpecialDataGroup]
 
     @staticmethod
     def from_et(et_element: ElementTree.Element,
                 doc_frags: List[OdxDocFragment]) -> "ComparamSubset":
 
-        category = et_element.get("CATEGORY")
-
-        short_name = odxrequire(et_element.findtext("SHORT-NAME"))
-        doc_frags = [OdxDocFragment(short_name, str(et_element.tag))]
-        kwargs = dataclass_fields_asdict(IdentifiableElement.from_et(et_element, doc_frags))
+        cat = OdxCategory.category_from_et(et_element, doc_frags, doc_type="COMPARAM-SUBSET")
+        doc_frags = cat.odx_id.doc_fragments
+        kwargs = dataclass_fields_asdict(cat)
 
-        admin_data = AdminData.from_et(et_element.find("ADMIN-DATA"), doc_frags)
-        company_datas = NamedItemList([
-            CompanyData.from_et(cde, doc_frags)
-            for cde in et_element.iterfind("COMPANY-DATAS/COMPANY-DATA")
-        ])
+        category = et_element.get("CATEGORY")
 
         data_object_props = NamedItemList([
             DataObjectProperty.from_et(el, doc_frags)
@@ -61,25 +52,16 @@ def from_et(et_element: ElementTree.Element,
         else:
             unit_spec = None
 
-        sdgs = [
-            SpecialDataGroup.from_et(sdge, doc_frags) for sdge in et_element.iterfind("SDGS/SDG")
-        ]
-
         return ComparamSubset(
             category=category,
-            admin_data=admin_data,
-            company_datas=company_datas,
             data_object_props=data_object_props,
             comparams=comparams,
             complex_comparams=complex_comparams,
             unit_spec=unit_spec,
-            sdgs=sdgs,
             **kwargs)
 
     def _build_odxlinks(self) -> Dict[OdxLinkId, Any]:
-        odxlinks: Dict[OdxLinkId, Any] = {}
-        if self.odx_id is not None:
-            odxlinks[self.odx_id] = self
+        odxlinks = super()._build_odxlinks()
 
         for dop in self.data_object_props:
             odxlinks[dop.odx_id] = dop
@@ -93,19 +75,11 @@ def _build_odxlinks(self) -> Dict[OdxLinkId, Any]:
         if self.unit_spec:
             odxlinks.update(self.unit_spec._build_odxlinks())
 
-        if self.admin_data is not None:
-            odxlinks.update(self.admin_data._build_odxlinks())
-
-        if self.company_datas is not None:
-            for cd in self.company_datas:
-                odxlinks.update(cd._build_odxlinks())
-
-        for sdg in self.sdgs:
-            odxlinks.update(sdg._build_odxlinks())
-
         return odxlinks
 
     def _resolve_odxlinks(self, odxlinks: OdxLinkDatabase) -> None:
+        super()._resolve_odxlinks(odxlinks)
+
         for dop in self.data_object_props:
             dop._resolve_odxlinks(odxlinks)
 
@@ -118,17 +92,12 @@ def _resolve_odxlinks(self, odxlinks: OdxLinkDatabase) -> None:
         if self.unit_spec:
             self.unit_spec._resolve_odxlinks(odxlinks)
 
-        if self.admin_data is not None:
-            self.admin_data._resolve_odxlinks(odxlinks)
-
-        if self.company_datas is not None:
-            for cd in self.company_datas:
-                cd._resolve_odxlinks(odxlinks)
-
-        for sdg in self.sdgs:
-            sdg._resolve_odxlinks(odxlinks)
+    def _finalize_init(self, database: "Database", odxlinks: OdxLinkDatabase) -> None:
+        super()._finalize_init(database, odxlinks)
 
     def _resolve_snrefs(self, context: SnRefContext) -> None:
+        super()._resolve_snrefs(context)
+
         for dop in self.data_object_props:
             dop._resolve_snrefs(context)
 
@@ -140,13 +109,3 @@ def _resolve_snrefs(self, context: SnRefContext) -> None:
 
         if self.unit_spec:
             self.unit_spec._resolve_snrefs(context)
-
-        if self.admin_data is not None:
-            self.admin_data._resolve_snrefs(context)
-
-        if self.company_datas is not None:
-            for cd in self.company_datas:
-                cd._resolve_snrefs(context)
-
-        for sdg in self.sdgs:
-            sdg._resolve_snrefs(context)
diff --git a/odxtools/database.py b/odxtools/database.py
index 74b9edcd..991143e3 100644
--- a/odxtools/database.py
+++ b/odxtools/database.py
@@ -145,15 +145,20 @@ def refresh(self) -> None:
         context = SnRefContext()
         context.database = self
 
+        # let the diaglayers sort out the inherited objects
+        for subset in self.comparam_subsets:
+            subset._finalize_init(self, self._odxlinks)
+        for spec in self.comparam_specs:
+            spec._finalize_init(self, self._odxlinks)
+        for dlc in self.diag_layer_containers:
+            dlc._finalize_init(self, self._odxlinks)
+
         for subset in self.comparam_subsets:
             subset._resolve_snrefs(context)
         for spec in self.comparam_specs:
             spec._resolve_snrefs(context)
-
-        # let the diaglayers sort out the inherited objects and the
-        # short name references
         for dlc in self.diag_layer_containers:
-            dlc._finalize_init(self, self._odxlinks)
+            dlc._resolve_snrefs(context)
 
     def _build_odxlinks(self) -> Dict[OdxLinkId, Any]:
         result: Dict[OdxLinkId, Any] = {}
diff --git a/odxtools/diaglayercontainer.py b/odxtools/diaglayercontainer.py
index de2d3c9d..e1c1c327 100644
--- a/odxtools/diaglayercontainer.py
+++ b/odxtools/diaglayercontainer.py
@@ -1,22 +1,19 @@
 # SPDX-License-Identifier: MIT
 from dataclasses import dataclass
 from itertools import chain
-from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
+from typing import TYPE_CHECKING, Any, Dict, List, Union
 from xml.etree import ElementTree
 
-from .admindata import AdminData
-from .companydata import CompanyData
 from .diaglayers.basevariant import BaseVariant
 from .diaglayers.diaglayer import DiagLayer
 from .diaglayers.ecushareddata import EcuSharedData
 from .diaglayers.ecuvariant import EcuVariant
 from .diaglayers.functionalgroup import FunctionalGroup
 from .diaglayers.protocol import Protocol
-from .element import IdentifiableElement
-from .exceptions import odxrequire
 from .nameditemlist import NamedItemList
+from .odxcategory import OdxCategory
 from .odxlink import OdxDocFragment, OdxLinkDatabase, OdxLinkId
-from .specialdatagroup import SpecialDataGroup
+from .snrefcontext import SnRefContext
 from .utils import dataclass_fields_asdict
 
 if TYPE_CHECKING:
@@ -24,15 +21,12 @@
 
 
 @dataclass
-class DiagLayerContainer(IdentifiableElement):
-    admin_data: Optional[AdminData]
-    company_datas: NamedItemList[CompanyData]
+class DiagLayerContainer(OdxCategory):
     ecu_shared_datas: NamedItemList[EcuSharedData]
     protocols: NamedItemList[Protocol]
     functional_groups: NamedItemList[FunctionalGroup]
     base_variants: NamedItemList[BaseVariant]
     ecu_variants: NamedItemList[EcuVariant]
-    sdgs: List[SpecialDataGroup]
 
     @property
     def ecus(self) -> NamedItemList[EcuVariant]:
@@ -54,17 +48,10 @@ def __post_init__(self) -> None:
     def from_et(et_element: ElementTree.Element,
                 doc_frags: List[OdxDocFragment]) -> "DiagLayerContainer":
 
-        short_name = odxrequire(et_element.findtext("SHORT-NAME"))
-        # create the current ODX "document fragment" (description of the
-        # current document for references and IDs)
-        doc_frags = [OdxDocFragment(short_name, "CONTAINER")]
-        kwargs = dataclass_fields_asdict(IdentifiableElement.from_et(et_element, doc_frags))
+        cat = OdxCategory.category_from_et(et_element, doc_frags, doc_type="CONTAINER")
+        doc_frags = cat.odx_id.doc_fragments
+        kwargs = dataclass_fields_asdict(cat)
 
-        admin_data = AdminData.from_et(et_element.find("ADMIN-DATA"), doc_frags)
-        company_datas = NamedItemList([
-            CompanyData.from_et(cde, doc_frags)
-            for cde in et_element.iterfind("COMPANY-DATAS/COMPANY-DATA")
-        ])
         ecu_shared_datas = NamedItemList([
             EcuSharedData.from_et(dl_element, doc_frags)
             for dl_element in et_element.iterfind("ECU-SHARED-DATAS/ECU-SHARED-DATA")
@@ -85,30 +72,17 @@ def from_et(et_element: ElementTree.Element,
             EcuVariant.from_et(dl_element, doc_frags)
             for dl_element in et_element.iterfind("ECU-VARIANTS/ECU-VARIANT")
         ])
-        sdgs = [
-            SpecialDataGroup.from_et(sdge, doc_frags) for sdge in et_element.iterfind("SDGS/SDG")
-        ]
 
         return DiagLayerContainer(
-            admin_data=admin_data,
-            company_datas=company_datas,
             ecu_shared_datas=ecu_shared_datas,
             protocols=protocols,
             functional_groups=functional_groups,
             base_variants=base_variants,
             ecu_variants=ecu_variants,
-            sdgs=sdgs,
             **kwargs)
 
     def _build_odxlinks(self) -> Dict[OdxLinkId, Any]:
-        result = {self.odx_id: self}
-
-        if self.admin_data is not None:
-            result.update(self.admin_data._build_odxlinks())
-        for cd in self.company_datas:
-            result.update(cd._build_odxlinks())
-        for sdg in self.sdgs:
-            result.update(sdg._build_odxlinks())
+        result = super()._build_odxlinks()
 
         for ecu_shared_data in self.ecu_shared_datas:
             result.update(ecu_shared_data._build_odxlinks())
@@ -124,12 +98,7 @@ def _build_odxlinks(self) -> Dict[OdxLinkId, Any]:
         return result
 
     def _resolve_odxlinks(self, odxlinks: OdxLinkDatabase) -> None:
-        if self.admin_data is not None:
-            self.admin_data._resolve_odxlinks(odxlinks)
-        for cd in self.company_datas:
-            cd._resolve_odxlinks(odxlinks)
-        for sdg in self.sdgs:
-            sdg._resolve_odxlinks(odxlinks)
+        super()._resolve_odxlinks(odxlinks)
 
         for ecu_shared_data in self.ecu_shared_datas:
             ecu_shared_data._resolve_odxlinks(odxlinks)
@@ -143,6 +112,8 @@ def _resolve_odxlinks(self, odxlinks: OdxLinkDatabase) -> None:
             ecu_variant._resolve_odxlinks(odxlinks)
 
     def _finalize_init(self, database: "Database", odxlinks: OdxLinkDatabase) -> None:
+        super()._finalize_init(database, odxlinks)
+
         for ecu_shared_data in self.ecu_shared_datas:
             ecu_shared_data._finalize_init(database, odxlinks)
         for protocol in self.protocols:
@@ -154,6 +125,9 @@ def _finalize_init(self, database: "Database", odxlinks: OdxLinkDatabase) -> Non
         for ecu_variant in self.ecu_variants:
             ecu_variant._finalize_init(database, odxlinks)
 
+    def _resolve_snrefs(self, context: SnRefContext) -> None:
+        super()._resolve_snrefs(context)
+
     @property
     def diag_layers(self) -> NamedItemList[DiagLayer]:
         return self._diag_layers
diff --git a/odxtools/odxcategory.py b/odxtools/odxcategory.py
new file mode 100644
index 00000000..777a346c
--- /dev/null
+++ b/odxtools/odxcategory.py
@@ -0,0 +1,83 @@
+# SPDX-License-Identifier: MIT
+from dataclasses import dataclass
+from typing import TYPE_CHECKING, Any, Dict, List, Optional
+from xml.etree import ElementTree
+
+from .admindata import AdminData
+from .companydata import CompanyData
+from .element import IdentifiableElement
+from .exceptions import odxrequire
+from .nameditemlist import NamedItemList
+from .odxlink import OdxDocFragment, OdxLinkDatabase, OdxLinkId
+from .snrefcontext import SnRefContext
+from .specialdatagroup import SpecialDataGroup
+from .utils import dataclass_fields_asdict
+
+if TYPE_CHECKING:
+    from .database import Database
+
+
+@dataclass
+class OdxCategory(IdentifiableElement):
+    """This is the base class for all top-level container classes in ODX"""
+
+    admin_data: Optional[AdminData]
+    company_datas: NamedItemList[CompanyData]
+    sdgs: List[SpecialDataGroup]
+
+    @staticmethod
+    def from_et(et_element: ElementTree.Element, doc_frags: List[OdxDocFragment]) -> "OdxCategory":
+        raise Exception("Calling `._from_et()` is not allowed for OdxCategory. "
+                        "Use `OdxCategory.category_from_et()`!")
+
+    @staticmethod
+    def category_from_et(et_element: ElementTree.Element, doc_frags: List[OdxDocFragment], *,
+                         doc_type: str) -> "OdxCategory":
+
+        short_name = odxrequire(et_element.findtext("SHORT-NAME"))
+        # create the current ODX "document fragment" (description of the
+        # current document for references and IDs)
+        doc_frags = [OdxDocFragment(short_name, doc_type)]
+        kwargs = dataclass_fields_asdict(IdentifiableElement.from_et(et_element, doc_frags))
+
+        admin_data = AdminData.from_et(et_element.find("ADMIN-DATA"), doc_frags)
+        company_datas = NamedItemList([
+            CompanyData.from_et(cde, doc_frags)
+            for cde in et_element.iterfind("COMPANY-DATAS/COMPANY-DATA")
+        ])
+        sdgs = [
+            SpecialDataGroup.from_et(sdge, doc_frags) for sdge in et_element.iterfind("SDGS/SDG")
+        ]
+
+        return OdxCategory(admin_data=admin_data, company_datas=company_datas, sdgs=sdgs, **kwargs)
+
+    def _build_odxlinks(self) -> Dict[OdxLinkId, Any]:
+        result = {self.odx_id: self}
+
+        if self.admin_data is not None:
+            result.update(self.admin_data._build_odxlinks())
+        for cd in self.company_datas:
+            result.update(cd._build_odxlinks())
+        for sdg in self.sdgs:
+            result.update(sdg._build_odxlinks())
+
+        return result
+
+    def _resolve_odxlinks(self, odxlinks: OdxLinkDatabase) -> None:
+        if self.admin_data is not None:
+            self.admin_data._resolve_odxlinks(odxlinks)
+        for cd in self.company_datas:
+            cd._resolve_odxlinks(odxlinks)
+        for sdg in self.sdgs:
+            sdg._resolve_odxlinks(odxlinks)
+
+    def _finalize_init(self, database: "Database", odxlinks: OdxLinkDatabase) -> None:
+        pass
+
+    def _resolve_snrefs(self, context: SnRefContext) -> None:
+        if self.admin_data is not None:
+            self.admin_data._resolve_snrefs(context)
+        for cd in self.company_datas:
+            cd._resolve_snrefs(context)
+        for sdg in self.sdgs:
+            sdg._resolve_snrefs(context)
diff --git a/odxtools/templates/comparam-spec.odx-c.xml.jinja2 b/odxtools/templates/comparam-spec.odx-c.xml.jinja2
index c72f233b..f539ed9b 100644
--- a/odxtools/templates/comparam-spec.odx-c.xml.jinja2
+++ b/odxtools/templates/comparam-spec.odx-c.xml.jinja2
@@ -5,32 +5,15 @@
  # This template writes an .odx-c file for a communication
  # parameter specification.
 -#}
-{%- import('macros/printAdminData.xml.jinja2') as pad -%}
-{%- import('macros/printCompanyData.xml.jinja2') as pcd -%}
+{%- import('macros/printOdxCategory.xml.jinja2') as poc %}
 {%- import('macros/printProtStack.xml.jinja2') as pps %}
-{%- import('macros/printDescription.xml.jinja2') as pd %}
 {#- -#}
 
 <?xml version="1.0" encoding="UTF-8" standalone="no" ?>
-<ODX MODEL-VERSION="2.2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="odx.xsd">
 <!-- Written using odxtools {{odxtools_version}} -->
- <COMPARAM-SPEC ID="{{comparam_spec.odx_id.local_id}}">
-   <SHORT-NAME>{{comparam_spec.short_name}}</SHORT-NAME>
-   {%- if comparam_spec.long_name is not none %}
-   <LONG-NAME>{{comparam_spec.long_name|e}}</LONG-NAME>
-   {%- endif %}
-   {{pd.printDescription(comparam_spec.description)}}
-   {%- if comparam_spec.admin_data is not none %}
-   {{- pad.printAdminData(comparam_spec.admin_data) | indent(3) }}
-   {%- endif %}
-   {%- if comparam_spec.company_datas %}
-   <COMPANY-DATAS>
-    {%- for cd in comparam_spec.company_datas %}
-     {{- pcd.printCompanyData(cd) | indent(5) -}}
-    {%- endfor %}
-   </COMPANY-DATAS>
-   {%- endif %}
-
+<ODX MODEL-VERSION="2.2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="odx.xsd">
+ <COMPARAM-SPEC  {{- poc.printOdxCategoryAttribs(comparam_spec) }}>
+   {{- poc.printOdxCategorySubtags(comparam_spec)|indent(3) }}
    {%- if comparam_spec.prot_stacks %}
    <PROT-STACKS>
     {%- for ps in comparam_spec.prot_stacks %}
diff --git a/odxtools/templates/comparam-subset.odx-cs.xml.jinja2 b/odxtools/templates/comparam-subset.odx-cs.xml.jinja2
index 70043c6b..477e6ca8 100644
--- a/odxtools/templates/comparam-subset.odx-cs.xml.jinja2
+++ b/odxtools/templates/comparam-subset.odx-cs.xml.jinja2
@@ -5,36 +5,18 @@
  # This template writes an .odx-cs file for a communication
  # parameter subset.
 -#}
+{%- import('macros/printOdxCategory.xml.jinja2') as poc %}
 {%- import('macros/printComparam.xml.jinja2') as pcp -%}
-{%- import('macros/printAdminData.xml.jinja2') as pad -%}
-{%- import('macros/printCompanyData.xml.jinja2') as pcd -%}
 {%- import('macros/printDOP.xml.jinja2') as pdop %}
 {%- import('macros/printUnitSpec.xml.jinja2') as pus %}
-{%- import('macros/printSpecialData.xml.jinja2') as psd %}
 {%- import('macros/printDescription.xml.jinja2') as pd %}
 {#- -#}
 
 <?xml version="1.0" encoding="UTF-8" standalone="no" ?>
-<ODX MODEL-VERSION="2.2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="odx.xsd">
 <!-- Written using odxtools {{odxtools_version}} -->
- <COMPARAM-SUBSET ID="{{comparam_subset.odx_id.local_id}}"
-                  CATEGORY="{{comparam_subset.category}}" >
-   <SHORT-NAME>{{comparam_subset.short_name}}</SHORT-NAME>
-   {%- if comparam_subset.long_name is not none %}
-   <LONG-NAME>{{comparam_subset.long_name|e}}</LONG-NAME>
-   {%- endif %}
-   {{pd.printDescription(comparam_subset.description)}}
-   {%- if comparam_subset.admin_data is not none %}
-   {{- pad.printAdminData(comparam_subset.admin_data) | indent(3) }}
-   {%- endif %}
-   {%- if comparam_subset.company_datas %}
-   <COMPANY-DATAS>
-     {%- for cd in comparam_subset.company_datas %}
-     {{- pcd.printCompanyData(cd) | indent(5, first=True) }}
-     {%- endfor %}
-   </COMPANY-DATAS>
-   {%- endif %}
-   {{- psd.printSpecialDataGroups(comparam_subset.sdgs)|indent(3, first=True) }}
+<ODX MODEL-VERSION="2.2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="odx.xsd">
+ <COMPARAM-SUBSET  {{- poc.printOdxCategoryAttribs(comparam_subset) }} {{make_xml_attrib("CATEGORY", comparam_subset.category)}}>
+   {{- poc.printOdxCategorySubtags(comparam_subset)|indent(3) }}
    {%- if comparam_subset.comparams %}
    <COMPARAMS>
      {%- for cp in comparam_subset.comparams %}
diff --git a/odxtools/templates/diag_layer_container.odx-d.xml.jinja2 b/odxtools/templates/diag_layer_container.odx-d.xml.jinja2
index b90d7cc0..3ef83b7d 100644
--- a/odxtools/templates/diag_layer_container.odx-d.xml.jinja2
+++ b/odxtools/templates/diag_layer_container.odx-d.xml.jinja2
@@ -2,9 +2,7 @@
  #
  # SPDX-License-Identifier: MIT
 -#}
-{%- import('macros/printAdminData.xml.jinja2') as pad -%}
-{%- import('macros/printCompanyData.xml.jinja2') as pcd -%}
-{%- import('macros/printSpecialData.xml.jinja2') as psd %}
+{%- import('macros/printOdxCategory.xml.jinja2') as poc %}
 {%- import('macros/printEcuSharedData.xml.jinja2') as pecusd -%}
 {%- import('macros/printProtocol.xml.jinja2') as pprot %}
 {%- import('macros/printFunctionalGroup.xml.jinja2') as pfuncgroup %}
@@ -14,29 +12,10 @@
 {#- -#}
 
 <?xml version="1.0" encoding="UTF-8" standalone="no" ?>
+<!-- Written using odxtools {{odxtools_version}} -->
 <ODX MODEL-VERSION="2.2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="odx.xsd">
- <!-- Written using odxtools {{odxtools_version}} -->
- <DIAG-LAYER-CONTAINER ID="{{dlc.odx_id.local_id}}">
-  <SHORT-NAME>{{dlc.short_name}}</SHORT-NAME>
-{%- if dlc.long_name %}
-  <LONG-NAME>{{dlc.long_name|e}}</LONG-NAME>
-{%- endif %}
-{%- if dlc.description %}
-  <DESC>
-{{dlc.description}}
-  </DESC>
-{%- endif %}
-{%- if dlc.admin_data %}
-  {{pad.printAdminData(dlc.admin_data)|indent(2)}}
-{%- endif %}
-{%- if dlc.company_datas %}
-  <COMPANY-DATAS>
- {%- for cd in dlc.company_datas %}
-   {{pcd.printCompanyData(cd)|indent(3)}}
- {%- endfor %}
-  </COMPANY-DATAS>
-{%- endif %}
-  {{- psd.printSpecialDataGroups(dlc.sdgs)|indent(2, first=True) }}
+ <DIAG-LAYER-CONTAINER {{- poc.printOdxCategoryAttribs(dlc) }}>
+  {{- poc.printOdxCategorySubtags(dlc)|indent(3) }}
 {%- if dlc.protocols %}
   <PROTOCOLS>
  {%- for dl in dlc.protocols %}
diff --git a/odxtools/templates/macros/printOdxCategory.xml.jinja2 b/odxtools/templates/macros/printOdxCategory.xml.jinja2
new file mode 100644
index 00000000..5e5b5589
--- /dev/null
+++ b/odxtools/templates/macros/printOdxCategory.xml.jinja2
@@ -0,0 +1,28 @@
+{#- -*- mode: sgml; tab-width: 1; indent-tabs-mode: nil -*-
+ #
+ # SPDX-License-Identifier: MIT
+-#}
+
+{%- import('macros/printElementId.xml.jinja2') as peid %}
+{%- import('macros/printAdminData.xml.jinja2') as pad -%}
+{%- import('macros/printCompanyData.xml.jinja2') as pcd -%}
+{%- import('macros/printSpecialData.xml.jinja2') as psd %}
+
+{%- macro printOdxCategoryAttribs(obj) -%}
+{#- #} {{- peid.printElementIdAttribs(obj) }}
+{%- endmacro -%}
+
+{%- macro printOdxCategorySubtags(obj) -%}
+{{ peid.printElementIdSubtags(obj) }}
+{%- if obj.admin_data %}
+{{pad.printAdminData(obj.admin_data)|indent(2)}}
+{%- endif %}
+{%- if obj.company_datas %}
+<COMPANY-DATAS>
+{%- for cd in obj.company_datas %}
+{{pcd.printCompanyData(cd)|indent(3)}}
+{%- endfor %}
+</COMPANY-DATAS>
+{%- endif %}
+{{- psd.printSpecialDataGroups(obj.sdgs)|indent(2, first=True) }}
+{%- endmacro -%}