Forráskód Böngészése

Refactored core, dynamic deploy, implementation of channels

Lukas Cerny 1 éve
szülő
commit
1be20d630c
34 módosított fájl, 1164 hozzáadás és 454 törlés
  1. 6 0
      Dockerfile
  2. 1 0
      build.gradle
  3. 38 0
      config/config.docker.dev.yaml
  4. 31 0
      config/config.docker.release.yaml
  5. 31 0
      config/config.local.dev.yaml
  6. 1 2
      config/config.yaml
  7. 6 1
      docker-compose.yaml
  8. 6 2
      docker.dev.env
  9. BIN
      gradle/wrapper/gradle-8.0.2-bin.zip
  10. BIN
      gradle/wrapper/gradle-wrapper.jar
  11. 1 2
      gradle/wrapper/gradle-wrapper.properties
  12. 6 2
      local.dev.env
  13. 98 51
      src/main/java/cz/senslog/messaging/app/Application.java
  14. 187 0
      src/main/java/cz/senslog/messaging/app/ChannelSubscriberDeployer.java
  15. 6 138
      src/main/java/cz/senslog/messaging/app/Configuration.java
  16. 11 0
      src/main/java/cz/senslog/messaging/app/ConfigurationType.java
  17. 7 11
      src/main/java/cz/senslog/messaging/app/EnvironmentalProperty.java
  18. 175 0
      src/main/java/cz/senslog/messaging/app/FileConfiguration.java
  19. 93 0
      src/main/java/cz/senslog/messaging/app/ServiceDeployer.java
  20. 0 51
      src/main/java/cz/senslog/messaging/app/VertxDeployer.java
  21. 4 0
      src/main/java/cz/senslog/messaging/domain/MessageLengthType.java
  22. 4 5
      src/main/java/cz/senslog/messaging/domain/ServiceDescriptor.java
  23. 34 1
      src/main/java/cz/senslog/messaging/domain/ServiceType.java
  24. 4 6
      src/main/java/cz/senslog/messaging/domain/SubscriberConfig.java
  25. 70 53
      src/main/java/cz/senslog/messaging/server/HttpVertxServer.java
  26. 61 25
      src/main/java/cz/senslog/messaging/service/AbstractService.java
  27. 36 60
      src/main/java/cz/senslog/messaging/service/EmailService.java
  28. 0 25
      src/main/java/cz/senslog/messaging/service/ServiceCreator.java
  29. 17 0
      src/main/java/cz/senslog/messaging/utils/JsonUtils.java
  30. 18 0
      src/main/java/cz/senslog/messaging/utils/PromiseSupport.java
  31. 1 1
      src/main/java/cz/senslog/messaging/utils/ResourcesUtils.java
  32. 169 18
      src/main/resources/openAPISpec.yaml
  33. 30 0
      src/main/resources/schemas/emailMessage.yaml
  34. 12 0
      src/main/resources/schemas/sensLogAlert.yaml

+ 6 - 0
Dockerfile

@@ -1,5 +1,8 @@
 FROM openjdk:17 AS builder
 
+ARG configFile
+
+COPY $configFile /app/config.yaml
 COPY src /app/src
 COPY gradle /app/gradle
 COPY build.gradle settings.gradle gradle.properties gradlew /app/
@@ -8,6 +11,7 @@ RUN ./gradlew assemble
 
 FROM openjdk:17 AS test
 
+COPY --from=builder /app/config.yaml /app/
 COPY --from=builder /app/build /app/build
 COPY --from=builder /app/gradle /app/gradle
 COPY --from=builder /app/build.gradle /app/
@@ -21,6 +25,7 @@ RUN ./gradlew test
 
 FROM openjdk:17-jdk-slim-buster AS production
 
+COPY --from=builder /app/config.yaml /app/
 COPY --from=builder /app/build/libs/ /app/
 COPY --from=builder /app/gradle.properties /app/
 
@@ -30,6 +35,7 @@ CMD java -cp "messaging.jar" cz.senslog.messaging.app.Main
 
 FROM openjdk:17-jdk-slim-buster AS dev-debug
 
+COPY --from=builder /app/config.yaml /app/
 COPY --from=builder /app/build/libs/ /app/
 COPY --from=builder /app/gradle.properties /app/
 

+ 1 - 0
build.gradle

