/* * This file is part of LibEuFin. * Copyright (C) 2020, 2024 Taler Systems S.A. * * LibEuFin is free software; you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation; either version 3, or * (at your option) any later version. * * LibEuFin 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 Affero General * Public License for more details. * * You should have received a copy of the GNU Affero General Public * License along with LibEuFin; see the file COPYING. If not, see * */ package tech.libeufin.nexus import org.w3c.dom.* import java.io.InputStream import java.io.StringWriter import java.time.LocalDate import java.time.LocalDateTime import java.time.format.DateTimeFormatter import javax.xml.parsers.* import javax.xml.stream.XMLOutputFactory import javax.xml.stream.XMLStreamWriter interface XmlBuilder { fun el(path: String, lambda: XmlBuilder.() -> Unit = {}) fun el(path: String, content: String) { el(path) { text(content) } } fun attr(namespace: String, name: String, value: String) fun attr(name: String, value: String) fun text(content: String) companion object { fun toString(root: String, f: XmlBuilder.() -> Unit): String { val factory = XMLOutputFactory.newFactory() val stream = StringWriter() var writer = factory.createXMLStreamWriter(stream) /** * NOTE: commenting out because it wasn't obvious how to output the * "standalone = 'yes' directive". Manual forge was therefore preferred. */ stream.write("") XmlStreamBuilder(writer).el(root) { this.f() } writer.writeEndDocument() return stream.buffer.toString() } fun toDom(root: String, schema: String?, f: XmlBuilder.() -> Unit): Document { val factory = DocumentBuilderFactory.newInstance(); factory.isNamespaceAware = true val builder = factory.newDocumentBuilder(); val doc = builder.newDocument(); doc.setXmlVersion("1.0") doc.setXmlStandalone(true) val root = doc.createElementNS(schema, root) doc.appendChild(root); XmlDOMBuilder(doc, schema, root).f() doc.normalize() return doc } } } private class XmlStreamBuilder(private val w: XMLStreamWriter): XmlBuilder { override fun el(path: String, lambda: XmlBuilder.() -> Unit) { path.splitToSequence('/').forEach { w.writeStartElement(it) } lambda() path.splitToSequence('/').forEach { w.writeEndElement() } } override fun attr(namespace: String, name: String, value: String) { w.writeAttribute(namespace, name, value) } override fun attr(name: String, value: String) { w.writeAttribute(name, value) } override fun text(content: String) { w.writeCharacters(content) } } private class XmlDOMBuilder(private val doc: Document, private val schema: String?, private var node: Element): XmlBuilder { override fun el(path: String, lambda: XmlBuilder.() -> Unit) { val current = node path.splitToSequence('/').forEach { val new = doc.createElementNS(schema, it) node.appendChild(new) node = new } lambda() node = current } override fun attr(namespace: String, name: String, value: String) { node.setAttributeNS(namespace, name, value) } override fun attr(name: String, value: String) { node.setAttribute(name, value) } override fun text(content: String) { node.appendChild(doc.createTextNode(content)); } } class DestructionError(m: String) : Exception(m) private fun Element.childrenByTag(tag: String): Sequence = sequence { for (i in 0..childNodes.length) { val el = childNodes.item(i) if (el !is Element) { continue } if (el.localName != tag) { continue } yield(el) } } class XmlDestructor internal constructor(private val el: Element) { fun each(path: String, f: XmlDestructor.() -> Unit) { el.childrenByTag(path).forEach { f(XmlDestructor(it)) } } fun map(path: String, f: XmlDestructor.() -> T): List { return el.childrenByTag(path).map { f(XmlDestructor(it)) }.toList() } fun one(path: String): XmlDestructor { val children = el.childrenByTag(path).iterator() if (!children.hasNext()) { throw DestructionError("expected a single $path child, got none instead at $el") } val el = children.next() if (children.hasNext()) { throw DestructionError("expected a single $path child, got ${children.asSequence() + 1} instead at $el") } return XmlDestructor(el) } fun opt(path: String): XmlDestructor? { val children = el.childrenByTag(path).iterator() if (!children.hasNext()) { return null } val el = children.next() if (children.hasNext()) { throw DestructionError("expected an optional $path child, got ${children.asSequence().count() + 1} instead at $el") } return XmlDestructor(el) } fun one(path: String, f: XmlDestructor.() -> T): T = f(one(path)) fun opt(path: String, f: XmlDestructor.() -> T): T? = opt(path)?.run(f) fun text(): String = el.textContent fun bool(): Boolean = el.textContent.toBoolean() fun date(): LocalDate = LocalDate.parse(text(), DateTimeFormatter.ISO_DATE) fun dateTime(): LocalDateTime = LocalDateTime.parse(text(), DateTimeFormatter.ISO_DATE_TIME) inline fun > enum(): T = java.lang.Enum.valueOf(T::class.java, text()) fun attr(index: String): String = el.getAttribute(index) companion object { fun fromStream(xml: InputStream, root: String, f: XmlDestructor.() -> T): T { val doc = XMLUtil.parseIntoDom(xml) return fromDoc(doc, root, f) } fun fromDoc(doc: Document, root: String, f: XmlDestructor.() -> T): T { if (doc.documentElement.tagName != root) { throw DestructionError("expected root '$root' got '${doc.documentElement.tagName}'") } val destr = XmlDestructor(doc.documentElement) return f(destr) } } } fun destructXml(xml: InputStream, root: String, f: XmlDestructor.() -> T): T = XmlDestructor.fromStream(xml, root, f)