Compare commits

...

4 Commits
0.0.5 ... 0.0.7

Author SHA1 Message Date
241d95fe1c added env variable and java properties substitution in configuration attributes
All checks were successful
CI / build (push) Successful in 3m29s
2025-01-16 21:11:35 +08:00
3b7030c302 fixed mainClassName for native image build
All checks were successful
CI / build (push) Successful in 3m22s
2025-01-16 17:23:03 +08:00
a8670277e7 fixed XML error handler for server command 2025-01-16 17:01:14 +08:00
03ee75266d added average turnaround time calculation in benchmark 2025-01-16 14:54:21 +08:00
22 changed files with 144 additions and 367 deletions

View File

@@ -0,0 +1,11 @@
package net.woggioni.gbcs.api.exception;
public class ConfigurationException extends GbcsException {
public ConfigurationException(String message, Throwable cause) {
super(message, cause);
}
public ConfigurationException(String message) {
this(message, null);
}
}

View File

@@ -6,8 +6,9 @@ plugins {
}
dependencies {
compileOnly project(':gbcs-api')
compileOnly catalog.slf4j.api
implementation project(':gbcs-api')
implementation catalog.slf4j.api
implementation catalog.jwo
}
publishing {

View File

@@ -3,6 +3,7 @@ module net.woggioni.gbcs.base {
requires java.logging;
requires org.slf4j;
requires kotlin.stdlib;
requires net.woggioni.jwo;
exports net.woggioni.gbcs.base;
}

View File

@@ -2,6 +2,7 @@ package net.woggioni.gbcs.base
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.slf4j.event.Level
import java.nio.file.Files
import java.nio.file.Path
import java.util.logging.LogManager
@@ -52,6 +53,12 @@ inline fun log(log : Logger,
}
}
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())

View File

@@ -1,6 +1,11 @@
package net.woggioni.gbcs.base
import net.woggioni.jwo.CollectionUtils.mapValues
import net.woggioni.jwo.CollectionUtils.toUnmodifiableTreeMap
import net.woggioni.jwo.JWO
import net.woggioni.jwo.MapBuilder
import org.slf4j.LoggerFactory
import org.slf4j.event.Level
import org.w3c.dom.Document
import org.w3c.dom.Element
import org.w3c.dom.Node
@@ -11,6 +16,8 @@ import org.xml.sax.SAXParseException
import java.io.InputStream
import java.io.OutputStream
import java.net.URL
import java.util.Collections
import java.util.TreeMap
import javax.xml.XMLConstants.ACCESS_EXTERNAL_DTD
import javax.xml.XMLConstants.ACCESS_EXTERNAL_SCHEMA
import javax.xml.XMLConstants.FEATURE_SECURE_PROCESSING
@@ -80,31 +87,36 @@ class Xml(val doc: Document, val element: Element) {
private val log = LoggerFactory.getLogger(ErrorHandler::class.java)
}
override fun warning(ex: SAXParseException) {
log.warn(
"Problem at {}:{}:{} parsing deployment configuration: {}",
fileURL, ex.lineNumber, ex.columnNumber, ex.message
)
}
override fun warning(ex: SAXParseException)= err(ex, Level.WARN)
override fun error(ex: SAXParseException) {
log.error(
"Problem at {}:{}:{} parsing deployment configuration: {}",
fileURL, ex.lineNumber, ex.columnNumber, ex.message
)
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 fatalError(ex: SAXParseException) {
log.error(
"Problem at {}:{}:{} parsing deployment configuration: {}",
fileURL, ex.lineNumber, ex.columnNumber, 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)
}
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) }

View File

