Step 4 of 7

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 user
  • ProviderSubscriptionPort — reacts to subscription lifecycle events
  • ConsoleNotificationPort — logs AMQP and messaging events
  • MessagePersistencePort — saves received messages to MariaDB
  • MessagePort — queries persisted messages
  • ConformanceHttpPort — executes mTLS HTTP requests for conformance tests
  • ConformanceTestPort — runs conformance scenarios by ID
  • ReceivedMessageRepository (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:

ScenarioDescription
API-01Subscribe — Happy Path (Yellow Profile REQ-0100)
API-02List Subscriptions (REQ-0120)
API-03Get Topics (REQ-0110)
API-04Unsubscribe (REQ-0150)
DM-01Required Fields in Subscription Response
DM-02Initial PAUSED Status
DM-03Topics Returns FficeService
DM-04message_type Filter Persisted
WFS-01WFS 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:

PathPurpose
GET /api/statusHealth check and config
POST /api/amqp/connectOpen AMQP connection for a user
GET /api/amqp/messages/{subId}List messages received on a subscription
POST /api/provider/subscriptionsProxy 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