import logging import re from typing import Optional, TypeAlias from xml.etree import ElementTree from xmlutils import pom_namespace as ns, find_tag_text logger = logging.getLogger(__name__) Properties: TypeAlias = dict[str, Optional[str]] class PropertyMissing(Exception): def __init__(self, prop: str): self.prop = prop class PackageError(Exception): pass class PackagePOM: name: str is_bom: bool parent: Optional[str] group_id: Optional[str] artifact_id: Optional[str] version: Optional[str] properties: Properties _raw_root: ElementTree.Element def __init__(self, name: str, pom: str): logger.debug(f'{name}: Parsing POM') self.name = name self._raw_root = ElementTree.fromstring(pom) self.group_id, self.artifact_id, self.version = name.split(':') if self.group_id is None or self.artifact_id is None or self.version is None: logger.warning( f'{name}: One of groupId={self.group_id}, artifactId={self.artifact_id}, version={self.version} was ' f'None. This can cause issues with dependency resolution' ) self.parent = None if (parent_tag := self._raw_root.find('parent', ns)) is not None: parent_group = find_tag_text(parent_tag, 'groupId') parent_artifact = find_tag_text(parent_tag, 'artifactId') parent_version = find_tag_text(parent_tag, 'version') if parent_group is None or parent_artifact is None or parent_version is None: raise PackageError(f'Invalid parent {parent_group}:{parent_artifact}:{parent_version}') else: self.parent = f'{parent_group}:{parent_artifact}:{parent_version}' if (packaging := self._raw_root.find('packaging', ns)) is not None: self.is_bom = packaging == 'pom' else: self.is_bom = False self.set_properties({}) @property def dependency_management(self) -> list[str]: return [ self._package_from_xml_dep(dep) for dep in self._raw_root.find('dependencyManagement/dependencies', ns) or [] ] def set_properties(self, parent_properties: Optional[Properties]): logger.debug(f'{self.name}: Parsing properties') props: Properties = parent_properties or {} for prop_tag in self._raw_root.findall('.//properties/*', ns): prop = prop_tag.tag.replace(f'{{{ns[""]}}}', '') value = prop_tag.text if prop_tag.text is not None else '' logger.debug(f'{self.name}: Setting prop {prop}={value}') props[prop] = value changed = True while changed: changed = False for prop, value in props.items(): new_value = self._prop_replace(value, props, True) if new_value != value: changed = True logger.debug(f'{self.name}: Setting prop {prop}={new_value}') props[prop] = new_value self.properties = props def _prop_replace(self, text, props: Optional[Properties] = None, quiet: bool = False) -> str: def lookup_prop(match) -> str: prop = match.group(1) if prop == 'project.groupId': value = str(self.group_id) elif prop == 'project.artifactId': value = str(self.artifact_id) elif prop == 'project.version': value = str(self.version) elif prop.startswith('project.build') or prop.startswith('env.') or prop.startswith('maven.'): value = None elif prop in ['project.basedir', 'basedir', 'user.home', 'debug.port']: value = None else: try: value = props[prop] if props is not None else self.properties[prop] except KeyError: value = None if value is None and not quiet: raise PropertyMissing(prop) else: logger.debug(f'{self.name}: Replacing property {prop} with {value}') return value return re.sub( r'\$\{([^}]*)}', lookup_prop, text, ) def _package_from_xml_dep(self, dep: ElementTree.Element) -> str: def prop_replace_tag(tag) -> str: return self._prop_replace( elem.text or '' if (elem := dep.find(tag, ns)) is not None else '', ) return f"{prop_replace_tag('groupId')}:{prop_replace_tag('artifactId')}:{prop_replace_tag('version')}"