diff --git a/sdk/api/@ohos.xml.ets b/sdk/api/@ohos.xml.ets index fcfd0a4371de27932d296e3df51ba9f660bc2783..b05de195ce2d0e3895fec8548ea87938ad313729 100644 --- a/sdk/api/@ohos.xml.ets +++ b/sdk/api/@ohos.xml.ets @@ -515,4 +515,499 @@ export namespace xml { } } } + + const STRING_INDEX_OF_ERROR_CODE = -1; + const INDEX_OUT_OF_RANGE = -1; + const NAMESPACE_INDEX_OFFSET = 1; + const DEPTH_OFFSET = 1; + const BUFFER_INDEX_OFFSET = 1; + const NAME_INDEX_OFFSET = 2; + const NAMESPACE_NUMBER_MULTIPLIER = 2; + const STACK_INDEX_MULTIPLIER = 3; + const TypeErrorCodeId: number = 401; + + class BusinessError extends Error { + code: number; + constructor(msg: string) { + super(msg); + this.name = 'BusinessError'; + this.code = TypeErrorCodeId; + } + + constructor(msg: string, code: number) { + super(msg); + this.name = 'BusinessError'; + this.code = code; + } + } + + export class XmlSerializer { + private out: string = ''; + private iPos: number = 0; + private buffer: ArrayBuffer | DataView; + private xmlSerializerError: string = ''; + private encoding: string = 'utf-8'; + private depth: int = 0; + private type: string = ''; + private elementStack = new Array(); + private multNsp = new Map>(); + private curNspNum: int = 0; + private isHasDecl: boolean = false; + + /** + * Constructs an instance of the class with the specified buffer and encoding. + * + * @param {ArrayBuffer | DataView} buffer - The buffer containing the data to be processed. + * @param {string} encoding - The encoding format of the data. Only 'utf-8' is supported. + * @throws {BusinessError} Throws an error if the encoding is not 'utf-8'. + */ + constructor(buffer: ArrayBuffer | DataView, encoding: string) { + if (encoding !== 'utf-8') { + throw new BusinessError('Parameter error.Just support utf-8'); + } + + this.buffer = buffer; + this.increaseStackLength(); + } + + /** + * Constructs an instance of the class with the specified buffer. + * + * @param {ArrayBuffer | DataView} buffer - The buffer containing the data to be processed. + */ + constructor(buffer: ArrayBuffer | DataView) { + this.buffer = buffer; + this.increaseStackLength(); + } + + /** + * Sets an attribute for the current XML element. + * + * @param {string} name - The name of the attribute to set. + * @param {string} value - The value of the attribute to set. + * @throws {BusinessError} Throws an error if the method is called in an illegal position. + */ + public setAttributes(name: string, value: string): void { + if (name.length === 0) { + throw new BusinessError('Parameter error. Parameter cannot be empty'); + } + if (this.type !== 'isStart' && this.type !== 'isAttri') { + this.xmlSerializerError = 'Illegal position for attribute'; + return; + } + + this.out = ''; + this.out += ` ${name}=\"`; + this.writeEscaped(value); + this.out += `\"`; + this.type = 'isAttri'; + this.writeToBuffer('SetAttributes'); + this.throwXmlError(); + } + + /** + * Adds an empty XML element with the specified name to the output buffer. + * + * @param {string} name - The name of the empty XML element to be added. + */ + public addEmptyElement(name: string): void { + if (name.length === 0) { + throw new BusinessError('Parameter error. Parameter cannot be empty'); + } + this.out = ''; + if (this.type === 'isStart' || this.type === 'isAttri') { + this.splicNsp(); + this.out += '>'; + } + if (this.type !== '') { + this.nextItem(); + } + + this.out += `<${name}/>`; + this.type = 'isAddEmpElem'; + this.writeToBuffer('AddEmptyElement'); + this.throwXmlError(); + } + + /** + * Sets the XML declaration for the current document. + * + * @throws {BusinessError} Throws an error if the declaration is set in an illegal position. + */ + public setDeclaration(): void { + if (this.isHasDecl) { + this.xmlSerializerError = 'illegal position for declaration'; + return; + } + + this.out = ''; + this.out += ``; + this.isHasDecl = true; + this.writeToBuffer('SetDeclaration'); + this.throwXmlError(); + } + + /** + * Handles the start of an XML element. + * + * @param {string} name - The name of the XML element being started. + */ + public startElement(name: string): void { + if (name.length === 0) { + throw new BusinessError('Parameter error. Parameter cannot be empty'); + } + this.out = ''; + if (this.type === 'isStart' || this.type === 'isAttri') { + this.splicNsp(); + this.out += '>'; + } + if (this.type !== '' && this.type !== 'isDecl') { + this.nextItem(); + } + + this.elementStack[this.stackIndex(this.depth, 'name')] = name; + this.out += '<'; + if (this.elementStack[this.stackIndex(this.depth, 'prefix')] !== '') { + this.out += `${this.elementStack[this.stackIndex(this.depth, 'prefix')]}:`; + } else if (this.depth !== 0) { + if (this.elementStack[this.stackIndex(this.depth - DEPTH_OFFSET, 'prefix')] !== '') { + this.elementStack[this.stackIndex(this.depth, 'prefix')] = + this.elementStack[this.stackIndex(this.depth - DEPTH_OFFSET, 'prefix')]; + this.out += `${this.elementStack[this.stackIndex(this.depth, 'prefix')]}:`; + } + } + this.out += `${this.elementStack[this.stackIndex(this.depth, 'name')]}` + this.type = 'isStart'; + this.depth++; + this.increaseStackLength(); + this.writeToBuffer('StartElement'); + this.throwXmlError(); + } + + /** + * Handles the end of an XML element during parsing. + */ + public endElement(): void { + this.out = ''; + if (this.type == 'isStart' || this.type == 'isAttri') { + this.splicNsp(); + this.out += '/>'; + this.type = 'isEndTag'; + this.depth--; + this.writeToBuffer('EndElement'); + return; + } + this.depth--; + if (this.type !== 'isText') { + this.nextItem(); + } + + this.out += ''; + this.writeToBuffer('EndElement'); + this.throwXmlError(); + } + + /** + * Sets the namespace for the current XML element. + * + * @param {string} prefix - The namespace prefix to be set. + * @param {string} namespace - The namespace URI associated with the prefix. + */ + public setNamespace(prefix: string, namespace: string): void { + if (prefix.length === 0) { + throw new BusinessError('Parameter error. Parameter cannot be empty'); + } + if (namespace.length === 0) { + throw new BusinessError(`Parameter error. The type of ${namespace} must be string`); + } + this.out = ''; + if (this.type === 'isStart' || this.type === 'isAttri') { + this.splicNsp(); + this.out += '>'; + } + + this.elementStack[this.stackIndex(this.depth, 'prefix')] = prefix; + this.elementStack[this.stackIndex(this.depth, 'namespace')] = namespace; + let tempMap = new Map(); + tempMap.set(this.mapKey(this.curNspNum, 'prefix'), + this.elementStack[this.stackIndex(this.depth, 'prefix')]); + tempMap.set(this.mapKey(this.curNspNum, 'namespace'), + this.elementStack[this.stackIndex(this.depth, 'namespace')]); + this.multNsp.set(this.depth, tempMap); + this.curNspNum++; + this.type = 'isNsp'; + this.writeToBuffer('SetNamespace'); + this.throwXmlError(); + } + + /** + * Sets a comment in the XML output. + * + * @param {string} comment - The comment string to be added to the XML output. + */ + public setComment(text: string): void { + if (text.length === 0) { + throw new BusinessError('Parameter error. Parameter cannot be empty'); + } + this.out = ''; + if (this.type === 'isStart' || this.type === 'isAttri') { + this.splicNsp(); + this.out += '>'; + } + if (this.type !== '') { + this.nextItem(); + } + + this.out += ``; + this.type = 'isCom'; + this.writeToBuffer('SetComment'); + this.throwXmlError(); + } + + /** + * Sets the CDATA section for the XML content. + * + * @param {string} data - The string data to be wrapped in a Cdata section. + */ + public setCDATA(text: string): void { + if (text.length === 0) { + throw new BusinessError('Parameter error. Parameter cannot be empty'); + } + this.out = ''; + if (this.type === 'isStart' || this.type === 'isAttri') { + this.splicNsp(); + this.out += '>'; + } + if (this.type !== '') { + this.nextItem(); + } + + text = this.replace(text, ']]>', ']]]]>'); + this.out += ``; + this.type = 'isCData'; + this.writeToBuffer('SetCdata'); + this.throwXmlError(); + } + + /** + * Sets the text content for the current XML element. + * + * @param {string} text - The text content to set for the current XML element. + */ + public setText(text: string): void { + if (text.length === 0) { + throw new BusinessError('Parameter error. Parameter cannot be empty'); + } + this.out = ''; + if (this.type === 'isStart' || this.type === 'isAttri') { + this.splicNsp(); + this.out += '>'; + } + + this.writeEscaped(text); + this.type = 'isText'; + this.writeToBuffer('SetText'); + this.throwXmlError(); + } + + /** + * Sets the document type (DOCTYPE) for the XML output. + * + * @param {string} text - The DOCTYPE declaration to be added to the XML output. + */ + public setDocType(text: string): void { + if (text.length === 0) { + throw new BusinessError('Parameter error. Parameter cannot be empty'); + } + this.out = ''; + if (this.type === 'isStart' || this.type === 'isAttri') { + this.splicNsp(); + this.out += '>'; + } + if (this.type !== '') { + this.nextItem(); + } + + this.out += ``; + this.type = 'isDocType'; + this.writeToBuffer('SetDocType'); + this.throwXmlError(); + } + + /** + * Calculates the stack index based on the given depth and type. + * + * @param {int} depth - The depth level in the stack. + * @param {string} type - The type of the stack element. + * @returns {int} The calculated stack index. + */ + private stackIndex(depth: int, type: string): int { + let index = depth * STACK_INDEX_MULTIPLIER; + switch (type) { + case 'prefix': + return index; + case 'namespace': + return index + NAMESPACE_INDEX_OFFSET; + case 'name': + return index + NAME_INDEX_OFFSET; + default: + return INDEX_OUT_OF_RANGE; + } + } + + /** + * Calculates the map key based on the provided type. + * + * @param {int} num - The current number of namespace. + * @param {string} type - The type of mapping to apply. + * @returns {int} The map key based on the provided type. + */ + private mapKey(num: int, type: string): int { + let key = num * NAMESPACE_NUMBER_MULTIPLIER; + switch (type) { + case 'prefix': + return key; + case 'namespace': + return key + NAMESPACE_INDEX_OFFSET; + default: + return INDEX_OUT_OF_RANGE; + } + } + + /** + * Increases the length of the element stack by pushing three empty string elements onto it. + * This method is used to expand the stack for additional elements. + */ + private increaseStackLength(): void { + this.elementStack.push(''); + this.elementStack.push(''); + this.elementStack.push(''); + } + + /** + * Retrieves the error message related to the XML serializer. + * + * @returns {string} The error message associated with the XML serializer. + */ + private getXmlSerializerError(): string { + return this.xmlSerializerError; + } + + /** + * Escapes special characters in the given string and appends the result to the output. + * + * @param {string} s - The input string to be escaped. + */ + private writeEscaped(s: string): void { + s = this.replace(s, '&', '&'); + s = this.replace(s, '\'', '''); + s = this.replace(s, '\"', '"'); + s = this.replace(s, '>', '>'); + s = this.replace(s, '<', '<'); + this.out += s; + } + + /** + * Replaces all occurrences of a specified substring within a string with a replacement string. + * + * @param {string} str - The original string in which the replacements will be made. + * @param {string} subStr - The substring to be replaced. + * @param {string} repStr - The string to replace the occurrences of `subStr`. + * @returns {string} - A new string with all occurrences of `subStr` replaced by `repStr`. + */ + private replace(str: string, subStr: string, repStr: string): string { + let result = ''; + let startIndex = 0; + let index = str.indexOf(subStr); + + while (index !== STRING_INDEX_OF_ERROR_CODE) { + result += str.substring(startIndex, index) + repStr; + startIndex = (index + subStr.length) as int; + index = str.indexOf(subStr, startIndex); + } + + result += str.substring(startIndex); + return result; + } + + /** + * Writes the content of the `out` array to the `buffer` starting at the current position (`iPos`). + * If the buffer does not have enough space to accommodate the data, an error is thrown. + * + * @param {string} name - The name used in the error message if the operation fails. + * @throws {BusinessError} Throws an error if the buffer does not have enough space to write the data. + */ + private writeToBuffer(name: string): void { + let iLenTemp = this.out.length; + if (this.buffer.byteLength > this.iPos + iLenTemp - BUFFER_INDEX_OFFSET) { + for (let i = 0; i < iLenTemp; i++) { + if (this.buffer instanceof ArrayBuffer) { + (this.buffer as ArrayBuffer).set((i + this.iPos) as int, this.out[i] as byte); + } else { + ((this.buffer as DataView).buffer as ArrayBuffer).set((i + this.iPos) as int, + this.out[i] as byte); + } + } + this.iPos += iLenTemp; + } else { + throw new BusinessError(`XmlSerializer:: ${name} memcpy_s failed`); + } + } + + /** + * Handles the namespace processing for the current element in the stack. + */ + private splicNsp(): void { + if (this.depth === 0) { + return; + } + this.elementStack[this.stackIndex(this.depth, 'prefix')] = + this.elementStack[this.stackIndex(this.depth - DEPTH_OFFSET, 'prefix')]; + this.elementStack[this.stackIndex(this.depth, 'namespace')] = + this.elementStack[this.stackIndex(this.depth - DEPTH_OFFSET, 'namespace')]; + if (this.multNsp.get(this.depth - DEPTH_OFFSET) === undefined) { + return; + } + if (this.type === 'isAttri' || this.type === 'isStart') { + for (let i = 0; i < this.curNspNum; i++) { + this.out += ` xmlns:${this.multNsp.get(this.depth - DEPTH_OFFSET)!.get(this.mapKey(i, 'prefix'))!}`; + this.out += `=\"`; + this.out += `${this.multNsp.get(this.depth - DEPTH_OFFSET)!.get(this.mapKey(i, 'namespace'))!}\"`; + } + this.multNsp.get(this.depth - DEPTH_OFFSET)!.clear(); + this.curNspNum = 0; + } + } + + /** + * Appends a newline character followed by indentation spaces to the `out` property. + */ + private nextItem(): void { + this.out += '\r\n'; + for (let i = 0; i < this.depth; i++) { + this.out += ' '; + } + } + + /** + * Throws a `BusinessError` if there is an XML serialization error. + * + * @throws {BusinessError} If an XML serialization error occurs. + */ + private throwXmlError(): void { + let errStr: string = this.getXmlSerializerError(); + this.xmlSerializerError = ''; + if (errStr.length !== 0) { + throw new BusinessError(errStr); + } + } + } }