Part 3: Provider Validator (swim-ffice-provider-validator)
The provider validator is a Quarkus application that acts as a SWIM consumer to test your provider. You build it from source — it lives alongside the provider in applications/swim-validators/swim-ffice-provider-validator/.
What the validator does
- Subscribes to the provider's REST API (
/swim/v1/subscriptions) - Connects to Artemis via AMQPS (mTLS) and receives FF-ICE events
- Persists received messages in MariaDB
- Runs 9 conformance scenarios against the provider's API
- Exposes REST endpoints for message inspection and conformance test execution
The validator is a full source project. When you run the end-to-end validation cycle, you delete and recreate it from scratch by following this tutorial. It is never treated as a black box.
3.1 Project structure
swim-ffice-provider-validator/
pom.xml
compose.yml
src/main/resources/application.properties
src/main/java/com/github/swim_developer/validator/ffice/provider/
application/usecase/
ConformanceAssertions.java
ConformanceTestService.java
ConsoleService.java
FficeMessageExtractor.java
MessageService.java
SubscriptionService.java
domain/model/
FficeMessage.java
HttpResult.java
ReceivedMessage.java
domain/port/in/
ConformanceHttpPort.java
ConformanceTestPort.java
ConnectionTrackerPort.java
ConsoleNotificationPort.java
MessagePersistencePort.java
MessagePort.java
ProviderSubscriptionPort.java
domain/port/out/
ReceivedMessageRepository.java
infrastructure/client/
ConformanceHttpClient.java
ProviderHttpClient.java
infrastructure/messaging/
AmqpConnectionCleanupScheduler.java
AmqpSslConfigurator.java
UserConnectionTracker.java
UserReceiverLifecycle.java
infrastructure/persistence/
ReceivedMessageMapper.java
ReceivedMessageRepositoryImpl.java
entity/ReceivedMessageEntity.java
infrastructure/rest/
AmqpApiResource.java
ApiResource.java
ConformanceTestResource.java
MessageResource.java
ProviderProxyResource.java
dto/ReceivedMessageDto.java
src/test/java/com/github/swim_developer/validator/ffice/provider/
FficeMessageExtractorTest.java
3.2 pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.github.swim-developer</groupId>
<artifactId>swim-validators</artifactId>
<version>1.0.0-SNAPSHOT</version>
<relativePath/>
</parent>
<artifactId>swim-ffice-provider-validator</artifactId>
<name>SWIM FF-ICE Provider Validator</name>
<dependencies>
<dependency>
<groupId>com.github.swim-developer</groupId>
<artifactId>swim-validator-provider</artifactId>
</dependency>
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-web-client</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>${quarkus.platform.group-id}</groupId>
<artifactId>quarkus-maven-plugin</artifactId>
<version>${quarkus.platform.version}</version>
<extensions>true</extensions>
<executions>
<execution>
<goals>
<goal>build</goal>
<goal>generate-code</goal>
<goal>generate-code-tests</goal>
<goal>native-image-agent</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration><parameters>true</parameters></configuration>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<systemPropertyVariables>
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
<maven.home>${maven.home}</maven.home>
</systemPropertyVariables>
</configuration>
</plugin>
</plugins>
</build>
</project>
3.3 application.properties
quarkus.application.name=swim-ffice-provider-validator
quarkus.http.port=${QUARKUS_HTTP_PORT:8080}
quarkus.ssl.native=true
keycloak.url=${KEYCLOAK_URL:https://rhbk.apps.ocp4.masales.cloud}
keycloak.realm=${KEYCLOAK_REALM:swim}
keycloak.client-id=${KEYCLOAK_CLIENT_ID:swim-public-client}
swim.provider.api.urls=${SWIM_PROVIDER_API_URLS:https://swim-ffice-provider-mtls-swim-sandbox.apps.ocp4.masales.cloud}
proxy.mtls.keystore.path=${PROXY_MTLS_KEYSTORE_PATH:certs/keystore.p12}
proxy.mtls.keystore.password=${PROXY_MTLS_KEYSTORE_PASSWORD:changeit}
proxy.mtls.keystore.type=${PROXY_MTLS_KEYSTORE_TYPE:PKCS12}
proxy.mtls.truststore.path=${PROXY_MTLS_TRUSTSTORE_PATH:certs/truststore.p12}
proxy.mtls.truststore.password=${PROXY_MTLS_TRUSTSTORE_PASSWORD:changeit}
proxy.mtls.truststore.type=${PROXY_MTLS_TRUSTSTORE_TYPE:PKCS12}
quarkus.datasource.db-kind=mariadb
quarkus.hibernate-orm.schema-management.strategy=update
quarkus.datasource.jdbc.min-size=2
quarkus.datasource.jdbc.max-size=10
%prod.quarkus.datasource.jdbc.url=jdbc:mariadb://${MARIADB_HOST:localhost}:${MARIADB_PORT:3306}/${MARIADB_DATABASE:swim_ffice_provider_validator}
%prod.quarkus.datasource.username=${MARIADB_USERNAME:swim}
%prod.quarkus.datasource.password=${MARIADB_PASSWORD:swim}
%dev.quarkus.datasource.jdbc.url=jdbc:mariadb://localhost:3311/swim_ffice_provider_validator
%dev.quarkus.datasource.username=swim
%dev.quarkus.datasource.password=swim
%test.quarkus.datasource.jdbc.url=jdbc:h2:mem:swim_ffice_validator_test
%test.quarkus.datasource.db-kind=h2
swim.provider.amqp.host=${SWIM_PROVIDER_AMQP_HOST:ffice-provider-artemis-swim-sandbox.apps.ocp4.masales.cloud}
swim.provider.amqp.port=${SWIM_PROVIDER_AMQP_PORT:443}
3.4 compose.yml (validator only)
services:
ffice-provider-validator-mariadb:
image: docker.io/library/mariadb:12-ubi
container_name: ffice-provider-validator-mariadb
ports:
- "3311:3306"
environment:
MARIADB_DATABASE: swim_ffice_provider_validator
MARIADB_USER: swim
MARIADB_PASSWORD: swim
MARIADB_ROOT_PASSWORD: root
volumes:
- ffice-provider-validator-mariadb-data:/var/lib/mysql
networks:
- swim-network
healthcheck:
test: ["CMD-SHELL", "healthcheck.sh --connect --innodb_initialized"]
interval: 15s
timeout: 5s
retries: 5
start_period: 30s
volumes:
ffice-provider-validator-mariadb-data:
networks:
swim-network:
name: swim-ffice-provider-validator-network
driver: bridge
3.5 Containerfile.jvm
Create src/main/docker/Containerfile.jvm. This file is used by compose.yml in the provider to build the validator image locally. Use the standard Red Hat UBI9/OpenJDK-21 base image — copy the exact content from section 3.5 of the Markdown tutorial.
Before running podman compose up in the provider, build the validator JAR first: cd swim-ffice-provider-validator && ../mvnw clean package -DskipTests
3.6 Domain models
FficeMessage.java
package com.github.swim_developer.validator.ffice.provider.domain.model;
public record FficeMessage(
String gufi,
String messageType,
String departureAerodrome,
String destinationAerodrome,
String aircraftIdentification,
String filingTime
) {}
HttpResult.java
package com.github.swim_developer.validator.ffice.provider.domain.model;
public record HttpResult(int statusCode, String body) {}
ReceivedMessage.java
package com.github.swim_developer.validator.ffice.provider.domain.model;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDateTime;
@Getter
@Setter
public class ReceivedMessage {
private Long id;
private String subscriptionId;
private String queueName;
private String messageId;
private String contentType;
private String messageType;
private String gufi;
private String departureAerodrome;
private String destinationAerodrome;
private String aircraftIdentification;
private String body;
private LocalDateTime receivedAt;
}
3.7 Ports
Create all port interfaces under domain/port/in/ and domain/port/out/. The full source code is provided in the Markdown tutorial (section 3.7). The key interfaces are:
ConnectionTrackerPort— manages AMQP connections per userProviderSubscriptionPort— reacts to subscription lifecycle eventsConsoleNotificationPort— logs AMQP and messaging eventsMessagePersistencePort— saves received messages to MariaDBMessagePort— queries persisted messagesConformanceHttpPort— executes mTLS HTTP requests for conformance testsConformanceTestPort— runs conformance scenarios by IDReceivedMessageRepository(out) — Panache-backed persistence port
3.8 FficeMessageExtractor.java
Detects the FF-ICE message type from the raw XML body using regex pattern matching. Supports: FILED_FLIGHT_PLAN, FILING_STATUS, FLIGHT_DEPARTURE, FLIGHT_ARRIVAL, FLIGHT_CANCELLATION, FLIGHT_PLAN_UPDATE, PLANNING_STATUS.
package com.github.swim_developer.validator.ffice.provider.application.usecase;
import com.github.swim_developer.validator.ffice.provider.domain.model.FficeMessage;
import jakarta.enterprise.context.ApplicationScoped;
import org.jboss.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ApplicationScoped
public class FficeMessageExtractor {
private static final Logger LOG = Logger.getLogger(FficeMessageExtractor.class);
private static final Pattern GUFI_PATTERN = Pattern.compile("<[^:]*:?gufi[^>]*>([^<]+)</");
public FficeMessage extract(String xmlBody) {
if (xmlBody == null || xmlBody.isBlank()) {
return new FficeMessage("UNKNOWN", "UNKNOWN", null, null, null, null);
}
try {
String messageType = detectMessageType(xmlBody);
String gufi = extractFirst(GUFI_PATTERN, xmlBody, "UNKNOWN");
return new FficeMessage(gufi, messageType, null, null, null, null);
} catch (Exception e) {
LOG.warnf("Failed to extract FF-ICE message data: %s", e.getMessage());
return new FficeMessage("UNKNOWN", "UNKNOWN", null, null, null, null);
}
}
private String detectMessageType(String xml) {
if (xml.contains("FiledFlightPlan") || xml.contains("filedFlightPlan")) return "FILED_FLIGHT_PLAN";
if (xml.contains("FilingStatus") || xml.contains("filingStatus")) return "FILING_STATUS";
if (xml.contains("FlightDeparture") || xml.contains("flightDeparture")) return "FLIGHT_DEPARTURE";
if (xml.contains("FlightArrival") || xml.contains("flightArrival")) return "FLIGHT_ARRIVAL";
if (xml.contains("FlightCancellation") || xml.contains("flightCancellation")) return "FLIGHT_CANCELLATION";
if (xml.contains("FlightPlanUpdate")|| xml.contains("flightPlanUpdate")) return "FLIGHT_PLAN_UPDATE";
if (xml.contains("PlanningStatus") || xml.contains("planningStatus")) return "PLANNING_STATUS";
return "UNKNOWN";
}
private String extractFirst(Pattern pattern, String input, String defaultValue) {
Matcher matcher = pattern.matcher(input);
return matcher.find() ? matcher.group(1) : defaultValue;
}
}
3.9 ConformanceTestService.java
Implements 9 conformance scenarios against the FF-ICE provider REST API:
| Scenario | Description |
|---|---|
API-01 | Subscribe — Happy Path (Yellow Profile REQ-0100) |
API-02 | List Subscriptions (REQ-0120) |
API-03 | Get Topics (REQ-0110) |
API-04 | Unsubscribe (REQ-0150) |
DM-01 | Required Fields in Subscription Response |
DM-02 | Initial PAUSED Status |
DM-03 | Topics Returns FficeService |
DM-04 | message_type Filter Persisted |
WFS-01 | WFS GetFeature Query |
The subscription body for FF-ICE uses the message_type filter:
{"message_type":["FILED_FLIGHT_PLAN","FLIGHT_DEPARTURE","FLIGHT_ARRIVAL"]}
3.10 Infrastructure — messaging
Four classes handle the AMQP mTLS lifecycle:
- AmqpSslConfigurator — loads PKCS12 keystore and truststore into Proton
ProtonClientOptions - UserReceiverLifecycle — creates Proton receivers per (userId, queueName), extracts FF-ICE metadata, persists to MariaDB
- UserConnectionTracker — manages connection state per user, tracks heartbeats, performs stale cleanup
- AmqpConnectionCleanupScheduler — runs
performCleanup()every 60 seconds via@Scheduled
3.11 Infrastructure — persistence
Three classes: ReceivedMessageEntity (Panache entity with gufi, messageType, departureAerodrome, destinationAerodrome, aircraftIdentification columns), ReceivedMessageMapper (static utility), ReceivedMessageRepositoryImpl.
3.12 Infrastructure — REST resources
Five endpoints:
| Path | Purpose |
|---|---|
GET /api/status | Health check and config |
POST /api/amqp/connect | Open AMQP connection for a user |
GET /api/amqp/messages/{subId} | List messages received on a subscription |
POST /api/provider/subscriptions | Proxy to provider subscription API (mTLS) |
POST /api/conformance/run/{scenarioId} | Execute a conformance test scenario |
3.13 Test
A plain JUnit 5 test (no Quarkus runtime) validates the extractor with 7 cases:
class FficeMessageExtractorTest {
private final FficeMessageExtractor extractor = new FficeMessageExtractor();
@Test void extractsFiledFlightPlanType() { ... }
@Test void extractsFlightDepartureType() { ... }
@Test void extractsFlightArrivalType() { ... }
@Test void extractsFlightCancellationType() { ... }
@Test void extractsGufi() { ... }
@Test void handlesNullBody() { ... }
@Test void handlesBlankBody() { ... }
}
3.14 Build and verify
# Install parent modules first (required for relativePath resolution)
cd applications/swim-validators
./mvnw clean install -DskipTests -q
# Build and test the validator
cd swim-ffice-provider-validator
../mvnw clean package -DskipTests
../mvnw test
Expected output:
Tests run: 7, Failures: 0, Errors: 0, Skipped: 0
BUILD SUCCESS