""" Java org.xmlpull.v1 APIs implemented in Python. org.xmlpull.v1 package - XmlPullParser, XmlSerializer, XmlPullParserFactory. Wraps Python xml.etree.ElementTree. """ from __future__ import annotations from .._core import Stub, AndroidOnlyError import xml.etree.ElementTree as _ET from io import StringIO as _StringIO from typing import List, Optional __all__ = [ "XmlPullParser", "XmlSerializer", "XmlPullParserFactory", "Factory", ] # ============================================================ # XmlPullParser # ============================================================ class XmlPullParser(Stub): """ org.xmlpull.v1.XmlPullParser An XML pull parser interface. """ # Event types START_TAG: int = 2 END_TAG: int = 3 TEXT: int = 4 CDSECT: int = 5 ENTITY_REF: int = 6 IGNORABLE_WHITESPACE: int = 7 PROCESSING_INSTRUCTION: int = 8 COMMENT: int = 9 DOCDECL: int = 10 START_DOCUMENT: int = 0 END_DOCUMENT: int = 1 TYPES: List[str] = [ "START_DOCUMENT", "END_DOCUMENT", "START_TAG", "END_TAG", "TEXT", "CDSECT", "ENTITY_REF", "IGNORABLE_WHITESPACE", "PROCESSING_INSTRUCTION", "COMMENT", "DOCDECL", ] NO_TYPES: List[str] = [] # Features FEATURE_PROCESS_NAMESPACES: str = "http://xmlpull.org/v1/doc/features.html#process-ns" FEATURE_REPORT_NAMESPACE_ATTRIBUTES: str = "http://xmlpull.org/v1/doc/features.html#report-ns-attributes" FEATURE_PROCESS_DOCDECL: str = "http://xmlpull.org/v1/doc/features.html#process-docdecl" FEATURE_VALIDATION: str = "http://xmlpull.org/v1/doc/features.html#validation" class _Event: def __init__(self, type_=0, name="", text="", namespace="", prefix="", attributes=None, depth=0, line=0, column=0): self.type = type_ self.name = name self.text = text self.namespace = namespace self.prefix = prefix self.attributes = attributes or [] self.depth = depth self.line = line self.column = column def __init__(self): super().__init__() self._events = [] self._pos = -1 self._depth = 0 self._features = { self.FEATURE_PROCESS_NAMESPACES: True, self.FEATURE_REPORT_NAMESPACE_ATTRIBUTES: False, self.FEATURE_PROCESS_DOCDECL: False, self.FEATURE_VALIDATION: False, } self._properties = {} self._input_encoding = "UTF-8" self._namespace_stack = [] def setInput(self, source: Any, input_encoding: str = None): """Set the input source for parsing.""" if input_encoding: self._input_encoding = input_encoding if isinstance(source, str): # Parse directly from XML string self._parse_string(source) elif hasattr(source, 'read'): # File-like object data = source.read() if isinstance(data, bytes): data = data.decode(input_encoding or "UTF-8") self._parse_string(data) else: self._parse_string(str(source)) def _parse_string(self, xml_str: str): """Parse XML string into a list of events.""" self._events = [] self._pos = -1 try: root = _ET.fromstring(xml_str) except _ET.ParseError as e: # Create minimal events if parsing fails self._events.append(XmlPullParser._Event(XmlPullParser.START_DOCUMENT)) self._events.append(XmlPullParser._Event(XmlPullParser.END_DOCUMENT)) return self._events.append(XmlPullParser._Event(XmlPullParser.START_DOCUMENT)) self._build_events(root, 0) self._events.append(XmlPullParser._Event(XmlPullParser.END_DOCUMENT)) def _build_events(self, element: _ET.Element, depth: int): """Recursively build events from ElementTree.""" attrs = [] for k, v in element.attrib.items(): ns = "" name = k prefix = "" if "}" in k: ns = k[1:k.index("}")] name = k[k.index("}") + 1:] attrs.append({"namespace": ns, "name": name, "prefix": prefix, "value": v}) # START_TAG tag = element.tag ns = "" prefix = "" if "}" in tag: ns = tag[1:tag.index("}")] tag = tag[tag.index("}") + 1:] self._events.append(XmlPullParser._Event( XmlPullParser.START_TAG, tag, "", ns, prefix, attrs, depth)) # Text content if element.text and element.text.strip(): self._events.append(XmlPullParser._Event( XmlPullParser.TEXT, "", element.text, "", "", [], depth + 1)) # Child elements for child in element: self._build_events(child, depth + 1) # END_TAG self._events.append(XmlPullParser._Event( XmlPullParser.END_TAG, tag, "", ns, prefix, [], depth)) # Tail text if element.tail and element.tail.strip(): self._events.append(XmlPullParser._Event( XmlPullParser.TEXT, "", element.tail, "", "", [], depth)) # --- Event navigation --- def getEventType(self) -> int: if 0 <= self._pos < len(self._events): return self._events[self._pos].type return self.END_DOCUMENT def next(self) -> int: """Move to next event.""" if self._pos < len(self._events) - 1: self._pos += 1 return self.getEventType() def nextToken(self) -> int: """Move to next token (same as next for this impl).""" return self.next() def nextText(self) -> str: """Returns the text content of the current element.""" if self.getEventType() != self.START_TAG: return "" self.next() text = self.getText() self.next() # Move to END_TAG return text or "" def nextTag(self) -> int: """Move to next tag event.""" self.next() while self.getEventType() not in (self.START_TAG, self.END_TAG, self.END_DOCUMENT): self.next() return self.getEventType() def require(self, type_: int, namespace: str, name: str): """Check that the current event matches expectations.""" if self.getEventType() != type_: raise AndroidOnlyError(f"Expected event {type_}, got {self.getEventType()}") if namespace is not None and self.getNamespace() != namespace: raise AndroidOnlyError(f"Expected namespace '{namespace}', got '{self.getNamespace()}'") if name is not None and self.getName() != name: raise AndroidOnlyError(f"Expected name '{name}', got '{self.getName()}'") # --- Position info --- def getDepth(self) -> int: if 0 <= self._pos < len(self._events): return self._events[self._pos].depth return 0 def getLineNumber(self) -> int: if 0 <= self._pos < len(self._events): return self._events[self._pos].line return -1 def getColumnNumber(self) -> int: if 0 <= self._pos < len(self._events): return self._events[self._pos].column return -1 def getInputEncoding(self) -> str: return self._input_encoding # --- Current event data --- def getName(self) -> str: if 0 <= self._pos < len(self._events): return self._events[self._pos].name return "" def getNamespace(self) -> str: if 0 <= self._pos < len(self._events): return self._events[self._pos].namespace return "" def getPrefix(self) -> str: if 0 <= self._pos < len(self._events): return self._events[self._pos].prefix return "" def getText(self) -> Optional[str]: if 0 <= self._pos < len(self._events): return self._events[self._pos].text return None def getTextCharacters(self, holder_for_start: List[int]) -> List[str]: """Returns text characters and fills start/length in holder.""" text = self.getText() or "" if len(holder_for_start) >= 2: holder_for_start[0] = 0 holder_for_start[1] = len(text) return list(text) # --- Attributes --- def getAttributeCount(self) -> int: if 0 <= self._pos < len(self._events): return len(self._events[self._pos].attributes) return 0 def getAttributeName(self, index: int) -> str: if 0 <= self._pos < len(self._events): attrs = self._events[self._pos].attributes if 0 <= index < len(attrs): return attrs[index]["name"] return "" def getAttributeNamespace(self, index: int) -> str: if 0 <= self._pos < len(self._events): attrs = self._events[self._pos].attributes if 0 <= index < len(attrs): return attrs[index]["namespace"] return "" def getAttributePrefix(self, index: int) -> str: if 0 <= self._pos < len(self._events): attrs = self._events[self._pos].attributes if 0 <= index < len(attrs): return attrs[index]["prefix"] return "" def getAttributeType(self, index: int) -> str: return "CDATA" def getAttributeValue(self, index_or_name: Any, namespace: str = None) -> str: if isinstance(index_or_name, int): if 0 <= self._pos < len(self._events): attrs = self._events[self._pos].attributes if 0 <= index_or_name < len(attrs): return attrs[index_or_name]["value"] else: # Lookup by name if 0 <= self._pos < len(self._events): attrs = self._events[self._pos].attributes for attr in attrs: if attr["name"] == index_or_name: if namespace is None or attr["namespace"] == namespace: return attr["value"] return "" def isAttributeDefault(self, index: int) -> bool: return False # --- Element state --- def isEmptyElementTag(self) -> bool: """Returns True if the current event is an empty element tag.""" if self.getEventType() == self.START_TAG: if self._pos + 1 < len(self._events): next_event = self._events[self._pos + 1] return next_event.type == self.END_TAG and next_event.name == self.getName() return False def isWhitespace(self) -> bool: text = self.getText() or "" return text.strip() == "" # --- Namespace support --- def getNamespaceCount(self, depth: int) -> int: return len(self._namespace_stack) def getNamespacePrefix(self, index: int) -> str: if 0 <= index < len(self._namespace_stack): return self._namespace_stack[index][0] return "" def getNamespaceUri(self, prefix: str) -> str: for p, uri in self._namespace_stack: if p == prefix: return uri return "" # --- Features / Properties --- def getFeature(self, name: str) -> bool: return self._features.get(name, False) def setFeature(self, name: str, value: bool): if name in self._features: self._features[name] = value else: raise AndroidOnlyError(f"Unknown feature: {name}") def getProperty(self, name: str): return self._properties.get(name) def setProperty(self, name: str, value: Any): self._properties[name] = value def toString(self) -> str: return f"XmlPullParser@{hex(id(self))}[event={self.getEventType()}]" def equals(self, other) -> bool: return self is other def hashCode(self) -> int: return hash(self) & 0x7FFFFFFF # ============================================================ # XmlSerializer # ============================================================ class XmlSerializer(Stub): """ org.xmlpull.v1.XmlSerializer Interface for serializing XML documents. """ def __init__(self): super().__init__() self._output = [] self._depth = 0 self._features = {} self._properties = {} self._prefixes = {} self._output_stream = None self._encoding = "UTF-8" def setOutput(self, output_stream, encoding: str = None): """Sets the output writer/stream.""" self._output_stream = output_stream self._encoding = encoding or "UTF-8" def getFeature(self, name: str) -> bool: return self._features.get(name, False) def setFeature(self, name: str, value: bool): self._features[name] = value def getProperty(self, name: str): return self._properties.get(name) def setProperty(self, name: str, value: Any): self._properties[name] = value def setPrefix(self, prefix: str, namespace_uri: str): """Sets the prefix for a namespace.""" self._prefixes[namespace_uri] = prefix if prefix: self._write(f' xmlns:{prefix}="{namespace_uri}"') else: self._write(f' xmlns="{namespace_uri}"') def startTag(self, namespace: str, name: str) -> "XmlSerializer": """Writes a start tag.""" self._write(f"<{name}") self._depth += 1 return self def attribute(self, namespace: str, name: str, value: str) -> "XmlSerializer": """Writes an attribute.""" if value is not None: escaped = value.replace("&", "&").replace("<", "<").replace(">", ">").replace('"', """) self._write(f' {name}="{escaped}"') return self def endTag(self, namespace: str, name: str) -> "XmlSerializer": """Writes an end tag.""" self._depth -= 1 self._write(f"") return self def text(self, text_or_buf: Any, start: int = None, length: int = None) -> "XmlSerializer": """Writes text content.""" if isinstance(text_or_buf, str): escaped = text_or_buf.replace("&", "&").replace("<", "<").replace(">", ">") self._write(escaped) elif isinstance(text_or_buf, (list, tuple)) and start is not None and length is not None: chars = text_or_buf[start:start + length] self._write("".join(chars)) return self def cdsect(self, text: str) -> "XmlSerializer": """Writes a CDATA section.""" self._write(f"") return self def entityRef(self, name: str) -> "XmlSerializer": """Writes an entity reference.""" self._write(f"&{name};") return self def processingInstruction(self, text: str) -> "XmlSerializer": """Writes a processing instruction.""" self._write(f"") return self def comment(self, text: str) -> "XmlSerializer": """Writes a comment.""" self._write(f"") return self def docdecl(self, text: str) -> "XmlSerializer": """Writes a document declaration.""" self._write(f"") return self def startDocument(self, encoding: str = None, standalone: bool = None): """Writes the XML declaration.""" encoding = encoding or self._encoding decl = f'" self._write(decl) def endDocument(self): """Finishes the document.""" self.flush() def flush(self): """Flushes the output.""" content = "".join(self._output) if self._output_stream: if hasattr(self._output_stream, 'write'): self._output_stream.write(content) self._output = [] def getDepth(self) -> int: return self._depth def getName(self) -> str: return "" def getNamespace(self) -> str: return "" def getPrefix(self, namespace: str, prefix: bool = False) -> str: return self._prefixes.get(namespace, "") def _write(self, s: str): self._output.append(s) def verbatim(self, text: str) -> "XmlSerializer": """Writes raw text without escaping.""" self._write(text) return self def write(self, text: str) -> "XmlSerializer": """Writes text.""" return self.text(text) def toString(self) -> str: return f"XmlSerializer@{hex(id(self))}" def equals(self, other) -> bool: return self is other def hashCode(self) -> int: return hash(self) & 0x7FFFFFFF # ============================================================ # XmlPullParserFactory # ============================================================ class XmlPullParserFactory(Stub): """ org.xmlpull.v1.XmlPullParserFactory Factory for creating XmlPullParser and XmlSerializer instances. """ def __init__(self): super().__init__() self._features = {} self._properties = {} @staticmethod def newInstance(unused: Any = None, unused_cls: Any = None) -> "XmlPullParserFactory": return XmlPullParserFactory() def setFeature(self, name: str, value: bool): self._features[name] = value def getFeature(self, name: str) -> bool: return self._features.get(name, False) def setProperty(self, name: str, value: Any): self._properties[name] = value def getProperty(self, name: str): return self._properties.get(name) def newPullParser(self) -> XmlPullParser: """Creates a new XmlPullParser.""" return XmlPullParser() def newSerializer(self) -> XmlSerializer: """Creates a new XmlSerializer.""" return XmlSerializer() def isNamespaceAware(self) -> bool: return self.getFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES) def isValidating(self) -> bool: return self.getFeature(XmlPullParser.FEATURE_VALIDATION) def toString(self) -> str: return f"XmlPullParserFactory@{hex(id(self))}" def equals(self, other) -> bool: return self is other def hashCode(self) -> int: return hash(self) & 0x7FFFFFFF # ============================================================ # Factory (alias) # ============================================================ class Factory(XmlPullParserFactory): """ Factory alias for XmlPullParserFactory. """ def __init__(self): super().__init__() @staticmethod def newInstance(unused: Any = None, unused_cls: Any = None) -> "Factory": return Factory() @staticmethod def getInstance() -> "Factory": return Factory() def toString(self) -> str: return f"Factory@{hex(id(self))}" def equals(self, other) -> bool: return isinstance(other, Factory) def hashCode(self) -> int: return hash("Factory") & 0x7FFFFFFF