@@ -59,6 +59,7 @@ dependencies {
     implementation 'io.vertx:vertx-core:4.5.3'
     implementation 'io.vertx:vertx-web:4.5.3'
     implementation 'io.vertx:vertx-web-openapi:4.5.3'
+    implementation 'com.noenv:vertx-jsonpath:4.5.8'
 
     implementation 'org.eclipse.angus:angus-mail:2.0.2'
 

+ 38 - 0
config/config.docker.dev.yaml

@@ -0,0 +1,38 @@
+
+subscribers:
+  # subscriber name
+  mikeEmailHandler:
+    # required for subscriber definition
+    serviceId: lspEmailService
+    # required message structure for the defined service (validation by schema defined by service)
+    recipients:
+      - email: "dev@lukascerny.me"
+        hideEmail: true
+      - email: "lcerny@lesprojekt.cz"
+        hideEmail: false
+
+
+services:
+  # service name
+  lspEmailService:
+    # required for service definition
+    type: EMAIL
+    schemaResourceFile: 'schemas/emailMessage.yaml'
+    # required for EMAIL service configuration
+    senderEmail: "watchdog@senslog.org"
+    smtpHost: "mail.lesprojekt.cz"
+    smtpPort: 465
+    authUsername: "watchdog@senslog.org"
+    authPassword: "5jspdD"
+
+channels:
+  # channel name
+  analytics_alert:
+    # required for channel definition
+    schemaResourceFile: 'schemas/sensLogAlert.yaml'
+    subscribers:
+      - id: mikeEmailHandler
+        messageTransform:
+          - $.title: "$.subject"
+          - $.message: "$.body"
+

+ 31 - 0
config/config.docker.release.yaml

@@ -0,0 +1,31 @@
+
+subscribers:
+  mikeEmailHandler:
+    service: lspEmailService
+    senderEmail: "watchdog@senslog.org"
+    recipientEmail:
+      - email: "dev@lukascerny.me"
+        hideEmail: true
+      - email: "lcerny@lesprojekt.cz"
+        hideEmail: false
+
+
+services:
+  lspEmailService:
+    type: EMAIL
+    messageLengthSupport: [ TINY, SMALL, LARGE ]
+    smtpHost: "mail.lesprojekt.cz"
+    smtpPort: 465
+    authUsername: "watchdog@senslog.org"
+    authPassword: "5jspdD"
+
+channels:
+  analytics_alert:
+    messageType: SMALL
+    subscribers:
+      - mikeEmailHandler
+
+#    {
+#      "title": "<title_as_subject>"
+#      "message": "<message_as_body>"
+#    }

+ 31 - 0
config/config.local.dev.yaml

@@ -0,0 +1,31 @@
+
+subscribers:
+  mikeEmailHandler:
+    service: lspEmailService
+    senderEmail: "watchdog@senslog.org"
+    recipientEmail:
+      - email: "dev@lukascerny.me"
+        hideEmail: true
+      - email: "lcerny@lesprojekt.cz"
+        hideEmail: false
+
+
+services:
+  lspEmailService:
+    type: EMAIL
+    messageLengthSupport: [ TINY, SMALL, LARGE ]
+    smtpHost: "mail.lesprojekt.cz"
+    smtpPort: 465
+    authUsername: "watchdog@senslog.org"
+    authPassword: "5jspdD"
+
+channels:
+  analytics_alert:
+    messageType: SMALL
+    subscribers:
+      - mikeEmailHandler
+
+#    {
+#      "title": "<title_as_subject>"
+#      "message": "<message_as_body>"
+#    }

+ 1 - 2
config/config.yaml

@@ -3,11 +3,10 @@ subscribers:
   lspReportHandler:
     service: lspEmailService
     senderEmail: "watchdog@senslog.org"
-    subject: "[Watchdog] Report"
     recipientEmail:
       - email: "dev@lukascerny.me"
         hideEmail: true
-      - email: "luc.cerny@gmail.com"
+      - email: "lcerny@lesprojekt.cz"
         hideEmail: false
 
   lspEmergencyHandler:

+ 6 - 1
docker-compose.yaml

@@ -7,6 +7,8 @@ services:
     build:
       target: production
       context: .
+      args:
+        configFile: config/config.docker.release.yaml
 
   messaging-dev:
     container_name: senslog_messaging_dev
@@ -14,7 +16,10 @@ services:
     build:
       target: dev-debug
       context: .
+      args:
+        configFile: config/config.docker.dev.yaml
     env_file:
       - docker.dev.env
     ports:
-      - "8080:8080"
+      - "9090:9090"
+      - "5005:5005"

+ 6 - 2
docker.dev.env

@@ -1,2 +1,6 @@
-CONFIG_FILE_PATH=./config.yaml
-SERVER_HTTP_PORT=8080
+# config type: FILE | DB
+APP_CONFIG_SOURCE_TYPE=FILE
+APP_CONFIG_FILE_PATH=/app/config.yaml
+
+# server
+SERVER_HTTP_PORT=9090

BIN
gradle/wrapper/gradle-8.0.2-bin.zip


BIN
gradle/wrapper/gradle-wrapper.jar


+ 1 - 2
gradle/wrapper/gradle-wrapper.properties

@@ -1,6 +1,5 @@
-#Sat Feb 17 18:01:39 CET 2024
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
+distributionUrl=gradle-8.0.2-bin.zip
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists

+ 6 - 2
local.dev.env

@@ -1,2 +1,6 @@
-CONFIG_FILE_PATH=./config/config.yaml
-SERVER_HTTP_PORT=8080
+# config type: FILE | DB
+APP_CONFIG_SOURCE_TYPE=FILE
+APP_CONFIG_FILE_PATH=./config/config.local.dev.yaml
+
+# server
+SERVER_HTTP_PORT=9090

+ 98 - 51
src/main/java/cz/senslog/messaging/app/Application.java

@@ -1,12 +1,11 @@
 package cz.senslog.messaging.app;
 
 
-import cz.senslog.messaging.domain.ServiceConfig;
-import cz.senslog.messaging.domain.SubscriberConfig;
 import cz.senslog.messaging.server.HttpVertxServer;
-import io.vertx.core.AbstractVerticle;
+import cz.senslog.messaging.utils.PromiseSupport;
+import cz.senslog.messaging.utils.Tuple;
+import io.vertx.core.*;
 import io.vertx.core.DeploymentOptions;
-import io.vertx.core.Vertx;
 import io.vertx.core.json.JsonArray;
 import io.vertx.core.json.JsonObject;
 import org.apache.logging.log4j.LogManager;
@@ -16,13 +15,11 @@ import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.time.Duration;
-import java.util.ArrayList;
 import java.util.List;
-import java.util.Map;
 import java.util.Properties;
 
-import static cz.senslog.messaging.service.ServiceCreator.createServices;
-import static cz.senslog.messaging.utils.ServiceUtil.createServiceSubscriberId;
+import static cz.senslog.messaging.app.ChannelSubscriberDeployer.CHANNEL_PATH_TO_DEPLOY;
+import static cz.senslog.messaging.app.ServiceDeployer.SERVICE_PATH_TO_DEPLOY;
 
 
 public final class Application {
@@ -69,59 +66,109 @@ public final class Application {
     public static void start() {
         logger.info("Starting app '{}', version '{}', build '{}'.", PROJECT_NAME, COMPILED_VERSION, BUILD_VERSION);
 
-        EnvironmentalProperty envProperties = EnvironmentalProperty.getInstance();
+        final EnvironmentalProperty envProperties = EnvironmentalProperty.getInstance();
 
-        Configuration configuration;
+        final Configuration configuration;
         try {
-            configuration = Configuration.createFromYaml(envProperties.getConfigPath());
-        } catch (IOException e) {
+            switch (envProperties.getConfigurationType()) {
+                case FILE -> configuration = FileConfiguration.createFromYaml(envProperties.getConfigFilePath());
+                default -> throw new IllegalStateException("Configuration type not supported: " + envProperties.getConfigurationType());
+            }
+        } catch (Exception e) {
             terminate(e.getMessage()); return;
         }
 
-        JsonArray channels = new JsonArray(configuration.getChannelsById().values().stream().map(ch -> JsonObject.of(
-                "id", ch.id(),
-                "maxLength", ch.messageType().maxLength(),
-                "subscribers", new JsonArray(ch.subscribers().stream()
-                        .map(s -> JsonObject.of(
-                                "id", createServiceSubscriberId(s.service().id(), s.id()),
-                                "serviceId", s.service().id(),
-                                "subscriberId", s.id()
-                        )).toList())
-        )).toList());
-
-        JsonObject services = new JsonObject();
-        for (ServiceConfig service : configuration.getServicesById().values()) {
-            services.put(service.id(), JsonObject.of(
-                    "config", service.config(),
-                    "subscribedRecipients", new JsonArray()
-            ));
-        }
-        for (SubscriberConfig subscriber : configuration.getSubscribersById().values()) {
-            services.getJsonObject(subscriber.service().id()).getJsonArray("subscribedRecipients").add(JsonObject.of(
-                    "id", subscriber.id(),
-                    "config", subscriber.config()
-            ));
-        }
-
-        DeploymentOptions opt = new DeploymentOptions()
-                .setConfig(envProperties.asJson().mergeIn(JsonObject.of(
-                        "channels", channels,
-                        "services", services
-                )));
+        Vertx vertx = Vertx.vertx();
+        deployServices(vertx, configuration).compose(tupleServices -> {
+            final String serviceDeploymentId = tupleServices.item1();
+            final List<JsonObject> services = tupleServices.item2();
+            services.forEach(r ->
+                    logger.info("Deployment '{}#{}' was successful.", r.getString("serviceId"), r.getString("deploymentId"))
+            );
+            return deployChannelSubscribers(vertx, configuration, serviceDeploymentId).map(t -> Tuple.of(serviceDeploymentId, t));
+        }).compose(tuple -> {
+            final String serviceDeploymentId = tuple.item1();
+            final Tuple<String, Object> tupleChannels = tuple.item2();
+            final String channelDeploymentId = tupleChannels.item1();
+            final Object channels = tupleChannels.item2();
+            return deployHttpServer(vertx, envProperties.getHttpServerPort(), serviceDeploymentId, channelDeploymentId);
+        }).onSuccess(httpServerDeploymentId -> {
+            logger.info("Http Server deployment '{}'", httpServerDeploymentId);
+            logger.info("Started in {} second.", uptime() / 1000.0);
+        }).onFailure(f -> {
+            logger.catching(f);
+            terminate(f.getMessage());
+        });
+    }
 
-        List<AbstractVerticle> verticles = new ArrayList<>();
+    private static Future<String> deployHttpServer(Vertx vertx, int httpPort, String serviceDeploymentId, String channelDeploymentId) {
+        final Promise<String> deployPromise = Promise.promise();
+        vertx.deployVerticle(HttpVertxServer.class, new DeploymentOptions()
+                .setThreadingModel(ThreadingModel.WORKER)
+                .setConfig(JsonObject.of(
+                        "server.http.port", httpPort,
+                        "services.deploymentId", serviceDeploymentId,
+                        "channels.deploymentId", channelDeploymentId
+                ))
+                .setInstances(1), PromiseSupport.onComplete(deployPromise));
+        return deployPromise.future();
+    }
 
-        verticles.add(new HttpVertxServer());
-        verticles.addAll(createServices(configuration.getServicesById().values()));
+    private static Future<Tuple<String, Object>> deployChannelSubscribers(Vertx vertx, Configuration configuration, String serviceDeploymentId) {
+        final Promise<Tuple<String, Object>> deployPromise = Promise.promise();
+        final DeploymentOptions deploymentOptions = new DeploymentOptions()
+                .setThreadingModel(ThreadingModel.WORKER)
+                .setConfig(JsonObject.of("services.deploymentId", serviceDeploymentId))
+                .setInstances(1);
+
+        vertx.deployVerticle(ChannelSubscriberDeployer.class, deploymentOptions, res -> {
+            if (res.succeeded()) {
+                final String channelDeploymentId = res.result();
+                final String channelDeployerPath = CHANNEL_PATH_TO_DEPLOY.apply(channelDeploymentId);
+                configuration.channelDescriptors().onFailure(deployPromise::fail)
+                        .onSuccess(descriptors -> vertx.eventBus().<JsonArray>request(channelDeployerPath, new JsonArray(descriptors), replay -> {
+                            if (replay.succeeded()) {
+                                deployPromise.complete(Tuple.of(channelDeploymentId, replay.result()));
+                            } else {
+                                deployPromise.fail(res.cause());
+                            }
+                        }));
+            } else {
+                deployPromise.fail(res.cause());
+            }
+        });
+        return deployPromise.future();
+    }
 
-        Vertx.vertx().deployVerticle(VertxDeployer.deploy(verticles.toArray(AbstractVerticle[]::new)), opt, res -> {
-            if(res.succeeded()) {
-                logger.info("Deployment id is: {}", res.result());
-                logger.info("Started in {} second.", uptime() / 1000.0);
+    private static Future<Tuple<String, List<JsonObject>>> deployServices(Vertx vertx, Configuration configuration) {
+        final Promise<Tuple<String, List<JsonObject>>> deployPromise = Promise.promise();
+        final DeploymentOptions deploymentOptions = new DeploymentOptions()
+                .setThreadingModel(ThreadingModel.WORKER)
+                .setInstances(1);
+
+        vertx.deployVerticle(ServiceDeployer.class, deploymentOptions, res -> {
+            if (res.succeeded()) {
+                final String serviceDeploymentId = res.result();
+                configuration.serviceDescriptors().onFailure(deployPromise::fail) // fail of retrieving service's descriptors
+                        .onSuccess(descriptors -> Future.all(descriptors.stream().map(d -> {
+                                    final Promise<JsonObject> promise = Promise.promise();
+                                    final String serviceDeployerPath = SERVICE_PATH_TO_DEPLOY.apply(serviceDeploymentId);
+                                    vertx.eventBus().<JsonObject>request(serviceDeployerPath, d, replay -> {
+                                        if (replay.succeeded()) {
+                                            promise.complete(replay.result().body());
+                                        } else {
+                                            promise.fail(replay.cause()); // fail of service deploy
+                                        }
+                                    });
+                                    return promise.future();
+                                }).toList())
+                                .onSuccess(compositeRes -> deployPromise.complete(Tuple.of(serviceDeploymentId, compositeRes.list()
+                                        .stream().map(JsonObject.class::cast).toList())))
+                                .onFailure(deployPromise::fail)); // fail of one and more services
             } else {
-                logger.error("Deployment failed! The reason is '{}'", res.cause().getMessage());
-                logger.catching(res.cause());
+                deployPromise.fail(res.cause()); // fail of ServiceDeployer
             }
         });
+        return deployPromise.future();
     }
 }

+ 187 - 0
src/main/java/cz/senslog/messaging/app/ChannelSubscriberDeployer.java

@@ -0,0 +1,187 @@
+package cz.senslog.messaging.app;
+
+import com.noenv.jsonpath.JsonPath;
+import cz.senslog.messaging.domain.ServiceType;
+import cz.senslog.messaging.service.AbstractService;
+import cz.senslog.messaging.utils.PromiseSupport;
+import io.vertx.core.AbstractVerticle;
+import io.vertx.core.Future;
+import io.vertx.core.Promise;
+import io.vertx.core.json.JsonArray;
+import io.vertx.core.json.JsonObject;
+import io.vertx.json.schema.*;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.util.*;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import static io.vertx.json.schema.common.dsl.Schemas.*;
+import static io.vertx.json.schema.common.dsl.Schemas.objectSchema;
+
+public class ChannelSubscriberDeployer extends AbstractVerticle {
+
+    private static final Logger logger = LogManager.getLogger(ChannelSubscriberDeployer.class);
+
+    public static final Function<String, String> CHANNEL_PATH_TO_DEPLOY = (deploymentID) -> ("___"+deploymentID+"#deploy");
+
+    public static final Function<String, String> CHANNEL_PATH_TO_LIST = (deploymentID) -> ("___"+deploymentID+"#channels");
+
+    public static final Function<String, String> CHANNEL_PATH_TO_SEND = (deploymentID) -> ("___"+deploymentID+"#send");
+
+    public static final Function<String, String> CHANNEL_PATH_TO_DESCRIPTOR = (deploymentID) -> ("___"+deploymentID+"#descriptor");
+
+    private static final JsonSchemaOptions JSON_SCHEMA_OPTIONS;
+
+    private static final Validator channelDescriptionValidator;
+
+    private String serviceDeploymentId;
+
+    private Map<String, ChannelDescriptor> registeredChannels = new HashMap<>();
+
+    static {
+        JSON_SCHEMA_OPTIONS = new JsonSchemaOptions()
+                .setBaseUri("http://localhost:8080")
+                .setDraft(Draft.DRAFT7);
+
+        final JsonObject schemaJson = arraySchema().items(objectSchema()
+                    .requiredProperty("id", stringSchema())
+                    .requiredProperty("schema", objectSchema())
+                    .requiredProperty("subscribers", arraySchema().items(objectSchema()
+                            .requiredProperty("serviceId", stringSchema())
+                            .requiredProperty("messageTransform", arraySchema().items(objectSchema())))
+                    ))
+                .toJson();
+
+        channelDescriptionValidator = Validator.create(JsonSchema.of(schemaJson), JSON_SCHEMA_OPTIONS) ;
+    }
+
+    private record ChannelDescriptor(String id, JsonObject messageSchema, Validator messageValidator, List<ServiceHandler> handlers) {}
+
+    private record ServiceHandler(String serviceId, ServiceType serviceType, JsonObject subscriberInfo, JsonArray transformRules) {
+        public JsonObject transformMessage(JsonObject message) {
+            final JsonObject copySubscriber = subscriberInfo.copy();
+            transformRules.stream().map(JsonObject.class::cast).forEach(rule -> {
+                for (String fieldName : rule.fieldNames()) {
+                    JsonPath.put(copySubscriber, rule.getString(fieldName), JsonPath.getString(message, fieldName));
+                }
+            });
+            return copySubscriber;
+        }
+    }
+
+    @Override
+    public void start(Promise<Void> startPromise) {
+        this.serviceDeploymentId = config().getString("services.deploymentId");
+        Future.all(registerEventBusDeploy(), registerEventBusListChannels(), registerEventBusSend(), registerEventBusDescriptor())
+                .onSuccess(s -> startPromise.complete()).onFailure(startPromise::fail);
+    }
+
+    private Future<Void> registerEventBusDescriptor() {
+        final Promise<Void> deployPromise = Promise.promise();
+        vertx.eventBus().<JsonObject>consumer(CHANNEL_PATH_TO_DESCRIPTOR.apply(deploymentID()), msg -> {
+            final String channelId = msg.headers().get("channelId");
+            final ChannelDescriptor channelDescriptor = registeredChannels.get(channelId);
+            if (channelDescriptor == null) {
+                msg.fail(204, "No subscribers for the channel <" + channelId + ">.");
+            } else {
+                msg.reply(JsonObject.of(
+                        "channelId", channelDescriptor.id(),
+                        "schema", channelDescriptor.messageSchema(),
+                        "subscribers", new JsonArray(channelDescriptor.handlers.stream().map(s -> JsonObject.of(
+                                "serviceId", s.serviceId(),
+                                "serviceType", s.serviceType()
+                        )).toList())
+                ));
+            }
+        }).completionHandler(PromiseSupport.onComplete(deployPromise));
+        return deployPromise.future();
+    }
+
+    private Future<Void> registerEventBusSend() {
+        final Promise<Void> deployPromise = Promise.promise();
+        vertx.eventBus().<JsonObject>consumer(CHANNEL_PATH_TO_SEND.apply(deploymentID()), msg -> {
+            final String channelId = msg.headers().get("channelId");
+            final JsonObject channelMessage = msg.body();
+            final ChannelDescriptor channelDescriptor = registeredChannels.get(channelId);
+            if (channelDescriptor == null) {
+                msg.fail(204, "No subscribers for the channel <" + channelId + ">.");
+            } else {
+                final OutputUnit validationRes = channelDescriptor.messageValidator().validate(channelMessage);
+                if (validationRes.getValid()) {
+                    channelDescriptor.handlers.forEach(h -> vertx.eventBus().
+                                    <JsonObject>request(AbstractService.EVENT_BUS_PATH_TO_SEND.apply(h.serviceId()), h.transformMessage(channelMessage), reply -> {
+                                if (reply.succeeded()) {
+                                    logger.info("Message sent to the channel <" + channelId + "> with the result: " + reply.result().body());
+                                } else {
+                                    logger.error("Message sent to the channel <" + channelId + "> failed: " + reply.cause());
+                                }
+                            })
+                    );
+                    msg.reply(JsonObject.of("message", "Accepted"));
+                } else {
+                    msg.fail(422, "Unprocessable message: " + validationRes.getError());
+                }
+            }
+
+        }).completionHandler(PromiseSupport.onComplete(deployPromise));
+        return deployPromise.future();
+    }
+
+    private Future<Void> registerEventBusListChannels() {
+        final Promise<Void> deployPromise = Promise.promise();
+        vertx.eventBus().<JsonArray>consumer(CHANNEL_PATH_TO_LIST.apply(deploymentID()), msg -> {
+            msg.reply(new JsonArray(registeredChannels.keySet().stream().map(channelId -> JsonObject.of(
+                    "name", channelId
+            )).toList()));
+        }).completionHandler(PromiseSupport.onComplete(deployPromise));
+        return deployPromise.future();
+    }
+
+    private Future<Void> registerEventBusDeploy() {
+        final Promise<Void> deployPromise = Promise.promise();
+        vertx.eventBus().<JsonArray>consumer(CHANNEL_PATH_TO_DEPLOY.apply(deploymentID()), msg -> {
+            final JsonArray descriptors = msg.body();
+            final OutputUnit vRes = channelDescriptionValidator.validate(descriptors);
+            if (!vRes.getValid()) {
+                msg.fail(400, vRes.getError());
+            } else {
+                vertx.eventBus().<JsonArray>request(ServiceDeployer.SERVICE_PATH_TO_LIST.apply(serviceDeploymentId), JsonArray.of(), reply -> {
+                   if (reply.failed()) {
+                       msg.fail(500, reply.cause().getMessage());
+                   } else {
+                       final Map<String, JsonObject> services = reply.result().body().stream().map(JsonObject.class::cast)
+                               .collect(Collectors.toMap(k -> k.getString("serviceId"), Function.identity()));
+                       final Map<String, JsonObject> channels = descriptors.stream().map(JsonObject.class::cast)
+                               .collect(Collectors.toMap(k -> k.getString("id"), Function.identity()));
+                       this.registeredChannels = mappingChannelsToServices(channels, services);
+                       msg.reply(new JsonArray(registeredChannels.keySet().stream().toList()));
+                   }
+                });
+            }
+        }).completionHandler(PromiseSupport.onComplete(deployPromise));
+        return deployPromise.future();
+    }
+
+    private Map<String, ChannelDescriptor> mappingChannelsToServices(Map<String, JsonObject> channels, Map<String, JsonObject> services) {
+        final Map<String, ChannelDescriptor> registeredChannels = new HashMap<>(channels.size());
+        for (Map.Entry<String, JsonObject> channelEntry : channels.entrySet()) {
+            final String channelId = channelEntry.getKey();
+            final JsonObject channelDescriptor = channelEntry.getValue();
+            final JsonObject channelSchema = channelDescriptor.getJsonObject("schema");
+            final JsonArray channelSubscribers = channelDescriptor.getJsonArray("subscribers");
+            final List<ServiceHandler> serviceHandlers = new ArrayList<>(channelSubscribers.size());
+            final Validator channelValidator = Validator.create(JsonSchema.of(channelSchema), JSON_SCHEMA_OPTIONS);
+            registeredChannels.put(channelId, new ChannelDescriptor(channelId, channelSchema, channelValidator, serviceHandlers));
+            channelSubscribers.stream().map(JsonObject.class::cast).forEach(subscriber -> {
+                final String serviceId = subscriber.getString("serviceId");
+                final JsonArray messageTransform = subscriber.getJsonArray("messageTransform");
+                final JsonObject serviceDescriptor = services.get(serviceId);
+                final ServiceType serviceType = ServiceType.of(serviceDescriptor.getString("type"));
+                serviceHandlers.add(new ServiceHandler(serviceId, serviceType, subscriber, messageTransform));
+            });
+        }
+        return registeredChannels;
+    }
+}

+ 6 - 138
src/main/java/cz/senslog/messaging/app/Configuration.java

@@ -1,147 +1,15 @@
 package cz.senslog.messaging.app;
 
-import cz.senslog.messaging.domain.*;
-import cz.senslog.messaging.server.HttpVertxServer;
-import io.vertx.core.json.JsonArray;
+import io.vertx.core.Future;
 import io.vertx.core.json.JsonObject;
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
-import org.yaml.snakeyaml.Yaml;
 
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.io.InputStream;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.*;
+import java.util.List;
 
-import static java.util.stream.Collectors.toSet;
+public interface Configuration {
 
-public final class Configuration {
+    Future<List<JsonObject>> serviceDescriptors();
 
-    private static final Logger logger = LogManager.getLogger(HttpVertxServer.class);
+    Future<List<JsonObject>> channelDescriptors();
 
-    public static Configuration createFromYaml(String path) throws IOException {
-        logger.info("Loading '{}' configuration file.", path);
-
-        if (!path.toLowerCase().endsWith(".yaml")) {
-            throw new IllegalArgumentException(path + "does not contain .yaml extension.");
-        }
-
-        Path filePath = Paths.get(path);
-        if (Files.notExists(filePath)) {
-            throw new FileNotFoundException(path + " does not exist");
-        }
-
-        Map<String, Object> properties;
-
-        logger.debug("Opening the file '{}'.", path);
-        try (InputStream fileStream = Files.newInputStream(filePath)) {
-            logger.debug("Parsing the yaml file '{}'.", path);
-            properties = new Yaml().load(fileStream);
-            logger.debug("The configuration yaml file '{}' was parsed successfully.", path);
-        } catch (IOException e) {
-            throw new RuntimeException(e);
-        }
-
-        if (properties == null || properties.isEmpty()) {
-            throw new IOException(String.format(
-                    "The configuration yaml file %s is empty or was not loaded successfully. ", path
-            ));
-        }
-
-        return new Configuration(
-                toJsonObject(properties, "subscribers"),
-                toJsonObject(properties, "services"),
-                toJsonObject(properties, "channels")
-        );
-    }
-
-    private final Map<String, ServiceConfig> services;
-    private final Map<String, SubscriberConfig> subscribers;
-    private final Map<String, ChannelConfig> channels;
-
-    private Configuration(JsonObject subscribers, JsonObject services, JsonObject channels) {
-        this.services = new HashMap<>(services.fieldNames().size());
-        this.subscribers = new HashMap<>(subscribers.fieldNames().size());
-        this.channels = new HashMap<>(channels.size());
-
-        for (String channelId : channels.fieldNames()) {
-
-            JsonObject chConfig = channels.getJsonObject(channelId);
-            JsonArray chSubscribers = chConfig.getJsonArray("subscribers");
-
-            List<SubscriberConfig> channelSubscribers = new ArrayList<>(chSubscribers.size());
-            ChannelConfig channel = ChannelConfig.of(channelId, MessageLengthType.valueOf(chConfig.getString("messageType")), channelSubscribers);
-            this.channels.put(channelId, channel);
-
-            for (Object chSubscriber : chSubscribers) {
-                String chSubscriberId = chSubscriber.toString();
-                String serviceId;
-                if (this.subscribers.containsKey(chSubscriberId)) {
-                    serviceId = this.subscribers.get(chSubscriberId).service().id();
-                } else if (subscribers.containsKey(chSubscriberId)) {
-                    serviceId = subscribers.getJsonObject(chSubscriberId).getString("service");
-                } else {
-                    throw new IllegalArgumentException(String.format("The subscriber '%s' does not exist.", chSubscriberId));
-                }
-
-                ServiceConfig service;
-                if (this.services.containsKey(serviceId)) {
-                    service = this.services.get(serviceId);
-                } else if (services.containsKey(serviceId)) {
-                    JsonObject serviceJson = services.getJsonObject(serviceId);
-                    service = ServiceConfig.of(serviceId,
-                            ServiceType.valueOf(serviceJson.getString("type")),
-                            serviceJson.getJsonArray("messageLengthSupport").stream().map(String.class::cast).map(MessageLengthType::valueOf).collect(toSet()),
-                            serviceJson
-                    );
-                    this.services.put(serviceId, service);
-                } else {
-                    throw new IllegalArgumentException(String.format("The service '%s' does not exist.", serviceId));
-                }
-
-                if (!service.messageLengthSupport().contains(channel.messageType())) {
-                    throw new IllegalArgumentException(String.format("Service '%s' does not support channel' message type upon the '%s'.", service.id(), channel.id()));
-                }
-
-                SubscriberConfig subscriberConfig = this.subscribers.get(chSubscriberId);
-                if (subscriberConfig == null) {
-                    subscriberConfig = SubscriberConfig.of(chSubscriberId,
-                            service, subscribers.getJsonObject(chSubscriberId)
-                    );
-                    this.subscribers.put(chSubscriberId, subscriberConfig);
-                }
-                channelSubscribers.add(subscriberConfig);
-            }
-        }
-    }
-
-    public Map<String, ServiceConfig> getServicesById() {
-        return services;
-    }
-
-    public Map<String, SubscriberConfig> getSubscribersById() {
-        return subscribers;
-    }
-
-    public Map<String, ChannelConfig> getChannelsById() {
-        return channels;
-    }
-
-    private static JsonObject toJsonObject(Map<String, Object> properties, String attribute) {
-        Object mapAsObject = properties.get(attribute);
-        if (mapAsObject == null) {
-            throw new IllegalArgumentException(String.format("Attribute '%s' does not exist.", attribute));
-        }
-
-        JsonObject property = new JsonObject();
-        if (mapAsObject instanceof Map<?, ?> map) {
-            for (Map.Entry<?, ?> entry : map.entrySet()) {
-                property.put(entry.getKey().toString(), entry.getValue());
-            }
-        }
-        return property;
-    }
+    Future<List<JsonObject>> channelSubscriptions();
 }

+ 11 - 0
src/main/java/cz/senslog/messaging/app/ConfigurationType.java

@@ -0,0 +1,11 @@
+package cz.senslog.messaging.app;
+
+public enum ConfigurationType {
+    FILE, DB
+
+    ;
+    public static ConfigurationType of(String configurationType) {
+        if (configurationType == null) { return null; }
+        return valueOf(configurationType.toUpperCase());
+    }
+}

+ 7 - 11
src/main/java/cz/senslog/messaging/app/EnvironmentalProperty.java

@@ -1,7 +1,5 @@
 package cz.senslog.messaging.app;
 
-import io.vertx.core.json.JsonObject;
-
 import java.util.Objects;
 
 public final class EnvironmentalProperty {
@@ -14,20 +12,18 @@ public final class EnvironmentalProperty {
         return INSTANCE;
     }
 
-    public int getServerPort() {
+    public int getHttpServerPort() {
         String portStr = System.getenv("SERVER_HTTP_PORT");
         return portStr != null ? Integer.parseInt(portStr) : 8080;
     }
 
-    public String getConfigPath() {
-        String configFilePath = System.getenv("CONFIG_FILE_PATH");
-        return Objects.requireNonNull(configFilePath, "System environmental variable 'CONFIG_FILE_PATH' is not set.");
+    public ConfigurationType getConfigurationType() {
+        String typeStr = System.getenv("APP_CONFIG_SOURCE_TYPE");
+        return Objects.requireNonNull(ConfigurationType.of(typeStr), "System environmental variable 'APP_CONFIG_SOURCE_TYPE' is not set.");
     }
 
-    public JsonObject asJson() {
-        return JsonObject.of(
-                "app.config.path", getConfigPath(),
-                "server.http.port", getServerPort()
-        );
+    public String getConfigFilePath() {
+        String configFilePath = System.getenv("APP_CONFIG_FILE_PATH");
+        return Objects.requireNonNull(configFilePath, "System environmental variable 'APP_CONFIG_FILE_PATH' is not set.");
     }
 }

+ 175 - 0
src/main/java/cz/senslog/messaging/app/FileConfiguration.java

@@ -0,0 +1,175 @@
+package cz.senslog.messaging.app;
+
+import cz.senslog.messaging.utils.JsonUtils;
+import io.vertx.core.Future;
+import io.vertx.core.json.JsonArray;
+import io.vertx.core.json.JsonObject;
+import org.yaml.snakeyaml.Yaml;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.*;
+
+import static java.nio.file.Files.newInputStream;
+
+public class FileConfiguration implements Configuration {
+
+    private final Map<String, Object> config;
+
+    public static FileConfiguration createFromYaml(final String yamlPath) throws IOException {
+        if (!yamlPath.toLowerCase().endsWith(".yaml")) {
+            throw new IllegalArgumentException(yamlPath + "does not contain .yaml extension.");
+        }
+        return createFromYaml(Paths.get(yamlPath));
+    }
+
+    public static FileConfiguration createFromYaml(final Path configFilePath) throws IOException {
+        if (Files.notExists(configFilePath)) {
+            throw new FileNotFoundException(configFilePath + " does not exist");
+        }
+
+        try (InputStream fileStream = newInputStream(configFilePath)) {
+            return createFromStream(fileStream);
+        }
+    }
+
+    public static FileConfiguration createFromStream(InputStream stream) throws IOException {
+        Map<String, Object> properties = new Yaml().load(stream);
+        if (properties == null || properties.isEmpty()) {
+            throw new IOException("The configuration file is empty or was not loaded successfully. ");
+        }
+        return new FileConfiguration(properties);
+    }
+
+    private FileConfiguration(Map<String, Object> config) {
+        Objects.requireNonNull(config);
+        this.config = config;
+    }
+
+    @Override
+    public Future<List<JsonObject>> serviceDescriptors() {
+        if (!config.containsKey("services")) {
+            return Future.failedFuture("Missing 'services' attribute.");
+        }
+
+        List<JsonObject> serviceDescriptors = new ArrayList<>();
+        Object services = config.get("services");
+        if (services instanceof Map<?, ?> map) {
+            for (Map.Entry<?, ?> entry : map.entrySet()) {
+                String serviceId = entry.getKey().toString();
+                Object descriptor = entry.getValue();
+                if (descriptor instanceof Map<?, ?> descriptorMap) {
+
+                    if (!descriptorMap.containsKey("schemaResourceFile")) {
+                        return Future.failedFuture("Missing '"+serviceId+"#schemaResourceFile' attribute.");
+                    }
+                    String schema = descriptorMap.remove("schemaResourceFile").toString();
+                    JsonObject schemaJson;
+                    try (InputStream schemaStream = ClassLoader.getSystemResourceAsStream(schema)) {
+                        Map<String, Object> properties = new Yaml().load(schemaStream);
+                        schemaJson = new JsonObject(properties);
+                    } catch (IOException e) {
+                        return Future.failedFuture(e);
+                    }
+
+                    JsonObject serviceConfig = new JsonObject();
+                    serviceConfig.put("id", serviceId);
+                    for (Map.Entry<?, ?> descriptorEntry : descriptorMap.entrySet()) {
+                        serviceConfig.put(descriptorEntry.getKey().toString(), descriptorEntry.getValue());
+                    }
+                    serviceConfig.put("schema", schemaJson);
+
+                    serviceDescriptors.add(serviceConfig);
+                }
+            }
+        }
+        return Future.succeededFuture(serviceDescriptors);
+    }
+
+    @Override
+    public Future<List<JsonObject>> channelDescriptors() {
+        if (!config.containsKey("channels")) {
+            return Future.failedFuture("Missing 'channels' attribute.");
+        }
+
+        Object channels = config.get("channels");
+        if (!(channels instanceof Map<?, ?> channelMap)) {
+            return Future.failedFuture("Wrong format of the 'channels' attribute.");
+        }
+
+        if (!config.containsKey("subscribers")) {
+            return Future.failedFuture("Missing 'subscribers' attribute.");
+        }
+
+        Object subscribers = config.get("subscribers");
+        if (!(subscribers instanceof Map<?, ?>)) {
+            return Future.failedFuture("Wrong format of the 'subscribers' attribute.");
+        }
+
+        Map<String, JsonObject> subscriberMap = parseSubscribers((Map<?, ?>) subscribers);
+
+        List<JsonObject> channelDescriptors = new ArrayList<>();
+        for (Map.Entry<?, ?> channelEntry : channelMap.entrySet()) {
+            String channelId = channelEntry.getKey().toString();
+            Object descriptor = channelEntry.getValue();
+            if (descriptor instanceof Map<?, ?> descriptorMap) {
+                if (!descriptorMap.containsKey("schemaResourceFile")) {
+                    return Future.failedFuture("Missing '"+channelId+"#schemaResourceFile' attribute.");
+                }
+                String schema = descriptorMap.remove("schemaResourceFile").toString();
+                JsonObject schemaJson;
+                try (InputStream schemaStream = ClassLoader.getSystemResourceAsStream(schema)) {
+                    Map<String, Object> properties = new Yaml().load(schemaStream);
+                    schemaJson = new JsonObject(properties);
+                } catch (IOException e) {
+                    return Future.failedFuture(e);
+                }
+
+                if (!descriptorMap.containsKey("subscribers")) {
+                    return Future.failedFuture("Missing '"+channelId+"#subscribers' attribute.");
+                }
+                if (descriptorMap.get("subscribers") instanceof List<?> subscriberList) {
+                    JsonArray channelSubscribers = new JsonArray();
+                    for (Object subscriberItem : subscriberList) {
+                        if (subscriberItem instanceof Map<?, ?> subscriberInfoMap) {
+                            JsonObject subscriberInfoJson = JsonUtils.mapToJsonObject(subscriberInfoMap);
+                            String subscriberId = subscriberInfoJson.getString("id");
+                            if (subscriberMap.containsKey(subscriberId)) {
+                                JsonObject subscriberDescriptor = subscriberMap.get(subscriberId).copy();
+                                channelSubscribers.add(subscriberDescriptor.mergeIn(subscriberInfoJson));
+                            }
+                        }
+                    }
+                    channelDescriptors.add(JsonObject.of(
+                            "id", channelId,
+                            "schema", schemaJson,
+                            "subscribers", channelSubscribers
+                    ));
+                }
+            }
+        }
+
+        return Future.succeededFuture(channelDescriptors);
+    }
+
+    private Map<String, JsonObject> parseSubscribers(Map<?, ?> subscriberMap) {
+        Map<String, JsonObject> subscribers = new HashMap<>(subscriberMap.size());
+        for (Map.Entry<?, ?> subEntry : subscriberMap.entrySet()) {
+            String subscriberId = subEntry.getKey().toString();
+            Object subInfo = subEntry.getValue();
+            if (subInfo instanceof Map<?, ?> subInfoMap) {
+                subscribers.put(subscriberId, JsonUtils.mapToJsonObject(subInfoMap));
+            }
+        }
+        return subscribers;
+    }
+
+    @Override
+    public Future<List<JsonObject>> channelSubscriptions() {
+        return null;
+    }
+}

+ 93 - 0
src/main/java/cz/senslog/messaging/app/ServiceDeployer.java

@@ -0,0 +1,93 @@
+package cz.senslog.messaging.app;
+
+import cz.senslog.messaging.domain.ServiceType;
+import cz.senslog.messaging.utils.PromiseSupport;
+import io.vertx.core.*;
+import io.vertx.core.DeploymentOptions;
+import io.vertx.core.json.JsonArray;
+import io.vertx.core.json.JsonObject;
+import io.vertx.json.schema.*;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Function;
+
+import static io.vertx.json.schema.common.dsl.Schemas.*;
+
+public final class ServiceDeployer extends AbstractVerticle {
+
+    public static final Function<String, String> SERVICE_PATH_TO_DEPLOY = (deploymentID) -> ("___"+deploymentID+"#deploy");
+
+    public static final Function<String, String> SERVICE_PATH_TO_LIST = (deploymentID) -> ("___"+deploymentID+"#services");
+
+    private static final Validator serviceDescriptionValidator;
+
+    private final Map<String, JsonObject> registeredServices = new HashMap<>();
+
+    static {
+        final JsonObject schemaJson = objectSchema()
+                .requiredProperty("id", stringSchema())
+                .requiredProperty("type", enumSchema((Object[]) ServiceType.values()))
+                .requiredProperty("schema", objectSchema())
+                .toJson();
+
+        serviceDescriptionValidator = Validator.create(JsonSchema.of(schemaJson), new JsonSchemaOptions()
+                .setBaseUri("http://localhost:8080")
+                .setDraft(Draft.DRAFT7));
+    }
+
+    @Override
+    public void start(Promise<Void> startPromise) {
+        Future.all(registerEventBusDeploy(), registerEventBusServices())
+                .onSuccess(c -> startPromise.complete()).onFailure(startPromise::fail);
+    }
+
+    private Future<Void> registerEventBusServices() {
+        final Promise<Void> compleatePromise = Promise.promise();
+        vertx.eventBus().<JsonArray>consumer(SERVICE_PATH_TO_LIST.apply(deploymentID()), msg -> msg
+                .reply(new JsonArray(registeredServices.values().stream().toList()))
+        ).completionHandler(PromiseSupport.onComplete(compleatePromise));
+        return compleatePromise.future();
+    }
+
+    private Future<Void> registerEventBusDeploy() {
+        final Promise<Void> compleatePromise = Promise.promise();
+        vertx.eventBus().<JsonObject>consumer(SERVICE_PATH_TO_DEPLOY.apply(deploymentID()), msg -> {
+            final JsonObject serviceDescriptor = msg.body();
+            final OutputUnit res = serviceDescriptionValidator.validate(serviceDescriptor);
+            if (res.getValid()) {
+                final ServiceType serviceType = ServiceType.of(serviceDescriptor.getString("type"));
+                final String serviceId = serviceDescriptor.getString("id");
+
+                final JsonSchemaOptions serviceSchemaOptions = new JsonSchemaOptions().setBaseUri("http://localhost:8080").setDraft(Draft.DRAFT7);
+                final Validator serviceConfigValidator = Validator.create(JsonSchema.of(serviceType.getSchema()), serviceSchemaOptions);
+                final OutputUnit serviceConfigValidatorResult = serviceConfigValidator.validate(serviceDescriptor);
+                if (serviceConfigValidatorResult.getValid()) {
+                    final DeploymentOptions opt = new DeploymentOptions()
+                            .setThreadingModel(ThreadingModel.WORKER)
+                            .setConfig(serviceDescriptor);
+
+                    vertx.deployVerticle(serviceType.getServiceClass(), opt, ar -> {
+                        if (ar.succeeded()) {
+                            final String deploymentId = ar.result();
+                            final JsonObject serviceInfo = JsonObject.of(
+                                    "serviceId", serviceId,
+                                    "deploymentId", deploymentId,
+                                    "type", serviceType
+                            );
+                            registeredServices.put(serviceId, serviceInfo);
+                            msg.reply(serviceInfo);
+                        } else {
+                            msg.fail(500, ar.cause().getMessage());
+                        }
+                    });
+                    } else {
+                        msg.fail(400, String.format("Invalid service '%s' configuration: %s", serviceId, serviceConfigValidatorResult.getError()));
+                    }
+            } else {
+                msg.fail(500, "Validation failed: " + res);
+            }
+        }).completionHandler(PromiseSupport.onComplete(compleatePromise));
+        return compleatePromise.future();
+    }
+}

+ 0 - 51
src/main/java/cz/senslog/messaging/app/VertxDeployer.java

@@ -1,51 +0,0 @@
-package cz.senslog.messaging.app;
-
-import io.vertx.core.*;
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
-
-import java.util.ArrayList;
-import java.util.List;
-
-public class VertxDeployer extends AbstractVerticle {
-
-    private static final Logger logger = LogManager.getLogger(VertxDeployer.class);
-
-    private final AbstractVerticle[] verticles;
-
-    private VertxDeployer(AbstractVerticle[] verticles) {
-        this.verticles = verticles;
-    }
-
-    public static VertxDeployer deploy(AbstractVerticle... verticles) {
-        return new VertxDeployer(verticles);
-    }
-
-    @Override
-    public void start(Promise<Void> startPromise) {
-        List<Future<?>> futureModules = new ArrayList<>(verticles.length);
-        for (AbstractVerticle v : verticles) {
-            DeploymentOptions options = new DeploymentOptions()
-                    .setThreadingModel(ThreadingModel.WORKER).setConfig(config());
-            futureModules.add(deployHelper(vertx, options, v));
-        }
-        Future.all(futureModules)
-                .onSuccess(v -> startPromise.complete()).onFailure(startPromise::fail);
-    }
-
-    private static Future<Void> deployHelper(Vertx vertx, DeploymentOptions options, AbstractVerticle verticle) {
-        logger.info("Deploying module: " + vertx.getClass().getName());
-        final Promise<Void> promise = Promise.promise();
-        vertx.deployVerticle(verticle, options, res -> {
-            if(res.failed()){
-                logger.error("Module '{}' was not deployed.", verticle.getClass().getSimpleName());
-                logger.catching(res.cause());
-                promise.fail(res.cause());
-            } else {
-                logger.info("Module '{}' was deployed successfully.", verticle.getClass().getSimpleName());
-                promise.complete();
-            }
-        });
-        return promise.future();
-    }
-}

+ 4 - 0
src/main/java/cz/senslog/messaging/domain/MessageLengthType.java

@@ -15,4 +15,8 @@ public enum MessageLengthType {
     public int maxLength() {
         return maxLength;
     }
+
+    public static MessageLengthType of(String type) {
+        return valueOf(type.toUpperCase());
+    }
 }

+ 4 - 5
src/main/java/cz/senslog/messaging/domain/ServiceConfig.java → src/main/java/cz/senslog/messaging/domain/ServiceDescriptor.java

@@ -2,21 +2,20 @@ package cz.senslog.messaging.domain;
 
 import io.vertx.core.json.JsonObject;
 
-import java.util.Map;
 import java.util.Set;
 
-public final class ServiceConfig {
+public final class ServiceDescriptor {
 
     private final String id;
     private final ServiceType type;
     private final Set<MessageLengthType> messageLengthSupport;
     private final JsonObject config;
 
-    public static ServiceConfig of(String id, ServiceType type, Set<MessageLengthType> messageLengthSupport, JsonObject config) {
-        return new ServiceConfig(id, type, messageLengthSupport, config);
+    public static ServiceDescriptor of(String id, ServiceType type, Set<MessageLengthType> messageLengthSupport, JsonObject config) {
+        return new ServiceDescriptor(id, type, messageLengthSupport, config);
     }
 
-    private ServiceConfig(String id, ServiceType type, Set<MessageLengthType> messageLengthSupport, JsonObject config) {
+    private ServiceDescriptor(String id, ServiceType type, Set<MessageLengthType> messageLengthSupport, JsonObject config) {
         this.id = id;
         this.type = type;
         this.messageLengthSupport = messageLengthSupport;

+ 34 - 1
src/main/java/cz/senslog/messaging/domain/ServiceType.java

@@ -1,6 +1,39 @@
 package cz.senslog.messaging.domain;
 
+import cz.senslog.messaging.service.AbstractService;
+import cz.senslog.messaging.service.EmailService;
+import io.vertx.core.json.JsonObject;
+import io.vertx.json.schema.common.dsl.ObjectSchemaBuilder;
+
+import static io.vertx.json.schema.common.dsl.Schemas.*;
+import static io.vertx.json.schema.draft7.dsl.Keywords.exclusiveMinimum;
+
 public enum ServiceType {
 
-    EMAIL, WHATSAPP, SMS
+    EMAIL       (EmailService.class, objectSchema()
+                    .requiredProperty("senderEmail", stringSchema())
+                    .requiredProperty("smtpHost", stringSchema())
+                    .requiredProperty("smtpPort", intSchema().with(exclusiveMinimum(0)))
+                    .requiredProperty("authUsername", stringSchema())
+                    .requiredProperty("authPassword", stringSchema())
+    ),
+
+    ;
+    private final JsonObject schema;
+    private final Class<? extends AbstractService> aClass;
+    ServiceType(Class<? extends AbstractService> serviceClass, ObjectSchemaBuilder schemaBuilder) {
+        this.schema = schemaBuilder.toJson();
+        this.aClass = serviceClass;
+    }
+    public JsonObject getSchema() {
+        return schema;
+    }
+
+    public Class<? extends AbstractService> getServiceClass() {
+        return aClass;
+    }
+
+    public static ServiceType of(String type) {
+        return valueOf(type.toUpperCase());
+    }
 }

+ 4 - 6
src/main/java/cz/senslog/messaging/domain/SubscriberConfig.java

@@ -2,19 +2,17 @@ package cz.senslog.messaging.domain;
 
 import io.vertx.core.json.JsonObject;
 
-import java.util.Map;
-
 public final class SubscriberConfig {
 
     private final String id;
-    private final ServiceConfig service;
+    private final ServiceDescriptor service;
     private final JsonObject config;
 
-    public static SubscriberConfig of(String id, ServiceConfig service, JsonObject config) {
+    public static SubscriberConfig of(String id, ServiceDescriptor service, JsonObject config) {
         return new SubscriberConfig(id, service, config);
     }
 
-    private SubscriberConfig(String id, ServiceConfig service, JsonObject config) {
+    private SubscriberConfig(String id, ServiceDescriptor service, JsonObject config) {
         this.id = id;
         this.service = service;
         this.config = config;
@@ -24,7 +22,7 @@ public final class SubscriberConfig {
         return id;
     }
 
-    public ServiceConfig service() {
+    public ServiceDescriptor service() {
         return service;
     }
 

+ 70 - 53
src/main/java/cz/senslog/messaging/server/HttpVertxServer.java

@@ -1,8 +1,7 @@
 package cz.senslog.messaging.server;
 
 import cz.senslog.messaging.app.Application;
-import cz.senslog.messaging.domain.ChannelConfig;
-import cz.senslog.messaging.domain.SubscriberConfig;
+import cz.senslog.messaging.app.ChannelSubscriberDeployer;
 import cz.senslog.messaging.utils.ResourcesUtils;
 import io.vertx.core.AbstractVerticle;
 import io.vertx.core.DeploymentOptions;
@@ -18,44 +17,25 @@ import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 
 import java.nio.file.Path;
-import java.util.*;
-import java.util.function.Function;
-import java.util.stream.Collectors;
 
+import static cz.senslog.messaging.app.ChannelSubscriberDeployer.*;
+import static cz.senslog.messaging.app.ServiceDeployer.SERVICE_PATH_TO_LIST;
+import static cz.senslog.messaging.service.AbstractService.EVENT_BUS_PATH_TO_DESCRIPTOR;
+import static cz.senslog.messaging.service.AbstractService.EVENT_BUS_PATH_TO_SEND;
 import static java.util.Objects.requireNonNull;
 
 public final class HttpVertxServer extends AbstractVerticle {
-    private static final Logger logger = LogManager.getLogger(HttpVertxServer.class);
 
-    private record Subscriber(String id, String serviceId, String subscriberId){
-        public static Subscriber of(JsonObject config) {
-            return new Subscriber(
-                    config.getString("id"),
-                    config.getString("serviceId"),
-                    config.getString("subscriberId")
-            );
-        }
-    }
+    private static final Logger logger = LogManager.getLogger(HttpVertxServer.class);
 
-    private record ChannelProperties(String id, int maxLength, List<Subscriber> subscribers) {
-        public static ChannelProperties of(JsonObject config) {
-            return new ChannelProperties(
-                    config.getString("id"),
-                    config.getInteger("maxLength"),
-                    config.getJsonArray("subscribers").stream().map(JsonObject.class::cast).map(Subscriber::of).toList()
-            );
-        }
-    }
 
     @Override
     public void start(Promise<Void> startPromise) {
-        Path openApiUrl = ResourcesUtils.getPath("openAPISpec.yaml");
+        final Path openApiUrl = ResourcesUtils.getResourcePath("openAPISpec.yaml");
         logger.info("Loading the OpenAPI spec from '{}'", openApiUrl);
 
-        final Map<String, ChannelProperties> channels = config().getJsonArray("channels", JsonArray.of()).stream()
-                .map(JsonObject.class::cast).map(ChannelProperties::of)
-                .collect(Collectors.toMap(ChannelProperties::id, Function.identity()));
-
+        final String servicesId = config().getString("services.deploymentId");
+        final String channelsId = config().getString("channels.deploymentId");
         RouterBuilder.create(vertx, requireNonNull(openApiUrl, "Open API Specification was not found as a resource.").toString())
                 .onSuccess(openAPIRouterBuilder -> {
                     logger.info("The OpenAPI specification was loaded successfully.");
@@ -84,30 +64,67 @@ public final class HttpVertxServer extends AbstractVerticle {
                             "uptimeMillis", Application.uptime()
                     ).encode()));
 
-                    openAPIRouterBuilder.operation("channelsGET").handler(rc -> rc.response().end(new JsonArray(channels.values().stream().map(ch -> JsonObject.of(
-                            "name", ch.id(),
-                            "maxLengthSize", ch.maxLength()
-                    )).collect(Collectors.toList())).encode()));
-
-                    openAPIRouterBuilder.operation("channelNamePOST").handler(rc -> {
-                        String channelName = rc.pathParam("name");
-                        ChannelProperties channel = Optional.ofNullable(channels.get(channelName))
-                                .orElseThrow(() -> new HttpIllegalArgumentException(404, "Channel name <%s> not found.", channelName));
-
-                        String messageEncoded = rc.body().asString();
-                        int messageLength = messageEncoded.length() / 4 * 3;
-                        if (messageLength > channel.maxLength()) {
-                            throw new HttpIllegalArgumentException(400, "Message length <%d> does not meet channel's requirements.", messageLength);
-                        }
-
-                        String message = new String(Base64.getDecoder().decode(messageEncoded));
-                        channel.subscribers().forEach(s -> vertx.eventBus().publish(s.id(), message, new DeliveryOptions()
-                                .addHeader("service", s.serviceId())
-                                .addHeader("subscriber", s.subscriberId())
-                        ));
-
-                        rc.response().end(JsonObject.of("status", "ACCEPTED").encode());
-                    });
+
+                    openAPIRouterBuilder.operation("channelsGET").handler(rc -> vertx.eventBus()
+                            .<JsonArray>request(CHANNEL_PATH_TO_LIST.apply(channelsId), JsonArray.of(), reply ->
+                                    rc.response().end(new JsonArray(reply.result().body().stream()
+                                            .map(JsonObject.class::cast).map(ch -> JsonObject.of(
+                                                    "name", ch.getString("name")
+                                            )).toList()).encode()))
+                    );
+
+                    openAPIRouterBuilder.operation("channelsNameGET").handler(rc -> vertx.eventBus()
+                            .<JsonObject>request(CHANNEL_PATH_TO_DESCRIPTOR.apply(channelsId), JsonObject.of(), new DeliveryOptions().addHeader("channelId", rc.pathParam("name")), reply -> {
+                                    if (reply.succeeded()) {
+                                        rc.response().end(reply.result().body()
+                                                    .getJsonObject("schema")
+                                                .encode()
+                                        );
+                                    } else {
+                                        rc.fail(reply.cause());
+                                    }
+                            }));
+
+                    openAPIRouterBuilder.operation("channelNamePOST").handler(rc -> vertx.eventBus()
+                            .<JsonObject>request(CHANNEL_PATH_TO_SEND.apply(channelsId), rc.body().asJsonObject(), new DeliveryOptions().addHeader("channelId", rc.pathParam("name")), reply -> {
+                               if (reply.succeeded()) {
+                                   rc.response().end(reply.result().body().encode());
+                               } else {
+                                   rc.fail(reply.cause());
+                               }
+                            }));
+
+                    openAPIRouterBuilder.operation("servicesGET").handler(rc -> vertx.eventBus()
+                            .<JsonArray>request(SERVICE_PATH_TO_LIST.apply(servicesId), JsonArray.of(), reply -> rc.response()
+                                    .end(new JsonArray(reply.result().body().stream().map(JsonObject.class::cast)
+                                            .map(o -> JsonObject.of(
+                                                    "name", o.getString("serviceId"),
+                                                    "type", o.getString("type")
+                                            )).toList()).encode()))
+                    );
+
+                    openAPIRouterBuilder.operation("servicesNameGET").handler(rc -> vertx.eventBus().
+                            <JsonObject>request(EVENT_BUS_PATH_TO_DESCRIPTOR.apply(rc.pathParam("name")), JsonObject.of(), reply -> {
+                                   if (reply.succeeded()) {
+                                       rc.response().end(reply.result().body()
+                                                    .getJsonObject("messageSchema")
+                                               .encode()
+                                       );
+                                   } else {
+                                       rc.fail(reply.cause());
+                                   }
+                            })
+                    );
+
+                    openAPIRouterBuilder.operation("servicesNamePOST").handler(rc -> vertx.eventBus()
+                            .<JsonObject>request(EVENT_BUS_PATH_TO_SEND.apply(rc.pathParam("name")), rc.body().asJsonObject(), reply -> {
+                                if (reply.succeeded()) {
+                                    rc.response().end(reply.result().body().encode());
+                                } else {
+                                    rc.fail(reply.cause());
+                                }
+                            }));
+
 
                     Router mainRouter = openAPIRouterBuilder.createRouter();
 

+ 61 - 25
src/main/java/cz/senslog/messaging/service/AbstractService.java

@@ -1,52 +1,88 @@
 package cz.senslog.messaging.service;
 
 import cz.senslog.messaging.domain.ServiceType;
+import cz.senslog.messaging.utils.PromiseSupport;
 import io.vertx.core.AbstractVerticle;
-import io.vertx.core.Handler;
-import io.vertx.core.eventbus.Message;
+import io.vertx.core.Future;
+import io.vertx.core.Promise;
 import io.vertx.core.json.JsonObject;
+import io.vertx.json.schema.*;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 
-import static cz.senslog.messaging.utils.ServiceUtil.createServiceSubscriberId;
+import java.util.function.Function;
+
 
 public abstract class AbstractService extends AbstractVerticle {
 
+    public static final Function<String, String> EVENT_BUS_PATH_TO_SEND = id -> String.format("___%s#send", id);
+    public static final Function<String, String> EVENT_BUS_PATH_TO_DESCRIPTOR = id -> String.format("___%s#descriptor", id);
+
     private static final Logger logger = LogManager.getLogger(AbstractService.class);
 
     private final ServiceType type;
-    private final String id;
+    private JsonObject messageSchema;
+
+    private Validator messageValidator;
 
-    protected AbstractService(ServiceType type, String id) {
+    protected AbstractService(ServiceType type) {
         this.type = type;
-        this.id = id;
     }
 
-    protected abstract String handleMessage(String subscriber, String message);
+    protected abstract String blockingMessageHandler(JsonObject message);
 
-    protected abstract void config(JsonObject config);
+    protected abstract void configure(Promise<Void> completePromise);
 
     @Override
-    public void start()  {
-        JsonObject fullServiceConfig = config().getJsonObject("services").getJsonObject(id);
-        config(fullServiceConfig);
-
-        Handler<Message<String>> subscriberHandler = msg -> {
-            String subscriber = msg.headers().get("subscriber");
-            logger.info("Received message {}/{}.", id(), subscriber);
-            vertx.executeBlocking(() -> handleMessage(subscriber, msg.body()))
-                    .onSuccess(logger::info).onFailure(logger::error);
-        };
-
-        fullServiceConfig.getJsonArray("subscribedRecipients").stream().map(JsonObject.class::cast).map(o -> o.getString("id"))
-                        .toList().forEach(subId -> vertx.eventBus().consumer(createServiceSubscriberId(id, subId), subscriberHandler));
+    public void start(Promise<Void> startPromise) {
+        final String id = config().getString("id");
+        logger.debug("Configuring the service: {}({}).", type.name(), id);
+
+        messageSchema = config().getJsonObject("schema");
+        messageValidator = Validator.create(JsonSchema.of(messageSchema), new JsonSchemaOptions()
+                .setBaseUri("http://localhost:8080")
+                .setDraft(Draft.DRAFT7));
+
+        final Promise<Void> configPromise = Promise.promise();
+        configure(configPromise);
+
+        Future.all(configPromise.future(), registerSend(id), registerDescriptor(id))
+                .onSuccess(c -> {
+                    logger.info("Configuration: {}({}) completed.", type.name(), id);
+                    startPromise.complete();
+                })
+                .onFailure(startPromise::fail);
     }
 
-    public ServiceType type() {
-        return type;
+    private Future<Void> registerDescriptor(final String id) {
+        final Promise<Void> completePromise = Promise.promise();
+        vertx.eventBus().<JsonObject>consumer(EVENT_BUS_PATH_TO_DESCRIPTOR.apply(id), msg -> msg.reply(
+                JsonObject.of(
+                        "serviceId", id,
+                        "deploymentId", deploymentID(),
+                        "type", type,
+                        "messageSchema", messageSchema)
+                )
+        ).completionHandler(PromiseSupport.onComplete(completePromise));
+        return completePromise.future();
     }
 
-    public String id() {
-        return id;
+    private Future<Void> registerSend(final String id) {
+        final Promise<Void> completePromise = Promise.promise();
+        vertx.eventBus().<JsonObject>consumer(EVENT_BUS_PATH_TO_SEND.apply(id), msg -> {
+            JsonObject messageJson = msg.body();
+            OutputUnit validationRes = messageValidator.validate(messageJson);
+            if (validationRes.getValid()) {
+                vertx.executeBlocking(() -> blockingMessageHandler(messageJson))
+                        .onSuccess(msgRes -> msg.reply(JsonObject.of(
+                            "message", msgRes,
+                            "type", type
+                        )))
+                        .onFailure(f -> msg.fail(500, f.getMessage()));
+            } else {
+                msg.fail(500, validationRes.getError());
+            }
+        }).completionHandler(PromiseSupport.onComplete(completePromise));
+        return completePromise.future();
     }
 }

+ 36 - 60
src/main/java/cz/senslog/messaging/service/EmailService.java

@@ -1,102 +1,76 @@
 package cz.senslog.messaging.service;
 
 import cz.senslog.messaging.domain.ServiceType;
-import io.vertx.core.json.JsonArray;
+import io.vertx.core.Promise;
 import io.vertx.core.json.JsonObject;
 import jakarta.mail.*;
-import jakarta.mail.internet.*;
+import jakarta.mail.internet.InternetAddress;
+import jakarta.mail.internet.MimeBodyPart;
+import jakarta.mail.internet.MimeMessage;
+import jakarta.mail.internet.MimeMultipart;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 
-import java.util.*;
+import java.util.Properties;
+
 
 public final class EmailService extends AbstractService {
 
-    private record EmailMessageConfig(String senderEmail, String subject, String recipientsTo, String recipientsCC) {}
 
     private static final Logger logger = LogManager.getLogger(EmailService.class);
 
-    private final Map<String, EmailMessageConfig> messageRecipientsConfigs;
-
     private Session emailSession;
+    private String senderEmail;
 
-    public EmailService(String id) {
-        super(ServiceType.EMAIL, id);
-        this.messageRecipientsConfigs = new HashMap<>();
+    public EmailService() {
+        super(ServiceType.EMAIL);
     }
 
     @Override
-    protected String handleMessage(String subscriber, String message) {
+    protected String blockingMessageHandler(final JsonObject message) {
         try {
-            EmailMessageConfig messageConfig = Optional.ofNullable(messageRecipientsConfigs.get(subscriber))
-                    .orElseThrow(() -> new IllegalArgumentException(String.format("The subscriber '%s' does not exist.", subscriber)));
-
-            Message emailMessage = new MimeMessage(emailSession);
-            emailMessage.setFrom(new InternetAddress(messageConfig.senderEmail()));
-
-            if (messageConfig.recipientsTo() != null) {
-                emailMessage.setRecipients(Message.RecipientType.TO, InternetAddress.parse(messageConfig.recipientsTo()));
-            }
-            if (messageConfig.recipientsCC() != null) {
-                emailMessage.setRecipients(Message.RecipientType.CC, InternetAddress.parse(messageConfig.recipientsCC()));
+            final Message emailMessage = new MimeMessage(emailSession);
+            emailMessage.setFrom(new InternetAddress(senderEmail));
+
+            for (final Object object : message.getJsonArray("recipients")) {
+                final JsonObject recipientEmail = (JsonObject) object;
+                final InternetAddress[] address = InternetAddress.parse(recipientEmail.getString("email"));
+                if (recipientEmail.getBoolean("hideEmail")) {
+                    emailMessage.setRecipients(Message.RecipientType.BCC, address);
+                } else {
+                    emailMessage.setRecipients(Message.RecipientType.TO, address);
+                }
             }
-            emailMessage.setSubject(messageConfig.subject());
 
-            MimeBodyPart mimeBodyPart = new MimeBodyPart();
-            mimeBodyPart.setContent(message, "text/html; charset=utf-8");
+            emailMessage.setSubject(message.getString("subject"));
 
-            Multipart multipart = new MimeMultipart();
-            multipart.addBodyPart(mimeBodyPart);
+            final MimeBodyPart mimeBodyPart = new MimeBodyPart();
+            mimeBodyPart.setContent(message.getString("body"), "text/html; charset=utf-8");
 
+            final Multipart multipart = new MimeMultipart();
+            multipart.addBodyPart(mimeBodyPart);
             emailMessage.setContent(multipart);
 
             Transport.send(emailMessage);
 
-            return String.format("The message for the '%s' was send successfully", subscriber);
+            return "Sent";
         } catch (MessagingException e) {
+            logger.error(e);
             throw new RuntimeException(e);
         }
     }
 
     @Override
-    protected void config(JsonObject config) {
-        logger.info("Configuring the {}({}) service'", type().name(), id());
-        JsonArray subscribedRecipients = config.getJsonArray("subscribedRecipients");
-        for (Object r : subscribedRecipients) {
-            JsonObject recipient = (JsonObject) r;
-            String subscriberId = recipient.getString("id");
-            JsonObject recipientConfig = recipient.getJsonObject("config");
-            String senderEmail = recipientConfig.getString("senderEmail");
-            String subject = recipientConfig.getString("subject");
-            JsonArray recipientEmails = recipientConfig.getJsonArray("recipientEmail");
-            List<String> recipientsTo = new ArrayList<>();
-            List<String> recipientsCC = new ArrayList<>();
-            for (Object e : recipientEmails) {
-                JsonObject emailConfig = (JsonObject) e;
-                String email = emailConfig.getString("email");
-                if (emailConfig.getBoolean("hideEmail")) {
-                    recipientsCC.add(email);
-                } else {
-                    recipientsTo.add(email);
-                }
-            }
-            this.messageRecipientsConfigs.put(subscriberId,
-                    new EmailMessageConfig(
-                            senderEmail, subject,
-                            (recipientsTo.isEmpty() ? null : String.join(",", recipientsTo)),
-                            (recipientsCC.isEmpty() ? null : String.join(",", recipientsCC))
-                    )
-            );
-        }
-
-        JsonObject serviceConfig = config.getJsonObject("config");
-        Properties prop = new Properties();
-        prop.put("mail.smtp.auth", true);
+    protected void configure(Promise<Void> completePromise) {
+        final JsonObject serviceConfig = config();
+        final Properties prop = new Properties();
         prop.put("mail.smtp.host", serviceConfig.getString("smtpHost"));
         prop.put("mail.smtp.port", serviceConfig.getInteger("smtpPort").toString());
+        prop.put("mail.smtp.auth", true);
         prop.put("mail.smtp.starttls.enable", "true");
         prop.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
 
+        this.senderEmail = serviceConfig.getString("senderEmail");
         this.emailSession = Session.getInstance(prop, new Authenticator() {
             @Override
             protected PasswordAuthentication getPasswordAuthentication() {
@@ -106,5 +80,7 @@ public final class EmailService extends AbstractService {
                 );
             }
         });
+
+        completePromise.complete();
     }
 }

+ 0 - 25
src/main/java/cz/senslog/messaging/service/ServiceCreator.java

@@ -1,25 +0,0 @@
-package cz.senslog.messaging.service;
-
-import cz.senslog.messaging.domain.ServiceConfig;
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
-
-public final class ServiceCreator {
-
-    private static final Logger logger = LogManager.getLogger(ServiceCreator.class);
-
-    public static List<AbstractService> createServices(Collection<ServiceConfig> serviceConfigs) {
-        List<AbstractService> services = new ArrayList<>(serviceConfigs.size());
-        for (ServiceConfig config : serviceConfigs) {
-            switch (config.type()) {
-                case EMAIL -> services.add(new EmailService(config.id()));
-                default -> logger.warn("Service type '{}' is not supported yet.", config.type());
-            }
-        }
-        return services;
-    }
-}

+ 17 - 0
src/main/java/cz/senslog/messaging/utils/JsonUtils.java

@@ -0,0 +1,17 @@
+package cz.senslog.messaging.utils;
+
+import io.vertx.core.json.JsonObject;
+
+import java.util.Map;
+
+public final class JsonUtils {
+
+    public static JsonObject mapToJsonObject(Map<?, ?> map) {
+        JsonObject json = new JsonObject();
+        for (Map.Entry<?, ?> entry : map.entrySet()) {
+            json.put(entry.getKey().toString(), entry.getValue());
+        }
+        return json;
+    }
+
+}

+ 18 - 0
src/main/java/cz/senslog/messaging/utils/PromiseSupport.java

@@ -0,0 +1,18 @@
+package cz.senslog.messaging.utils;
+
+import io.vertx.core.AsyncResult;
+import io.vertx.core.Handler;
+import io.vertx.core.Promise;
+
+public final class PromiseSupport {
+
+    public static <T> Handler<AsyncResult<T>> onComplete(Promise<T> compleatePromise) {
+        return res -> {
+            if (res.succeeded()) {
+                compleatePromise.complete(res.result());
+            } else {
+                compleatePromise.fail(res.cause());
+            }
+        };
+    }
+}

+ 1 - 1
src/main/java/cz/senslog/messaging/utils/ResourcesUtils.java

@@ -5,7 +5,7 @@ import java.nio.file.Paths;
 
 public final class ResourcesUtils {
 
-    public static Path getPath(String resourceFile) {
+    public static Path getResourcePath(String resourceFile) {
         return Paths.get(resourceFile);
     }
 }

+ 169 - 18
src/main/resources/openAPISpec.yaml

@@ -3,7 +3,7 @@ info:
   version: 1.0.0
   title: SensLog Messaging
 servers:
-  - url: http://127.0.0.1:8080
+  - url: http://127.0.0.1:9090
 paths:
   /info:
     get:
@@ -62,14 +62,8 @@ paths:
                   properties:
                     name:
                       type: string
-                    lengthType:
-                      type: string
-                    maxLengthSize:
-                      type: integer
                   example:
                     name: "channelName"
-                    lengthType: "LARGE"
-                    maxLengthSize: 2147483647
         default:
           description: unexpected error
           content:
@@ -77,8 +71,29 @@ paths:
               schema:
                 $ref: "#/components/schemas/Error"
 
-
   /channels/{name}:
+    get:
+      operationId: channelsNameGET
+      summary: Publish JSON Schema of the channel
+      parameters:
+        - in: path
+          name: name
+          schema:
+            type: string
+          required: true
+      responses:
+        200:
+          description: JSON Schema
+          content:
+            application/json+schema:
+              schema:
+                type: object
+        default:
+          description: unexpected error
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Error'
     post:
       operationId: channelNamePOST
       summary: Receive message to send in Base64 Encoding
@@ -91,24 +106,98 @@ paths:
       requestBody:
         required: true
         content:
-          text/plain;encoding=base64:
+          application/json:
             schema:
-              type: string
+              anyOf:
+                - $ref: './schemas/sensLogAlert.yaml'
       responses:
         200:
           description: JSON object with a resulting status
           content:
             application/json:
               schema:
+                $ref: '#/components/schemas/MessageStatus'
+        default:
+          description: unexpected error
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Error'
+
+  /services:
+    get:
+      operationId: servicesGET
+      summary: Publishing info about all services
+      responses:
+        200:
+          description: JSON Array of basic info about each service
+          content:
+            application/json:
+              schema:
+                type: array
+                items:
+                  properties:
+                    name:
+                      type: string
+                    type:
+                      $ref: '#/components/schemas/ServiceType'
+                    supportedLengthTypes:
+                      type: array
+                      items:
+                        $ref: '#/components/schemas/MessageLengthType'
+        default:
+          description: unexpected error
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Error'
+
+  /services/{name}:
+    get:
+      operationId: servicesNameGET
+      summary: Publishing JSON Schema of the service
+      parameters:
+        - in: path
+          name: name
+          schema:
+            type: string
+          required: true
+      responses:
+        200:
+          description: JSON Schema
+          content:
+            application/json+schema:
+              schema:
                 type: object
-                properties:
-                  status:
-                    type: string
-                    enum:
-                      - ACCEPTED
-                      - REJECTED
-                example:
-                  status: "ACCEPTED"
+        default:
+          description: unexpected error
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Error'
+    post:
+      operationId: servicesNamePOST
+      summary: Send message via the service
+      parameters:
+        - in: path
+          name: name
+          schema:
+            type: string
+          required: true
+      requestBody:
+        required: true
+        content:
+          application/json:
+            schema:
+              anyOf:
+                - $ref: './schemas/emailMessage.yaml'
+      responses:
+        200:
+          description: JSON object with a resulting status
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/MessageStatus'
         default:
           description: unexpected error
           content:
@@ -116,8 +205,57 @@ paths:
               schema:
                 $ref: '#/components/schemas/Error'
 
+
 components:
   schemas:
+    EmailMessage:
+      type: object
+      required:
+        - sender
+        - recipients
+        - subject
+        - body
+      properties:
+        sender:
+          type: string
+        recipients:
+          type: array
+          items:
+            type: object
+            required:
+              - email
+            properties:
+              email:
+                type: string
+              hideEmail:
+                type: boolean
+        subject:
+          type: string
+        body:
+          type: string
+      example:
+        sender: "messaging@lesprojekt.cz"
+        recipients:
+          - email: "test1@lesprojekt.cz"
+            hideEmail: true
+          - email: "test2@lesprojekt.cz"
+            hideEmail: false
+        subject: "Subject of the email"
+        body: "Full email body"
+
+    MessageStatus:
+      type: object
+      required:
+        - status
+      properties:
+        status:
+          type: string
+          enum:
+            - ACCEPTED
+            - REJECTED
+      example:
+        status: "ACCEPTED"
+
     Error:
       type: object
       required:
@@ -132,3 +270,16 @@ components:
       example:
         code: 404
         message: "Not Found"
+
+
+    MessageLengthType:
+      type: string
+      enum:
+        - TINY
+        - SMALL
+        - LARGE
+
+    ServiceType:
+      type: string
+      enum:
+        - EMAIL

+ 30 - 0
src/main/resources/schemas/emailMessage.yaml

@@ -0,0 +1,30 @@
+type: object
+required:
+  - recipients
+  - subject
+  - body
+properties:
+  subject:
+    type: string
+  body:
+    type: string
+  recipients:
+    type: array
+    items:
+      type: object
+      required:
+        - email
+      properties:
+        email:
+          type: string
+        hideEmail:
+          type: boolean
+          default: true
+example:
+  subject: "Subject of the email"
+  body: "Full email body"
+  recipients:
+    - email: "test1@lesprojekt.cz"
+      hideEmail: true
+    - email: "test2@lesprojekt.cz"
+      hideEmail: false

+ 12 - 0
src/main/resources/schemas/sensLogAlert.yaml

@@ -0,0 +1,12 @@
+type: object
+required:
+  - title
+  - message
+properties:
+  title:
+    type: string
+  message:
+    type: string
+example:
+  title: "Title of the message"
+  message: "Content of the message"