"""parse XML files and build a space with objects it aggregates this files and manage redefine and exists attributes Created by: EOLE (http://eole.orion.education.fr) Copyright (C) 2005-2018 Forked by: Cadoles (http://www.cadoles.com) Copyright (C) 2019-2021 distribued with GPL-2 or later license This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ from typing import Optional from .i18n import _ from .xmlreflector import XMLReflector from .utils import valid_variable_family_name from .error import SpaceObjShallNotBeUpdated, DictConsistencyError from .path import Path # RougailObjSpace's elements that shall be forced to the Redefinable type FORCE_REDEFINABLES = ('family', 'follower', 'service', 'disknod', 'variables') # RougailObjSpace's elements that shall be forced to the UnRedefinable type FORCE_UNREDEFINABLES = ('value',) # RougailObjSpace's elements that shall not be modify UNREDEFINABLE = ('multi', 'type',) # RougailObjSpace's elements that did not created automaticly FORCE_ELEMENTS = ('property_', 'information') # XML text are convert has name FORCED_TEXT_ELTS_AS_NAME = ('choice', 'property', 'value',) FORCE_TAG = {'family': 'variable'} # _____________________________________________________________________________ # special types definitions for the Object Space's internal representation class RootRougailObject: # pylint: disable=R0903 """Root object """ def __init__(self, xmlfiles, name=None, ): if not isinstance(xmlfiles, list): xmlfiles = [xmlfiles] self.xmlfiles = xmlfiles if name: self.name = name class Atom(RootRougailObject): # pylint: disable=R0903 """Atomic object (means can only define one time) """ class Redefinable(RootRougailObject): # pylint: disable=R0903 """Object that could be redefine """ class UnRedefinable(RootRougailObject): # pylint: disable=R0903 """Object that could not be redefine """ class ObjSpace: # pylint: disable=R0903 """ Base object space """ def convert_boolean(value: str) -> bool: """Boolean coercion. The Rougail XML may contain srings like `True` or `False` """ if isinstance(value, bool): return value if value == 'True': return True return False class RougailObjSpace: """Rougail ObjectSpace is an object's reflexion of the XML elements """ def __init__(self, xmlreflector: XMLReflector, rougailconfig: 'RougailConfig', ) -> None: self.space = ObjSpace() self.paths = Path(rougailconfig) self.forced_text_elts_as_name = set(FORCED_TEXT_ELTS_AS_NAME) self.list_conditions = {} self.valid_enums = {} self.booleans_attributs = [] self.has_dyn_option = False self.types = {} self.make_object_space_classes(xmlreflector) self.rougailconfig = rougailconfig def make_object_space_classes(self, xmlreflector: XMLReflector, ) -> None: """Create Rougail ObjectSpace class types from DDT file It enables us to create objects like: File(), Variable(), Ip(), Family(), Constraints()... and so on. """ for dtd_elt in xmlreflector.dtd.iterelements(): attrs = {} if dtd_elt.name in FORCE_REDEFINABLES: clstype = Redefinable elif not dtd_elt.attributes() and dtd_elt.name not in FORCE_UNREDEFINABLES: clstype = Atom else: clstype = UnRedefinable forced_text_elt = dtd_elt.type == 'mixed' for dtd_attr in dtd_elt.iterattributes(): if set(dtd_attr.itervalues()) == {'True', 'False'}: # it's a boolean self.booleans_attributs.append(dtd_attr.name) if dtd_attr.default_value: # set default value for this attribute default_value = dtd_attr.default_value if dtd_attr.name in self.booleans_attributs: default_value = convert_boolean(default_value) attrs[dtd_attr.name] = default_value if dtd_attr.name.endswith('_type'): self.types[dtd_attr.name] = default_value if dtd_attr.name == 'redefine': # has a redefine attribute, so it's a Redefinable object clstype = Redefinable if dtd_attr.name == 'name' and forced_text_elt: # child.text should be transform has a "name" attribute forced_text_elt = False if forced_text_elt is True: self.forced_text_elts_as_name.add(dtd_elt.name) # create ObjectSpace object setattr(self, dtd_elt.name, type(dtd_elt.name.capitalize(), (clstype,), attrs)) for elt in FORCE_ELEMENTS: setattr(self, elt, type(self._get_elt_name(elt), (RootRougailObject,), dict())) @staticmethod def _get_elt_name(elt) -> str: name = elt.capitalize() if name.endswith('_'): name = name[:-1] return name def xml_parse_document(self, xmlfile, document, namespace, ): """Parses a Rougail XML file and populates the RougailObjSpace """ redefine_variables = [] self._xml_parse(xmlfile, document, self.space, namespace, redefine_variables, False, ) def _xml_parse(self, # pylint: disable=R0913 xmlfile, document, space, namespace, redefine_variables, is_dynamic, ) -> None: # var to check unique family name in a XML file family_names = [] for child in document: if is_dynamic: is_sub_dynamic = True else: is_sub_dynamic = document.attrib.get('dynamic') is not None if not isinstance(child.tag, str): # doesn't proceed the XML commentaries continue if child.tag == 'family': if child.attrib['name'] in family_names: msg = _(f'Family "{child.attrib["name"]}" is set several times') raise DictConsistencyError(msg, 44, [xmlfile]) family_names.append(child.attrib['name']) try: # variable objects creation exists, variableobj = self.get_variableobj(xmlfile, child, space, namespace, redefine_variables, ) except SpaceObjShallNotBeUpdated: continue self.set_text(child, variableobj, ) self.set_attributes(xmlfile, child, variableobj, ) self.remove(child, variableobj, redefine_variables, ) if not exists: self.set_path(namespace, document, variableobj, space, is_sub_dynamic, ) self.add_to_tree_structure(variableobj, space, child, namespace, ) if list(child) != []: self._xml_parse(xmlfile, child, variableobj, namespace, redefine_variables, is_sub_dynamic, ) def get_variableobj(self, xmlfile: str, child: list, space, namespace, redefine_variables, ): # pylint: disable=R0913 """ retrieves or creates Rougail Object Subspace objects """ tag = FORCE_TAG.get(child.tag, child.tag) obj = getattr(self, tag) name = self._get_name(child, namespace) if Redefinable in obj.__mro__: return self.create_or_update_redefinable_object(xmlfile, child.attrib, space, child, name, namespace, redefine_variables, ) if Atom in obj.__mro__: if child.tag in vars(space): # Atom instance has to be a singleton here # we do not re-create it, we reuse it return False, getattr(space, child.tag) return False, obj(xmlfile, name) # UnRedefinable object if child.tag not in vars(space): setattr(space, child.tag, []) return False, obj(xmlfile, name) def _get_name(self, child, namespace: str, ) -> Optional[str]: if child.tag == 'variables': return namespace if child.tag == 'service': return child.attrib['name'] + '.' + child.attrib.get('type', 'service') if 'name' in child.attrib: return child.attrib['name'] if child.text and child.tag in self.forced_text_elts_as_name: return child.text.strip() return None def create_or_update_redefinable_object(self, xmlfile, subspace, space, child, name, namespace, redefine_variables, ): # pylint: disable=R0913 """A redefinable object could be created or updated """ existed_var = self.get_existed_obj(name, xmlfile, space, child, namespace, ) if existed_var: # if redefine is set to object, default value is False # otherwise it's always a redefinable object default_redefine = child.tag in FORCE_REDEFINABLES redefine = convert_boolean(subspace.get('redefine', default_redefine)) if redefine is True: if isinstance(existed_var, self.variable): # pylint: disable=E1101 if namespace == self.rougailconfig['variable_namespace']: redefine_variables.append(name) else: redefine_variables.append(space.path + '.' + name) existed_var.xmlfiles.append(xmlfile) return True, existed_var exists = convert_boolean(subspace.get('exists', True)) if exists is False: raise SpaceObjShallNotBeUpdated() msg = _(f'"{child.tag}" named "{name}" cannot be re-created in "{xmlfile}", ' f'already defined') raise DictConsistencyError(msg, 45, existed_var.xmlfiles) # object deos not exists exists = convert_boolean(subspace.get('exists', False)) if exists is True: # manage object only if already exists, so cancel raise SpaceObjShallNotBeUpdated() redefine = convert_boolean(subspace.get('redefine', False)) if redefine is True: # cannot redefine an inexistant object msg = _(f'Redefined object: "{name}" does not exist yet') raise DictConsistencyError(msg, 46, [xmlfile]) tag = FORCE_TAG.get(child.tag, child.tag) if tag not in vars(space): setattr(space, tag, {}) obj = getattr(self, child.tag)(xmlfile, name) return False, obj def get_existed_obj(self, name: str, xmlfile: str, space: str, child, namespace: str, ) -> None: """if an object exists, return it """ if child.tag in ['variable', 'family']: valid_variable_family_name(name, [xmlfile]) if child.tag == 'variable': # pylint: disable=E1101 if namespace != self.rougailconfig['variable_namespace']: name = space.path + '.' + name if not self.paths.path_is_defined(name): return None old_family_name = self.paths.get_variable_family_path(name) if space.path != old_family_name: msg = _(f'Variable "{name}" was previously create in family "{old_family_name}", ' f'now it is in "{space.path}"') raise DictConsistencyError(msg, 47, space.xmlfiles) return self.paths.get_variable(name) # it's not a family tag = FORCE_TAG.get(child.tag, child.tag) children = getattr(space, tag, {}) if name in children and isinstance(children[name], getattr(self, child.tag)): return children[name] return None def set_text(self, child, variableobj, ) -> None: """set text """ if child.text is None or child.tag in self.forced_text_elts_as_name: return text = child.text.strip() if text: variableobj.text = text def set_attributes(self, xmlfile, child, variableobj, ): """ set attributes to an object """ redefine = convert_boolean(child.attrib.get('redefine', False)) if redefine and child.tag == 'variable': # delete old values has_value = hasattr(variableobj, 'value') if has_value and len(child) != 0: del variableobj.value for attr, val in child.attrib.items(): if redefine and attr in UNREDEFINABLE: msg = _(f'cannot redefine attribute "{attr}" for variable "{child.attrib["name"]}"' f' already defined') raise DictConsistencyError(msg, 48, variableobj.xmlfiles[:-1]) if attr in self.booleans_attributs: val = convert_boolean(val) if attr == 'name' and getattr(variableobj, 'name', None): # do not redefine name continue setattr(variableobj, attr, val) def remove(self, child, variableobj, redefine_variables, ): """Rougail object tree manipulations """ if child.tag == 'variable': if child.attrib.get('remove_choice', False): if variableobj.type != 'choice': msg = _(f'cannot remove choices for variable "{variableobj.path}"' f' the variable has type "{variableobj.type}"') raise DictConsistencyError(msg, 33, variableobj.xmlfiles) variableobj.choice = [] if child.attrib.get('remove_check', False): self.remove_check(variableobj.name) if child.attrib.get('remove_condition', False): self.remove_condition(variableobj.name) if child.attrib.get('remove_fill', False): self.remove_fill(variableobj.name) if child.tag == 'fill': for target in child: if target.tag == 'target' and target.text in redefine_variables: self.remove_fill(target.text) def remove_check(self, name): """Remove a check with a specified target """ if hasattr(self.space.constraints, 'check'): remove_checks = [] for idx, check in enumerate(self.space.constraints.check): # pylint: disable=E1101 for target in check.target: if target.name == name: remove_checks.append(idx) remove_checks.sort(reverse=True) for idx in remove_checks: self.space.constraints.check.pop(idx) # pylint: disable=E1101 def remove_condition(self, name: str, ) -> None: """Remove a condition with a specified source """ remove_conditions = [] for idx, condition in enumerate(self.space.constraints.condition): # pylint: disable=E1101 if condition.source == name: remove_conditions.append(idx) remove_conditions.sort(reverse=True) for idx in remove_conditions: del self.space.constraints.condition[idx] # pylint: disable=E1101 def remove_fill(self, name: str, ) -> None: """Remove a fill with a specified target """ remove_fills = [] for idx, fill in enumerate(self.space.constraints.fill): # pylint: disable=E1101 for target in fill.target: if target.name == name: remove_fills.append(idx) remove_fills.sort(reverse=True) for idx in remove_fills: self.space.constraints.fill.pop(idx) # pylint: disable=E1101 def set_path(self, namespace, document, variableobj, space, is_dynamic, ): """Fill self.paths attributes """ if isinstance(variableobj, self.variable): # pylint: disable=E1101 if 'name' in document.attrib: family_name = document.attrib['name'] else: family_name = namespace if isinstance(space, self.family) and space.leadership: leader = space.path else: leader = None self.paths.add_variable(namespace, variableobj.name, space.path, is_dynamic, variableobj, leader, ) elif isinstance(variableobj, self.family): # pylint: disable=E1101 family_name = variableobj.name if namespace != self.rougailconfig['variable_namespace']: family_name = space.path + '.' + family_name self.paths.add_family(namespace, family_name, variableobj, space.path, ) elif isinstance(variableobj, self.variables): variableobj.path = variableobj.name @staticmethod def add_to_tree_structure(variableobj, space, child, namespace: str, ) -> None: """add a variable to the tree """ variableobj.namespace = namespace if isinstance(variableobj, Redefinable): name = variableobj.name tag = FORCE_TAG.get(child.tag, child.tag) getattr(space, tag)[name] = variableobj elif isinstance(variableobj, UnRedefinable): getattr(space, child.tag).append(variableobj) else: setattr(space, child.tag, variableobj)