@@ -4,6 +4,8 @@ plugins {
alias catalog.plugins.envelope
alias catalog.plugins.sambal
alias catalog.plugins.graalvm.native.image
alias catalog.plugins.graalvm.jlink
alias catalog.plugins.jpms.check
id 'maven-publish'
}
@@ -12,6 +14,8 @@ import net.woggioni.gradle.envelope.EnvelopeJarTask
import net.woggioni.gradle.graalvm.NativeImageConfigurationTask
import net.woggioni.gradle.graalvm.NativeImagePlugin
import net.woggioni.gradle.graalvm.NativeImageTask
import net.woggioni.gradle.graalvm.JlinkPlugin
import net.woggioni.gradle.graalvm.JlinkTask
Property<String> mainClassName = objects.property(String.class)
mainClassName.set('net.woggioni.gbcs.cli.GradleBuildCacheServerCli')
@@ -56,15 +60,20 @@ Provider<EnvelopeJarTask> envelopeJarTaskProvider = tasks.named('envelopeJar', E
}
tasks.named(NativeImagePlugin.CONFIGURE_NATIVE_IMAGE_TASK_NAME, NativeImageConfigurationTask) {
mainClass = 'net.woggioni.gbcs.GraalNativeImageConfiguration'
mainClass = mainClassName
}
tasks.named(NativeImagePlugin.NATIVE_IMAGE_TASK_NAME, NativeImageTask) {
mainClass = 'net.woggioni.gbcs.GradleBuildCacheServer'
mainClass = mainClassName
useMusl = true
buildStaticImage = true
}
tasks.named(JlinkPlugin.JLINK_TASK_NAME, JlinkTask) {
mainClass = mainClassName
mainModule = 'net.woggioni.gbcs.cli'
}
artifacts {
release(envelopeJarTaskProvider)
}

View File

