Add optional OpenTelemetry Netty server instrumentation

- Update lys.version to 2026.04.14

- Add optional compileOnly dependency on opentelemetry-netty-4.1 in rbcs-server

- Add runtime guard to only activate instrumentation when OTel classes are on classpath

- Insert OTel combined handler after HttpServerCodec in the Netty pipeline

- Add requires-static JPMS directives for optional module support

- Add enableTelemetry config attribute to rbcs:server with default false

- Update Configuration DTO, XSD schema, Parser, Serializer, and all tests
This commit is contained in:
OpenCode
2026-04-28 14:59:08 +00:00
parent 70eccf83a8
commit ee7bc7e850
198 changed files with 11040 additions and 4 deletions
@@ -0,0 +1 @@
net.woggioni.rbcs.common.RbcsUrlStreamHandlerFactory
@@ -0,0 +1,15 @@
package net.woggioni.rbcs.common
import io.netty.buffer.ByteBuf
import io.netty.buffer.ByteBufAllocator
import io.netty.buffer.CompositeByteBuf
fun extractChunk(buf: CompositeByteBuf, alloc: ByteBufAllocator): ByteBuf {
val chunk = alloc.compositeBuffer()
for (component in buf.decompose(0, buf.readableBytes())) {
chunk.addComponent(true, component.retain())
}
buf.removeComponents(0, buf.numComponents())
buf.clear()
return chunk
}
@@ -0,0 +1,25 @@
package net.woggioni.rbcs.common
import io.netty.buffer.ByteBuf
import java.io.InputStream
class ByteBufInputStream(private val buf : ByteBuf) : InputStream() {
override fun read(): Int {
return buf.takeIf {
it.readableBytes() > 0
}?.let(ByteBuf::readByte)
?.let(Byte::toInt) ?: -1
}
override fun read(b: ByteArray, off: Int, len: Int): Int {
val readableBytes = buf.readableBytes()
if(readableBytes == 0) return -1
val result = len.coerceAtMost(readableBytes)
buf.readBytes(b, off, result)
return result
}
override fun close() {
buf.release()
}
}
@@ -0,0 +1,18 @@
package net.woggioni.rbcs.common
import io.netty.buffer.ByteBuf
import java.io.OutputStream
class ByteBufOutputStream(private val buf : ByteBuf) : OutputStream() {
override fun write(b: Int) {
buf.writeByte(b)
}
override fun write(b: ByteArray, off: Int, len: Int) {
buf.writeBytes(b, off, len)
}
override fun close() {
buf.release()
}
}
@@ -0,0 +1,62 @@
package net.woggioni.rbcs.common
import java.net.InetAddress
data class Cidr private constructor(
val networkAddress: InetAddress,
val prefixLength: Int
) {
companion object {
fun from(cidr: String) : Cidr {
val separator = cidr.indexOf("/")
if(separator < 0) {
throw IllegalArgumentException("Invalid CIDR format: $cidr")
}
val networkAddress = InetAddress.getByName(cidr.substring(0, separator))
val prefixLength = cidr.substring(separator + 1, cidr.length).toInt()
// Validate prefix length
val maxPrefix = if (networkAddress.address.size == 4) 32 else 128
require(prefixLength in 0..maxPrefix) { "Invalid prefix length: $prefixLength" }
return Cidr(networkAddress, prefixLength)
}
}
fun contains(address: InetAddress): Boolean {
val networkBytes = networkAddress.address
val addressBytes = address.address
if (networkBytes.size != addressBytes.size) {
return false
}
// Calculate how many full bytes and remaining bits to check
val fullBytes = prefixLength / 8
val remainingBits = prefixLength % 8
// Check full bytes
for (i in 0..<fullBytes) {
if (networkBytes[i] != addressBytes[i]) {
return false
}
}
// Check remaining bits if any
if (remainingBits > 0 && fullBytes < networkBytes.size) {
val mask = (0xFF shl (8 - remainingBits)).toByte()
if ((networkBytes[fullBytes].toInt() and mask.toInt()) != (addressBytes[fullBytes].toInt() and mask.toInt())) {
return false
}
}
return true
}
override fun toString(): String {
return networkAddress.hostAddress + "/" + prefixLength
}
}
@@ -0,0 +1,7 @@
package net.woggioni.rbcs.common
class ResourceNotFoundException(msg : String? = null, cause: Throwable? = null) : RuntimeException(msg, cause) {
}
class ModuleNotFoundException(msg : String? = null, cause: Throwable? = null) : RuntimeException(msg, cause) {
}
@@ -0,0 +1,8 @@
package net.woggioni.rbcs.common
data class HostAndPort(val host: String, val port: Int = 0) {
override fun toString(): String {
return "$host:$port"
}
}
@@ -0,0 +1,191 @@
package net.woggioni.rbcs.common
import io.netty.channel.Channel
import io.netty.channel.ChannelHandlerContext
import java.nio.file.Files
import java.nio.file.Path
import java.util.logging.LogManager
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.slf4j.MDC
import org.slf4j.event.Level
import org.slf4j.spi.LoggingEventBuilder
inline fun <reified T> T.contextLogger() = LoggerFactory.getLogger(T::class.java)
inline fun <reified T> createLogger() = LoggerFactory.getLogger(T::class.java)
inline fun Logger.traceParam(messageBuilder: () -> Pair<String, Array<Any>>) {
if (isTraceEnabled) {
val (format, params) = messageBuilder()
trace(format, params)
}
}
inline fun Logger.debugParam(messageBuilder: () -> Pair<String, Array<Any>>) {
if (isDebugEnabled) {
val (format, params) = messageBuilder()
info(format, params)
}
}
inline fun Logger.infoParam(messageBuilder: () -> Pair<String, Array<Any>>) {
if (isInfoEnabled) {
val (format, params) = messageBuilder()
info(format, params)
}
}
inline fun Logger.warnParam(messageBuilder: () -> Pair<String, Array<Any>>) {
if (isWarnEnabled) {
val (format, params) = messageBuilder()
warn(format, params)
}
}
inline fun Logger.errorParam(messageBuilder: () -> Pair<String, Array<Any>>) {
if (isErrorEnabled) {
val (format, params) = messageBuilder()
error(format, params)
}
}
inline fun log(
log: Logger,
filter: Logger.() -> Boolean,
loggerMethod: Logger.(String) -> Unit, messageBuilder: () -> String
) {
if (log.filter()) {
log.loggerMethod(messageBuilder())
}
}
fun withMDC(params: Array<Pair<String, String>>, cb: () -> Unit) {
object : AutoCloseable {
override fun close() {
for ((key, _) in params) MDC.remove(key)
}
}.use {
for ((key, value) in params) MDC.put(key, value)
cb()
}
}
inline fun Logger.log(level: Level, channel: Channel, crossinline messageBuilder: (LoggingEventBuilder) -> Unit ) {
if (isEnabledForLevel(level)) {
val params = arrayOf<Pair<String, String>>(
"channel-id-short" to channel.id().asShortText(),
"channel-id-long" to channel.id().asLongText(),
"remote-address" to channel.remoteAddress().toString(),
"local-address" to channel.localAddress().toString(),
)
withMDC(params) {
val builder = makeLoggingEventBuilder(level)
messageBuilder(builder)
builder.log()
}
}
}
inline fun Logger.log(level: Level, channel: Channel, crossinline messageBuilder: () -> String) {
log(level, channel) { builder ->
builder.setMessage(messageBuilder())
}
}
inline fun Logger.trace(ch: Channel, crossinline messageBuilder: () -> String) {
log(Level.TRACE, ch, messageBuilder)
}
inline fun Logger.debug(ch: Channel, crossinline messageBuilder: () -> String) {
log(Level.DEBUG, ch, messageBuilder)
}
inline fun Logger.info(ch: Channel, crossinline messageBuilder: () -> String) {
log(Level.INFO, ch, messageBuilder)
}
inline fun Logger.warn(ch: Channel, crossinline messageBuilder: () -> String) {
log(Level.WARN, ch, messageBuilder)
}
inline fun Logger.error(ch: Channel, crossinline messageBuilder: () -> String) {
log(Level.ERROR, ch, messageBuilder)
}
inline fun Logger.trace(ctx: ChannelHandlerContext, crossinline messageBuilder: () -> String) {
log(Level.TRACE, ctx.channel(), messageBuilder)
}
inline fun Logger.debug(ctx: ChannelHandlerContext, crossinline messageBuilder: () -> String) {
log(Level.DEBUG, ctx.channel(), messageBuilder)
}
inline fun Logger.info(ctx: ChannelHandlerContext, crossinline messageBuilder: () -> String) {
log(Level.INFO, ctx.channel(), messageBuilder)
}
inline fun Logger.warn(ctx: ChannelHandlerContext, crossinline messageBuilder: () -> String) {
log(Level.WARN, ctx.channel(), messageBuilder)
}
inline fun Logger.error(ctx: ChannelHandlerContext, crossinline messageBuilder: () -> String) {
log(Level.ERROR, ctx.channel(), messageBuilder)
}
inline fun Logger.log(level: Level, messageBuilder: () -> String) {
if (isEnabledForLevel(level)) {
makeLoggingEventBuilder(level).log(messageBuilder())
}
}
inline fun Logger.trace(messageBuilder: () -> String) {
if (isTraceEnabled) {
trace(messageBuilder())
}
}
inline fun Logger.debug(messageBuilder: () -> String) {
if (isDebugEnabled) {
debug(messageBuilder())
}
}
inline fun Logger.info(messageBuilder: () -> String) {
if (isInfoEnabled) {
info(messageBuilder())
}
}
inline fun Logger.warn(messageBuilder: () -> String) {
if (isWarnEnabled) {
warn(messageBuilder())
}
}
inline fun Logger.error(messageBuilder: () -> String) {
if (isErrorEnabled) {
error(messageBuilder())
}
}
class LoggingConfig {
init {
val logManager = LogManager.getLogManager()
System.getProperty("log.config.source")?.let withSource@{ source ->
val urls = LoggingConfig::class.java.classLoader.getResources(source)
while (urls.hasMoreElements()) {
val url = urls.nextElement()
url.openStream().use { inputStream ->
logManager.readConfiguration(inputStream)
return@withSource
}
}
Path.of(source).takeIf(Files::exists)
?.let(Files::newInputStream)
?.use(logManager::readConfiguration)
}
}
}
@@ -0,0 +1,57 @@
package net.woggioni.rbcs.common
import java.security.SecureRandom
import java.security.spec.KeySpec
import java.util.Base64
import javax.crypto.SecretKeyFactory
import javax.crypto.spec.PBEKeySpec
object PasswordSecurity {
enum class Algorithm(
val codeName : String,
val keyLength : Int,
val iterations : Int) {
PBEWithHmacSHA512_224AndAES_256("PBEWithHmacSHA512/224AndAES_256", 64, 1),
PBEWithHmacSHA1AndAES_256("PBEWithHmacSHA1AndAES_256",64, 1),
PBEWithHmacSHA384AndAES_128("PBEWithHmacSHA384AndAES_128", 64,1),
PBEWithHmacSHA384AndAES_256("PBEWithHmacSHA384AndAES_256",64,1),
PBKDF2WithHmacSHA512("PBKDF2WithHmacSHA512",512, 1),
PBKDF2WithHmacSHA384("PBKDF2WithHmacSHA384",384, 1);
}
private fun concat(arr1: ByteArray, arr2: ByteArray): ByteArray {
val result = ByteArray(arr1.size + arr2.size)
var j = 0
for(element in arr1) {
result[j] = element
j += 1
}
for(element in arr2) {
result[j] = element
j += 1
}
return result
}
fun hashPassword(password : String, salt : String? = null, algorithm : Algorithm = Algorithm.PBKDF2WithHmacSHA512) : String {
val actualSalt = salt?.let(Base64.getDecoder()::decode) ?: SecureRandom().run {
val result = ByteArray(16)
nextBytes(result)
result
}
val spec: KeySpec = PBEKeySpec(password.toCharArray(), actualSalt, algorithm.iterations, algorithm.keyLength)
val factory = SecretKeyFactory.getInstance(algorithm.codeName)
val hash = factory.generateSecret(spec).encoded
return String(Base64.getEncoder().encode(concat(hash, actualSalt)))
}
fun decodePasswordHash(encodedPasswordHash : String, algorithm: Algorithm = Algorithm.PBKDF2WithHmacSHA512) : Pair<ByteArray, ByteArray> {
val decoded = Base64.getDecoder().decode(encodedPasswordHash)
val hash = ByteArray(algorithm.keyLength / 8)
val salt = ByteArray(decoded.size - algorithm.keyLength / 8)
System.arraycopy(decoded, 0, hash, 0, hash.size)
System.arraycopy(decoded, hash.size, salt, 0, salt.size)
return hash to salt
}
}
@@ -0,0 +1,167 @@
package net.woggioni.rbcs.common
import java.io.IOException
import java.net.InetAddress
import java.net.ServerSocket
import java.net.URI
import java.net.URL
import java.nio.file.Files
import java.nio.file.Path
import java.security.KeyStore
import java.security.MessageDigest
import java.security.cert.CertPathValidator
import java.security.cert.CertPathValidatorException
import java.security.cert.CertificateException
import java.security.cert.CertificateFactory
import java.security.cert.PKIXParameters
import java.security.cert.PKIXRevocationChecker
import java.security.cert.X509Certificate
import java.util.EnumSet
import javax.net.ssl.TrustManagerFactory
import javax.net.ssl.X509TrustManager
import net.woggioni.jwo.JWO
import net.woggioni.jwo.Tuple2
object RBCS {
fun String.toUrl(): URL = URL.of(URI(this), null)
const val RBCS_NAMESPACE_URI: String = "urn:net.woggioni.rbcs.server"
const val RBCS_PREFIX: String = "rbcs"
const val XML_SCHEMA_NAMESPACE_URI = "http://www.w3.org/2001/XMLSchema-instance"
fun ByteArray.toInt(index: Int = 0): Long {
if (index + 4 > size) throw IllegalArgumentException("Not enough bytes to decode a 32 bits integer")
var value: Long = 0
for (b in index until index + 4) {
value = (value shl 8) + (get(b).toInt() and 0xFF)
}
return value
}
fun ByteArray.toLong(index: Int = 0): Long {
if (index + 8 > size) throw IllegalArgumentException("Not enough bytes to decode a 64 bits long integer")
var value: Long = 0
for (b in index until index + 8) {
value = (value shl 8) + (get(b).toInt() and 0xFF)
}
return value
}
fun digest(
data: ByteArray,
md: MessageDigest
): ByteArray {
md.update(data)
return md.digest()
}
fun digestString(
data: ByteArray,
md: MessageDigest
): String {
return JWO.bytesToHex(digest(data, md))
}
fun processCacheKey(key: String, keyPrefix: String?, digestAlgorithm: String?) : ByteArray {
val prefixedKey = if (keyPrefix == null) {
key
} else {
key + keyPrefix
}.toByteArray(Charsets.UTF_8)
return digestAlgorithm
?.let(MessageDigest::getInstance)
?.let { md ->
digest(prefixedKey, md)
} ?: prefixedKey
}
fun Long.toIntOrNull(): Int? {
return if (this >= Int.MIN_VALUE && this <= Int.MAX_VALUE) {
toInt()
} else {
null
}
}
fun getFreePort(): Int {
var count = 0
while (count < 50) {
try {
ServerSocket(0, 50, InetAddress.getLocalHost()).use { serverSocket ->
val candidate = serverSocket.localPort
if (candidate > 0) {
return candidate
} else {
throw RuntimeException("Got invalid port number: $candidate")
}
}
} catch (ignored: IOException) {
++count
}
}
throw RuntimeException("Error trying to find an open port")
}
fun loadKeystore(file: Path, password: String?): KeyStore {
val ext = JWO.splitExtension(file)
.map(Tuple2<String, String>::get_2)
.orElseThrow {
IllegalArgumentException(
"Keystore file '${file}' must have .jks, .p12, .pfx extension"
)
}
val keystore = when (ext.substring(1).lowercase()) {
"jks" -> KeyStore.getInstance("JKS")
"p12", "pfx" -> KeyStore.getInstance("PKCS12")
else -> throw IllegalArgumentException(
"Keystore file '${file}' must have .jks, .p12, .pfx extension"
)
}
Files.newInputStream(file).use {
keystore.load(it, password?.let(String::toCharArray))
}
return keystore
}
fun getTrustManager(trustStore: KeyStore?, certificateRevocationEnabled: Boolean): X509TrustManager {
return if (trustStore != null) {
val certificateFactory = CertificateFactory.getInstance("X.509")
val validator = CertPathValidator.getInstance("PKIX").apply {
val rc = revocationChecker as PKIXRevocationChecker
rc.options = EnumSet.of(
PKIXRevocationChecker.Option.NO_FALLBACK
)
}
val params = PKIXParameters(trustStore).apply {
isRevocationEnabled = certificateRevocationEnabled
}
object : X509TrustManager {
override fun checkClientTrusted(chain: Array<out X509Certificate>, authType: String) {
val clientCertificateChain = certificateFactory.generateCertPath(chain.toList())
try {
validator.validate(clientCertificateChain, params)
} catch (ex: CertPathValidatorException) {
throw CertificateException(ex)
}
}
override fun checkServerTrusted(chain: Array<out X509Certificate>, authType: String) {
throw NotImplementedError()
}
private val acceptedIssuers = trustStore.aliases().asSequence()
.filter(trustStore::isCertificateEntry)
.map(trustStore::getCertificate)
.map { it as X509Certificate }
.toList()
.toTypedArray()
override fun getAcceptedIssuers() = acceptedIssuers
}
} else {
val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
trustManagerFactory.trustManagers.asSequence().filter { it is X509TrustManager }
.single() as X509TrustManager
}
}
}
@@ -0,0 +1,113 @@
package net.woggioni.rbcs.common
import java.io.IOException
import java.io.InputStream
import java.net.URL
import java.net.URLConnection
import java.net.URLStreamHandler
import java.net.spi.URLStreamHandlerProvider
import java.util.concurrent.atomic.AtomicBoolean
import java.util.stream.Collectors
class RbcsUrlStreamHandlerFactory : URLStreamHandlerProvider() {
private class ClasspathHandler(private val classLoader: ClassLoader = RbcsUrlStreamHandlerFactory::class.java.classLoader) :
URLStreamHandler() {
override fun openConnection(u: URL): URLConnection? {
return javaClass.module
?.takeIf { m: Module -> m.layer != null }
?.let {
val path = u.path
val i = path.lastIndexOf('/')
val packageName = path.substring(0, i).replace('/', '.')
val modules = packageMap[packageName]!!
ClasspathResourceURLConnection(
u,
modules
)
}
?: classLoader.getResource(u.path)?.let(URL::openConnection)
}
}
private class JpmsHandler : URLStreamHandler() {
override fun openConnection(u: URL): URLConnection {
val moduleName = u.host
val thisModule = javaClass.module
val sourceModule =
thisModule
?.let(Module::getLayer)
?.let { layer: ModuleLayer ->
layer.findModule(moduleName).orElse(null)
} ?: if(thisModule.layer == null) {
thisModule
} else throw ModuleNotFoundException("Module '$moduleName' not found")
return JpmsResourceURLConnection(u, sourceModule)
}
}
private class JpmsResourceURLConnection(url: URL, private val module: Module) : URLConnection(url) {
override fun connect() {
}
@Throws(IOException::class)
override fun getInputStream(): InputStream {
val resource = getURL().path
return module.getResourceAsStream(resource)
?: throw ResourceNotFoundException("Resource '$resource' not found in module '${module.name}'")
}
}
override fun createURLStreamHandler(protocol: String): URLStreamHandler? {
return when (protocol) {
"classpath" -> ClasspathHandler()
"jpms" -> JpmsHandler()
else -> null
}
}
private class ClasspathResourceURLConnection(url: URL?, private val modules: List<Module>) :
URLConnection(url) {
override fun connect() {}
override fun getInputStream(): InputStream? {
for (module in modules) {
val result = module.getResourceAsStream(getURL().path)
if (result != null) return result
}
return null
}
}
companion object {
private val installed = AtomicBoolean(false)
fun install() {
if (!installed.getAndSet(true)) {
URL.setURLStreamHandlerFactory(RbcsUrlStreamHandlerFactory())
}
}
private val packageMap: Map<String, List<Module>> by lazy {
RbcsUrlStreamHandlerFactory::class.java.module.layer
.modules()
.stream()
.flatMap { m: Module ->
m.packages.stream()
.map { p: String -> p to m }
}
.collect(
Collectors.groupingBy(
Pair<String, Module>::first,
Collectors.mapping(
Pair<String, Module>::second,
Collectors.toUnmodifiableList<Module>()
)
)
)
}
}
}
@@ -0,0 +1,243 @@
package net.woggioni.rbcs.common
import java.io.InputStream
import java.io.OutputStream
import java.net.URL
import javax.xml.XMLConstants.ACCESS_EXTERNAL_DTD
import javax.xml.XMLConstants.ACCESS_EXTERNAL_SCHEMA
import javax.xml.XMLConstants.FEATURE_SECURE_PROCESSING
import javax.xml.XMLConstants.W3C_XML_SCHEMA_NS_URI
import javax.xml.parsers.DocumentBuilder
import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.transform.OutputKeys
import javax.xml.transform.TransformerFactory
import javax.xml.transform.dom.DOMSource
import javax.xml.transform.stream.StreamResult
import javax.xml.transform.stream.StreamSource
import javax.xml.validation.Schema
import javax.xml.validation.SchemaFactory
import net.woggioni.jwo.JWO
import org.slf4j.event.Level
import org.w3c.dom.Document
import org.w3c.dom.Element
import org.w3c.dom.Node
import org.w3c.dom.NodeList
import org.xml.sax.ErrorHandler as ErrHandler
import org.xml.sax.SAXNotRecognizedException
import org.xml.sax.SAXNotSupportedException
import org.xml.sax.SAXParseException
class NodeListIterator(private val nodeList: NodeList) : Iterator<Node> {
private var cursor: Int = 0
override fun hasNext(): Boolean {
return cursor < nodeList.length
}
override fun next(): Node {
return if (hasNext()) nodeList.item(cursor++) else throw NoSuchElementException()
}
}
class ElementIterator(parent: Element, name: String? = null) : Iterator<Element> {
private val it: NodeListIterator
private val name: String?
private var next: Element?
init {
it = NodeListIterator(parent.childNodes)
this.name = name
next = getNext()
}
override fun hasNext(): Boolean {
return next != null
}
override fun next(): Element {
val result = next ?: throw NoSuchElementException()
next = getNext()
return result
}
private fun getNext(): Element? {
var result: Element? = null
while (it.hasNext()) {
val node: Node = it.next()
if (node is Element && (name == null || name == node.tagName)) {
result = node
break
}
}
return result
}
}
class Xml(val doc: Document, val element: Element) {
class ErrorHandler(private val fileURL: URL) : ErrHandler {
companion object {
private val log = createLogger<ErrorHandler>()
}
override fun warning(ex: SAXParseException)= err(ex, Level.WARN)
private fun err(ex: SAXParseException, level: Level) {
log.log(level) {
"Problem at ${fileURL}:${ex.lineNumber}:${ex.columnNumber} parsing deployment configuration: ${ex.message}"
}
throw ex
}
override fun error(ex: SAXParseException) = err(ex, Level.ERROR)
override fun fatalError(ex: SAXParseException) = err(ex, Level.ERROR)
}
companion object {
private val dictMap: Map<String, Map<String, Any>> = sequenceOf(
"env" to System.getenv().asSequence().map { (k, v) -> k to (v as Any) }.toMap(),
"sys" to System.getProperties().asSequence().map { (k, v) -> k as String to (v as Any) }.toMap()
).toMap()
private fun renderConfigurationTemplate(template: String): String {
return JWO.renderTemplate(template, emptyMap(), dictMap).replace("$$", "$")
}
fun Element.renderAttribute(name : String, namespaceURI: String? = null) = if(namespaceURI == null) {
getAttribute(name)
} else {
getAttributeNS(name, namespaceURI)
}.takeIf(String::isNotEmpty)?.let(Companion::renderConfigurationTemplate)
fun Element.asIterable() = Iterable { ElementIterator(this, null) }
fun NodeList.asIterable() = Iterable { NodeListIterator(this) }
private fun disableProperty(dbf: DocumentBuilderFactory, propertyName: String) {
try {
dbf.setAttribute(propertyName, "")
} catch (iae: IllegalArgumentException) {
// Property not supported.
}
}
private fun disableProperty(sf: SchemaFactory, propertyName: String) {
try {
sf.setProperty(propertyName, "")
} catch (ex: SAXNotRecognizedException) {
// Property not supported.
} catch (ex: SAXNotSupportedException) {
}
}
fun getSchema(schema: URL): Schema {
val sf = SchemaFactory.newInstance(W3C_XML_SCHEMA_NS_URI)
sf.setFeature(FEATURE_SECURE_PROCESSING, false)
sf.errorHandler = ErrorHandler(schema)
return sf.newSchema(schema)
}
fun getSchema(inputStream: InputStream): Schema {
val sf = SchemaFactory.newInstance(W3C_XML_SCHEMA_NS_URI)
sf.setFeature(FEATURE_SECURE_PROCESSING, true)
return sf.newSchema(StreamSource(inputStream))
}
fun newDocumentBuilderFactory(schemaResourceURL: URL?): DocumentBuilderFactory {
val dbf = DocumentBuilderFactory.newInstance()
dbf.setFeature(FEATURE_SECURE_PROCESSING, false)
dbf.setAttribute(ACCESS_EXTERNAL_SCHEMA, "all")
disableProperty(dbf, ACCESS_EXTERNAL_DTD)
dbf.isExpandEntityReferences = true
dbf.isIgnoringComments = true
dbf.isNamespaceAware = true
dbf.isValidating = schemaResourceURL == null
dbf.setFeature("http://apache.org/xml/features/validation/schema", true)
schemaResourceURL?.let {
dbf.schema = getSchema(it)
}
return dbf
}
fun newDocumentBuilder(resource: URL, schemaResourceURL: URL?): DocumentBuilder {
val db = newDocumentBuilderFactory(schemaResourceURL).newDocumentBuilder()
db.setErrorHandler(ErrorHandler(resource))
return db
}
fun parseXmlResource(resource: URL, schemaResourceURL: URL?): Document {
val db = newDocumentBuilder(resource, schemaResourceURL)
return resource.openStream().use(db::parse)
}
fun parseXml(sourceURL: URL, sourceStream: InputStream? = null, schemaResourceURL: URL? = null): Document {
val db = newDocumentBuilder(sourceURL, schemaResourceURL)
return sourceStream?.let(db::parse) ?: sourceURL.openStream().use(db::parse)
}
fun write(doc: Document, output: OutputStream) {
val transformerFactory = TransformerFactory.newInstance()
val transformer = transformerFactory.newTransformer()
transformer.setOutputProperty(OutputKeys.INDENT, "yes")
transformer.setOutputProperty(OutputKeys.INDENT, "yes")
transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4")
transformer.setOutputProperty(OutputKeys.STANDALONE, "yes")
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8")
val source = DOMSource(doc)
val result = StreamResult(output)
transformer.transform(source, result)
}
fun of(
namespaceURI: String,
qualifiedName: String,
schemaResourceURL: URL? = null,
cb: Xml.(el: Element) -> Unit
): Document {
val dbf = newDocumentBuilderFactory(schemaResourceURL)
val db = dbf.newDocumentBuilder()
val doc = db.newDocument()
val root = doc.createElementNS(namespaceURI, qualifiedName)
.also(doc::appendChild)
Xml(doc, root).cb(root)
return doc
}
fun of(doc: Document, el: Element, cb: Xml.(el: Element) -> Unit): Element {
Xml(doc, el).cb(el)
return el
}
fun Element.removeChildren() {
while (true) {
removeChild(firstChild ?: break)
}
}
}
fun node(
name: String,
namespaceURI: String? = null,
attrs: Map<String, String> = emptyMap(),
cb: Xml.(el: Element) -> Unit = {}
): Element {
val child = doc.createElementNS(namespaceURI, name)
for ((key, value) in attrs) {
child.setAttribute(key, value)
}
return child
.also {
element.appendChild(it)
Xml(doc, it).cb(it)
}
}
fun attr(key: String, value: String, namespaceURI: String? = null) {
element.setAttributeNS(namespaceURI, key, value)
}
fun text(txt: String) {
element.appendChild(doc.createTextNode(txt))
}
}