diff --git a/checkstyle-filter.xml b/checkstyle-filter.xml
index 51488db3..686f51a6 100644
--- a/checkstyle-filter.xml
+++ b/checkstyle-filter.xml
@@ -199,4 +199,7 @@
+
+
diff --git a/com.io7m.idstore.documentation/src/main/resources/com/io7m/idstore/documentation/configuration.xml b/com.io7m.idstore.documentation/src/main/resources/com/io7m/idstore/documentation/configuration.xml
index 531ec781..fe3439ed 100644
--- a/com.io7m.idstore.documentation/src/main/resources/com/io7m/idstore/documentation/configuration.xml
+++ b/com.io7m.idstore.documentation/src/main/resources/com/io7m/idstore/documentation/configuration.xml
@@ -30,42 +30,60 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
+
-
-
+
+
]]>
@@ -131,11 +149,7 @@
The ListenAddress
and ListenPort attributes specify the address and port to which to the HTTP
- service will bind. It is recommended that the service be bound to
- localhost
- and a reverse proxy such as nginx be used to
- provide TLS
- .
+ service will bind.
The ExternalAddress attribute specifies the external address that clients will
@@ -145,6 +159,15 @@
By convention, the Admin API should listen on TCP port 51000.
+
+ The HTTPServiceAdminAPI element must contain either a
+ TLSEnabled or TLSDisabled element specifying
+ whether TLS should be enabled or disabled, respectively. The TLSEnabled
+ element describes the key store and trust store. The idstore server
+ automatically reloads certificates periodically in order to work well in environments using the
+ ACME protocol to
+ issue certificates.
+
@@ -153,11 +176,7 @@
The ListenAddress
and ListenPort attributes specify the address and port to which to the HTTP
- service will bind. It is recommended that the service be bound to
- localhost
- and a reverse proxy such as nginx be used to
- provide TLS
- .
+ service will bind.
The ExternalAddress attribute specifies the external address that clients will
@@ -167,6 +186,15 @@
By convention, the User API should listen on TCP port 50000.
+
+ The HTTPServiceUserAPI element must contain either a
+ TLSEnabled or TLSDisabled element specifying
+ whether TLS should be enabled or disabled, respectively. The TLSEnabled
+ element describes the key store and trust store. The idstore server
+ automatically reloads certificates periodically in order to work well in environments using the
+ ACME protocol to
+ issue certificates.
+
@@ -175,11 +203,7 @@
The ListenAddress
and ListenPort attributes specify the address and port to which to the HTTP
- service will bind. It is recommended that the service be bound to
- localhost
- and a reverse proxy such as nginx be used to
- provide TLS
- .
+ service will bind.
The ExternalAddress attribute specifies the external address that clients will
@@ -189,6 +213,15 @@
By convention, the User API should listen on TCP port 50001.
+
+ The HTTPServiceUserView element must contain either a
+ TLSEnabled or TLSDisabled element specifying
+ whether TLS should be enabled or disabled, respectively. The TLSEnabled
+ element describes the key store and trust store. The idstore server
+ automatically reloads certificates periodically in order to work well in environments using the
+ ACME protocol to
+ issue certificates.
+
@@ -199,13 +232,28 @@
+ ExternalURI="http://localhost:51000/">
+
+
+
+
+
+ ExternalURI="http://localhost:50000/">
+
+
+ ExternalURI="http://localhost:50001/">
+
+
]]>
@@ -646,21 +694,18 @@
The XSD schema for the configuration file is as follows:
-
+
-
+
+
+
+
+
-
- Note: It is extremely important that any reverse proxy used provides the correct
- RFC 7239
- headers in order to tell the idstore server that a reverse proxy is present.
- Otherwise, the idstore server will apply
- rate-limiting decisions to the address of the proxy
- as opposed to the address of the user connecting through the proxy, as intended.
-
-
diff --git a/com.io7m.idstore.main/pom.xml b/com.io7m.idstore.main/pom.xml
index 6e8ee745..6e4378fc 100644
--- a/com.io7m.idstore.main/pom.xml
+++ b/com.io7m.idstore.main/pom.xml
@@ -75,6 +75,14 @@
ch.qos.logback
logback-classic
+
+ com.io7m.anethum
+ com.io7m.anethum.slf4j
+
+
+ com.io7m.anethum
+ com.io7m.anethum.api
+
org.osgi
diff --git a/com.io7m.idstore.main/src/main/java/com/io7m/idstore/main/internal/IdMCmdInitialAdmin.java b/com.io7m.idstore.main/src/main/java/com/io7m/idstore/main/internal/IdMCmdInitialAdmin.java
index 5d2138ac..2ee376cf 100644
--- a/com.io7m.idstore.main/src/main/java/com/io7m/idstore/main/internal/IdMCmdInitialAdmin.java
+++ b/com.io7m.idstore.main/src/main/java/com/io7m/idstore/main/internal/IdMCmdInitialAdmin.java
@@ -16,12 +16,13 @@
package com.io7m.idstore.main.internal;
+import com.io7m.anethum.slf4j.ParseStatusLogging;
import com.io7m.idstore.model.IdEmail;
import com.io7m.idstore.model.IdName;
import com.io7m.idstore.model.IdRealName;
import com.io7m.idstore.server.api.IdServerConfigurations;
import com.io7m.idstore.server.api.IdServerFactoryType;
-import com.io7m.idstore.server.service.configuration.IdServerConfigurationFiles;
+import com.io7m.idstore.server.service.configuration.IdServerConfigurationParsers;
import com.io7m.quarrel.core.QCommandContextType;
import com.io7m.quarrel.core.QCommandMetadata;
import com.io7m.quarrel.core.QCommandStatus;
@@ -30,6 +31,8 @@
import com.io7m.quarrel.core.QParameterNamedType;
import com.io7m.quarrel.core.QStringType.QConstant;
import com.io7m.quarrel.ext.logback.QLogback;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import org.slf4j.bridge.SLF4JBridgeHandler;
import java.nio.file.Path;
@@ -49,6 +52,9 @@
public final class IdMCmdInitialAdmin implements QCommandType
{
+ private static final Logger LOG =
+ LoggerFactory.getLogger(IdMCmdInitialAdmin.class);
+
private static final QParameterNamed1 CONFIGURATION_FILE =
new QParameterNamed1<>(
"--configuration",
@@ -157,9 +163,13 @@ public QCommandStatus onExecute(
final var configurationFile =
context.parameterValue(CONFIGURATION_FILE);
+ final var parsers =
+ new IdServerConfigurationParsers();
final var configFile =
- new IdServerConfigurationFiles()
- .parse(configurationFile);
+ parsers.parseFile(
+ configurationFile,
+ status -> ParseStatusLogging.logWithAll(LOG, status)
+ );
final var configuration =
IdServerConfigurations.ofFile(
diff --git a/com.io7m.idstore.main/src/main/java/com/io7m/idstore/main/internal/IdMCmdServer.java b/com.io7m.idstore.main/src/main/java/com/io7m/idstore/main/internal/IdMCmdServer.java
index eb6e5d0e..bcf6f45f 100644
--- a/com.io7m.idstore.main/src/main/java/com/io7m/idstore/main/internal/IdMCmdServer.java
+++ b/com.io7m.idstore.main/src/main/java/com/io7m/idstore/main/internal/IdMCmdServer.java
@@ -16,9 +16,10 @@
package com.io7m.idstore.main.internal;
+import com.io7m.anethum.slf4j.ParseStatusLogging;
import com.io7m.idstore.server.api.IdServerConfigurations;
import com.io7m.idstore.server.api.IdServerFactoryType;
-import com.io7m.idstore.server.service.configuration.IdServerConfigurationFiles;
+import com.io7m.idstore.server.service.configuration.IdServerConfigurationParsers;
import com.io7m.quarrel.core.QCommandContextType;
import com.io7m.quarrel.core.QCommandMetadata;
import com.io7m.quarrel.core.QCommandStatus;
@@ -27,6 +28,8 @@
import com.io7m.quarrel.core.QParameterNamedType;
import com.io7m.quarrel.core.QStringType;
import com.io7m.quarrel.ext.logback.QLogback;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import org.slf4j.bridge.SLF4JBridgeHandler;
import java.nio.file.Path;
@@ -45,6 +48,9 @@
public final class IdMCmdServer implements QCommandType
{
+ private static final Logger LOG =
+ LoggerFactory.getLogger(IdMCmdServer.class);
+
private static final QParameterNamed1 CONFIGURATION_FILE =
new QParameterNamed1<>(
"--configuration",
@@ -98,9 +104,16 @@ public QCommandStatus onExecute(
QLogback.configure(context);
+ final var configurationFile =
+ context.parameterValue(CONFIGURATION_FILE);
+
+ final var parsers =
+ new IdServerConfigurationParsers();
final var configFile =
- new IdServerConfigurationFiles()
- .parse(context.parameterValue(CONFIGURATION_FILE));
+ parsers.parseFile(
+ configurationFile,
+ status -> ParseStatusLogging.logWithAll(LOG, status)
+ );
final var configuration =
IdServerConfigurations.ofFile(
diff --git a/com.io7m.idstore.main/src/main/java/module-info.java b/com.io7m.idstore.main/src/main/java/module-info.java
index 059182bb..1358f21f 100644
--- a/com.io7m.idstore.main/src/main/java/module-info.java
+++ b/com.io7m.idstore.main/src/main/java/module-info.java
@@ -56,6 +56,8 @@
requires com.io7m.idstore.shell.admin;
requires com.io7m.idstore.strings;
+ requires com.io7m.anethum.api;
+ requires com.io7m.anethum.slf4j;
requires com.io7m.quarrel.core;
requires com.io7m.quarrel.ext.logback;
requires com.io7m.repetoir.core;
diff --git a/com.io7m.idstore.server.admin_v1/pom.xml b/com.io7m.idstore.server.admin_v1/pom.xml
index 22b2f2f8..27606727 100644
--- a/com.io7m.idstore.server.admin_v1/pom.xml
+++ b/com.io7m.idstore.server.admin_v1/pom.xml
@@ -86,6 +86,11 @@
com.io7m.idstore.server.service.health
${project.version}
+
+ ${project.groupId}
+ com.io7m.idstore.server.service.tls
+ ${project.version}
+
${project.groupId}
com.io7m.idstore.model
@@ -111,6 +116,11 @@
com.io7m.idstore.strings
${project.version}
+
+ ${project.groupId}
+ com.io7m.idstore.tls
+ ${project.version}
+
com.io7m.jxtrand
@@ -136,6 +146,10 @@
io.helidon.common
helidon-common-parameters
+
+ io.helidon.common
+ helidon-common-tls
+
org.slf4j
slf4j-api
diff --git a/com.io7m.idstore.server.admin_v1/src/main/java/com/io7m/idstore/server/admin_v1/IdA1Server.java b/com.io7m.idstore.server.admin_v1/src/main/java/com/io7m/idstore/server/admin_v1/IdA1Server.java
index 9e25c1f7..fd383889 100644
--- a/com.io7m.idstore.server.admin_v1/src/main/java/com/io7m/idstore/server/admin_v1/IdA1Server.java
+++ b/com.io7m.idstore.server.admin_v1/src/main/java/com/io7m/idstore/server/admin_v1/IdA1Server.java
@@ -22,7 +22,10 @@
import com.io7m.idstore.server.service.clock.IdServerClock;
import com.io7m.idstore.server.service.configuration.IdServerConfigurationService;
import com.io7m.idstore.server.service.telemetry.api.IdMetricsServiceType;
+import com.io7m.idstore.server.service.tls.IdTLSContextServiceType;
+import com.io7m.idstore.tls.IdTLSEnabled;
import com.io7m.repetoir.core.RPServiceDirectoryType;
+import io.helidon.common.tls.TlsConfig;
import io.helidon.webserver.WebServer;
import io.helidon.webserver.WebServerConfig;
import io.helidon.webserver.http.HttpRouting;
@@ -67,6 +70,8 @@ public static WebServer createAdminAPIServer(
{
final var configurationService =
services.requireService(IdServerConfigurationService.class);
+ final var tlsService =
+ services.requireService(IdTLSContextServiceType.class);
final var configuration =
configurationService.configuration();
final var httpConfig =
@@ -94,8 +99,27 @@ public static WebServer createAdminAPIServer(
new IdA1HandlerHealth(services))
.build();
+ final var webServerBuilder =
+ WebServerConfig.builder();
+
+ if (httpConfig.tlsConfiguration() instanceof final IdTLSEnabled enabled) {
+ final var tlsContext =
+ tlsService.create(
+ "UserAPI",
+ enabled.keyStore(),
+ enabled.trustStore()
+ );
+
+ webServerBuilder.tls(
+ TlsConfig.builder()
+ .enabled(true)
+ .sslContext(tlsContext.context())
+ .build()
+ );
+ }
+
final var webServer =
- WebServerConfig.builder()
+ webServerBuilder
.port(httpConfig.listenPort())
.address(InetAddress.getByName(httpConfig.listenAddress()))
.routing(routing)
diff --git a/com.io7m.idstore.server.admin_v1/src/main/java/module-info.java b/com.io7m.idstore.server.admin_v1/src/main/java/module-info.java
index 6d3b140f..5af7f05e 100644
--- a/com.io7m.idstore.server.admin_v1/src/main/java/module-info.java
+++ b/com.io7m.idstore.server.admin_v1/src/main/java/module-info.java
@@ -39,8 +39,10 @@
requires com.io7m.idstore.server.service.reqlimit;
requires com.io7m.idstore.server.service.sessions;
requires com.io7m.idstore.server.service.telemetry.api;
+ requires com.io7m.idstore.server.service.tls;
requires com.io7m.idstore.server.service.verdant;
requires com.io7m.idstore.strings;
+ requires com.io7m.idstore.tls;
requires com.io7m.verdant.core;
requires io.helidon.webserver;
diff --git a/com.io7m.idstore.server.api/pom.xml b/com.io7m.idstore.server.api/pom.xml
index 797e0118..570a4d85 100644
--- a/com.io7m.idstore.server.api/pom.xml
+++ b/com.io7m.idstore.server.api/pom.xml
@@ -39,6 +39,11 @@
com.io7m.idstore.strings
${project.version}
+
+ ${project.groupId}
+ com.io7m.idstore.tls
+ ${project.version}
+
com.io7m.cxbutton
diff --git a/com.io7m.idstore.server.api/src/main/java/com/io7m/idstore/server/api/IdColor.java b/com.io7m.idstore.server.api/src/main/java/com/io7m/idstore/server/api/IdColor.java
index fb5794ed..53bfa0e9 100644
--- a/com.io7m.idstore.server.api/src/main/java/com/io7m/idstore/server/api/IdColor.java
+++ b/com.io7m.idstore.server.api/src/main/java/com/io7m/idstore/server/api/IdColor.java
@@ -64,6 +64,25 @@ public String toString()
);
}
+ @Override
+ public boolean equals(final Object o)
+ {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || !this.getClass().equals(o.getClass())) {
+ return false;
+ }
+ final IdColor idColor = (IdColor) o;
+ return this.toString().equals(idColor.toString());
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return this.toString().hashCode();
+ }
+
/**
* Scale this color by the given factor. Factors less than 1.0 make the color
* darker. Factors greater than 1.0 make the color lighter.
diff --git a/com.io7m.idstore.server.api/src/main/java/com/io7m/idstore/server/api/IdServerHTTPServiceConfiguration.java b/com.io7m.idstore.server.api/src/main/java/com/io7m/idstore/server/api/IdServerHTTPServiceConfiguration.java
index 09d98001..52990969 100644
--- a/com.io7m.idstore.server.api/src/main/java/com/io7m/idstore/server/api/IdServerHTTPServiceConfiguration.java
+++ b/com.io7m.idstore.server.api/src/main/java/com/io7m/idstore/server/api/IdServerHTTPServiceConfiguration.java
@@ -16,34 +16,40 @@
package com.io7m.idstore.server.api;
+import com.io7m.idstore.tls.IdTLSConfigurationType;
+
import java.net.URI;
import java.util.Objects;
/**
* Configuration for individual HTTP services.
*
- * @param listenAddress The listen address
- * @param listenPort The listen port
- * @param externalAddress The externally visible address
+ * @param listenAddress The listen address
+ * @param listenPort The listen port
+ * @param externalAddress The externally visible address
+ * @param tlsConfiguration The TLS configuration
*/
public record IdServerHTTPServiceConfiguration(
String listenAddress,
int listenPort,
- URI externalAddress)
+ URI externalAddress,
+ IdTLSConfigurationType tlsConfiguration)
implements IdServerJSONConfigurationElementType
{
/**
* Configuration for the part of the server that serves over HTTP.
*
- * @param listenAddress The listen address
- * @param listenPort The listen port
- * @param externalAddress The externally visible address
+ * @param listenAddress The listen address
+ * @param listenPort The listen port
+ * @param externalAddress The externally visible address
+ * @param tlsConfiguration The TLS configuration
*/
public IdServerHTTPServiceConfiguration
{
Objects.requireNonNull(listenAddress, "listenAddress");
Objects.requireNonNull(externalAddress, "externalAddress");
+ Objects.requireNonNull(tlsConfiguration, "tlsConfiguration");
}
}
diff --git a/com.io7m.idstore.server.api/src/main/java/module-info.java b/com.io7m.idstore.server.api/src/main/java/module-info.java
index a5243cd9..9e5a9728 100644
--- a/com.io7m.idstore.server.api/src/main/java/module-info.java
+++ b/com.io7m.idstore.server.api/src/main/java/module-info.java
@@ -24,8 +24,9 @@
requires static org.osgi.annotation.versioning;
requires com.io7m.idstore.database.api;
- requires com.io7m.idstore.strings;
requires com.io7m.idstore.model;
+ requires com.io7m.idstore.strings;
+ requires com.io7m.idstore.tls;
requires com.io7m.cxbutton.core;
diff --git a/com.io7m.idstore.server.service.configuration/pom.xml b/com.io7m.idstore.server.service.configuration/pom.xml
index b2960c00..1b80ec28 100644
--- a/com.io7m.idstore.server.service.configuration/pom.xml
+++ b/com.io7m.idstore.server.service.configuration/pom.xml
@@ -31,31 +31,42 @@
${project.groupId}
- com.io7m.idstore.database.api
+ com.io7m.idstore.server.service.telemetry.api
${project.version}
${project.groupId}
- com.io7m.idstore.server.service.telemetry.api
+ com.io7m.idstore.tls
${project.version}
- com.io7m.repetoir
- com.io7m.repetoir.core
+ com.io7m.jxe
+ com.io7m.jxe.core
- com.io7m.cxbutton
- com.io7m.cxbutton.core
+ com.io7m.jlexing
+ com.io7m.jlexing.core
-
- jakarta.xml.bind
- jakarta.xml.bind-api
+ com.io7m.anethum
+ com.io7m.anethum.api
- com.sun.xml.bind
- jaxb-impl
+ com.io7m.blackthorne
+ com.io7m.blackthorne.core
+
+
+ com.io7m.blackthorne
+ com.io7m.blackthorne.jxe
+
+
+ com.io7m.repetoir
+ com.io7m.repetoir.core
+
+
+ com.io7m.cxbutton
+ com.io7m.cxbutton.core
@@ -70,40 +81,4 @@
-
-
-
-
- org.apache.maven.plugins
- maven-dependency-plugin
-
- true
-
- com.sun.xml.bind:jaxb-impl:*
-
-
-
-
-
- org.codehaus.mojo
- jaxb2-maven-plugin
-
-
- generate-java
- generate-sources
-
- xjc
-
-
- com.io7m.idstore.server.service.configuration.jaxb
-
-
-
-
-
-
-
-
-
-
diff --git a/com.io7m.idstore.server.service.configuration/src/main/java/com/io7m/idstore/server/service/configuration/IdServerConfigurationFiles.java b/com.io7m.idstore.server.service.configuration/src/main/java/com/io7m/idstore/server/service/configuration/IdServerConfigurationFiles.java
deleted file mode 100644
index 9a813b3a..00000000
--- a/com.io7m.idstore.server.service.configuration/src/main/java/com/io7m/idstore/server/service/configuration/IdServerConfigurationFiles.java
+++ /dev/null
@@ -1,976 +0,0 @@
-/*
- * Copyright © 2023 Mark Raynsford https://www.io7m.com
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
- * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
- * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-package com.io7m.idstore.server.service.configuration;
-
-import com.io7m.cxbutton.core.CxButtonColors;
-import com.io7m.cxbutton.core.CxButtonStateColors;
-import com.io7m.cxbutton.core.CxColor;
-import com.io7m.idstore.database.api.IdDatabaseConfiguration;
-import com.io7m.idstore.database.api.IdDatabaseCreate;
-import com.io7m.idstore.database.api.IdDatabaseUpgrade;
-import com.io7m.idstore.server.api.IdColor;
-import com.io7m.idstore.server.api.IdServerBrandingConfiguration;
-import com.io7m.idstore.server.api.IdServerColorScheme;
-import com.io7m.idstore.server.api.IdServerConfiguration;
-import com.io7m.idstore.server.api.IdServerConfigurationFile;
-import com.io7m.idstore.server.api.IdServerDatabaseConfiguration;
-import com.io7m.idstore.server.api.IdServerDatabaseKind;
-import com.io7m.idstore.server.api.IdServerHTTPConfiguration;
-import com.io7m.idstore.server.api.IdServerHTTPServiceConfiguration;
-import com.io7m.idstore.server.api.IdServerHistoryConfiguration;
-import com.io7m.idstore.server.api.IdServerMailAuthenticationConfiguration;
-import com.io7m.idstore.server.api.IdServerMailConfiguration;
-import com.io7m.idstore.server.api.IdServerMailTransportConfigurationType;
-import com.io7m.idstore.server.api.IdServerMailTransportSMTP;
-import com.io7m.idstore.server.api.IdServerMailTransportSMTPS;
-import com.io7m.idstore.server.api.IdServerMailTransportSMTP_TLS;
-import com.io7m.idstore.server.api.IdServerOpenTelemetryConfiguration;
-import com.io7m.idstore.server.api.IdServerOpenTelemetryConfiguration.IdLogs;
-import com.io7m.idstore.server.api.IdServerOpenTelemetryConfiguration.IdMetrics;
-import com.io7m.idstore.server.api.IdServerOpenTelemetryConfiguration.IdOTLPProtocol;
-import com.io7m.idstore.server.api.IdServerOpenTelemetryConfiguration.IdTraces;
-import com.io7m.idstore.server.api.IdServerPasswordExpirationConfiguration;
-import com.io7m.idstore.server.api.IdServerRateLimitConfiguration;
-import com.io7m.idstore.server.api.IdServerSessionConfiguration;
-import com.io7m.idstore.server.service.configuration.jaxb.Branding;
-import com.io7m.idstore.server.service.configuration.jaxb.ButtonColors;
-import com.io7m.idstore.server.service.configuration.jaxb.ButtonStateColors;
-import com.io7m.idstore.server.service.configuration.jaxb.ColorScheme;
-import com.io7m.idstore.server.service.configuration.jaxb.ColorType;
-import com.io7m.idstore.server.service.configuration.jaxb.Configuration;
-import com.io7m.idstore.server.service.configuration.jaxb.Database;
-import com.io7m.idstore.server.service.configuration.jaxb.HTTPServiceType;
-import com.io7m.idstore.server.service.configuration.jaxb.HTTPServices;
-import com.io7m.idstore.server.service.configuration.jaxb.History;
-import com.io7m.idstore.server.service.configuration.jaxb.Logs;
-import com.io7m.idstore.server.service.configuration.jaxb.Mail;
-import com.io7m.idstore.server.service.configuration.jaxb.MailAuthentication;
-import com.io7m.idstore.server.service.configuration.jaxb.Metrics;
-import com.io7m.idstore.server.service.configuration.jaxb.OpenTelemetry;
-import com.io7m.idstore.server.service.configuration.jaxb.OpenTelemetryProtocol;
-import com.io7m.idstore.server.service.configuration.jaxb.PasswordExpiration;
-import com.io7m.idstore.server.service.configuration.jaxb.RateLimiting;
-import com.io7m.idstore.server.service.configuration.jaxb.SMTPSType;
-import com.io7m.idstore.server.service.configuration.jaxb.SMTPTLSType;
-import com.io7m.idstore.server.service.configuration.jaxb.SMTPType;
-import com.io7m.idstore.server.service.configuration.jaxb.Sessions;
-import com.io7m.idstore.server.service.configuration.jaxb.Traces;
-import com.io7m.repetoir.core.RPServiceType;
-import jakarta.xml.bind.JAXBContext;
-import jakarta.xml.bind.JAXBException;
-import org.xml.sax.SAXException;
-
-import javax.xml.XMLConstants;
-import javax.xml.datatype.DatatypeFactory;
-import javax.xml.transform.stream.StreamSource;
-import javax.xml.validation.Schema;
-import javax.xml.validation.SchemaFactory;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.time.Duration;
-import java.util.List;
-import java.util.Locale;
-import java.util.Objects;
-import java.util.Optional;
-
-import static com.io7m.idstore.server.api.IdServerDatabaseKind.POSTGRESQL;
-
-/**
- * The configuration file parser.
- */
-
-public final class IdServerConfigurationFiles
- implements RPServiceType
-{
- /**
- * The Public API v1 message protocol.
- */
-
- public IdServerConfigurationFiles()
- {
-
- }
-
- /**
- * Parse a configuration file.
- *
- * @param source The source URI
- * @param stream The input stream
- *
- * @return The file
- *
- * @throws IOException On errors
- */
-
- public IdServerConfigurationFile parse(
- final URI source,
- final InputStream stream)
- throws IOException
- {
- Objects.requireNonNull(source, "source");
- Objects.requireNonNull(stream, "stream");
-
- try {
- final Schema schema =
- createSchema();
- final var context =
- JAXBContext.newInstance(
- "com.io7m.idstore.server.service.configuration.jaxb");
- final var unmarshaller =
- context.createUnmarshaller();
-
- unmarshaller.setSchema(schema);
-
- final var streamSource =
- new StreamSource(stream, source.toString());
-
- return parseConfiguration(
- (Configuration) unmarshaller.unmarshal(streamSource)
- );
- } catch (final SAXException | JAXBException | URISyntaxException e) {
- throw new IOException(e);
- }
- }
-
- private static Schema createSchema()
- throws SAXException
- {
- final var schemas =
- SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
- return schemas.newSchema(
- IdServerConfigurationFiles.class.getResource(
- "/com/io7m/idstore/server/service/configuration/configuration.xsd")
- );
- }
-
- /**
- * Serialize a configuration file.
- *
- * @param output The output stream
- * @param configuration The configuration
- *
- * @throws IOException On errors
- */
-
- public void serialize(
- final OutputStream output,
- final IdServerConfiguration configuration)
- throws IOException
- {
- Objects.requireNonNull(output, "output");
- Objects.requireNonNull(configuration, "configuration");
-
- try {
- final Schema schema =
- createSchema();
-
- final var context =
- JAXBContext.newInstance(
- "com.io7m.idstore.server.service.configuration.jaxb");
- final var marshaller =
- context.createMarshaller();
- final var types =
- DatatypeFactory.newInstance();
-
- marshaller.setSchema(schema);
- marshaller.marshal(serializeConfiguration(types, configuration), output);
- } catch (final Exception e) {
- throw new IOException(e);
- }
- }
-
- private static Configuration serializeConfiguration(
- final DatatypeFactory types,
- final IdServerConfiguration configuration)
- {
- final var c = new Configuration();
- c.setBranding(
- serializeBranding(configuration.branding()));
- c.setMail(
- serializeMail(types, configuration.mailConfiguration()));
- c.setHTTPServices(
- serializeHTTP(
- configuration.adminApiAddress(),
- configuration.userApiAddress(),
- configuration.userViewAddress())
- );
- c.setDatabase(
- serializeDatabase(configuration.databaseConfiguration()));
- c.setHistory(
- serializeHistory(configuration.history()));
- c.setSessions(
- serializeSessions(types, configuration.sessions()));
- c.setRateLimiting(
- serializeRateLimiting(types, configuration.rateLimit()));
- c.setPasswordExpiration(
- serializePasswordExpiration(types, configuration.passwordExpiration()));
- c.setOpenTelemetry(
- serializeOpenTelemetry(configuration.openTelemetry()));
- return c;
- }
-
- private static OpenTelemetry serializeOpenTelemetry(
- final Optional c)
- {
- if (c.isEmpty()) {
- return null;
- }
-
- final var cc = c.get();
- final var r = new OpenTelemetry();
-
- r.setTraces(
- cc.traces()
- .map(IdServerConfigurationFiles::serializeOpenTelemetryTraces)
- .orElse(null)
- );
- r.setMetrics(
- cc.metrics()
- .map(IdServerConfigurationFiles::serializeOpenTelemetryMetrics)
- .orElse(null)
- );
- r.setLogs(
- cc.logs()
- .map(IdServerConfigurationFiles::serializeOpenTelemetryLogs)
- .orElse(null)
- );
- r.setLogicalServiceName(cc.logicalServiceName());
- return r;
- }
-
- private static Logs serializeOpenTelemetryLogs(
- final IdLogs c)
- {
- final var r = new Logs();
- r.setEndpoint(c.endpoint().toString());
- r.setProtocol(serializeOTProtocol(c.protocol()));
- return r;
- }
-
- private static Metrics serializeOpenTelemetryMetrics(
- final IdMetrics c)
- {
- final var r = new Metrics();
- r.setEndpoint(c.endpoint().toString());
- r.setProtocol(serializeOTProtocol(c.protocol()));
- return r;
- }
-
- private static Traces serializeOpenTelemetryTraces(
- final IdTraces c)
- {
- final var r = new Traces();
- r.setEndpoint(c.endpoint().toString());
- r.setProtocol(serializeOTProtocol(c.protocol()));
- return r;
- }
-
- private static OpenTelemetryProtocol serializeOTProtocol(
- final IdOTLPProtocol protocol)
- {
- return switch (protocol) {
- case GRPC -> OpenTelemetryProtocol.GRPC;
- case HTTP -> OpenTelemetryProtocol.HTTP;
- };
- }
-
- private static PasswordExpiration serializePasswordExpiration(
- final DatatypeFactory types,
- final IdServerPasswordExpirationConfiguration c)
- {
- final var r = new PasswordExpiration();
- r.setAdminPasswordValidityDuration(
- c.adminPasswordValidityDuration()
- .map(d -> serializeDuration(types, d))
- .orElse(null)
- );
- r.setUserPasswordValidityDuration(
- c.userPasswordValidityDuration()
- .map(d -> serializeDuration(types, d))
- .orElse(null)
- );
- return r;
- }
-
- private static RateLimiting serializeRateLimiting(
- final DatatypeFactory types,
- final IdServerRateLimitConfiguration c)
- {
- final var r = new RateLimiting();
- r.setAdminLoginDelay(
- serializeDuration(types, c.adminLoginDelay()));
- r.setAdminLoginRateLimit(
- serializeDuration(types, c.adminLoginRateLimit()));
- r.setEmailVerificationRateLimit(
- serializeDuration(types, c.emailVerificationRateLimit()));
- r.setPasswordResetRateLimit(
- serializeDuration(types, c.passwordResetRateLimit()));
- r.setUserLoginRateLimit(
- serializeDuration(types, c.userLoginRateLimit()));
- r.setUserLoginDelay(
- serializeDuration(types, c.userLoginDelay()));
- return r;
- }
-
- private static Sessions serializeSessions(
- final DatatypeFactory types,
- final IdServerSessionConfiguration s)
- {
- final var r = new Sessions();
- r.setAdminSessionExpiration(
- serializeDuration(types, s.adminSessionExpiration()));
- r.setUserSessionExpiration(
- serializeDuration(types, s.userSessionExpiration()));
- return r;
- }
-
- private static History serializeHistory(
- final IdServerHistoryConfiguration history)
- {
- final var r = new History();
- r.setAdminLoginHistoryLimit(history.adminLoginHistoryLimit());
- r.setUserLoginHistoryLimit(history.userLoginHistoryLimit());
- return r;
- }
-
- private static Database serializeDatabase(
- final IdDatabaseConfiguration c)
- {
- final var r = new Database();
- r.setAddress(c.address());
- r.setCreate(c.create() == IdDatabaseCreate.CREATE_DATABASE);
- r.setKind("POSTGRESQL");
- r.setName(c.databaseName());
- r.setOwnerRoleName(c.ownerRoleName());
- r.setOwnerRolePassword(c.ownerRolePassword());
- r.setPort(c.port());
- r.setReaderRolePassword(c.readerRolePassword().orElse(null));
- r.setUpgrade(c.upgrade() == IdDatabaseUpgrade.UPGRADE_DATABASE);
- r.setWorkerRolePassword(c.workerRolePassword());
- return r;
- }
-
- private static HTTPServices serializeHTTP(
- final IdServerHTTPServiceConfiguration adminAPI,
- final IdServerHTTPServiceConfiguration userAPI,
- final IdServerHTTPServiceConfiguration userView)
- {
- final var r = new HTTPServices();
- r.setHTTPServiceAdminAPI(serializeHTTPService(adminAPI));
- r.setHTTPServiceUserAPI(serializeHTTPService(userAPI));
- r.setHTTPServiceUserView(serializeHTTPService(userView));
- return r;
- }
-
- private static HTTPServiceType serializeHTTPService(
- final IdServerHTTPServiceConfiguration s)
- {
- final var r = new HTTPServiceType();
- r.setExternalURI(s.externalAddress().toString());
- r.setListenAddress(s.listenAddress());
- r.setListenPort(s.listenPort());
- return r;
- }
-
- private static Mail serializeMail(
- final DatatypeFactory types,
- final IdServerMailConfiguration c)
- {
- final var m = new Mail();
-
- final var t = c.transportConfiguration();
- if (t instanceof final IdServerMailTransportSMTP ts) {
- m.setSMTP(serializeMailSMTP(ts));
- } else if (t instanceof final IdServerMailTransportSMTPS ts) {
- m.setSMTPS(serializeMailSMTPS(ts));
- } else if (t instanceof final IdServerMailTransportSMTP_TLS ts) {
- m.setSMTPTLS(serializeMailSMTPTLS(ts));
- }
- m.setSenderAddress(
- c.senderAddress());
- m.setVerificationExpiration(
- serializeDuration(types, c.verificationExpiration()));
- m.setMailAuthentication(
- serializeMailAuthentication(c.authenticationConfiguration()));
-
- return m;
- }
-
- private static MailAuthentication serializeMailAuthentication(
- final Optional auth)
- {
- if (auth.isEmpty()) {
- return null;
- }
-
- final var a = auth.get();
- final var r = new MailAuthentication();
- r.setUsername(a.userName());
- r.setPassword(a.password());
- return r;
- }
-
- private static javax.xml.datatype.Duration serializeDuration(
- final DatatypeFactory types,
- final Duration duration)
- {
- return types.newDuration(duration.toString());
- }
-
- private static SMTPType serializeMailSMTP(
- final IdServerMailTransportSMTP ts)
- {
- final var s = new SMTPType();
- s.setHost(ts.host());
- s.setPort(ts.port());
- return s;
- }
-
- private static SMTPSType serializeMailSMTPS(
- final IdServerMailTransportSMTPS ts)
- {
- final var s = new SMTPSType();
- s.setHost(ts.host());
- s.setPort(ts.port());
- return s;
- }
-
- private static SMTPTLSType serializeMailSMTPTLS(
- final IdServerMailTransportSMTP_TLS ts)
- {
- final var s = new SMTPTLSType();
- s.setHost(ts.host());
- s.setPort(ts.port());
- return s;
- }
-
- private static Branding serializeBranding(
- final IdServerBrandingConfiguration branding)
- {
- final var b = new Branding();
- b.setColorScheme(
- serializeBrandingColorScheme(branding.scheme()));
- b.setLogo(
- serializeBrandingLogo(branding.logo()));
- b.setLoginExtra(
- serializeBrandingLoginExtra(branding.loginExtra()));
- b.setProductTitle(
- serializeBrandingProductTitle(branding.productTitle()));
- return b;
- }
-
- private static ColorScheme serializeBrandingColorScheme(
- final Optional scheme)
- {
- if (scheme.isEmpty()) {
- return null;
- }
-
- final var s = scheme.get();
- final var c = new ColorScheme();
- c.setButtonColors(
- serializeBrandingButtonColors(s.buttonColors()));
- c.setErrorBorderColor(
- serializeColorType(s.errorBorderColor()));
-
- c.setHeaderBackgroundColor(
- serializeColorType(s.headerBackgroundColor()));
- c.setHeaderLinkColor(
- serializeColorType(s.headerLinkColor()));
- c.setHeaderBackgroundColor(
- serializeColorType(s.headerBackgroundColor()));
- c.setHeaderTextColor(
- serializeColorType(s.headerTextColor()));
-
- c.setMainBackgroundColor(
- serializeColorType(s.mainBackgroundColor()));
- c.setMainLinkColor(
- serializeColorType(s.mainLinkColor()));
- c.setMainBackgroundColor(
- serializeColorType(s.mainBackgroundColor()));
- c.setMainTextColor(
- serializeColorType(s.mainTextColor()));
- c.setMainTableBorderColor(
- serializeColorType(s.mainTableBorderColor()));
- c.setMainMessageBorderColor(
- serializeColorType(s.mainMessageBorderColor()));
-
- return c;
- }
-
- private static ButtonColors serializeBrandingButtonColors(
- final CxButtonColors cxButtonColors)
- {
- final var c = new ButtonColors();
-
- c.setDisabled(
- serializeBrandingButtonStateColors(cxButtonColors.disabled()));
- c.setEnabled(
- serializeBrandingButtonStateColors(cxButtonColors.enabled()));
- c.setHover(
- serializeBrandingButtonStateColors(cxButtonColors.hover()));
- c.setPressed(
- serializeBrandingButtonStateColors(cxButtonColors.pressed()));
-
- return c;
- }
-
- private static ButtonStateColors serializeBrandingButtonStateColors(
- final CxButtonStateColors s)
- {
- final var c = new ButtonStateColors();
- c.setBodyColor(serializeCxColorType(s.bodyColor()));
- c.setBorderColor(serializeCxColorType(s.borderColor()));
- c.setEmbossEColor(serializeCxColorType(s.embossEColor()));
- c.setEmbossNColor(serializeCxColorType(s.embossNColor()));
- c.setEmbossWColor(serializeCxColorType(s.embossWColor()));
- c.setEmbossSColor(serializeCxColorType(s.embossSColor()));
- c.setTextColor(serializeCxColorType(s.textColor()));
- return c;
- }
-
- private static ColorType serializeCxColorType(
- final CxColor cxColor)
- {
- final var c = new ColorType();
- c.setRed(cxColor.red());
- c.setGreen(cxColor.green());
- c.setBlue(cxColor.blue());
- return c;
- }
-
- private static ColorType serializeColorType(
- final IdColor idColor)
- {
- final var c = new ColorType();
- c.setRed(idColor.red());
- c.setGreen(idColor.green());
- c.setBlue(idColor.blue());
- return c;
- }
-
- private static String serializeBrandingLogo(
- final Optional logo)
- {
- return logo.map(Path::toString).orElse(null);
- }
-
- private static String serializeBrandingLoginExtra(
- final Optional path)
- {
- return path.map(Path::toString).orElse(null);
- }
-
- private static String serializeBrandingProductTitle(
- final String s)
- {
- return s;
- }
-
- private static IdServerConfigurationFile parseConfiguration(
- final Configuration input)
- throws URISyntaxException
- {
- return new IdServerConfigurationFile(
- parseBranding(input.getBranding()),
- parseMail(input.getMail()),
- parseHTTP(input.getHTTPServices()),
- parseDatabase(input.getDatabase()),
- parseHistory(input.getHistory()),
- parseSessions(input.getSessions()),
- parseRateLimit(input.getRateLimiting()),
- parsePasswordExpiration(input.getPasswordExpiration()),
- parseOpenTelemetry(input.getOpenTelemetry())
- );
- }
-
- private static IdServerPasswordExpirationConfiguration parsePasswordExpiration(
- final PasswordExpiration passwordExpiration)
- {
- if (passwordExpiration == null) {
- return new IdServerPasswordExpirationConfiguration(
- Optional.empty(),
- Optional.empty()
- );
- }
-
- return new IdServerPasswordExpirationConfiguration(
- Optional.ofNullable(passwordExpiration.getUserPasswordValidityDuration())
- .map(IdServerConfigurationFiles::parseDuration),
- Optional.ofNullable(passwordExpiration.getAdminPasswordValidityDuration())
- .map(IdServerConfigurationFiles::parseDuration)
- );
- }
-
- private static Optional parseOpenTelemetry(
- final OpenTelemetry openTelemetry)
- {
- if (openTelemetry == null) {
- return Optional.empty();
- }
-
- final var metrics =
- Optional.ofNullable(openTelemetry.getMetrics())
- .map(m -> new IdMetrics(
- URI.create(m.getEndpoint()),
- parseProtocol(m.getProtocol())
- ));
-
- final var traces =
- Optional.ofNullable(openTelemetry.getTraces())
- .map(m -> new IdTraces(
- URI.create(m.getEndpoint()),
- parseProtocol(m.getProtocol())
- ));
-
- final var logs =
- Optional.ofNullable(openTelemetry.getLogs())
- .map(m -> new IdLogs(
- URI.create(m.getEndpoint()),
- parseProtocol(m.getProtocol())
- ));
-
- return Optional.of(
- new IdServerOpenTelemetryConfiguration(
- openTelemetry.getLogicalServiceName(),
- logs,
- metrics,
- traces
- )
- );
- }
-
- private static IdOTLPProtocol parseProtocol(
- final OpenTelemetryProtocol protocol)
- {
- return switch (protocol) {
- case GRPC -> IdOTLPProtocol.GRPC;
- case HTTP -> IdOTLPProtocol.HTTP;
- };
- }
-
- private static IdServerRateLimitConfiguration parseRateLimit(
- final RateLimiting rateLimiting)
- {
- return new IdServerRateLimitConfiguration(
- parseDuration(
- rateLimiting.getEmailVerificationRateLimit()),
- parseDuration(
- rateLimiting.getPasswordResetRateLimit()),
- parseDurationOrDefault(
- rateLimiting.getUserLoginRateLimit(),
- Duration.ofSeconds(5L)
- ),
- parseDurationOrDefault(
- rateLimiting.getUserLoginDelay(),
- Duration.ofSeconds(1L)
- ),
- parseDurationOrDefault(
- rateLimiting.getAdminLoginRateLimit(),
- Duration.ofSeconds(5L)
- ),
- parseDurationOrDefault(
- rateLimiting.getAdminLoginDelay(),
- Duration.ofSeconds(1L)
- )
- );
- }
-
- private static Duration parseDuration(
- final javax.xml.datatype.Duration duration)
- {
- return Duration.parse(duration.toString());
- }
-
- private static Duration parseDurationOrDefault(
- final javax.xml.datatype.Duration duration,
- final Duration defaultValue)
- {
- if (duration == null) {
- return defaultValue;
- }
-
- return Duration.parse(duration.toString());
- }
-
- private static IdServerSessionConfiguration parseSessions(
- final Sessions sessions)
- {
- return new IdServerSessionConfiguration(
- parseDuration(sessions.getUserSessionExpiration()),
- parseDuration(sessions.getAdminSessionExpiration())
- );
- }
-
- private static IdServerHistoryConfiguration parseHistory(
- final History history)
- {
- return new IdServerHistoryConfiguration(
- Math.toIntExact(history.getUserLoginHistoryLimit()),
- Math.toIntExact(history.getAdminLoginHistoryLimit())
- );
- }
-
- private static IdServerDatabaseConfiguration parseDatabase(
- final Database database)
- {
- return new IdServerDatabaseConfiguration(
- parseDatabaseKind(database.getKind()),
- database.getOwnerRoleName(),
- database.getOwnerRolePassword(),
- database.getWorkerRolePassword(),
- Optional.ofNullable(database.getReaderRolePassword()),
- database.getAddress(),
- Math.toIntExact(database.getPort()),
- database.getName(),
- database.isCreate(),
- database.isUpgrade()
- );
- }
-
- private static IdServerDatabaseKind parseDatabaseKind(
- final String kind)
- {
- return switch (kind.toLowerCase(Locale.ROOT)) {
- case "postgresql" -> POSTGRESQL;
- default -> {
- throw new IllegalArgumentException(
- "Unrecognized database kind: %s (must be one of %s)"
- .formatted(kind, List.of(IdServerDatabaseKind.values()))
- );
- }
- };
- }
-
- private static IdServerHTTPConfiguration parseHTTP(
- final HTTPServices httpServices)
- throws URISyntaxException
- {
- return new IdServerHTTPConfiguration(
- parseHTTPService(httpServices.getHTTPServiceAdminAPI()),
- parseHTTPService(httpServices.getHTTPServiceUserAPI()),
- parseHTTPService(httpServices.getHTTPServiceUserView())
- );
- }
-
- private static IdServerHTTPServiceConfiguration parseHTTPService(
- final HTTPServiceType service)
- throws URISyntaxException
- {
- return new IdServerHTTPServiceConfiguration(
- service.getListenAddress(),
- Math.toIntExact(service.getListenPort()),
- new URI(service.getExternalURI())
- );
- }
-
- private static IdServerMailConfiguration parseMail(
- final Mail mail)
- {
- final IdServerMailTransportConfigurationType transport;
- if (mail.getSMTP() != null) {
- transport = parseSMTP(mail.getSMTP());
- } else if (mail.getSMTPS() != null) {
- transport = parseSMTPS(mail.getSMTPS());
- } else if (mail.getSMTPTLS() != null) {
- transport = parseSMTPTLS(mail.getSMTPTLS());
- } else {
- throw new IllegalStateException();
- }
-
- return new IdServerMailConfiguration(
- transport,
- parseMailAuth(mail.getMailAuthentication()),
- mail.getSenderAddress(),
- parseDuration(mail.getVerificationExpiration())
- );
- }
-
- private static IdServerMailTransportConfigurationType parseSMTP(
- final SMTPType smtp)
- {
- return new IdServerMailTransportSMTP(
- smtp.getHost(),
- Math.toIntExact(smtp.getPort())
- );
- }
-
- private static IdServerMailTransportConfigurationType parseSMTPS(
- final SMTPSType smtp)
- {
- return new IdServerMailTransportSMTPS(
- smtp.getHost(),
- Math.toIntExact(smtp.getPort())
- );
- }
-
- private static IdServerMailTransportConfigurationType parseSMTPTLS(
- final SMTPTLSType smtp)
- {
- return new IdServerMailTransportSMTP_TLS(
- smtp.getHost(),
- Math.toIntExact(smtp.getPort())
- );
- }
-
- private static Optional parseMailAuth(
- final MailAuthentication mailAuthentication)
- {
- if (mailAuthentication == null) {
- return Optional.empty();
- }
-
- return Optional.of(
- new IdServerMailAuthenticationConfiguration(
- mailAuthentication.getUsername(),
- mailAuthentication.getPassword()
- )
- );
- }
-
- private static IdServerBrandingConfiguration parseBranding(
- final Branding branding)
- {
- return new IdServerBrandingConfiguration(
- branding.getProductTitle(),
- parseLogo(branding.getLogo()),
- parseLoginExtra(branding.getLoginExtra()),
- parseColorScheme(branding.getColorScheme())
- );
- }
-
- private static Optional parseColorScheme(
- final ColorScheme colorScheme)
- {
- if (colorScheme == null) {
- return Optional.empty();
- }
-
- return Optional.of(
- new IdServerColorScheme(
- parseButtonColors(colorScheme.getButtonColors()),
- parseColor(colorScheme.getErrorBorderColor()),
- parseColor(colorScheme.getHeaderBackgroundColor()),
- parseColor(colorScheme.getHeaderLinkColor()),
- parseColor(colorScheme.getHeaderTextColor()),
- parseColor(colorScheme.getMainBackgroundColor()),
- parseColor(colorScheme.getMainLinkColor()),
- parseColor(colorScheme.getMainMessageBorderColor()),
- parseColor(colorScheme.getMainTableBorderColor()),
- parseColor(colorScheme.getMainTextColor())
- )
- );
- }
-
- private static CxButtonColors parseButtonColors(
- final ButtonColors buttonColors)
- {
- return new CxButtonColors(
- parseButtonStateColors(buttonColors.getEnabled()),
- parseButtonStateColors(buttonColors.getDisabled()),
- parseButtonStateColors(buttonColors.getPressed()),
- parseButtonStateColors(buttonColors.getHover())
- );
- }
-
- private static CxButtonStateColors parseButtonStateColors(
- final ButtonStateColors state)
- {
- return new CxButtonStateColors(
- parseCxColor(state.getTextColor()),
- parseCxColor(state.getBodyColor()),
- parseCxColor(state.getBorderColor()),
- parseCxColor(state.getEmbossEColor()),
- parseCxColor(state.getEmbossNColor()),
- parseCxColor(state.getEmbossSColor()),
- parseCxColor(state.getEmbossWColor())
- );
- }
-
- private static CxColor parseCxColor(
- final ColorType color)
- {
- return new CxColor(color.getRed(), color.getGreen(), color.getBlue());
- }
-
- private static IdColor parseColor(
- final ColorType color)
- {
- return new IdColor(
- color.getRed(),
- color.getGreen(),
- color.getBlue()
- );
- }
-
- private static Optional parseLoginExtra(
- final String loginExtra)
- {
- if (loginExtra == null) {
- return Optional.empty();
- }
-
- return Optional.of(Path.of(loginExtra));
- }
-
- private static Optional parseLogo(
- final String logo)
- {
- if (logo == null) {
- return Optional.empty();
- }
-
- return Optional.of(Path.of(logo));
- }
-
- /**
- * Parse a configuration file.
- *
- * @param file The input file
- *
- * @return The file
- *
- * @throws IOException On errors
- */
-
- public IdServerConfigurationFile parse(
- final Path file)
- throws IOException
- {
- try (var stream = Files.newInputStream(file)) {
- return this.parse(file.toUri(), stream);
- }
- }
-
- @Override
- public String description()
- {
- return "Server configuration elements.";
- }
-
- @Override
- public String toString()
- {
- return "[IdServerConfigurationFiles 0x%s]"
- .formatted(Long.toUnsignedString(this.hashCode(), 16));
- }
-}
diff --git a/com.io7m.idstore.server.service.configuration/src/main/java/com/io7m/idstore/server/service/configuration/IdServerConfigurationParser.java b/com.io7m.idstore.server.service.configuration/src/main/java/com/io7m/idstore/server/service/configuration/IdServerConfigurationParser.java
new file mode 100644
index 00000000..a7080cc1
--- /dev/null
+++ b/com.io7m.idstore.server.service.configuration/src/main/java/com/io7m/idstore/server/service/configuration/IdServerConfigurationParser.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright © 2023 Mark Raynsford https://www.io7m.com
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+ * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
+ * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+
+package com.io7m.idstore.server.service.configuration;
+
+import com.io7m.anethum.api.ParseSeverity;
+import com.io7m.anethum.api.ParseStatus;
+import com.io7m.anethum.api.ParsingException;
+import com.io7m.blackthorne.core.BTException;
+import com.io7m.blackthorne.core.BTParseError;
+import com.io7m.blackthorne.core.BTPreserveLexical;
+import com.io7m.blackthorne.jxe.BlackthorneJXE;
+import com.io7m.idstore.server.api.IdServerConfigurationFile;
+import com.io7m.idstore.server.service.configuration.v1.IdC1Configuration;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Consumer;
+
+import static com.io7m.blackthorne.core.BTPreserveLexical.PRESERVE_LEXICAL_INFORMATION;
+import static com.io7m.idstore.server.service.configuration.v1.IdC1Names.qName;
+import static java.util.Map.entry;
+
+final class IdServerConfigurationParser
+ implements IdServerConfigurationParserType
+{
+ private final BTPreserveLexical context;
+ private final URI source;
+ private final InputStream stream;
+ private final Consumer statusConsumer;
+
+ IdServerConfigurationParser(
+ final BTPreserveLexical inContext,
+ final URI inSource,
+ final InputStream inStream,
+ final Consumer inStatusConsumer)
+ {
+ this.context =
+ Objects.requireNonNullElse(inContext, PRESERVE_LEXICAL_INFORMATION);
+ this.source =
+ Objects.requireNonNull(inSource, "source");
+ this.stream =
+ Objects.requireNonNull(inStream, "stream");
+ this.statusConsumer =
+ Objects.requireNonNull(inStatusConsumer, "statusConsumer");
+ }
+
+ @Override
+ public String toString()
+ {
+ return "[IdServerConfigurationParser 0x%x]"
+ .formatted(Integer.valueOf(this.hashCode()));
+ }
+
+ @Override
+ public IdServerConfigurationFile execute()
+ throws ParsingException
+ {
+ try {
+ return BlackthorneJXE.parse(
+ this.source,
+ this.stream,
+ Map.ofEntries(
+ entry(qName("Configuration"), IdC1Configuration::new)
+ ),
+ IdServerConfigurationSchemas.schemas(),
+ this.context
+ );
+ } catch (final BTException e) {
+ final var statuses =
+ e.errors()
+ .stream()
+ .map(IdServerConfigurationParser::mapParseError)
+ .toList();
+
+ for (final var status : statuses) {
+ this.statusConsumer.accept(status);
+ }
+
+ final var ex =
+ new ParsingException(e.getMessage(), List.copyOf(statuses));
+ ex.addSuppressed(e);
+ throw ex;
+ }
+ }
+
+ @Override
+ public void close()
+ throws IOException
+ {
+ this.stream.close();
+ }
+
+ private static ParseStatus mapParseError(
+ final BTParseError error)
+ {
+ return ParseStatus.builder("parse-error", error.message())
+ .withSeverity(mapSeverity(error.severity()))
+ .withLexical(error.lexical())
+ .build();
+ }
+
+ private static ParseSeverity mapSeverity(
+ final BTParseError.Severity severity)
+ {
+ return switch (severity) {
+ case ERROR -> ParseSeverity.PARSE_ERROR;
+ case WARNING -> ParseSeverity.PARSE_WARNING;
+ };
+ }
+}
diff --git a/com.io7m.idstore.server.service.configuration/src/main/java/com/io7m/idstore/server/service/configuration/IdServerConfigurationParserFactoryType.java b/com.io7m.idstore.server.service.configuration/src/main/java/com/io7m/idstore/server/service/configuration/IdServerConfigurationParserFactoryType.java
new file mode 100644
index 00000000..bf192bb2
--- /dev/null
+++ b/com.io7m.idstore.server.service.configuration/src/main/java/com/io7m/idstore/server/service/configuration/IdServerConfigurationParserFactoryType.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright © 2023 Mark Raynsford https://www.io7m.com
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+ * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
+ * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+
+package com.io7m.idstore.server.service.configuration;
+
+import com.io7m.anethum.api.ParserFactoryType;
+import com.io7m.blackthorne.core.BTPreserveLexical;
+import com.io7m.idstore.server.api.IdServerConfigurationFile;
+
+/**
+ * A factory of configuration file parsers.
+ */
+
+public interface IdServerConfigurationParserFactoryType
+ extends ParserFactoryType<
+ BTPreserveLexical,
+ IdServerConfigurationFile,
+ IdServerConfigurationParserType>
+{
+
+}
diff --git a/com.io7m.idstore.server.service.configuration/src/main/java/com/io7m/idstore/server/service/configuration/IdServerConfigurationParserType.java b/com.io7m.idstore.server.service.configuration/src/main/java/com/io7m/idstore/server/service/configuration/IdServerConfigurationParserType.java
new file mode 100644
index 00000000..2a55836b
--- /dev/null
+++ b/com.io7m.idstore.server.service.configuration/src/main/java/com/io7m/idstore/server/service/configuration/IdServerConfigurationParserType.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright © 2023 Mark Raynsford https://www.io7m.com
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+ * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
+ * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+
+package com.io7m.idstore.server.service.configuration;
+
+import com.io7m.anethum.api.ParserType;
+import com.io7m.idstore.server.api.IdServerConfigurationFile;
+
+/**
+ * A configuration file parser.
+ */
+
+public interface IdServerConfigurationParserType
+ extends ParserType
+{
+
+}
diff --git a/com.io7m.idstore.server.service.configuration/src/main/java/com/io7m/idstore/server/service/configuration/IdServerConfigurationParsers.java b/com.io7m.idstore.server.service.configuration/src/main/java/com/io7m/idstore/server/service/configuration/IdServerConfigurationParsers.java
new file mode 100644
index 00000000..dd9b7512
--- /dev/null
+++ b/com.io7m.idstore.server.service.configuration/src/main/java/com/io7m/idstore/server/service/configuration/IdServerConfigurationParsers.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright © 2023 Mark Raynsford https://www.io7m.com
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+ * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
+ * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+
+package com.io7m.idstore.server.service.configuration;
+
+import com.io7m.anethum.api.ParseStatus;
+import com.io7m.blackthorne.core.BTPreserveLexical;
+
+import java.io.InputStream;
+import java.net.URI;
+import java.util.function.Consumer;
+
+/**
+ * A factory of configuration file parsers.
+ */
+
+public final class IdServerConfigurationParsers
+ implements IdServerConfigurationParserFactoryType
+{
+ /**
+ * A factory of configuration file parsers.
+ */
+
+ public IdServerConfigurationParsers()
+ {
+
+ }
+
+ @Override
+ public IdServerConfigurationParserType createParserWithContext(
+ final BTPreserveLexical context,
+ final URI source,
+ final InputStream stream,
+ final Consumer statusConsumer)
+ {
+ return new IdServerConfigurationParser(context, source, stream, statusConsumer);
+ }
+}
diff --git a/com.io7m.idstore.server.service.configuration/src/main/java/com/io7m/idstore/server/service/configuration/IdServerConfigurationSchemas.java b/com.io7m.idstore.server.service.configuration/src/main/java/com/io7m/idstore/server/service/configuration/IdServerConfigurationSchemas.java
new file mode 100644
index 00000000..68c970ed
--- /dev/null
+++ b/com.io7m.idstore.server.service.configuration/src/main/java/com/io7m/idstore/server/service/configuration/IdServerConfigurationSchemas.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright © 2023 Mark Raynsford https://www.io7m.com
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+ * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
+ * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+
+package com.io7m.idstore.server.service.configuration;
+
+import com.io7m.jxe.core.JXESchemaDefinition;
+import com.io7m.jxe.core.JXESchemaResolutionMappings;
+
+import java.net.URI;
+
+/**
+ * Configuration XML schemas.
+ */
+
+public final class IdServerConfigurationSchemas
+{
+ private static final JXESchemaDefinition SCHEMA_1 =
+ JXESchemaDefinition.builder()
+ .setFileIdentifier("configuration-1.xsd")
+ .setLocation(IdServerConfigurationSchemas.class.getResource(
+ "/com/io7m/idstore/server/service/configuration/configuration-1.xsd"))
+ .setNamespace(URI.create("urn:com.io7m.idstore:configuration:1"))
+ .build();
+
+ private static final JXESchemaDefinition TLS_1 =
+ JXESchemaDefinition.builder()
+ .setFileIdentifier("tls-1.xsd")
+ .setLocation(IdServerConfigurationSchemas.class.getResource(
+ "/com/io7m/idstore/server/service/configuration/tls-1.xsd"))
+ .setNamespace(URI.create("urn:com.io7m.idstore.tls:1"))
+ .build();
+
+ private static final JXESchemaResolutionMappings SCHEMA_MAPPINGS =
+ JXESchemaResolutionMappings.builder()
+ .putMappings(SCHEMA_1.namespace(), SCHEMA_1)
+ .putMappings(TLS_1.namespace(), TLS_1)
+ .build();
+
+ /**
+ * @return The v1 schema
+ */
+
+ public static JXESchemaDefinition schema1()
+ {
+ return SCHEMA_1;
+ }
+
+ /**
+ * @return The TLS v1 schema
+ */
+
+ public static JXESchemaDefinition tls1()
+ {
+ return TLS_1;
+ }
+
+ /**
+ * @return The set of supported schemas.
+ */
+
+ public static JXESchemaResolutionMappings schemas()
+ {
+ return SCHEMA_MAPPINGS;
+ }
+
+ private IdServerConfigurationSchemas()
+ {
+
+ }
+}
diff --git a/com.io7m.idstore.server.service.configuration/src/main/java/com/io7m/idstore/server/service/configuration/IdServerConfigurationSerializer.java b/com.io7m.idstore.server.service.configuration/src/main/java/com/io7m/idstore/server/service/configuration/IdServerConfigurationSerializer.java
new file mode 100644
index 00000000..0ed24a2d
--- /dev/null
+++ b/com.io7m.idstore.server.service.configuration/src/main/java/com/io7m/idstore/server/service/configuration/IdServerConfigurationSerializer.java
@@ -0,0 +1,625 @@
+/*
+ * Copyright © 2023 Mark Raynsford https://www.io7m.com
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+ * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
+ * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+
+package com.io7m.idstore.server.service.configuration;
+
+import com.io7m.anethum.api.SerializationException;
+import com.io7m.cxbutton.core.CxButtonColors;
+import com.io7m.cxbutton.core.CxButtonStateColors;
+import com.io7m.cxbutton.core.CxColor;
+import com.io7m.idstore.server.api.IdColor;
+import com.io7m.idstore.server.api.IdServerBrandingConfiguration;
+import com.io7m.idstore.server.api.IdServerColorScheme;
+import com.io7m.idstore.server.api.IdServerConfigurationFile;
+import com.io7m.idstore.server.api.IdServerDatabaseConfiguration;
+import com.io7m.idstore.server.api.IdServerHTTPConfiguration;
+import com.io7m.idstore.server.api.IdServerHTTPServiceConfiguration;
+import com.io7m.idstore.server.api.IdServerHistoryConfiguration;
+import com.io7m.idstore.server.api.IdServerMailAuthenticationConfiguration;
+import com.io7m.idstore.server.api.IdServerMailConfiguration;
+import com.io7m.idstore.server.api.IdServerMailTransportSMTP;
+import com.io7m.idstore.server.api.IdServerMailTransportSMTPS;
+import com.io7m.idstore.server.api.IdServerMailTransportSMTP_TLS;
+import com.io7m.idstore.server.api.IdServerOpenTelemetryConfiguration;
+import com.io7m.idstore.server.api.IdServerPasswordExpirationConfiguration;
+import com.io7m.idstore.server.api.IdServerRateLimitConfiguration;
+import com.io7m.idstore.server.api.IdServerSessionConfiguration;
+import com.io7m.idstore.tls.IdTLSConfigurationType;
+import com.io7m.idstore.tls.IdTLSDisabled;
+import com.io7m.idstore.tls.IdTLSEnabled;
+
+import javax.xml.stream.XMLOutputFactory;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamWriter;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.URI;
+import java.util.Objects;
+import java.util.Optional;
+
+import static java.lang.Integer.toUnsignedString;
+
+final class IdServerConfigurationSerializer
+ implements IdServerConfigurationSerializerType
+{
+ private final OutputStream stream;
+ private final XMLStreamWriter output;
+
+ IdServerConfigurationSerializer(
+ final URI inTarget,
+ final OutputStream inStream)
+ {
+ Objects.requireNonNull(inTarget, "target");
+
+ this.stream =
+ Objects.requireNonNull(inStream, "stream");
+
+ try {
+ this.output =
+ XMLOutputFactory.newFactory()
+ .createXMLStreamWriter(this.stream, "UTF-8");
+ } catch (final XMLStreamException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ private static String findNS()
+ {
+ return IdServerConfigurationSchemas.schema1().namespace().toString();
+ }
+
+ @Override
+ public String toString()
+ {
+ return "[IdServerConfigurationSerializer 0x%x]"
+ .formatted(Integer.valueOf(this.hashCode()));
+ }
+
+ @Override
+ public void execute(
+ final IdServerConfigurationFile value)
+ throws SerializationException
+ {
+ try {
+ this.output.writeStartDocument("UTF-8", "1.0");
+ this.serializeFile(value);
+ this.output.writeEndDocument();
+ } catch (final XMLStreamException e) {
+ throw new SerializationException(e.getMessage(), e);
+ }
+ }
+
+ private void serializeFile(
+ final IdServerConfigurationFile value)
+ throws XMLStreamException
+ {
+ this.output.writeStartElement("Configuration");
+ this.output.writeDefaultNamespace(findNS());
+ this.output.writeNamespace("tls", findTLSNS());
+
+ this.serializeBranding(value.brandingConfiguration());
+ this.serializeDatabase(value.databaseConfiguration());
+ this.serializeHTTP(value.httpConfiguration());
+ this.serializeHistory(value.historyConfiguration());
+ this.serializeMail(value.mailConfiguration());
+ this.serializeOpenTelemetryOpt(value.openTelemetry());
+ this.serializePasswordExpiration(value.passwordExpiration());
+ this.serializeRateLimit(value.rateLimit());
+ this.serializeSessions(value.sessionConfiguration());
+
+ this.output.writeEndElement();
+ }
+
+ private void serializeHTTP(
+ final IdServerHTTPConfiguration http)
+ throws XMLStreamException
+ {
+ this.output.writeStartElement("HTTPServices");
+
+ this.serializeHTTPAdminAPI(http.adminAPIService());
+ this.serializeHTTPUserAPI(http.userAPIService());
+ this.serializeHTTPUserView(http.userViewService());
+
+ this.output.writeEndElement();
+ }
+
+ private void serializeHTTPUserView(
+ final IdServerHTTPServiceConfiguration c)
+ throws XMLStreamException
+ {
+ this.output.writeStartElement("HTTPServiceUserView");
+ this.output.writeAttribute("ListenAddress", c.listenAddress());
+ this.output.writeAttribute("ListenPort", toUnsignedString(c.listenPort()));
+ this.output.writeAttribute("ExternalURI", c.externalAddress().toString());
+ this.serializeTLS(c.tlsConfiguration());
+ this.output.writeEndElement();
+ }
+
+ private void serializeTLS(
+ final IdTLSConfigurationType c)
+ throws XMLStreamException
+ {
+ final var tlsNs = findTLSNS();
+
+ switch (c) {
+ case final IdTLSDisabled ignored -> {
+ this.output.writeStartElement("tls", "TLSDisabled", tlsNs);
+ this.output.writeEndElement();
+ }
+ case final IdTLSEnabled e -> {
+ this.output.writeStartElement("tls", "TLSEnabled", tlsNs);
+
+ final var ks = e.keyStore();
+ this.output.writeStartElement("tls", "KeyStore", tlsNs);
+ this.output.writeAttribute("Type", ks.storeType());
+ this.output.writeAttribute("Provider", ks.storeProvider());
+ this.output.writeAttribute("Password", ks.storePassword());
+ this.output.writeAttribute("File", ks.storePath().toString());
+ this.output.writeEndElement();
+
+ final var ts = e.trustStore();
+ this.output.writeStartElement("tls", "TrustStore", tlsNs);
+ this.output.writeAttribute("Type", ts.storeType());
+ this.output.writeAttribute("Provider", ts.storeProvider());
+ this.output.writeAttribute("Password", ts.storePassword());
+ this.output.writeAttribute("File", ts.storePath().toString());
+ this.output.writeEndElement();
+
+ this.output.writeEndElement();
+ }
+ }
+ }
+
+ private static String findTLSNS()
+ {
+ return IdServerConfigurationSchemas.tls1().namespace().toString();
+ }
+
+ private void serializeHTTPUserAPI(
+ final IdServerHTTPServiceConfiguration c)
+ throws XMLStreamException
+ {
+ this.output.writeStartElement("HTTPServiceUserAPI");
+ this.output.writeAttribute("ListenAddress", c.listenAddress());
+ this.output.writeAttribute("ListenPort", toUnsignedString(c.listenPort()));
+ this.output.writeAttribute("ExternalURI", c.externalAddress().toString());
+ this.serializeTLS(c.tlsConfiguration());
+ this.output.writeEndElement();
+ }
+
+ private void serializeHTTPAdminAPI(
+ final IdServerHTTPServiceConfiguration c)
+ throws XMLStreamException
+ {
+ this.output.writeStartElement("HTTPServiceAdminAPI");
+ this.output.writeAttribute("ListenAddress", c.listenAddress());
+ this.output.writeAttribute("ListenPort", toUnsignedString(c.listenPort()));
+ this.output.writeAttribute("ExternalURI", c.externalAddress().toString());
+ this.serializeTLS(c.tlsConfiguration());
+ this.output.writeEndElement();
+ }
+
+ private void serializeMail(
+ final IdServerMailConfiguration c)
+ throws XMLStreamException
+ {
+ this.output.writeStartElement("Mail");
+ this.output.writeAttribute(
+ "SenderAddress",
+ c.senderAddress()
+ );
+ this.output.writeAttribute(
+ "VerificationExpiration",
+ c.verificationExpiration().toString()
+ );
+
+ switch (c.transportConfiguration()) {
+ case final IdServerMailTransportSMTP s -> {
+ this.output.writeStartElement("SMTP");
+ this.output.writeAttribute("Host", s.host());
+ this.output.writeAttribute("Port", toUnsignedString(s.port()));
+ this.output.writeEndElement();
+ }
+ case final IdServerMailTransportSMTPS s -> {
+ this.output.writeStartElement("SMTPS");
+ this.output.writeAttribute("Host", s.host());
+ this.output.writeAttribute("Port", toUnsignedString(s.port()));
+ this.output.writeEndElement();
+ }
+ case final IdServerMailTransportSMTP_TLS s -> {
+ this.output.writeStartElement("SMTPTLS");
+ this.output.writeAttribute("Host", s.host());
+ this.output.writeAttribute("Port", toUnsignedString(s.port()));
+ this.output.writeEndElement();
+ }
+ }
+
+ if (c.authenticationConfiguration().isPresent()) {
+ this.serializeMailAuthentication(c.authenticationConfiguration().get());
+ }
+
+ this.output.writeEndElement();
+ }
+
+ private void serializeMailAuthentication(
+ final IdServerMailAuthenticationConfiguration c)
+ throws XMLStreamException
+ {
+ this.output.writeStartElement("MailAuthentication");
+ this.output.writeAttribute("Username", c.userName());
+ this.output.writeAttribute("Password", c.password());
+ this.output.writeEndElement();
+ }
+
+ private void serializeOpenTelemetryOpt(
+ final Optional c)
+ throws XMLStreamException
+ {
+ if (c.isPresent()) {
+ this.serializeOpenTelemetry(c.get());
+ }
+ }
+
+ private void serializeOpenTelemetry(
+ final IdServerOpenTelemetryConfiguration c)
+ throws XMLStreamException
+ {
+ this.output.writeStartElement("OpenTelemetry");
+ this.output.writeAttribute("LogicalServiceName", c.logicalServiceName());
+
+ if (c.logs().isPresent()) {
+ final var e = c.logs().get();
+ this.output.writeStartElement("Logs");
+ this.output.writeAttribute("Endpoint", e.endpoint().toString());
+ this.output.writeAttribute("Protocol", e.protocol().toString());
+ this.output.writeEndElement();
+ }
+
+ if (c.metrics().isPresent()) {
+ final var e = c.metrics().get();
+ this.output.writeStartElement("Metrics");
+ this.output.writeAttribute("Endpoint", e.endpoint().toString());
+ this.output.writeAttribute("Protocol", e.protocol().toString());
+ this.output.writeEndElement();
+ }
+
+ if (c.traces().isPresent()) {
+ final var e = c.traces().get();
+ this.output.writeStartElement("Traces");
+ this.output.writeAttribute("Endpoint", e.endpoint().toString());
+ this.output.writeAttribute("Protocol", e.protocol().toString());
+ this.output.writeEndElement();
+ }
+
+ this.output.writeEndElement();
+ }
+
+ private void serializeSessions(
+ final IdServerSessionConfiguration c)
+ throws XMLStreamException
+ {
+ this.output.writeStartElement("Sessions");
+ this.output.writeAttribute(
+ "UserSessionExpiration",
+ c.userSessionExpiration().toString()
+ );
+ this.output.writeAttribute(
+ "AdminSessionExpiration",
+ c.adminSessionExpiration().toString()
+ );
+ this.output.writeEndElement();
+ }
+
+ private void serializePasswordExpiration(
+ final IdServerPasswordExpirationConfiguration c)
+ throws XMLStreamException
+ {
+ this.output.writeStartElement("PasswordExpiration");
+
+ if (c.userPasswordValidityDuration().isPresent()) {
+ this.output.writeAttribute(
+ "UserPasswordValidityDuration",
+ c.userPasswordValidityDuration().get().toString()
+ );
+ }
+
+ if (c.adminPasswordValidityDuration().isPresent()) {
+ this.output.writeAttribute(
+ "AdminPasswordValidityDuration",
+ c.adminPasswordValidityDuration().get().toString()
+ );
+ }
+
+ this.output.writeEndElement();
+ }
+
+ private void serializeRateLimit(
+ final IdServerRateLimitConfiguration c)
+ throws XMLStreamException
+ {
+ this.output.writeStartElement("RateLimiting");
+ this.output.writeAttribute(
+ "UserLoginDelay",
+ c.userLoginDelay().toString()
+ );
+ this.output.writeAttribute(
+ "UserLoginRateLimit",
+ c.userLoginRateLimit().toString()
+ );
+ this.output.writeAttribute(
+ "AdminLoginDelay",
+ c.adminLoginDelay().toString()
+ );
+ this.output.writeAttribute(
+ "AdminLoginRateLimit",
+ c.adminLoginRateLimit().toString()
+ );
+ this.output.writeAttribute(
+ "EmailVerificationRateLimit",
+ c.emailVerificationRateLimit().toString()
+ );
+ this.output.writeAttribute(
+ "PasswordResetRateLimit",
+ c.passwordResetRateLimit().toString()
+ );
+ this.output.writeEndElement();
+ }
+
+ private void serializeHistory(
+ final IdServerHistoryConfiguration c)
+ throws XMLStreamException
+ {
+ this.output.writeStartElement("History");
+ this.output.writeAttribute(
+ "UserLoginHistoryLimit",
+ toUnsignedString(c.userLoginHistoryLimit())
+ );
+ this.output.writeAttribute(
+ "AdminLoginHistoryLimit",
+ toUnsignedString(c.adminLoginHistoryLimit())
+ );
+ this.output.writeEndElement();
+ }
+
+ private void serializeDatabase(
+ final IdServerDatabaseConfiguration c)
+ throws XMLStreamException
+ {
+ this.output.writeStartElement("Database");
+ this.output.writeAttribute(
+ "OwnerRoleName",
+ c.ownerRoleName()
+ );
+ this.output.writeAttribute(
+ "OwnerRolePassword",
+ c.ownerRolePassword()
+ );
+ this.output.writeAttribute(
+ "WorkerRolePassword",
+ c.workerRolePassword()
+ );
+
+ if (c.readerRolePassword().isPresent()) {
+ final var r = c.readerRolePassword().get();
+ this.output.writeAttribute("ReaderRolePassword", r);
+ }
+
+ this.output.writeAttribute(
+ "Kind",
+ c.kind().name()
+ );
+ this.output.writeAttribute(
+ "Name",
+ c.databaseName()
+ );
+ this.output.writeAttribute(
+ "Address",
+ c.address()
+ );
+ this.output.writeAttribute(
+ "Port",
+ toUnsignedString(c.port())
+ );
+ this.output.writeAttribute(
+ "Create",
+ Boolean.toString(c.create())
+ );
+ this.output.writeAttribute(
+ "Upgrade",
+ Boolean.toString(c.upgrade())
+ );
+ this.output.writeEndElement();
+ }
+
+ private void serializeBranding(
+ final IdServerBrandingConfiguration c)
+ throws XMLStreamException
+ {
+ this.output.writeStartElement("Branding");
+ this.output.writeAttribute("ProductTitle", c.productTitle());
+
+ final var logoOpt = c.logo();
+ if (logoOpt.isPresent()) {
+ final var logo = logoOpt.get();
+ this.output.writeAttribute("Logo", logo.toString());
+ }
+
+ final var loginExtraOpt = c.loginExtra();
+ if (loginExtraOpt.isPresent()) {
+ final var loginExtra = loginExtraOpt.get();
+ this.output.writeAttribute("LoginExtra", loginExtra.toString());
+ }
+
+ final var schemeOpt = c.scheme();
+ if (schemeOpt.isPresent()) {
+ final var scheme = schemeOpt.get();
+ this.serializeScheme(scheme);
+ }
+
+ this.output.writeEndElement();
+ }
+
+ private void serializeScheme(
+ final IdServerColorScheme scheme)
+ throws XMLStreamException
+ {
+ this.output.writeStartElement("ColorScheme");
+ this.serializeButtonColors(scheme.buttonColors());
+ this.serializeColor(
+ "ErrorBorderColor",
+ scheme.errorBorderColor()
+ );
+ this.serializeColor(
+ "HeaderBackgroundColor",
+ scheme.headerBackgroundColor()
+ );
+ this.serializeColor(
+ "HeaderLinkColor",
+ scheme.headerLinkColor()
+ );
+ this.serializeColor(
+ "HeaderTextColor",
+ scheme.headerTextColor()
+ );
+ this.serializeColor(
+ "MainBackgroundColor",
+ scheme.mainBackgroundColor()
+ );
+ this.serializeColor(
+ "MainLinkColor",
+ scheme.mainLinkColor()
+ );
+ this.serializeColor(
+ "MainMessageBorderColor",
+ scheme.mainMessageBorderColor()
+ );
+ this.serializeColor(
+ "MainTableBorderColor",
+ scheme.mainTableBorderColor()
+ );
+ this.serializeColor(
+ "MainTextColor",
+ scheme.mainTextColor()
+ );
+ this.output.writeEndElement();
+ }
+
+ private void serializeColor(
+ final String name,
+ final IdColor idColor)
+ throws XMLStreamException
+ {
+ this.output.writeStartElement(name);
+ this.output.writeAttribute(
+ "Red",
+ String.format("%.03f", Double.valueOf(idColor.red()))
+ );
+ this.output.writeAttribute(
+ "Green",
+ String.format("%.03f", Double.valueOf(idColor.green()))
+ );
+ this.output.writeAttribute(
+ "Blue",
+ String.format("%.03f", Double.valueOf(idColor.blue()))
+ );
+ this.output.writeEndElement();
+ }
+
+ private void serializeButtonColors(
+ final CxButtonColors colors)
+ throws XMLStreamException
+ {
+ this.output.writeStartElement("ButtonColors");
+ this.serializeButtonColorsDisabled(colors.disabled());
+ this.serializeButtonColorsEnabled(colors.enabled());
+ this.serializeButtonColorsHover(colors.hover());
+ this.serializeButtonColorsPressed(colors.pressed());
+ this.output.writeEndElement();
+ }
+
+ private void serializeButtonColorsPressed(
+ final CxButtonStateColors c)
+ throws XMLStreamException
+ {
+ this.serializeButtonStateColors("Pressed", c);
+ }
+
+ private void serializeButtonColorsHover(
+ final CxButtonStateColors c)
+ throws XMLStreamException
+ {
+ this.serializeButtonStateColors("Hover", c);
+ }
+
+ private void serializeButtonColorsEnabled(
+ final CxButtonStateColors c)
+ throws XMLStreamException
+ {
+ this.serializeButtonStateColors("Enabled", c);
+ }
+
+ private void serializeButtonColorsDisabled(
+ final CxButtonStateColors c)
+ throws XMLStreamException
+ {
+ this.serializeButtonStateColors("Disabled", c);
+ }
+
+ private void serializeButtonStateColors(
+ final String name,
+ final CxButtonStateColors c)
+ throws XMLStreamException
+ {
+ this.output.writeStartElement(name);
+ this.serializeCxColor("BodyColor", c.bodyColor());
+ this.serializeCxColor("BorderColor", c.borderColor());
+ this.serializeCxColor("EmbossEColor", c.embossEColor());
+ this.serializeCxColor("EmbossNColor", c.embossNColor());
+ this.serializeCxColor("EmbossSColor", c.embossSColor());
+ this.serializeCxColor("EmbossWColor", c.embossWColor());
+ this.serializeCxColor("TextColor", c.textColor());
+ this.output.writeEndElement();
+ }
+
+ private void serializeCxColor(
+ final String name,
+ final CxColor c)
+ throws XMLStreamException
+ {
+ this.output.writeStartElement(name);
+ this.output.writeAttribute(
+ "Red",
+ String.format("%.03f", Double.valueOf(c.red()))
+ );
+ this.output.writeAttribute(
+ "Green",
+ String.format("%.03f", Double.valueOf(c.green()))
+ );
+ this.output.writeAttribute(
+ "Blue",
+ String.format("%.03f", Double.valueOf(c.blue()))
+ );
+ this.output.writeEndElement();
+ }
+
+ @Override
+ public void close()
+ throws IOException
+ {
+ this.stream.close();
+ }
+}
diff --git a/com.io7m.idstore.server.service.configuration/src/main/java/com/io7m/idstore/server/service/configuration/IdServerConfigurationSerializerFactoryType.java b/com.io7m.idstore.server.service.configuration/src/main/java/com/io7m/idstore/server/service/configuration/IdServerConfigurationSerializerFactoryType.java
new file mode 100644
index 00000000..cd51e698
--- /dev/null
+++ b/com.io7m.idstore.server.service.configuration/src/main/java/com/io7m/idstore/server/service/configuration/IdServerConfigurationSerializerFactoryType.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright © 2023 Mark Raynsford https://www.io7m.com
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+ * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
+ * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+
+package com.io7m.idstore.server.service.configuration;
+
+import com.io7m.anethum.api.SerializerFactoryType;
+import com.io7m.idstore.server.api.IdServerConfigurationFile;
+
+/**
+ * A factory of configuration file serializers.
+ */
+
+public interface IdServerConfigurationSerializerFactoryType
+ extends SerializerFactoryType<
+ Void,
+ IdServerConfigurationFile,
+ IdServerConfigurationSerializerType>
+{
+
+}
diff --git a/com.io7m.idstore.server.service.configuration/src/main/java/com/io7m/idstore/server/service/configuration/IdServerConfigurationSerializerType.java b/com.io7m.idstore.server.service.configuration/src/main/java/com/io7m/idstore/server/service/configuration/IdServerConfigurationSerializerType.java
new file mode 100644
index 00000000..0a044b46
--- /dev/null
+++ b/com.io7m.idstore.server.service.configuration/src/main/java/com/io7m/idstore/server/service/configuration/IdServerConfigurationSerializerType.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright © 2023 Mark Raynsford https://www.io7m.com
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+ * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
+ * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+
+package com.io7m.idstore.server.service.configuration;
+
+import com.io7m.anethum.api.SerializerType;
+import com.io7m.idstore.server.api.IdServerConfigurationFile;
+
+/**
+ * A configuration file serializer.
+ */
+
+public interface IdServerConfigurationSerializerType
+ extends SerializerType
+{
+
+}
diff --git a/com.io7m.idstore.server.service.configuration/src/main/java/com/io7m/idstore/server/service/configuration/IdServerConfigurationSerializers.java b/com.io7m.idstore.server.service.configuration/src/main/java/com/io7m/idstore/server/service/configuration/IdServerConfigurationSerializers.java
new file mode 100644
index 00000000..e034cf93
--- /dev/null
+++ b/com.io7m.idstore.server.service.configuration/src/main/java/com/io7m/idstore/server/service/configuration/IdServerConfigurationSerializers.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright © 2023 Mark Raynsford https://www.io7m.com
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+ * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
+ * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+
+package com.io7m.idstore.server.service.configuration;
+
+import java.io.OutputStream;
+import java.net.URI;
+
+/**
+ * A factory of configuration file serializers.
+ */
+
+public final class IdServerConfigurationSerializers
+ implements IdServerConfigurationSerializerFactoryType
+{
+ /**
+ * A factory of configuration file serializers.
+ */
+
+ public IdServerConfigurationSerializers()
+ {
+
+ }
+
+ @Override
+ public IdServerConfigurationSerializerType createSerializerWithContext(
+ final Void context,
+ final URI target,
+ final OutputStream stream)
+ {
+ return new IdServerConfigurationSerializer(target, stream);
+ }
+}
diff --git a/com.io7m.idstore.server.service.configuration/src/main/java/com/io7m/idstore/server/service/configuration/v1/IdC1AbstractButtonStateColors.java b/com.io7m.idstore.server.service.configuration/src/main/java/com/io7m/idstore/server/service/configuration/v1/IdC1AbstractButtonStateColors.java
new file mode 100644
index 00000000..0ecf3e84
--- /dev/null
+++ b/com.io7m.idstore.server.service.configuration/src/main/java/com/io7m/idstore/server/service/configuration/v1/IdC1AbstractButtonStateColors.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright © 2023 Mark Raynsford https://www.io7m.com
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+ * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
+ * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+
+package com.io7m.idstore.server.service.configuration.v1;
+
+import com.io7m.blackthorne.core.BTElementHandlerConstructorType;
+import com.io7m.blackthorne.core.BTElementHandlerType;
+import com.io7m.blackthorne.core.BTElementParsingContextType;
+import com.io7m.blackthorne.core.BTQualifiedName;
+import com.io7m.cxbutton.core.CxButtonStateColors;
+import com.io7m.cxbutton.core.CxColor;
+
+import java.util.Map;
+import java.util.Objects;
+
+import static com.io7m.idstore.server.service.configuration.v1.IdC1Names.qName;
+import static java.util.Map.entry;
+
+abstract class IdC1AbstractButtonStateColors
+ implements BTElementHandlerType
+{
+ private final String semantic;
+ private IdC1Color bodyColor;
+ private IdC1Color borderColor;
+ private IdC1Color embossEColor;
+ private IdC1Color embossWColor;
+ private IdC1Color embossSColor;
+ private IdC1Color embossNColor;
+ private IdC1Color textColor;
+
+ IdC1AbstractButtonStateColors(
+ final String inSemantic,
+ final BTElementParsingContextType context)
+ {
+ this.semantic =
+ Objects.requireNonNull(inSemantic, "semantic");
+ }
+
+ @Override
+ public final Map>
+ onChildHandlersRequested(
+ final BTElementParsingContextType context)
+ {
+ return Map.ofEntries(
+ entry(qName("BodyColor"), IdC1BodyColor::new),
+ entry(qName("BorderColor"), IdC1BorderColor::new),
+ entry(qName("EmbossEColor"), IdC1EmbossEColor::new),
+ entry(qName("EmbossWColor"), IdC1EmbossWColor::new),
+ entry(qName("EmbossSColor"), IdC1EmbossSColor::new),
+ entry(qName("EmbossNColor"), IdC1EmbossNColor::new),
+ entry(qName("TextColor"), IdC1TextColor::new)
+ );
+ }
+
+ @Override
+ public final void onChildValueProduced(
+ final BTElementParsingContextType context,
+ final IdC1Color color)
+ {
+ switch (color.semantic()) {
+ case "BodyColor" -> {
+ this.bodyColor = color;
+ }
+ case "BorderColor" -> {
+ this.borderColor = color;
+ }
+ case "EmbossEColor" -> {
+ this.embossEColor = color;
+ }
+ case "EmbossWColor" -> {
+ this.embossWColor = color;
+ }
+ case "EmbossSColor" -> {
+ this.embossSColor = color;
+ }
+ case "EmbossNColor" -> {
+ this.embossNColor = color;
+ }
+ case "TextColor" -> {
+ this.textColor = color;
+ }
+ default -> {
+ throw new IllegalArgumentException(
+ "Unrecognized color semantic: %s".formatted(color.semantic())
+ );
+ }
+ }
+ }
+
+ @Override
+ public final IdC1ButtonStateColors onElementFinished(
+ final BTElementParsingContextType context)
+ {
+ return new IdC1ButtonStateColors(
+ this.semantic,
+ new CxButtonStateColors(
+ cx(this.textColor),
+ cx(this.bodyColor),
+ cx(this.borderColor),
+ cx(this.embossEColor),
+ cx(this.embossNColor),
+ cx(this.embossSColor),
+ cx(this.embossWColor)
+ )
+ );
+ }
+
+ private static CxColor cx(
+ final IdC1Color c)
+ {
+ final var cc = c.color();
+ return new CxColor(
+ cc.red(),
+ cc.green(),
+ cc.blue()
+ );
+ }
+}
diff --git a/com.io7m.idstore.server.service.configuration/src/main/java/com/io7m/idstore/server/service/configuration/v1/IdC1AbstractColor.java b/com.io7m.idstore.server.service.configuration/src/main/java/com/io7m/idstore/server/service/configuration/v1/IdC1AbstractColor.java
new file mode 100644
index 00000000..532b36bc
--- /dev/null
+++ b/com.io7m.idstore.server.service.configuration/src/main/java/com/io7m/idstore/server/service/configuration/v1/IdC1AbstractColor.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright © 2023 Mark Raynsford https://www.io7m.com
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+ * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
+ * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+
+package com.io7m.idstore.server.service.configuration.v1;
+
+import com.io7m.blackthorne.core.BTElementHandlerType;
+import com.io7m.blackthorne.core.BTElementParsingContextType;
+import com.io7m.idstore.server.api.IdColor;
+import org.xml.sax.Attributes;
+
+import java.util.Objects;
+
+abstract class IdC1AbstractColor
+ implements BTElementHandlerType