@@ -6,6 +6,7 @@ module net.woggioni.gbcs.cli {
requires net.woggioni.gbcs.client;
requires kotlin.stdlib;
requires net.woggioni.jwo;
requires net.woggioni.gbcs.api;
exports net.woggioni.gbcs.cli.impl.converters to info.picocli;
opens net.woggioni.gbcs.cli.impl.commands to info.picocli;

View File

@@ -13,6 +13,7 @@ import java.util.Base64
import java.util.concurrent.ExecutionException
import java.util.concurrent.Future
import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.atomic.AtomicLong
import kotlin.random.Random
@CommandLine.Command(
@@ -53,9 +54,12 @@ class BenchmarkCommand : GbcsCommand() {
val entries = let {
val completionQueue = LinkedBlockingQueue<Future<Pair<String, ByteArray>>>(numberOfEntries)
val start = Instant.now()
val totalElapsedTime = AtomicLong(0)
entryGenerator.take(numberOfEntries).forEach { entry ->
val requestStart = System.nanoTime()
val future = client.put(entry.first, entry.second).thenApply { entry }
future.whenComplete { _, _ ->
totalElapsedTime.addAndGet((System.nanoTime() - requestStart))
completionQueue.put(future)
}
}
@@ -78,6 +82,9 @@ class BenchmarkCommand : GbcsCommand() {
val elapsed = Duration.between(start, end).toMillis()
"Insertion rate: ${numberOfEntries.toDouble() / elapsed * 1000} ops/s"
}
log.info {
"Average time per insertion: ${totalElapsedTime.get() / numberOfEntries.toDouble() * 1000} ms"
}
inserted
}
log.info {
@@ -86,8 +93,11 @@ class BenchmarkCommand : GbcsCommand() {
if (entries.isNotEmpty()) {
val completionQueue = LinkedBlockingQueue<Future<Unit>>(entries.size)
val start = Instant.now()
val totalElapsedTime = AtomicLong(0)
entries.forEach { entry ->
val requestStart = System.nanoTime()
val future = client.get(entry.first).thenApply {
totalElapsedTime.addAndGet((System.nanoTime() - requestStart))
if (it == null) {
log.error {
"Missing entry for key '${entry.first}'"
@@ -112,6 +122,9 @@ class BenchmarkCommand : GbcsCommand() {
val elapsed = Duration.between(start, end).toMillis()
"Retrieval rate: ${entries.size.toDouble() / elapsed * 1000} ops/s"
}
log.info {
"Average time per retrieval: ${totalElapsedTime.get() / numberOfEntries.toDouble() * 1e6} ms"
}
} else {
log.error("Skipping retrieval benchmark as it was not possible to insert any entry in the cache")
}

View File

@@ -2,11 +2,11 @@ package net.woggioni.gbcs.cli.impl.commands
import net.woggioni.gbcs.GradleBuildCacheServer
import net.woggioni.gbcs.GradleBuildCacheServer.Companion.DEFAULT_CONFIGURATION_URL
import net.woggioni.gbcs.api.Configuration
import net.woggioni.gbcs.base.contextLogger
import net.woggioni.gbcs.base.debug
import net.woggioni.gbcs.base.info
import net.woggioni.gbcs.cli.impl.GbcsCommand
import net.woggioni.gbcs.client.GbcsClient
import net.woggioni.jwo.Application
import net.woggioni.jwo.JWO
import picocli.CommandLine
@@ -42,8 +42,8 @@ class ServerCommand(app : Application) : GbcsCommand() {
)
private var configurationFile: Path = findConfigurationFile(app, "gbcs-server.xml")
val configuration : GbcsClient.Configuration by lazy {
GbcsClient.Configuration.parse(configurationFile)
val configuration : Configuration by lazy {
GradleBuildCacheServer.loadConfiguration(configurationFile)
}
override fun run() {

View File

@@ -15,8 +15,6 @@
<root level="info">
<appender-ref ref="console"/>
</root>
<logger name="io.netty" level="debug"/>
<logger name="io.netty.handler.ssl.BouncyCastlePemReader" level="info"/>
<logger name="com.google.code.yanf4j" level="warn"/>
<logger name="net.rubyeye.xmemcached" level="warn"/>
</configuration>

View File

@@ -4,6 +4,7 @@ plugins {
}
dependencies {
implementation project(':gbcs-api')
implementation project(':gbcs-base')
implementation catalog.picocli
implementation catalog.slf4j.api

View File

@@ -1,262 +0,0 @@
package net.woggioni.gbcs.benchmark;
import lombok.Getter;
import lombok.SneakyThrows;
import net.woggioni.jwo.Fun;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Level;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.TearDown;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyStore;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
public class Main {
@SneakyThrows
private static Properties loadProperties() {
Properties properties = new Properties();
try (final var is = Main.class.getResourceAsStream("/benchmark.properties")) {
properties.load(is);
}
return properties;
}
private static final Properties properties = loadProperties();
@State(Scope.Thread)
public static class ExecutionPlan {
private final Random random = new Random(101325);
@Getter
private final HttpClient client = createHttpClient();
private final Map<String, byte[]> entries = new HashMap<>();
private HttpClient createHttpClient() {
final var clientBuilder = HttpClient.newBuilder();
getSslContext().ifPresent(clientBuilder::sslContext);
return clientBuilder.build();
}
public final Map<String, byte[]> getEntries() {
return Collections.unmodifiableMap(entries);
}
public Map.Entry<String, byte[]> newEntry() {
final var keyBuffer = new byte[0x20];
random.nextBytes(keyBuffer);
final var key = Base64.getUrlEncoder().encodeToString(keyBuffer);
final var value = new byte[0x1000];
random.nextBytes(value);
return Map.entry(key, value);
}
@SneakyThrows
public HttpRequest.Builder newRequestBuilder(String key) {
final var requestBuilder = HttpRequest.newBuilder()
.uri(getServerURI().resolve(key));
String user = getUser();
if (user != null) {
requestBuilder.header("Authorization", buildAuthorizationHeader(user, getPassword()));
}
return requestBuilder;
}
@SneakyThrows
public URI getServerURI() {
return new URI(properties.getProperty("gbcs.server.url"));
}
@SneakyThrows
public Optional<String> getClientTrustStorePassword() {
return Optional.ofNullable(properties.getProperty("gbcs.client.ssl.truststore.password"))
.filter(Predicate.not(String::isEmpty));
}
@SneakyThrows
public Optional<KeyStore> getClientTrustStore() {
return Optional.ofNullable(properties.getProperty("gbcs.client.ssl.truststore.file"))
.filter(Predicate.not(String::isEmpty))
.map(Path::of)
.map((Fun<Path, KeyStore>) keyStoreFile -> {
final var keyStore = KeyStore.getInstance("PKCS12");
try (final var is = Files.newInputStream(keyStoreFile)) {
keyStore.load(is, getClientTrustStorePassword().map(String::toCharArray).orElse(null));
}
return keyStore;
});
}
@SneakyThrows
public Optional<KeyStore> getClientKeyStore() {
return Optional.ofNullable(properties.getProperty("gbcs.client.ssl.keystore.file"))
.filter(Predicate.not(String::isEmpty))
.map(Path::of)
.map((Fun<Path, KeyStore>) keyStoreFile -> {
final var keyStore = KeyStore.getInstance("PKCS12");
try (final var is = Files.newInputStream(keyStoreFile)) {
keyStore.load(is, getClientKeyStorePassword().map(String::toCharArray).orElse(null));
}
return keyStore;
});
}
@SneakyThrows
public Optional<String> getClientKeyStorePassword() {
return Optional.ofNullable(properties.getProperty("gbcs.client.ssl.keystore.password"))
.filter(Predicate.not(String::isEmpty));
}
@SneakyThrows
public Optional<String> getClientKeyPassword() {
return Optional.ofNullable(properties.getProperty("gbcs.client.ssl.key.password"))
.filter(Predicate.not(String::isEmpty));
}
@SneakyThrows
public String getUser() {
return Optional.ofNullable(properties.getProperty("gbcs.server.username"))
.filter(Predicate.not(String::isEmpty))
.orElse(null);
}
@SneakyThrows
public String getPassword() {
return Optional.ofNullable(properties.getProperty("gbcs.server.password"))
.filter(Predicate.not(String::isEmpty))
.orElse(null);
}
private String buildAuthorizationHeader(String user, String password) {
final var b64 = Base64.getEncoder().encode(String.format("%s:%s", user, password).getBytes(StandardCharsets.UTF_8));
return "Basic " + new String(b64);
}
@SneakyThrows
private Optional<SSLContext> getSslContext() {
return getClientKeyStore().map((Fun<KeyStore, SSLContext>) clientKeyStore -> {
final var kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(clientKeyStore, getClientKeyStorePassword().map(String::toCharArray).orElse(null));
// Set up trust manager factory with the truststore
final var trustManagers = getClientTrustStore().map((Fun<KeyStore, TrustManager[]>) ts -> {
final var tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(ts);
return tmf.getTrustManagers();
}).orElse(new TrustManager[0]);
// Create SSL context with the key and trust managers
final var sslContext = SSLContext.getInstance("TLS");
sslContext.init(kmf.getKeyManagers(), trustManagers, null);
return sslContext;
});
}
@SneakyThrows
@Setup(Level.Trial)
public void setUp() {
final var client = getClient();
for (int i = 0; i < 1000; i++) {
final var pair = newEntry();
final var requestBuilder = newRequestBuilder(pair.getKey())
.header("Content-Type", "application/octet-stream")
.PUT(HttpRequest.BodyPublishers.ofByteArray(pair.getValue()));
final var response = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString());
if (201 != response.statusCode()) {
throw new IllegalStateException(Integer.toString(response.statusCode()));
} else {
entries.put(pair.getKey(), pair.getValue());
}
}
}
@TearDown
public void tearDown() {
client.close();
}
private Iterator<Map.Entry<String, byte[]>> it = null;
private Map.Entry<String, byte[]> nextEntry() {
if (it == null || !it.hasNext()) {
it = getEntries().entrySet().iterator();
}
return it.next();
}
}
@SneakyThrows
@Benchmark
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.SECONDS)
public void get(ExecutionPlan plan) {
final var client = plan.getClient();
final var entry = plan.nextEntry();
final var requestBuilder = plan.newRequestBuilder(entry.getKey())
.header("Accept", "application/octet-stream")
.GET();
final var response = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofByteArray());
if (200 != response.statusCode()) {
throw new IllegalStateException(Integer.toString(response.statusCode()));
} else {
if (!Arrays.equals(entry.getValue(), response.body())) {
throw new IllegalStateException("Retrieved unexpected value");
}
}
}
@SneakyThrows
@Benchmark
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.SECONDS)
public void put(Main.ExecutionPlan plan) {
final var client = plan.getClient();
final var entry = plan.nextEntry();
final var requestBuilder = plan.newRequestBuilder(entry.getKey())
.header("Content-Type", "application/octet-stream")
.PUT(HttpRequest.BodyPublishers.ofByteArray(entry.getValue()));
final var response = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofByteArray());
if (201 != response.statusCode()) {
throw new IllegalStateException(Integer.toString(response.statusCode()));
}
}
}

View File

@@ -1,6 +0,0 @@
gbcs.server.url= https://gbcs.woggioni.net:443
gbcs.client.ssl.keystore.file=conf/woggioni@c962475fa38.p12
gbcs.client.ssl.keystore.password=password
gbcs.client.ssl.key.password=password
gbcs.client.ssl.truststore.file=conf/truststore.pfx
gbcs.client.ssl.truststore.password=password

View File

@@ -7,6 +7,7 @@ module net.woggioni.gbcs.client {
requires io.netty.buffer;
requires java.xml;
requires net.woggioni.gbcs.base;
requires net.woggioni.gbcs.api;
requires io.netty.codec;
requires org.slf4j;

View File

@@ -1,6 +1,8 @@
package net.woggioni.gbcs.client.impl
import net.woggioni.gbcs.api.exception.ConfigurationException
import net.woggioni.gbcs.base.Xml.Companion.asIterable
import net.woggioni.gbcs.base.Xml.Companion.renderAttribute
import net.woggioni.gbcs.client.GbcsClient
import org.w3c.dom.Document
import java.net.URI
@@ -21,17 +23,17 @@ object Parser {
val tagName = child.localName
when (tagName) {
"profile" -> {
val name = child.getAttribute("name")
val uri = child.getAttribute("base-url").let(::URI)
val name = child.renderAttribute("name") ?: throw ConfigurationException("name attribute is required")
val uri = child.renderAttribute("base-url")?.let(::URI) ?: throw ConfigurationException("base-url attribute is required")
var authentication: GbcsClient.Configuration.Authentication? = null
for (gchild in child.asIterable()) {
when (gchild.localName) {
"tls-client-auth" -> {
val keyStoreFile = gchild.getAttribute("key-store-file")
val keyStoreFile = gchild.renderAttribute("key-store-file")
val keyStorePassword =
gchild.getAttribute("key-store-password").takeIf(String::isNotEmpty)
val keyAlias = gchild.getAttribute("key-alias")
val keyPassword = gchild.getAttribute("key-password").takeIf(String::isNotEmpty)
gchild.renderAttribute("key-store-password")
val keyAlias = gchild.renderAttribute("key-alias")
val keyPassword = gchild.renderAttribute("key-password")
val keystore = KeyStore.getInstance("PKCS12").apply {
Files.newInputStream(Path.of(keyStoreFile)).use {
@@ -48,15 +50,14 @@ object Parser {
}
"basic-auth" -> {
val username = gchild.getAttribute("user")
val password = gchild.getAttribute("password")
val username = gchild.renderAttribute("user") ?: throw ConfigurationException("username attribute is required")
val password = gchild.renderAttribute("password") ?: throw ConfigurationException("password attribute is required")
authentication =
GbcsClient.Configuration.Authentication.BasicAuthenticationCredentials(username, password)
}
}
}
val maxConnections = child.getAttribute("max-connections")
.takeIf(String::isNotEmpty)
val maxConnections = child.renderAttribute("max-connections")
?.let(String::toInt)
?: 50
profiles[name] = GbcsClient.Configuration.Profile(uri, authentication, maxConnections)

View File

@@ -23,14 +23,14 @@
</xs:complexType>
<xs:complexType name="basicAuthType">
<xs:attribute name="user" type="xs:string" use="required"/>
<xs:attribute name="user" type="xs:token" use="required"/>
<xs:attribute name="password" type="xs:string" use="required"/>
</xs:complexType>
<xs:complexType name="tlsClientAuthType">
<xs:attribute name="key-store-file" type="xs:anyURI" use="required"/>
<xs:attribute name="key-store-password" type="xs:string" use="required"/>
<xs:attribute name="key-alias" type="xs:string" use="required"/>
<xs:attribute name="key-alias" type="xs:token" use="required"/>
<xs:attribute name="key-password" type="xs:string" use="optional"/>
</xs:complexType>

View File

@@ -2,10 +2,12 @@ package net.woggioni.gbcs.memcached
import net.rubyeye.xmemcached.transcoders.CompressionMode
import net.woggioni.gbcs.api.CacheProvider
import net.woggioni.gbcs.api.exception.ConfigurationException
import net.woggioni.gbcs.base.GBCS
import net.woggioni.gbcs.base.HostAndPort
import net.woggioni.gbcs.base.Xml
import net.woggioni.gbcs.base.Xml.Companion.asIterable
import net.woggioni.gbcs.base.Xml.Companion.renderAttribute
import org.w3c.dom.Document
import org.w3c.dom.Element
import java.time.Duration
@@ -22,20 +24,13 @@ class MemcachedCacheProvider : CacheProvider<MemcachedCacheConfiguration> {
override fun deserialize(el: Element): MemcachedCacheConfiguration {
val servers = mutableListOf<HostAndPort>()
val maxAge = el.getAttribute("max-age")
.takeIf(String::isNotEmpty)
val maxAge = el.renderAttribute("max-age")
?.let(Duration::parse)
?: Duration.ofDays(1)
val maxSize = el.getAttribute("max-size")
.takeIf(String::isNotEmpty)
val maxSize = el.renderAttribute("max-size")
?.let(String::toInt)
?: 0x100000
val enableCompression = el.getAttribute("enable-compression")
.takeIf(String::isNotEmpty)
?.let(String::toBoolean)
?: false
val compressionMode = el.getAttribute("compression-mode")
.takeIf(String::isNotEmpty)
val compressionMode = el.renderAttribute("compression-mode")
?.let {
when (it) {
"gzip" -> CompressionMode.GZIP
@@ -44,11 +39,13 @@ class MemcachedCacheProvider : CacheProvider<MemcachedCacheConfiguration> {
}
}
?: CompressionMode.ZIP
val digestAlgorithm = el.getAttribute("digest").takeIf(String::isNotEmpty)
val digestAlgorithm = el.renderAttribute("digest")
for (child in el.asIterable()) {
when (child.nodeName) {
"server" -> {
servers.add(HostAndPort(child.getAttribute("host"), child.getAttribute("port").toInt()))
val host = child.renderAttribute("host") ?: throw ConfigurationException("host attribute is required")
val port = child.renderAttribute("port")?.toInt() ?: throw ConfigurationException("port attribute is required")
servers.add(HostAndPort(host, port))
}
}
}

View File

@@ -7,7 +7,7 @@
<xs:import schemaLocation="classpath:net/woggioni/gbcs/schema/gbcs.xsd" namespace="urn:net.woggioni.gbcs"/>
<xs:complexType name="memcachedServerType">
<xs:attribute name="host" type="xs:string" use="required"/>
<xs:attribute name="host" type="xs:token" use="required"/>
<xs:attribute name="port" type="xs:positiveInteger" use="required"/>
</xs:complexType>

View File

@@ -507,9 +507,9 @@ class GradleBuildCacheServer(private val cfg: Configuration) {
val DEFAULT_CONFIGURATION_URL by lazy { "classpath:net/woggioni/gbcs/gbcs-default.xml".toUrl() }
fun loadConfiguration(configurationFile: Path): Configuration {
val dbf = Xml.newDocumentBuilderFactory(null)
val db = dbf.newDocumentBuilder()
val doc = Files.newInputStream(configurationFile).use(db::parse)
val doc = Files.newInputStream(configurationFile).use {
Xml.parseXml(configurationFile.toUri().toURL(), it)
}
return Parser.parse(doc)
}

View File

@@ -3,6 +3,7 @@ package net.woggioni.gbcs.cache
import net.woggioni.gbcs.api.CacheProvider
import net.woggioni.gbcs.base.GBCS
import net.woggioni.gbcs.base.Xml
import net.woggioni.gbcs.base.Xml.Companion.renderAttribute
import org.w3c.dom.Document
import org.w3c.dom.Element
import java.nio.file.Path
@@ -18,22 +19,18 @@ class FileSystemCacheProvider : CacheProvider<FileSystemCacheConfiguration> {
override fun getXmlNamespace() = "urn:net.woggioni.gbcs"
override fun deserialize(el: Element): FileSystemCacheConfiguration {
val path = el.getAttribute("path")
.takeIf(String::isNotEmpty)
val path = el.renderAttribute("path")
?.let(Path::of)
val maxAge = el.getAttribute("max-age")
.takeIf(String::isNotEmpty)
val maxAge = el.renderAttribute("max-age")
?.let(Duration::parse)
?: Duration.ofDays(1)
val enableCompression = el.getAttribute("enable-compression")
.takeIf(String::isNotEmpty)
val enableCompression = el.renderAttribute("enable-compression")
?.let(String::toBoolean)
?: true
val compressionLevel = el.getAttribute("compression-level")
.takeIf(String::isNotEmpty)
val compressionLevel = el.renderAttribute("compression-level")
?.let(String::toInt)
?: Deflater.DEFAULT_COMPRESSION
val digestAlgorithm = el.getAttribute("digest").takeIf(String::isNotEmpty) ?: "MD5"
val digestAlgorithm = el.renderAttribute("digest") ?: "MD5"
return FileSystemCacheConfiguration(
path,

View File

@@ -12,14 +12,15 @@ import net.woggioni.gbcs.api.Configuration.TlsCertificateExtractor
import net.woggioni.gbcs.api.Configuration.TrustStore
import net.woggioni.gbcs.api.Configuration.User
import net.woggioni.gbcs.api.Role
import net.woggioni.gbcs.api.exception.ConfigurationException
import net.woggioni.gbcs.base.Xml.Companion.asIterable
import net.woggioni.gbcs.base.Xml.Companion.renderAttribute
import org.w3c.dom.Document
import org.w3c.dom.Element
import org.w3c.dom.TypeInfo
import java.nio.file.Paths
object Parser {
fun parse(document: Document): Configuration {
val root = document.documentElement
val anonymousUser = User("", null, emptySet())
@@ -29,9 +30,8 @@ object Parser {
var users : Map<String, User> = mapOf(anonymousUser.name to anonymousUser)
var groups = emptyMap<String, Group>()
var tls: Tls? = null
val serverPath = root.getAttribute("path")
val useVirtualThread = root.getAttribute("useVirtualThreads")
.takeIf(String::isNotEmpty)
val serverPath = root.renderAttribute("path")
val useVirtualThread = root.renderAttribute("useVirtualThreads")
?.let(String::toBoolean) ?: true
var authentication: Authentication? = null
for (child in root.asIterable()) {
@@ -54,8 +54,8 @@ object Parser {
}
"bind" -> {
host = child.getAttribute("host")
port = Integer.parseInt(child.getAttribute("port"))
host = child.renderAttribute("host") ?: throw ConfigurationException("host attribute is required")
port = Integer.parseInt(child.renderAttribute("port"))
}
"cache" -> {
@@ -80,14 +80,14 @@ object Parser {
for (ggchild in gchild.asIterable()) {
when (ggchild.localName) {
"group-extractor" -> {
val attrName = ggchild.getAttribute("attribute-name")
val pattern = ggchild.getAttribute("pattern")
val attrName = ggchild.renderAttribute("attribute-name")
val pattern = ggchild.renderAttribute("pattern")
tlsExtractorGroup = TlsCertificateExtractor(attrName, pattern)
}
"user-extractor" -> {
val attrName = ggchild.getAttribute("attribute-name")
val pattern = ggchild.getAttribute("pattern")
val attrName = ggchild.renderAttribute("attribute-name")
val pattern = ggchild.renderAttribute("pattern")
tlsExtractorUser = TlsCertificateExtractor(attrName, pattern)
}
}
@@ -99,20 +99,17 @@ object Parser {
}
"tls" -> {
val verifyClients = child.getAttribute("verify-clients")
.takeIf(String::isNotEmpty)
val verifyClients = child.renderAttribute("verify-clients")
?.let(String::toBoolean) ?: false
var keyStore: KeyStore? = null
var trustStore: TrustStore? = null
for (granChild in child.asIterable()) {
when (granChild.localName) {
"keystore" -> {
val keyStoreFile = Paths.get(granChild.getAttribute("file"))
val keyStorePassword = granChild.getAttribute("password")
.takeIf(String::isNotEmpty)
val keyAlias = granChild.getAttribute("key-alias")
val keyPassword = granChild.getAttribute("key-password")
.takeIf(String::isNotEmpty)
val keyStoreFile = Paths.get(granChild.renderAttribute("file"))
val keyStorePassword = granChild.renderAttribute("password")
val keyAlias = granChild.renderAttribute("key-alias")
val keyPassword = granChild.renderAttribute("key-password")
keyStore = KeyStore(
keyStoreFile,
keyStorePassword,
@@ -122,11 +119,9 @@ object Parser {
}
"truststore" -> {
val trustStoreFile = Paths.get(granChild.getAttribute("file"))
val trustStorePassword = granChild.getAttribute("password")
.takeIf(String::isNotEmpty)
val checkCertificateStatus = granChild.getAttribute("check-certificate-status")
.takeIf(String::isNotEmpty)
val trustStoreFile = Paths.get(granChild.renderAttribute("file"))
val trustStorePassword = granChild.renderAttribute("password")
val checkCertificateStatus = granChild.renderAttribute("check-certificate-status")
?.let(String::toBoolean)
?: false
trustStore = TrustStore(
@@ -153,15 +148,15 @@ object Parser {
}.toSet()
private fun parseUserRefs(root: Element) = root.asIterable().asSequence().map {
it.getAttribute("ref")
it.renderAttribute("ref")
}.toSet()
private fun parseUsers(root: Element): Sequence<User> {
return root.asIterable().asSequence().filter {
it.localName == "user"
}.map { el ->
val username = el.getAttribute("name")
val password = el.getAttribute("password").takeIf(String::isNotEmpty)
val username = el.renderAttribute("name")
val password = el.renderAttribute("password")
User(username, password, emptySet())
}
}
@@ -172,7 +167,7 @@ object Parser {
val groups = root.asIterable().asSequence().filter {
it.localName == "group"
}.map { el ->
val groupName = el.getAttribute("name")
val groupName = el.renderAttribute("name") ?: throw ConfigurationException("Group name is required")
var roles = emptySet<Role>()
for (child in el.asIterable()) {
when (child.localName) {

View File

@@ -28,7 +28,7 @@
</xs:complexType>
<xs:complexType name="bindType">
<xs:attribute name="host" type="xs:string" use="required"/>
<xs:attribute name="host" type="xs:token" use="required"/>
<xs:attribute name="port" type="xs:unsignedShort" use="required"/>
</xs:complexType>
@@ -48,14 +48,14 @@
<xs:complexType name="tlsCertificateAuthorizationType">
<xs:sequence>
<xs:element name="group-extractor" type="gbcs:X500NameExtractorType"/>
<xs:element name="user-extractor" type="gbcs:X500NameExtractorType"/>
<xs:element name="group-extractor" type="gbcs:X500NameExtractorType" minOccurs="0"/>
<xs:element name="user-extractor" type="gbcs:X500NameExtractorType" minOccurs="0"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="X500NameExtractorType">
<xs:attribute name="attribute-name" type="xs:string"/>
<xs:attribute name="pattern" type="xs:string"/>
<xs:attribute name="attribute-name" type="xs:token"/>
<xs:attribute name="pattern" type="xs:token"/>
</xs:complexType>
<xs:complexType name="authorizationType">
@@ -85,7 +85,7 @@
</xs:complexType>
<xs:complexType name="userType">
<xs:attribute name="name" type="xs:string" use="required"/>
<xs:attribute name="name" type="xs:token" use="required"/>
<xs:attribute name="password" type="xs:string" use="optional"/>
</xs:complexType>
@@ -105,11 +105,11 @@
</xs:element>
<xs:element name="roles" type="gbcs:rolesType" maxOccurs="1" minOccurs="0"/>
</xs:sequence>
<xs:attribute name="name" type="xs:string"/>
<xs:attribute name="name" type="xs:token"/>
</xs:complexType>
<xs:simpleType name="role" final="restriction" >
<xs:restriction base="xs:string">
<xs:restriction base="xs:token">
<xs:enumeration value="READER" />
<xs:enumeration value="WRITER" />
</xs:restriction>