forked from woggioni/rbcs
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:
@@ -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))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user